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