Source: lib/dash/segment_base.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.dash.SegmentBase');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.dash.MpdUtils');
  9. goog.require('shaka.log');
  10. goog.require('shaka.media.InitSegmentReference');
  11. goog.require('shaka.media.Mp4SegmentIndexParser');
  12. goog.require('shaka.media.SegmentIndex');
  13. goog.require('shaka.media.WebmSegmentIndexParser');
  14. goog.require('shaka.util.Error');
  15. goog.require('shaka.util.ManifestParserUtils');
  16. goog.require('shaka.util.ObjectUtils');
  17. goog.require('shaka.util.StringUtils');
  18. goog.require('shaka.util.TXml');
  19. goog.requireType('shaka.dash.DashParser');
  20. goog.requireType('shaka.media.PresentationTimeline');
  21. goog.requireType('shaka.media.SegmentReference');
  22. /**
  23. * @summary A set of functions for parsing SegmentBase elements.
  24. */
  25. shaka.dash.SegmentBase = class {
  26. /**
  27. * Creates an init segment reference from a Context object.
  28. *
  29. * @param {shaka.dash.DashParser.Context} context
  30. * @param {function(?shaka.dash.DashParser.InheritanceFrame):
  31. * ?shaka.extern.xml.Node} callback
  32. * @param {shaka.extern.aesKey|undefined} aesKey
  33. * @return {shaka.media.InitSegmentReference}
  34. */
  35. static createInitSegment(context, callback, aesKey) {
  36. const MpdUtils = shaka.dash.MpdUtils;
  37. const TXml = shaka.util.TXml;
  38. const ManifestParserUtils = shaka.util.ManifestParserUtils;
  39. const StringUtils = shaka.util.StringUtils;
  40. const initialization =
  41. MpdUtils.inheritChild(context, callback, 'Initialization');
  42. if (!initialization) {
  43. return null;
  44. }
  45. let resolvedUris = context.representation.getBaseUris();
  46. const uri = initialization.attributes['sourceURL'];
  47. if (uri) {
  48. resolvedUris = ManifestParserUtils.resolveUris(resolvedUris, [
  49. StringUtils.htmlUnescape(uri),
  50. ]);
  51. }
  52. let startByte = 0;
  53. let endByte = null;
  54. const range = TXml.parseAttr(initialization, 'range', TXml.parseRange);
  55. if (range) {
  56. startByte = range.start;
  57. endByte = range.end;
  58. }
  59. const getUris = () => resolvedUris;
  60. const qualityInfo = shaka.dash.SegmentBase.createQualityInfo(context);
  61. const ref = new shaka.media.InitSegmentReference(
  62. getUris,
  63. startByte,
  64. endByte,
  65. qualityInfo,
  66. /* timescale= */ null,
  67. /* segmentData= */ null,
  68. aesKey);
  69. ref.codecs = context.representation.codecs;
  70. ref.mimeType = context.representation.mimeType;
  71. return ref;
  72. }
  73. /**
  74. * Creates a new StreamInfo object.
  75. *
  76. * @param {shaka.dash.DashParser.Context} context
  77. * @param {shaka.dash.DashParser.RequestSegmentCallback} requestSegment
  78. * @param {shaka.extern.aesKey|undefined} aesKey
  79. * @return {shaka.dash.DashParser.StreamInfo}
  80. */
  81. static createStreamInfo(context, requestSegment, aesKey) {
  82. goog.asserts.assert(context.representation.segmentBase,
  83. 'Should only be called with SegmentBase');
  84. // Since SegmentBase does not need updates, simply treat any call as
  85. // the initial parse.
  86. const MpdUtils = shaka.dash.MpdUtils;
  87. const SegmentBase = shaka.dash.SegmentBase;
  88. const TXml = shaka.util.TXml;
  89. const unscaledPresentationTimeOffset = Number(MpdUtils.inheritAttribute(
  90. context, SegmentBase.fromInheritance_, 'presentationTimeOffset')) || 0;
  91. const timescaleStr = MpdUtils.inheritAttribute(
  92. context, SegmentBase.fromInheritance_, 'timescale');
  93. let timescale = 1;
  94. if (timescaleStr) {
  95. timescale = TXml.parsePositiveInt(timescaleStr) || 1;
  96. }
  97. const scaledPresentationTimeOffset =
  98. (unscaledPresentationTimeOffset / timescale) || 0;
  99. const initSegmentReference = SegmentBase.createInitSegment(
  100. context, SegmentBase.fromInheritance_, aesKey);
  101. // Throws an immediate error if the format is unsupported.
  102. SegmentBase.checkSegmentIndexRangeSupport_(context, initSegmentReference);
  103. // Direct fields of context will be reassigned by the parser before
  104. // generateSegmentIndex is called. So we must make a shallow copy first,
  105. // and use that in the generateSegmentIndex callbacks.
  106. const shallowCopyOfContext =
  107. shaka.util.ObjectUtils.shallowCloneObject(context);
  108. return {
  109. generateSegmentIndex: () => {
  110. return SegmentBase.generateSegmentIndex_(
  111. shallowCopyOfContext, requestSegment, initSegmentReference,
  112. scaledPresentationTimeOffset);
  113. },
  114. };
  115. }
  116. /**
  117. * Creates a SegmentIndex for the given URIs and context.
  118. *
  119. * @param {shaka.dash.DashParser.Context} context
  120. * @param {shaka.dash.DashParser.RequestSegmentCallback} requestSegment
  121. * @param {shaka.media.InitSegmentReference} initSegmentReference
  122. * @param {!Array.<string>} uris
  123. * @param {number} startByte
  124. * @param {?number} endByte
  125. * @param {number} scaledPresentationTimeOffset
  126. * @return {!Promise.<shaka.media.SegmentIndex>}
  127. */
  128. static async generateSegmentIndexFromUris(
  129. context, requestSegment, initSegmentReference, uris, startByte,
  130. endByte, scaledPresentationTimeOffset) {
  131. // Unpack context right away, before we start an async process.
  132. // This immunizes us against changes to the context object later.
  133. /** @type {shaka.media.PresentationTimeline} */
  134. const presentationTimeline = context.presentationTimeline;
  135. const fitLast = !context.dynamic || !context.periodInfo.isLastPeriod;
  136. const periodStart = context.periodInfo.start;
  137. const periodDuration = context.periodInfo.duration;
  138. const containerType = context.representation.mimeType.split('/')[1];
  139. // Create a local variable to bind to so we can set to null to help the GC.
  140. let localRequest = requestSegment;
  141. let segmentIndex = null;
  142. const responses = [
  143. localRequest(uris, startByte, endByte, /* isInit= */ false),
  144. containerType == 'webm' ?
  145. localRequest(
  146. initSegmentReference.getUris(),
  147. initSegmentReference.startByte,
  148. initSegmentReference.endByte,
  149. /* isInit= */ true) :
  150. null,
  151. ];
  152. localRequest = null;
  153. const results = await Promise.all(responses);
  154. const indexData = results[0];
  155. const initData = results[1] || null;
  156. /** @type {Array.<!shaka.media.SegmentReference>} */
  157. let references = null;
  158. const timestampOffset = periodStart - scaledPresentationTimeOffset;
  159. const appendWindowStart = periodStart;
  160. const appendWindowEnd = periodDuration ?
  161. periodStart + periodDuration : Infinity;
  162. if (containerType == 'mp4') {
  163. references = shaka.media.Mp4SegmentIndexParser.parse(
  164. indexData, startByte, uris, initSegmentReference, timestampOffset,
  165. appendWindowStart, appendWindowEnd);
  166. } else {
  167. goog.asserts.assert(initData, 'WebM requires init data');
  168. references = shaka.media.WebmSegmentIndexParser.parse(
  169. indexData, initData, uris, initSegmentReference, timestampOffset,
  170. appendWindowStart, appendWindowEnd);
  171. }
  172. for (const ref of references) {
  173. ref.codecs = context.representation.codecs;
  174. ref.mimeType = context.representation.mimeType;
  175. }
  176. presentationTimeline.notifySegments(references);
  177. // Since containers are never updated, we don't need to store the
  178. // segmentIndex in the map.
  179. goog.asserts.assert(!segmentIndex,
  180. 'Should not call generateSegmentIndex twice');
  181. segmentIndex = new shaka.media.SegmentIndex(references);
  182. if (fitLast) {
  183. segmentIndex.fit(appendWindowStart, appendWindowEnd, /* isNew= */ true);
  184. }
  185. return segmentIndex;
  186. }
  187. /**
  188. * @param {?shaka.dash.DashParser.InheritanceFrame} frame
  189. * @return {?shaka.extern.xml.Node}
  190. * @private
  191. */
  192. static fromInheritance_(frame) {
  193. return frame.segmentBase;
  194. }
  195. /**
  196. * Compute the byte range of the segment index from the container.
  197. *
  198. * @param {shaka.dash.DashParser.Context} context
  199. * @return {?{start: number, end: number}}
  200. * @private
  201. */
  202. static computeIndexRange_(context) {
  203. const MpdUtils = shaka.dash.MpdUtils;
  204. const SegmentBase = shaka.dash.SegmentBase;
  205. const TXml = shaka.util.TXml;
  206. const representationIndex = MpdUtils.inheritChild(
  207. context, SegmentBase.fromInheritance_, 'RepresentationIndex');
  208. const indexRangeElem = MpdUtils.inheritAttribute(
  209. context, SegmentBase.fromInheritance_, 'indexRange');
  210. let indexRange = TXml.parseRange(indexRangeElem || '');
  211. if (representationIndex) {
  212. indexRange = TXml.parseAttr(
  213. representationIndex, 'range', TXml.parseRange, indexRange);
  214. }
  215. return indexRange;
  216. }
  217. /**
  218. * Compute the URIs of the segment index from the container.
  219. *
  220. * @param {shaka.dash.DashParser.Context} context
  221. * @return {!Array.<string>}
  222. * @private
  223. */
  224. static computeIndexUris_(context) {
  225. const ManifestParserUtils = shaka.util.ManifestParserUtils;
  226. const MpdUtils = shaka.dash.MpdUtils;
  227. const SegmentBase = shaka.dash.SegmentBase;
  228. const StringUtils = shaka.util.StringUtils;
  229. const representationIndex = MpdUtils.inheritChild(
  230. context, SegmentBase.fromInheritance_, 'RepresentationIndex');
  231. let indexUris = context.representation.getBaseUris();
  232. if (representationIndex) {
  233. const representationUri =
  234. StringUtils.htmlUnescape(representationIndex.attributes['sourceURL']);
  235. if (representationUri) {
  236. indexUris = ManifestParserUtils.resolveUris(
  237. indexUris, [representationUri]);
  238. }
  239. }
  240. return indexUris;
  241. }
  242. /**
  243. * Check if this type of segment index is supported. This allows for
  244. * immediate errors during parsing, as opposed to an async error from
  245. * createSegmentIndex().
  246. *
  247. * Also checks for a valid byte range, which is not required for callers from
  248. * SegmentTemplate.
  249. *
  250. * @param {shaka.dash.DashParser.Context} context
  251. * @param {shaka.media.InitSegmentReference} initSegmentReference
  252. * @private
  253. */
  254. static checkSegmentIndexRangeSupport_(context, initSegmentReference) {
  255. const SegmentBase = shaka.dash.SegmentBase;
  256. SegmentBase.checkSegmentIndexSupport(context, initSegmentReference);
  257. const indexRange = SegmentBase.computeIndexRange_(context);
  258. if (!indexRange) {
  259. shaka.log.error(
  260. 'SegmentBase does not contain sufficient segment information:',
  261. 'the SegmentBase does not contain @indexRange',
  262. 'or a RepresentationIndex element.',
  263. context.representation);
  264. throw new shaka.util.Error(
  265. shaka.util.Error.Severity.CRITICAL,
  266. shaka.util.Error.Category.MANIFEST,
  267. shaka.util.Error.Code.DASH_NO_SEGMENT_INFO);
  268. }
  269. }
  270. /**
  271. * Check if this type of segment index is supported. This allows for
  272. * immediate errors during parsing, as opposed to an async error from
  273. * createSegmentIndex().
  274. *
  275. * @param {shaka.dash.DashParser.Context} context
  276. * @param {shaka.media.InitSegmentReference} initSegmentReference
  277. */
  278. static checkSegmentIndexSupport(context, initSegmentReference) {
  279. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  280. const contentType = context.representation.contentType;
  281. const containerType = context.representation.mimeType.split('/')[1];
  282. if (contentType != ContentType.TEXT && containerType != 'mp4' &&
  283. containerType != 'webm') {
  284. shaka.log.error(
  285. 'SegmentBase specifies an unsupported container type.',
  286. context.representation);
  287. throw new shaka.util.Error(
  288. shaka.util.Error.Severity.CRITICAL,
  289. shaka.util.Error.Category.MANIFEST,
  290. shaka.util.Error.Code.DASH_UNSUPPORTED_CONTAINER);
  291. }
  292. if ((containerType == 'webm') && !initSegmentReference) {
  293. shaka.log.error(
  294. 'SegmentBase does not contain sufficient segment information:',
  295. 'the SegmentBase uses a WebM container,',
  296. 'but does not contain an Initialization element.',
  297. context.representation);
  298. throw new shaka.util.Error(
  299. shaka.util.Error.Severity.CRITICAL,
  300. shaka.util.Error.Category.MANIFEST,
  301. shaka.util.Error.Code.DASH_WEBM_MISSING_INIT);
  302. }
  303. }
  304. /**
  305. * Generate a SegmentIndex from a Context object.
  306. *
  307. * @param {shaka.dash.DashParser.Context} context
  308. * @param {shaka.dash.DashParser.RequestSegmentCallback} requestSegment
  309. * @param {shaka.media.InitSegmentReference} initSegmentReference
  310. * @param {number} scaledPresentationTimeOffset
  311. * @return {!Promise.<shaka.media.SegmentIndex>}
  312. * @private
  313. */
  314. static generateSegmentIndex_(
  315. context, requestSegment, initSegmentReference,
  316. scaledPresentationTimeOffset) {
  317. const SegmentBase = shaka.dash.SegmentBase;
  318. const indexUris = SegmentBase.computeIndexUris_(context);
  319. const indexRange = SegmentBase.computeIndexRange_(context);
  320. goog.asserts.assert(indexRange, 'Index range should not be null!');
  321. return shaka.dash.SegmentBase.generateSegmentIndexFromUris(
  322. context, requestSegment, initSegmentReference, indexUris,
  323. indexRange.start, indexRange.end,
  324. scaledPresentationTimeOffset);
  325. }
  326. /**
  327. * Create a MediaQualityInfo object from a Context object.
  328. *
  329. * @param {!shaka.dash.DashParser.Context} context
  330. * @return {!shaka.extern.MediaQualityInfo}
  331. */
  332. static createQualityInfo(context) {
  333. const representation = context.representation;
  334. return {
  335. bandwidth: context.bandwidth,
  336. audioSamplingRate: representation.audioSamplingRate,
  337. codecs: representation.codecs,
  338. contentType: representation.contentType,
  339. frameRate: representation.frameRate || null,
  340. height: representation.height || null,
  341. mimeType: representation.mimeType,
  342. channelsCount: representation.numChannels,
  343. pixelAspectRatio: representation.pixelAspectRatio || null,
  344. width: representation.width || null,
  345. };
  346. }
  347. };