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 {!Map<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.get(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, context.bandwidth, context.urlParams);
  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. endTime: -1,
  77. timeline: -1,
  78. generateSegmentIndex: () => {
  79. if (!segmentIndex || segmentIndex.isEmpty()) {
  80. segmentIndex.merge(references);
  81. }
  82. return Promise.resolve(segmentIndex);
  83. },
  84. timescale: info.timescale,
  85. };
  86. }
  87. /**
  88. * @param {?shaka.dash.DashParser.InheritanceFrame} frame
  89. * @return {?shaka.extern.xml.Node}
  90. * @private
  91. */
  92. static fromInheritance_(frame) {
  93. return frame.segmentList;
  94. }
  95. /**
  96. * Parses the SegmentList items to create an info object.
  97. *
  98. * @param {shaka.dash.DashParser.Context} context
  99. * @return {shaka.dash.SegmentList.SegmentListInfo}
  100. * @private
  101. */
  102. static parseSegmentListInfo_(context) {
  103. const SegmentList = shaka.dash.SegmentList;
  104. const MpdUtils = shaka.dash.MpdUtils;
  105. const mediaSegments = SegmentList.parseMediaSegments_(context);
  106. const segmentInfo =
  107. MpdUtils.parseSegmentInfo(context, SegmentList.fromInheritance_);
  108. let startNumber = segmentInfo.startNumber;
  109. if (startNumber == 0) {
  110. shaka.log.warning('SegmentList@startNumber must be > 0');
  111. startNumber = 1;
  112. }
  113. let startTime = 0;
  114. if (segmentInfo.segmentDuration) {
  115. // See DASH sec. 5.3.9.5.3
  116. // Don't use presentationTimeOffset for @duration.
  117. startTime = segmentInfo.segmentDuration * (startNumber - 1);
  118. } else if (segmentInfo.timeline && segmentInfo.timeline.length > 0) {
  119. // The presentationTimeOffset was considered in timeline creation.
  120. startTime = segmentInfo.timeline[0].start;
  121. }
  122. return {
  123. segmentDuration: segmentInfo.segmentDuration,
  124. startTime: startTime,
  125. startNumber: startNumber,
  126. scaledPresentationTimeOffset: segmentInfo.scaledPresentationTimeOffset,
  127. timescale: segmentInfo.timescale,
  128. timeline: segmentInfo.timeline,
  129. mediaSegments: mediaSegments,
  130. };
  131. }
  132. /**
  133. * Checks whether a SegmentListInfo object is valid.
  134. *
  135. * @param {shaka.dash.DashParser.Context} context
  136. * @param {shaka.dash.SegmentList.SegmentListInfo} info
  137. * @private
  138. */
  139. static checkSegmentListInfo_(context, info) {
  140. if (!info.segmentDuration && !info.timeline &&
  141. info.mediaSegments.length > 1) {
  142. shaka.log.warning(
  143. 'SegmentList does not contain sufficient segment information:',
  144. 'the SegmentList specifies multiple segments,',
  145. 'but does not specify a segment duration or timeline.',
  146. context.representation);
  147. throw new shaka.util.Error(
  148. shaka.util.Error.Severity.CRITICAL,
  149. shaka.util.Error.Category.MANIFEST,
  150. shaka.util.Error.Code.DASH_NO_SEGMENT_INFO);
  151. }
  152. if (!info.segmentDuration && !context.periodInfo.duration &&
  153. !info.timeline && info.mediaSegments.length == 1) {
  154. shaka.log.warning(
  155. 'SegmentList does not contain sufficient segment information:',
  156. 'the SegmentList specifies one segment,',
  157. 'but does not specify a segment duration, period duration,',
  158. 'or timeline.',
  159. context.representation);
  160. throw new shaka.util.Error(
  161. shaka.util.Error.Severity.CRITICAL,
  162. shaka.util.Error.Category.MANIFEST,
  163. shaka.util.Error.Code.DASH_NO_SEGMENT_INFO);
  164. }
  165. if (info.timeline && info.timeline.length == 0) {
  166. shaka.log.warning(
  167. 'SegmentList does not contain sufficient segment information:',
  168. 'the SegmentList has an empty timeline.',
  169. context.representation);
  170. throw new shaka.util.Error(
  171. shaka.util.Error.Severity.CRITICAL,
  172. shaka.util.Error.Category.MANIFEST,
  173. shaka.util.Error.Code.DASH_NO_SEGMENT_INFO);
  174. }
  175. }
  176. /**
  177. * Creates an array of segment references for the given data.
  178. *
  179. * @param {number} periodStart in seconds.
  180. * @param {?number} periodDuration in seconds.
  181. * @param {number} startNumber
  182. * @param {function(): !Array<string>} getBaseUris
  183. * @param {shaka.dash.SegmentList.SegmentListInfo} info
  184. * @param {shaka.media.InitSegmentReference} initSegmentReference
  185. * @param {shaka.extern.aesKey|undefined} aesKey
  186. * @param {string} mimeType
  187. * @param {string} codecs
  188. * @param {number} bandwidth
  189. * @param {function():string} urlParams
  190. * @return {!Array<!shaka.media.SegmentReference>}
  191. * @private
  192. */
  193. static createSegmentReferences_(
  194. periodStart, periodDuration, startNumber, getBaseUris, info,
  195. initSegmentReference, aesKey, mimeType, codecs, bandwidth, urlParams) {
  196. const ManifestParserUtils = shaka.util.ManifestParserUtils;
  197. let max = info.mediaSegments.length;
  198. if (info.timeline && info.timeline.length != info.mediaSegments.length) {
  199. max = Math.min(info.timeline.length, info.mediaSegments.length);
  200. shaka.log.warning(
  201. 'The number of items in the segment timeline and the number of ',
  202. 'segment URLs do not match, truncating', info.mediaSegments.length,
  203. 'to', max);
  204. }
  205. const timestampOffset = periodStart - info.scaledPresentationTimeOffset;
  206. const appendWindowStart = periodStart;
  207. const appendWindowEnd = periodDuration ?
  208. periodStart + periodDuration : Infinity;
  209. /** @type {!Array<!shaka.media.SegmentReference>} */
  210. const references = [];
  211. let prevEndTime = info.startTime;
  212. for (let i = 0; i < max; i++) {
  213. const segment = info.mediaSegments[i];
  214. const startTime = prevEndTime;
  215. let endTime;
  216. if (info.segmentDuration != null) {
  217. endTime = startTime + info.segmentDuration;
  218. } else if (info.timeline) {
  219. // Ignore the timepoint start since they are continuous.
  220. endTime = info.timeline[i].end;
  221. } else {
  222. // If segmentDuration and timeline are null then there must
  223. // be exactly one segment.
  224. goog.asserts.assert(
  225. info.mediaSegments.length == 1 && periodDuration,
  226. 'There should be exactly one segment with a Period duration.');
  227. endTime = startTime + periodDuration;
  228. }
  229. let uris = null;
  230. const getUris = () => {
  231. if (uris == null) {
  232. uris = ManifestParserUtils.resolveUris(
  233. getBaseUris(), [segment.mediaUri], urlParams());
  234. }
  235. return uris;
  236. };
  237. const ref = new shaka.media.SegmentReference(
  238. periodStart + startTime,
  239. periodStart + endTime,
  240. getUris,
  241. segment.start,
  242. segment.end,
  243. initSegmentReference,
  244. timestampOffset,
  245. appendWindowStart, appendWindowEnd,
  246. /* partialReferences= */ [],
  247. /* tilesLayout= */ '',
  248. /* tileDuration= */ null,
  249. /* syncTime= */ null,
  250. shaka.media.SegmentReference.Status.AVAILABLE,
  251. aesKey);
  252. ref.codecs = codecs;
  253. ref.mimeType = mimeType;
  254. ref.bandwidth = bandwidth;
  255. references.push(ref);
  256. prevEndTime = endTime;
  257. }
  258. return references;
  259. }
  260. /**
  261. * Parses the media URIs from the context.
  262. *
  263. * @param {shaka.dash.DashParser.Context} context
  264. * @return {!Array<shaka.dash.SegmentList.MediaSegment>}
  265. * @private
  266. */
  267. static parseMediaSegments_(context) {
  268. const Functional = shaka.util.Functional;
  269. /** @type {!Array<!shaka.extern.xml.Node>} */
  270. const segmentLists = [
  271. context.representation.segmentList,
  272. context.adaptationSet.segmentList,
  273. context.period.segmentList,
  274. ].filter(Functional.isNotNull);
  275. const TXml = shaka.util.TXml;
  276. const StringUtils = shaka.util.StringUtils;
  277. // Search each SegmentList for one with at least one SegmentURL element,
  278. // select the first one, and convert each SegmentURL element to a tuple.
  279. return segmentLists
  280. .map((node) => { return TXml.findChildren(node, 'SegmentURL'); })
  281. .reduce((all, part) => { return all.length > 0 ? all : part; })
  282. .map((urlNode) => {
  283. if (urlNode.attributes['indexRange'] &&
  284. !context.indexRangeWarningGiven) {
  285. context.indexRangeWarningGiven = true;
  286. shaka.log.warning(
  287. 'We do not support the SegmentURL@indexRange attribute on ' +
  288. 'SegmentList. We only use the SegmentList@duration ' +
  289. 'attribute or SegmentTimeline, which must be accurate.');
  290. }
  291. const uri = StringUtils.htmlUnescape(urlNode.attributes['media']);
  292. const range = TXml.parseAttr(
  293. urlNode, 'mediaRange', TXml.parseRange,
  294. {start: 0, end: null});
  295. return {mediaUri: uri, start: range.start, end: range.end};
  296. });
  297. }
  298. };
  299. /**
  300. * @typedef {{
  301. * mediaUri: string,
  302. * start: number,
  303. * end: ?number,
  304. * }}
  305. *
  306. * @property {string} mediaUri
  307. * The URI of the segment.
  308. * @property {number} start
  309. * The start byte of the segment.
  310. * @property {?number} end
  311. * The end byte of the segment, or null.
  312. */
  313. shaka.dash.SegmentList.MediaSegment;
  314. /**
  315. * @typedef {{
  316. * segmentDuration: ?number,
  317. * startTime: number,
  318. * startNumber: number,
  319. * scaledPresentationTimeOffset: number,
  320. * timescale: number,
  321. * timeline: Array<shaka.media.PresentationTimeline.TimeRange>,
  322. * mediaSegments: !Array<shaka.dash.SegmentList.MediaSegment>,
  323. * }}
  324. * @private
  325. *
  326. * @description
  327. * Contains information about a SegmentList.
  328. *
  329. * @property {?number} segmentDuration
  330. * The duration of the segments, if given.
  331. * @property {number} startTime
  332. * The start time of the first segment, in seconds.
  333. * @property {number} startNumber
  334. * The start number of the segments; 1 or greater.
  335. * @property {number} scaledPresentationTimeOffset
  336. * The scaledPresentationTimeOffset of the representation, in seconds.
  337. * @property {number} timescale
  338. * The timescale of the representation.
  339. * @property {Array<shaka.media.PresentationTimeline.TimeRange>} timeline
  340. * The timeline of the representation, if given. Times in seconds.
  341. * @property {!Array<shaka.dash.SegmentList.MediaSegment>} mediaSegments
  342. * The URI and byte-ranges of the media segments.
  343. */
  344. shaka.dash.SegmentList.SegmentListInfo;