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