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