Source: lib/dash/segment_template.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.dash.SegmentTemplate');
  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.ManifestParserUtils');
  16. goog.require('shaka.util.ObjectUtils');
  17. goog.requireType('shaka.dash.DashParser');
  18. /**
  19. * @summary A set of functions for parsing SegmentTemplate elements.
  20. */
  21. shaka.dash.SegmentTemplate = class {
  22. /**
  23. * Creates a new StreamInfo object.
  24. * Updates the existing SegmentIndex, if any.
  25. *
  26. * @param {shaka.dash.DashParser.Context} context
  27. * @param {shaka.dash.DashParser.RequestInitSegmentCallback}
  28. * requestInitSegment
  29. * @param {!Object.<string, !shaka.media.SegmentIndex>} segmentIndexMap
  30. * @param {boolean} isUpdate True if the manifest is being updated.
  31. * @param {number} segmentLimit The maximum number of segments to generate for
  32. * a SegmentTemplate with fixed duration.
  33. * @param {!Object.<string, number>} periodDurationMap
  34. * @return {shaka.dash.DashParser.StreamInfo}
  35. */
  36. static createStreamInfo(
  37. context, requestInitSegment, segmentIndexMap, isUpdate,
  38. segmentLimit, periodDurationMap) {
  39. goog.asserts.assert(context.representation.segmentTemplate,
  40. 'Should only be called with SegmentTemplate');
  41. const SegmentTemplate = shaka.dash.SegmentTemplate;
  42. const initSegmentReference = SegmentTemplate.createInitSegment_(context);
  43. const info = SegmentTemplate.parseSegmentTemplateInfo_(context);
  44. SegmentTemplate.checkSegmentTemplateInfo_(context, info);
  45. // Direct fields of context will be reassigned by the parser before
  46. // generateSegmentIndex is called. So we must make a shallow copy first,
  47. // and use that in the generateSegmentIndex callbacks.
  48. const shallowCopyOfContext =
  49. shaka.util.ObjectUtils.shallowCloneObject(context);
  50. if (info.indexTemplate) {
  51. shaka.dash.SegmentBase.checkSegmentIndexSupport(
  52. context, initSegmentReference);
  53. return {
  54. generateSegmentIndex: () => {
  55. return SegmentTemplate.generateSegmentIndexFromIndexTemplate_(
  56. shallowCopyOfContext, requestInitSegment, initSegmentReference,
  57. info);
  58. },
  59. };
  60. } else if (info.segmentDuration) {
  61. if (!isUpdate && context.adaptationSet.contentType !== 'image') {
  62. context.presentationTimeline.notifyMaxSegmentDuration(
  63. info.segmentDuration);
  64. context.presentationTimeline.notifyMinSegmentStartTime(
  65. context.periodInfo.start);
  66. }
  67. return {
  68. generateSegmentIndex: () => {
  69. return SegmentTemplate.generateSegmentIndexFromDuration_(
  70. shallowCopyOfContext, info, segmentLimit, initSegmentReference,
  71. periodDurationMap);
  72. },
  73. };
  74. } else {
  75. /** @type {shaka.media.SegmentIndex} */
  76. let segmentIndex = null;
  77. let id = null;
  78. if (context.period.id && context.representation.id) {
  79. // Only check/store the index if period and representation IDs are set.
  80. id = context.period.id + ',' + context.representation.id;
  81. segmentIndex = segmentIndexMap[id];
  82. }
  83. const references = SegmentTemplate.createFromTimeline_(
  84. shallowCopyOfContext, info, initSegmentReference);
  85. const periodStart = context.periodInfo.start;
  86. const periodEnd = context.periodInfo.duration ?
  87. context.periodInfo.start + context.periodInfo.duration : Infinity;
  88. /* When to fit segments. All refactors should honor/update this table:
  89. *
  90. * | dynamic | infinite | last | should | notes |
  91. * | | period | period | fit | |
  92. * | ------- | -------- | ------ | ------ | ------------------------- |
  93. * | F | F | X | T | typical VOD |
  94. * | F | T | X | X | impossible: infinite VOD |
  95. * | T | F | F | T | typical live, old period |
  96. * | T | F | T | F | typical IPR |
  97. * | T | T | F | X | impossible: old, infinite |
  98. * | T | T | T | F | typical live, new period |
  99. */
  100. // We never fit the final period of dynamic content, which could be
  101. // infinite live (with no limit to fit to) or IPR (which would expand the
  102. // most recent segment to the end of the presentation).
  103. const shouldFit = !(context.dynamic && context.periodInfo.isLastPeriod);
  104. if (segmentIndex) {
  105. if (shouldFit) {
  106. // Fit the new references before merging them, so that the merge
  107. // algorithm has a more accurate view of their start and end times.
  108. const wrapper = new shaka.media.SegmentIndex(references);
  109. wrapper.fit(periodStart, periodEnd, /* isNew= */ true);
  110. }
  111. segmentIndex.mergeAndEvict(references,
  112. context.presentationTimeline.getSegmentAvailabilityStart());
  113. } else {
  114. segmentIndex = new shaka.media.SegmentIndex(references);
  115. if (id && context.dynamic) {
  116. segmentIndexMap[id] = segmentIndex;
  117. }
  118. }
  119. context.presentationTimeline.notifySegments(references);
  120. if (shouldFit) {
  121. segmentIndex.fit(periodStart, periodEnd);
  122. }
  123. return {
  124. generateSegmentIndex: () => Promise.resolve(segmentIndex),
  125. };
  126. }
  127. }
  128. /**
  129. * @param {?shaka.dash.DashParser.InheritanceFrame} frame
  130. * @return {Element}
  131. * @private
  132. */
  133. static fromInheritance_(frame) {
  134. return frame.segmentTemplate;
  135. }
  136. /**
  137. * Parses a SegmentTemplate element into an info object.
  138. *
  139. * @param {shaka.dash.DashParser.Context} context
  140. * @return {shaka.dash.SegmentTemplate.SegmentTemplateInfo}
  141. * @private
  142. */
  143. static parseSegmentTemplateInfo_(context) {
  144. const SegmentTemplate = shaka.dash.SegmentTemplate;
  145. const MpdUtils = shaka.dash.MpdUtils;
  146. const segmentInfo =
  147. MpdUtils.parseSegmentInfo(context, SegmentTemplate.fromInheritance_);
  148. const media = MpdUtils.inheritAttribute(
  149. context, SegmentTemplate.fromInheritance_, 'media');
  150. const index = MpdUtils.inheritAttribute(
  151. context, SegmentTemplate.fromInheritance_, 'index');
  152. return {
  153. segmentDuration: segmentInfo.segmentDuration,
  154. timescale: segmentInfo.timescale,
  155. startNumber: segmentInfo.startNumber,
  156. scaledPresentationTimeOffset: segmentInfo.scaledPresentationTimeOffset,
  157. unscaledPresentationTimeOffset:
  158. segmentInfo.unscaledPresentationTimeOffset,
  159. timeline: segmentInfo.timeline,
  160. mediaTemplate: media,
  161. indexTemplate: index,
  162. };
  163. }
  164. /**
  165. * Verifies a SegmentTemplate info object.
  166. *
  167. * @param {shaka.dash.DashParser.Context} context
  168. * @param {shaka.dash.SegmentTemplate.SegmentTemplateInfo} info
  169. * @private
  170. */
  171. static checkSegmentTemplateInfo_(context, info) {
  172. let n = 0;
  173. n += info.indexTemplate ? 1 : 0;
  174. n += info.timeline ? 1 : 0;
  175. n += info.segmentDuration ? 1 : 0;
  176. if (n == 0) {
  177. shaka.log.error(
  178. 'SegmentTemplate does not contain any segment information:',
  179. 'the SegmentTemplate must contain either an index URL template',
  180. 'a SegmentTimeline, or a segment duration.',
  181. context.representation);
  182. throw new shaka.util.Error(
  183. shaka.util.Error.Severity.CRITICAL,
  184. shaka.util.Error.Category.MANIFEST,
  185. shaka.util.Error.Code.DASH_NO_SEGMENT_INFO);
  186. } else if (n != 1) {
  187. shaka.log.warning(
  188. 'SegmentTemplate containes multiple segment information sources:',
  189. 'the SegmentTemplate should only contain an index URL template,',
  190. 'a SegmentTimeline or a segment duration.',
  191. context.representation);
  192. if (info.indexTemplate) {
  193. shaka.log.info('Using the index URL template by default.');
  194. info.timeline = null;
  195. info.segmentDuration = null;
  196. } else {
  197. goog.asserts.assert(info.timeline, 'There should be a timeline');
  198. shaka.log.info('Using the SegmentTimeline by default.');
  199. info.segmentDuration = null;
  200. }
  201. }
  202. if (!info.indexTemplate && !info.mediaTemplate) {
  203. shaka.log.error(
  204. 'SegmentTemplate does not contain sufficient segment information:',
  205. 'the SegmentTemplate\'s media URL template is missing.',
  206. context.representation);
  207. throw new shaka.util.Error(
  208. shaka.util.Error.Severity.CRITICAL,
  209. shaka.util.Error.Category.MANIFEST,
  210. shaka.util.Error.Code.DASH_NO_SEGMENT_INFO);
  211. }
  212. }
  213. /**
  214. * Generates a SegmentIndex from an index URL template.
  215. *
  216. * @param {shaka.dash.DashParser.Context} context
  217. * @param {shaka.dash.DashParser.RequestInitSegmentCallback}
  218. * requestInitSegment
  219. * @param {shaka.media.InitSegmentReference} init
  220. * @param {shaka.dash.SegmentTemplate.SegmentTemplateInfo} info
  221. * @return {!Promise.<shaka.media.SegmentIndex>}
  222. * @private
  223. */
  224. static generateSegmentIndexFromIndexTemplate_(
  225. context, requestInitSegment, init, info) {
  226. const MpdUtils = shaka.dash.MpdUtils;
  227. const ManifestParserUtils = shaka.util.ManifestParserUtils;
  228. goog.asserts.assert(info.indexTemplate, 'must be using index template');
  229. const filledTemplate = MpdUtils.fillUriTemplate(
  230. info.indexTemplate, context.representation.id,
  231. null, context.bandwidth || null, null);
  232. const resolvedUris = ManifestParserUtils.resolveUris(
  233. context.representation.baseUris, [filledTemplate]);
  234. return shaka.dash.SegmentBase.generateSegmentIndexFromUris(
  235. context, requestInitSegment, init, resolvedUris, 0, null,
  236. info.scaledPresentationTimeOffset);
  237. }
  238. /**
  239. * Generates a SegmentIndex from fixed-duration segments.
  240. *
  241. * @param {shaka.dash.DashParser.Context} context
  242. * @param {shaka.dash.SegmentTemplate.SegmentTemplateInfo} info
  243. * @param {number} segmentLimit The maximum number of segments to generate.
  244. * @param {shaka.media.InitSegmentReference} initSegmentReference
  245. * @param {!Object.<string, number>} periodDurationMap
  246. * @return {!Promise.<shaka.media.SegmentIndex>}
  247. * @private
  248. */
  249. static generateSegmentIndexFromDuration_(
  250. context, info, segmentLimit, initSegmentReference, periodDurationMap) {
  251. goog.asserts.assert(info.mediaTemplate,
  252. 'There should be a media template with duration');
  253. const MpdUtils = shaka.dash.MpdUtils;
  254. const ManifestParserUtils = shaka.util.ManifestParserUtils;
  255. const presentationTimeline = context.presentationTimeline;
  256. // Capture values that could change as the parsing context moves on to
  257. // other parts of the manifest.
  258. const periodStart = context.periodInfo.start;
  259. const periodId = context.period.id;
  260. const initialPeriodDuration = context.periodInfo.duration;
  261. // For multi-period live streams the period duration may not be known until
  262. // the following period appears in an updated manifest. periodDurationMap
  263. // provides the updated period duration.
  264. const getPeriodEnd = () => {
  265. const periodDuration =
  266. (periodId != null && periodDurationMap[periodId]) ||
  267. initialPeriodDuration;
  268. const periodEnd = periodDuration ?
  269. (periodStart + periodDuration) : Infinity;
  270. return periodEnd;
  271. };
  272. const segmentDuration = info.segmentDuration;
  273. goog.asserts.assert(
  274. segmentDuration != null, 'Segment duration must not be null!');
  275. const startNumber = info.startNumber;
  276. const timescale = info.timescale;
  277. const template = info.mediaTemplate;
  278. const bandwidth = context.bandwidth || null;
  279. const id = context.representation.id;
  280. const baseUris = context.representation.baseUris;
  281. const timestampOffset = periodStart - info.scaledPresentationTimeOffset;
  282. // Computes the range of presentation timestamps both within the period and
  283. // available. This is an intersection of the period range and the
  284. // availability window.
  285. const computeAvailablePeriodRange = () => {
  286. return [
  287. Math.max(
  288. presentationTimeline.getSegmentAvailabilityStart(),
  289. periodStart),
  290. Math.min(
  291. presentationTimeline.getSegmentAvailabilityEnd(),
  292. getPeriodEnd()),
  293. ];
  294. };
  295. // Computes the range of absolute positions both within the period and
  296. // available. The range is inclusive. These are the positions for which we
  297. // will generate segment references.
  298. const computeAvailablePositionRange = () => {
  299. // In presentation timestamps.
  300. const availablePresentationTimes = computeAvailablePeriodRange();
  301. goog.asserts.assert(availablePresentationTimes.every(isFinite),
  302. 'Available presentation times must be finite!');
  303. goog.asserts.assert(availablePresentationTimes.every((x) => x >= 0),
  304. 'Available presentation times must be positive!');
  305. goog.asserts.assert(segmentDuration != null,
  306. 'Segment duration must not be null!');
  307. // In period-relative timestamps.
  308. const availablePeriodTimes =
  309. availablePresentationTimes.map((x) => x - periodStart);
  310. // These may sometimes be reversed ([1] <= [0]) if the period is
  311. // completely unavailable. The logic will still work if this happens,
  312. // because we will simply generate no references.
  313. // In period-relative positions (0-based).
  314. const availablePeriodPositions = [
  315. Math.ceil(availablePeriodTimes[0] / segmentDuration),
  316. Math.ceil(availablePeriodTimes[1] / segmentDuration) - 1,
  317. ];
  318. // In absolute positions.
  319. const availablePresentationPositions =
  320. availablePeriodPositions.map((x) => x + startNumber);
  321. return availablePresentationPositions;
  322. };
  323. // For Live, we must limit the initial SegmentIndex in size, to avoid
  324. // consuming too much CPU or memory for content with gigantic
  325. // timeShiftBufferDepth (which can have values up to and including
  326. // Infinity).
  327. const range = computeAvailablePositionRange();
  328. const minPosition = context.dynamic ?
  329. Math.max(range[0], range[1] - segmentLimit + 1) :
  330. range[0];
  331. const maxPosition = range[1];
  332. const references = [];
  333. const createReference = (position) => {
  334. // These inner variables are all scoped to the inner loop, and can be used
  335. // safely in the callback below.
  336. goog.asserts.assert(segmentDuration != null,
  337. 'Segment duration must not be null!');
  338. // Relative to the period start.
  339. const positionWithinPeriod = position - startNumber;
  340. const segmentPeriodTime = positionWithinPeriod * segmentDuration;
  341. // What will appear in the actual segment files. The media timestamp is
  342. // what is expected in the $Time$ template.
  343. const segmentMediaTime = segmentPeriodTime +
  344. info.scaledPresentationTimeOffset;
  345. const getUris = () => {
  346. const mediaUri = MpdUtils.fillUriTemplate(
  347. template, id, position, bandwidth,
  348. segmentMediaTime * timescale);
  349. return ManifestParserUtils.resolveUris(baseUris, [mediaUri]);
  350. };
  351. // Relative to the presentation.
  352. const segmentStart = segmentPeriodTime + periodStart;
  353. const trueSegmentEnd = segmentStart + segmentDuration;
  354. // Cap the segment end at the period end so that references from the
  355. // next period will fit neatly after it.
  356. const segmentEnd = Math.min(trueSegmentEnd, getPeriodEnd());
  357. // This condition will be true unless the segmentStart was >= periodEnd.
  358. // If we've done the position calculations correctly, this won't happen.
  359. goog.asserts.assert(segmentStart < segmentEnd,
  360. 'Generated a segment outside of the period!');
  361. const ref = new shaka.media.SegmentReference(
  362. segmentStart,
  363. segmentEnd,
  364. getUris,
  365. /* startByte= */ 0,
  366. /* endByte= */ null,
  367. initSegmentReference,
  368. timestampOffset,
  369. /* appendWindowStart= */ periodStart,
  370. /* appendWindowEnd= */ getPeriodEnd());
  371. // This is necessary information for thumbnail streams:
  372. ref.trueEndTime = trueSegmentEnd;
  373. return ref;
  374. };
  375. for (let position = minPosition; position <= maxPosition; ++position) {
  376. const reference = createReference(position);
  377. references.push(reference);
  378. }
  379. /** @type {shaka.media.SegmentIndex} */
  380. const segmentIndex = new shaka.media.SegmentIndex(references);
  381. // If the availability timeline currently ends before the period, we will
  382. // need to add references over time.
  383. const willNeedToAddReferences =
  384. presentationTimeline.getSegmentAvailabilityEnd() < getPeriodEnd();
  385. // When we start a live stream with a period that ends within the
  386. // availability window we will not need to add more references, but we will
  387. // need to evict old references.
  388. const willNeedToEvictReferences = presentationTimeline.isLive();
  389. if (willNeedToAddReferences || willNeedToEvictReferences) {
  390. // The period continues to get longer over time, so check for new
  391. // references once every |segmentDuration| seconds.
  392. // We clamp to |minPosition| in case the initial range was reversed and no
  393. // references were generated. Otherwise, the update would start creating
  394. // negative positions for segments in periods which begin in the future.
  395. let nextPosition = Math.max(minPosition, maxPosition + 1);
  396. segmentIndex.updateEvery(segmentDuration, () => {
  397. // Evict any references outside the window.
  398. const availabilityStartTime =
  399. presentationTimeline.getSegmentAvailabilityStart();
  400. segmentIndex.evict(availabilityStartTime);
  401. // Compute any new references that need to be added.
  402. const [_, maxPosition] = computeAvailablePositionRange();
  403. const references = [];
  404. while (nextPosition <= maxPosition) {
  405. const reference = createReference(nextPosition);
  406. references.push(reference);
  407. nextPosition++;
  408. }
  409. // The timer must continue firing until the entire period is
  410. // unavailable, so that all references will be evicted.
  411. if (availabilityStartTime > getPeriodEnd() && !references.length) {
  412. // Signal stop.
  413. return null;
  414. }
  415. return references;
  416. });
  417. }
  418. return Promise.resolve(segmentIndex);
  419. }
  420. /**
  421. * Creates segment references from a timeline.
  422. *
  423. * @param {shaka.dash.DashParser.Context} context
  424. * @param {shaka.dash.SegmentTemplate.SegmentTemplateInfo} info
  425. * @param {shaka.media.InitSegmentReference} initSegmentReference
  426. * @return {!Array.<!shaka.media.SegmentReference>}
  427. * @private
  428. */
  429. static createFromTimeline_(context, info, initSegmentReference) {
  430. const MpdUtils = shaka.dash.MpdUtils;
  431. const ManifestParserUtils = shaka.util.ManifestParserUtils;
  432. const periodStart = context.periodInfo.start;
  433. const periodDuration = context.periodInfo.duration;
  434. const timestampOffset = periodStart - info.scaledPresentationTimeOffset;
  435. const appendWindowStart = periodStart;
  436. const appendWindowEnd = periodDuration ?
  437. periodStart + periodDuration : Infinity;
  438. /** @type {!Array.<!shaka.media.SegmentReference>} */
  439. const references = [];
  440. for (let i = 0; i < info.timeline.length; i++) {
  441. const {start, unscaledStart, end} = info.timeline[i];
  442. // Note: i = k - 1, where k indicates the k'th segment listed in the MPD.
  443. // (See section 5.3.9.5.3 of the DASH spec.)
  444. const segmentReplacement = i + info.startNumber;
  445. // Consider the presentation time offset in segment uri computation
  446. const timeReplacement = unscaledStart +
  447. info.unscaledPresentationTimeOffset;
  448. const repId = context.representation.id;
  449. const bandwidth = context.bandwidth || null;
  450. const mediaTemplate = info.mediaTemplate;
  451. const baseUris = context.representation.baseUris;
  452. // This callback must not capture any non-local
  453. // variables, such as info, context, etc. Make
  454. // sure any values you reference here have
  455. // been assigned to local variables within the
  456. // loop, or else we will end up with a leak.
  457. const createUris =
  458. () => {
  459. goog.asserts.assert(
  460. mediaTemplate,
  461. 'There should be a media template with a timeline');
  462. const mediaUri = MpdUtils.fillUriTemplate(
  463. mediaTemplate, repId,
  464. segmentReplacement, bandwidth || null, timeReplacement);
  465. return ManifestParserUtils
  466. .resolveUris(baseUris, [mediaUri])
  467. .map((g) => {
  468. return g.toString();
  469. });
  470. };
  471. references.push(new shaka.media.SegmentReference(
  472. periodStart + start,
  473. periodStart + end,
  474. createUris,
  475. /* startByte= */ 0,
  476. /* endByte= */ null,
  477. initSegmentReference,
  478. timestampOffset,
  479. appendWindowStart,
  480. appendWindowEnd));
  481. }
  482. return references;
  483. }
  484. /**
  485. * Creates an init segment reference from a context object.
  486. *
  487. * @param {shaka.dash.DashParser.Context} context
  488. * @return {shaka.media.InitSegmentReference}
  489. * @private
  490. */
  491. static createInitSegment_(context) {
  492. const MpdUtils = shaka.dash.MpdUtils;
  493. const ManifestParserUtils = shaka.util.ManifestParserUtils;
  494. const SegmentTemplate = shaka.dash.SegmentTemplate;
  495. const initialization = MpdUtils.inheritAttribute(
  496. context, SegmentTemplate.fromInheritance_, 'initialization');
  497. if (!initialization) {
  498. return null;
  499. }
  500. const repId = context.representation.id;
  501. const bandwidth = context.bandwidth || null;
  502. const baseUris = context.representation.baseUris;
  503. const getUris = () => {
  504. goog.asserts.assert(initialization, 'Should have returned earler');
  505. const filledTemplate = MpdUtils.fillUriTemplate(
  506. initialization, repId, null, bandwidth, null);
  507. const resolvedUris = ManifestParserUtils.resolveUris(
  508. baseUris, [filledTemplate]);
  509. return resolvedUris;
  510. };
  511. return new shaka.media.InitSegmentReference(getUris, 0, null);
  512. }
  513. };
  514. /**
  515. * @typedef {{
  516. * timescale: number,
  517. * segmentDuration: ?number,
  518. * startNumber: number,
  519. * scaledPresentationTimeOffset: number,
  520. * unscaledPresentationTimeOffset: number,
  521. * timeline: Array.<shaka.dash.MpdUtils.TimeRange>,
  522. * mediaTemplate: ?string,
  523. * indexTemplate: ?string
  524. * }}
  525. * @private
  526. *
  527. * @description
  528. * Contains information about a SegmentTemplate.
  529. *
  530. * @property {number} timescale
  531. * The time-scale of the representation.
  532. * @property {?number} segmentDuration
  533. * The duration of the segments in seconds, if given.
  534. * @property {number} startNumber
  535. * The start number of the segments; 1 or greater.
  536. * @property {number} scaledPresentationTimeOffset
  537. * The presentation time offset of the representation, in seconds.
  538. * @property {number} unscaledPresentationTimeOffset
  539. * The presentation time offset of the representation, in timescale units.
  540. * @property {Array.<shaka.dash.MpdUtils.TimeRange>} timeline
  541. * The timeline of the representation, if given. Times in seconds.
  542. * @property {?string} mediaTemplate
  543. * The media URI template, if given.
  544. * @property {?string} indexTemplate
  545. * The index URI template, if given.
  546. */
  547. shaka.dash.SegmentTemplate.SegmentTemplateInfo;