Source: lib/dash/segment_list.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.dash.SegmentList');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.dash.MpdUtils');
  9. goog.require('shaka.dash.SegmentBase');
  10. goog.require('shaka.log');
  11. goog.require('shaka.media.InitSegmentReference');
  12. goog.require('shaka.media.SegmentIndex');
  13. goog.require('shaka.media.SegmentReference');
  14. goog.require('shaka.util.Error');
  15. goog.require('shaka.util.Functional');
  16. goog.require('shaka.util.ManifestParserUtils');
  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. /**
  22. * @summary A set of functions for parsing SegmentList elements.
  23. */
  24. shaka.dash.SegmentList = class {
  25. /**
  26. * Creates a new StreamInfo object.
  27. * Updates the existing SegmentIndex, if any.
  28. *
  29. * @param {shaka.dash.DashParser.Context} context
  30. * @param {!Object.<string, !shaka.extern.Stream>} streamMap
  31. * @param {shaka.extern.aesKey|undefined} aesKey
  32. * @return {shaka.dash.DashParser.StreamInfo}
  33. */
  34. static createStreamInfo(context, streamMap, aesKey) {
  35. goog.asserts.assert(context.representation.segmentList,
  36. 'Should only be called with SegmentList');
  37. const SegmentList = shaka.dash.SegmentList;
  38. const initSegmentReference = shaka.dash.SegmentBase.createInitSegment(
  39. context, SegmentList.fromInheritance_, aesKey);
  40. const info = SegmentList.parseSegmentListInfo_(context);
  41. SegmentList.checkSegmentListInfo_(context, info);
  42. /** @type {shaka.media.SegmentIndex} */
  43. let segmentIndex = null;
  44. let stream = null;
  45. if (context.period.id && context.representation.id) {
  46. // Only check/store the index if period and representation IDs are set.
  47. const id = context.period.id + ',' + context.representation.id;
  48. stream = streamMap[id];
  49. if (stream) {
  50. segmentIndex = stream.segmentIndex;
  51. }
  52. }
  53. const references = SegmentList.createSegmentReferences_(
  54. context.periodInfo.start, context.periodInfo.duration,
  55. info.startNumber, context.representation.getBaseUris, info,
  56. initSegmentReference, aesKey, context.representation.mimeType,
  57. context.representation.codecs);
  58. const isNew = !segmentIndex;
  59. if (segmentIndex) {
  60. const start = context.presentationTimeline.getSegmentAvailabilityStart();
  61. segmentIndex.mergeAndEvict(references, start);
  62. } else {
  63. segmentIndex = new shaka.media.SegmentIndex(references);
  64. }
  65. context.presentationTimeline.notifySegments(references);
  66. if (!context.dynamic || !context.periodInfo.isLastPeriod) {
  67. const periodStart = context.periodInfo.start;
  68. const periodEnd = context.periodInfo.duration ?
  69. context.periodInfo.start + context.periodInfo.duration : Infinity;
  70. segmentIndex.fit(periodStart, periodEnd, isNew);
  71. }
  72. if (stream) {
  73. stream.segmentIndex = segmentIndex;
  74. }
  75. return {
  76. generateSegmentIndex: () => {
  77. if (!segmentIndex || segmentIndex.isEmpty()) {
  78. segmentIndex.merge(references);
  79. }
  80. return Promise.resolve(segmentIndex);
  81. },
  82. };
  83. }
  84. /**
  85. * @param {?shaka.dash.DashParser.InheritanceFrame} frame
  86. * @return {?shaka.extern.xml.Node}
  87. * @private
  88. */
  89. static fromInheritance_(frame) {
  90. return frame.segmentList;
  91. }
  92. /**
  93. * Parses the SegmentList items to create an info object.
  94. *
  95. * @param {shaka.dash.DashParser.Context} context
  96. * @return {shaka.dash.SegmentList.SegmentListInfo}
  97. * @private
  98. */
  99. static parseSegmentListInfo_(context) {
  100. const SegmentList = shaka.dash.SegmentList;
  101. const MpdUtils = shaka.dash.MpdUtils;
  102. const mediaSegments = SegmentList.parseMediaSegments_(context);
  103. const segmentInfo =
  104. MpdUtils.parseSegmentInfo(context, SegmentList.fromInheritance_);
  105. let startNumber = segmentInfo.startNumber;
  106. if (startNumber == 0) {
  107. shaka.log.warning('SegmentList@startNumber must be > 0');
  108. startNumber = 1;
  109. }
  110. let startTime = 0;
  111. if (segmentInfo.segmentDuration) {
  112. // See DASH sec. 5.3.9.5.3
  113. // Don't use presentationTimeOffset for @duration.
  114. startTime = segmentInfo.segmentDuration * (startNumber - 1);
  115. } else if (segmentInfo.timeline && segmentInfo.timeline.length > 0) {
  116. // The presentationTimeOffset was considered in timeline creation.
  117. startTime = segmentInfo.timeline[0].start;
  118. }
  119. return {
  120. segmentDuration: segmentInfo.segmentDuration,
  121. startTime: startTime,
  122. startNumber: startNumber,
  123. scaledPresentationTimeOffset: segmentInfo.scaledPresentationTimeOffset,
  124. timeline: segmentInfo.timeline,
  125. mediaSegments: mediaSegments,
  126. };
  127. }
  128. /**
  129. * Checks whether a SegmentListInfo object is valid.
  130. *
  131. * @param {shaka.dash.DashParser.Context} context
  132. * @param {shaka.dash.SegmentList.SegmentListInfo} info
  133. * @private
  134. */
  135. static checkSegmentListInfo_(context, info) {
  136. if (!info.segmentDuration && !info.timeline &&
  137. info.mediaSegments.length > 1) {
  138. shaka.log.warning(
  139. 'SegmentList does not contain sufficient segment information:',
  140. 'the SegmentList specifies multiple segments,',
  141. 'but does not specify a segment duration or timeline.',
  142. context.representation);
  143. throw new shaka.util.Error(
  144. shaka.util.Error.Severity.CRITICAL,
  145. shaka.util.Error.Category.MANIFEST,
  146. shaka.util.Error.Code.DASH_NO_SEGMENT_INFO);
  147. }
  148. if (!info.segmentDuration && !context.periodInfo.duration &&
  149. !info.timeline && info.mediaSegments.length == 1) {
  150. shaka.log.warning(
  151. 'SegmentList does not contain sufficient segment information:',
  152. 'the SegmentList specifies one segment,',
  153. 'but does not specify a segment duration, period duration,',
  154. 'or timeline.',
  155. context.representation);
  156. throw new shaka.util.Error(
  157. shaka.util.Error.Severity.CRITICAL,
  158. shaka.util.Error.Category.MANIFEST,
  159. shaka.util.Error.Code.DASH_NO_SEGMENT_INFO);
  160. }
  161. if (info.timeline && info.timeline.length == 0) {
  162. shaka.log.warning(
  163. 'SegmentList does not contain sufficient segment information:',
  164. 'the SegmentList has an empty timeline.',
  165. context.representation);
  166. throw new shaka.util.Error(
  167. shaka.util.Error.Severity.CRITICAL,
  168. shaka.util.Error.Category.MANIFEST,
  169. shaka.util.Error.Code.DASH_NO_SEGMENT_INFO);
  170. }
  171. }
  172. /**
  173. * Creates an array of segment references for the given data.
  174. *
  175. * @param {number} periodStart in seconds.
  176. * @param {?number} periodDuration in seconds.
  177. * @param {number} startNumber
  178. * @param {function():!Array.<string>} getBaseUris
  179. * @param {shaka.dash.SegmentList.SegmentListInfo} info
  180. * @param {shaka.media.InitSegmentReference} initSegmentReference
  181. * @param {shaka.extern.aesKey|undefined} aesKey
  182. * @param {string} mimeType
  183. * @param {string} codecs
  184. * @return {!Array.<!shaka.media.SegmentReference>}
  185. * @private
  186. */
  187. static createSegmentReferences_(
  188. periodStart, periodDuration, startNumber, getBaseUris, info,
  189. initSegmentReference, aesKey, mimeType, codecs) {
  190. const ManifestParserUtils = shaka.util.ManifestParserUtils;
  191. let max = info.mediaSegments.length;
  192. if (info.timeline && info.timeline.length != info.mediaSegments.length) {
  193. max = Math.min(info.timeline.length, info.mediaSegments.length);
  194. shaka.log.warning(
  195. 'The number of items in the segment timeline and the number of ',
  196. 'segment URLs do not match, truncating', info.mediaSegments.length,
  197. 'to', max);
  198. }
  199. const timestampOffset = periodStart - info.scaledPresentationTimeOffset;
  200. const appendWindowStart = periodStart;
  201. const appendWindowEnd = periodDuration ?
  202. periodStart + periodDuration : Infinity;
  203. /** @type {!Array.<!shaka.media.SegmentReference>} */
  204. const references = [];
  205. let prevEndTime = info.startTime;
  206. for (let i = 0; i < max; i++) {
  207. const segment = info.mediaSegments[i];
  208. const startTime = prevEndTime;
  209. let endTime;
  210. if (info.segmentDuration != null) {
  211. endTime = startTime + info.segmentDuration;
  212. } else if (info.timeline) {
  213. // Ignore the timepoint start since they are continuous.
  214. endTime = info.timeline[i].end;
  215. } else {
  216. // If segmentDuration and timeline are null then there must
  217. // be exactly one segment.
  218. goog.asserts.assert(
  219. info.mediaSegments.length == 1 && periodDuration,
  220. 'There should be exactly one segment with a Period duration.');
  221. endTime = startTime + periodDuration;
  222. }
  223. let uris = null;
  224. const getUris = () => {
  225. if (uris == null) {
  226. uris = ManifestParserUtils.resolveUris(
  227. getBaseUris(), [segment.mediaUri]);
  228. }
  229. return uris;
  230. };
  231. const ref = new shaka.media.SegmentReference(
  232. periodStart + startTime,
  233. periodStart + endTime,
  234. getUris,
  235. segment.start,
  236. segment.end,
  237. initSegmentReference,
  238. timestampOffset,
  239. appendWindowStart, appendWindowEnd,
  240. /* partialReferences= */ [],
  241. /* tilesLayout= */ '',
  242. /* tileDuration= */ null,
  243. /* syncTime= */ null,
  244. shaka.media.SegmentReference.Status.AVAILABLE,
  245. aesKey);
  246. ref.codecs = codecs;
  247. ref.mimeType = mimeType;
  248. references.push(ref);
  249. prevEndTime = endTime;
  250. }
  251. return references;
  252. }
  253. /**
  254. * Parses the media URIs from the context.
  255. *
  256. * @param {shaka.dash.DashParser.Context} context
  257. * @return {!Array.<shaka.dash.SegmentList.MediaSegment>}
  258. * @private
  259. */
  260. static parseMediaSegments_(context) {
  261. const Functional = shaka.util.Functional;
  262. /** @type {!Array.<!shaka.extern.xml.Node>} */
  263. const segmentLists = [
  264. context.representation.segmentList,
  265. context.adaptationSet.segmentList,
  266. context.period.segmentList,
  267. ].filter(Functional.isNotNull);
  268. const TXml = shaka.util.TXml;
  269. const StringUtils = shaka.util.StringUtils;
  270. // Search each SegmentList for one with at least one SegmentURL element,
  271. // select the first one, and convert each SegmentURL element to a tuple.
  272. return segmentLists
  273. .map((node) => { return TXml.findChildren(node, 'SegmentURL'); })
  274. .reduce((all, part) => { return all.length > 0 ? all : part; })
  275. .map((urlNode) => {
  276. if (urlNode.attributes['indexRange'] &&
  277. !context.indexRangeWarningGiven) {
  278. context.indexRangeWarningGiven = true;
  279. shaka.log.warning(
  280. 'We do not support the SegmentURL@indexRange attribute on ' +
  281. 'SegmentList. We only use the SegmentList@duration ' +
  282. 'attribute or SegmentTimeline, which must be accurate.');
  283. }
  284. const uri = StringUtils.htmlUnescape(urlNode.attributes['media']);
  285. const range = TXml.parseAttr(
  286. urlNode, 'mediaRange', TXml.parseRange,
  287. {start: 0, end: null});
  288. return {mediaUri: uri, start: range.start, end: range.end};
  289. });
  290. }
  291. };
  292. /**
  293. * @typedef {{
  294. * mediaUri: string,
  295. * start: number,
  296. * end: ?number
  297. * }}
  298. *
  299. * @property {string} mediaUri
  300. * The URI of the segment.
  301. * @property {number} start
  302. * The start byte of the segment.
  303. * @property {?number} end
  304. * The end byte of the segment, or null.
  305. */
  306. shaka.dash.SegmentList.MediaSegment;
  307. /**
  308. * @typedef {{
  309. * segmentDuration: ?number,
  310. * startTime: number,
  311. * startNumber: number,
  312. * scaledPresentationTimeOffset: number,
  313. * timeline: Array.<shaka.media.PresentationTimeline.TimeRange>,
  314. * mediaSegments: !Array.<shaka.dash.SegmentList.MediaSegment>
  315. * }}
  316. * @private
  317. *
  318. * @description
  319. * Contains information about a SegmentList.
  320. *
  321. * @property {?number} segmentDuration
  322. * The duration of the segments, if given.
  323. * @property {number} startTime
  324. * The start time of the first segment, in seconds.
  325. * @property {number} startNumber
  326. * The start number of the segments; 1 or greater.
  327. * @property {number} scaledPresentationTimeOffset
  328. * The scaledPresentationTimeOffset of the representation, in seconds.
  329. * @property {Array.<shaka.media.PresentationTimeline.TimeRange>} timeline
  330. * The timeline of the representation, if given. Times in seconds.
  331. * @property {!Array.<shaka.dash.SegmentList.MediaSegment>} mediaSegments
  332. * The URI and byte-ranges of the media segments.
  333. */
  334. shaka.dash.SegmentList.SegmentListInfo;