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