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.IReleasable');
  16. goog.require('shaka.util.ManifestParserUtils');
  17. goog.require('shaka.util.ObjectUtils');
  18. goog.require('shaka.util.StringUtils');
  19. goog.require('shaka.util.TXml');
  20. goog.requireType('shaka.dash.DashParser');
  21. goog.requireType('shaka.media.PresentationTimeline');
  22. /**
  23. * @summary A set of functions for parsing SegmentTemplate elements.
  24. */
  25. shaka.dash.SegmentTemplate = class {
  26. /**
  27. * Creates a new StreamInfo object.
  28. * Updates the existing SegmentIndex, if any.
  29. *
  30. * @param {shaka.dash.DashParser.Context} context
  31. * @param {shaka.dash.DashParser.RequestSegmentCallback} requestSegment
  32. * @param {!Object.<string, !shaka.extern.Stream>} streamMap
  33. * @param {boolean} isUpdate True if the manifest is being updated.
  34. * @param {number} segmentLimit The maximum number of segments to generate for
  35. * a SegmentTemplate with fixed duration.
  36. * @param {!Object.<string, number>} periodDurationMap
  37. * @param {shaka.extern.aesKey|undefined} aesKey
  38. * @param {?number} lastSegmentNumber
  39. * @param {boolean} isPatchUpdate
  40. * @return {shaka.dash.DashParser.StreamInfo}
  41. */
  42. static createStreamInfo(
  43. context, requestSegment, streamMap, isUpdate, segmentLimit,
  44. periodDurationMap, aesKey, lastSegmentNumber, isPatchUpdate) {
  45. goog.asserts.assert(context.representation.segmentTemplate,
  46. 'Should only be called with SegmentTemplate ' +
  47. 'or segment info defined');
  48. const MpdUtils = shaka.dash.MpdUtils;
  49. const SegmentTemplate = shaka.dash.SegmentTemplate;
  50. const TimelineSegmentIndex = shaka.dash.TimelineSegmentIndex;
  51. if (!isPatchUpdate && !context.representation.initialization) {
  52. context.representation.initialization =
  53. MpdUtils.inheritAttribute(
  54. context, SegmentTemplate.fromInheritance_, 'initialization');
  55. }
  56. const initSegmentReference = context.representation.initialization ?
  57. SegmentTemplate.createInitSegment_(context, aesKey) : null;
  58. /** @type {shaka.dash.SegmentTemplate.SegmentTemplateInfo} */
  59. const info = SegmentTemplate.parseSegmentTemplateInfo_(context);
  60. SegmentTemplate.checkSegmentTemplateInfo_(context, info);
  61. // Direct fields of context will be reassigned by the parser before
  62. // generateSegmentIndex is called. So we must make a shallow copy first,
  63. // and use that in the generateSegmentIndex callbacks.
  64. const shallowCopyOfContext =
  65. shaka.util.ObjectUtils.shallowCloneObject(context);
  66. if (info.indexTemplate) {
  67. shaka.dash.SegmentBase.checkSegmentIndexSupport(
  68. context, initSegmentReference);
  69. return {
  70. generateSegmentIndex: () => {
  71. return SegmentTemplate.generateSegmentIndexFromIndexTemplate_(
  72. shallowCopyOfContext, requestSegment, initSegmentReference,
  73. info);
  74. },
  75. };
  76. } else if (info.segmentDuration) {
  77. if (!isUpdate &&
  78. context.adaptationSet.contentType !== 'image' &&
  79. context.adaptationSet.contentType !== 'text') {
  80. const periodStart = context.periodInfo.start;
  81. const periodId = context.period.id;
  82. const initialPeriodDuration = context.periodInfo.duration;
  83. const periodDuration =
  84. (periodId != null && periodDurationMap[periodId]) ||
  85. initialPeriodDuration;
  86. const periodEnd = periodDuration ?
  87. (periodStart + periodDuration) : Infinity;
  88. context.presentationTimeline.notifyMaxSegmentDuration(
  89. info.segmentDuration);
  90. context.presentationTimeline.notifyPeriodDuration(
  91. periodStart, periodEnd);
  92. }
  93. return {
  94. generateSegmentIndex: () => {
  95. return SegmentTemplate.generateSegmentIndexFromDuration_(
  96. shallowCopyOfContext, info, segmentLimit, initSegmentReference,
  97. periodDurationMap, aesKey, lastSegmentNumber,
  98. context.representation.segmentSequenceCadence);
  99. },
  100. };
  101. } else {
  102. /** @type {shaka.media.SegmentIndex} */
  103. let segmentIndex = null;
  104. let id = null;
  105. let stream = null;
  106. if (context.period.id && context.representation.id) {
  107. // Only check/store the index if period and representation IDs are set.
  108. id = context.period.id + ',' + context.representation.id;
  109. stream = streamMap[id];
  110. if (stream) {
  111. segmentIndex = stream.segmentIndex;
  112. }
  113. }
  114. const periodStart = context.periodInfo.start;
  115. const periodEnd = context.periodInfo.duration ? periodStart +
  116. context.periodInfo.duration : Infinity;
  117. shaka.log.debug(`New manifest ${periodStart} - ${periodEnd}`);
  118. /* When to fit segments. All refactors should honor/update this table:
  119. *
  120. * | dynamic | infinite | last | should | notes |
  121. * | | period | period | fit | |
  122. * | ------- | -------- | ------ | ------ | ------------------------- |
  123. * | F | F | X | T | typical VOD |
  124. * | F | T | X | X | impossible: infinite VOD |
  125. * | T | F | F | T | typical live, old period |
  126. * | T | F | T | F | typical IPR |
  127. * | T | T | F | X | impossible: old, infinite |
  128. * | T | T | T | F | typical live, new period |
  129. */
  130. // We never fit the final period of dynamic content, which could be
  131. // infinite live (with no limit to fit to) or IPR (which would expand the
  132. // most recent segment to the end of the presentation).
  133. const shouldFit = !(context.dynamic && context.periodInfo.isLastPeriod);
  134. if (!segmentIndex) {
  135. shaka.log.debug(`Creating TSI with end ${periodEnd}`);
  136. segmentIndex = new TimelineSegmentIndex(
  137. info,
  138. context.representation.id,
  139. context.bandwidth,
  140. context.representation.getBaseUris,
  141. context.urlParams,
  142. periodStart,
  143. periodEnd,
  144. initSegmentReference,
  145. shouldFit,
  146. aesKey,
  147. context.representation.segmentSequenceCadence,
  148. );
  149. } else {
  150. const tsi = /** @type {!TimelineSegmentIndex} */(segmentIndex);
  151. tsi.appendTemplateInfo(
  152. info, periodStart, periodEnd, shouldFit, initSegmentReference);
  153. const availabilityStart =
  154. context.presentationTimeline.getSegmentAvailabilityStart();
  155. tsi.evict(availabilityStart);
  156. }
  157. if (info.timeline &&
  158. context.adaptationSet.contentType !== 'image' &&
  159. context.adaptationSet.contentType !== 'text') {
  160. const timeline = info.timeline;
  161. context.presentationTimeline.notifyTimeRange(
  162. timeline,
  163. periodStart);
  164. }
  165. if (stream && context.dynamic) {
  166. stream.segmentIndex = segmentIndex;
  167. }
  168. return {
  169. generateSegmentIndex: () => {
  170. // If segmentIndex is deleted, or segmentIndex's references are
  171. // released by closeSegmentIndex(), we should set the value of
  172. // segmentIndex again.
  173. if (segmentIndex instanceof shaka.dash.TimelineSegmentIndex &&
  174. segmentIndex.isEmpty()) {
  175. segmentIndex.appendTemplateInfo(info, periodStart,
  176. periodEnd, shouldFit, initSegmentReference);
  177. }
  178. return Promise.resolve(segmentIndex);
  179. },
  180. };
  181. }
  182. }
  183. /**
  184. * Ingests Patch MPD segments into timeline.
  185. *
  186. * @param {!shaka.dash.DashParser.Context} context
  187. * @param {shaka.extern.xml.Node} patchNode
  188. */
  189. static modifyTimepoints(context, patchNode) {
  190. const MpdUtils = shaka.dash.MpdUtils;
  191. const SegmentTemplate = shaka.dash.SegmentTemplate;
  192. const TXml = shaka.util.TXml;
  193. const timelineNode = MpdUtils.inheritChild(context,
  194. SegmentTemplate.fromInheritance_, 'SegmentTimeline');
  195. goog.asserts.assert(timelineNode, 'timeline node not found');
  196. const timepoints = TXml.findChildren(timelineNode, 'S');
  197. goog.asserts.assert(timepoints, 'timepoints should exist');
  198. TXml.modifyNodes(timepoints, patchNode);
  199. timelineNode.children = timepoints;
  200. }
  201. /**
  202. * Removes all segments from timeline.
  203. *
  204. * @param {!shaka.dash.DashParser.Context} context
  205. */
  206. static removeTimepoints(context) {
  207. const MpdUtils = shaka.dash.MpdUtils;
  208. const SegmentTemplate = shaka.dash.SegmentTemplate;
  209. const timelineNode = MpdUtils.inheritChild(context,
  210. SegmentTemplate.fromInheritance_, 'SegmentTimeline');
  211. goog.asserts.assert(timelineNode, 'timeline node not found');
  212. timelineNode.children = [];
  213. }
  214. /**
  215. * @param {?shaka.dash.DashParser.InheritanceFrame} frame
  216. * @return {?shaka.extern.xml.Node}
  217. * @private
  218. */
  219. static fromInheritance_(frame) {
  220. return frame.segmentTemplate;
  221. }
  222. /**
  223. * Parses a SegmentTemplate element into an info object.
  224. *
  225. * @param {shaka.dash.DashParser.Context} context
  226. * @return {shaka.dash.SegmentTemplate.SegmentTemplateInfo}
  227. * @private
  228. */
  229. static parseSegmentTemplateInfo_(context) {
  230. const SegmentTemplate = shaka.dash.SegmentTemplate;
  231. const MpdUtils = shaka.dash.MpdUtils;
  232. const StringUtils = shaka.util.StringUtils;
  233. const segmentInfo =
  234. MpdUtils.parseSegmentInfo(context, SegmentTemplate.fromInheritance_);
  235. const media = MpdUtils.inheritAttribute(
  236. context, SegmentTemplate.fromInheritance_, 'media');
  237. const index = MpdUtils.inheritAttribute(
  238. context, SegmentTemplate.fromInheritance_, 'index');
  239. const k = MpdUtils.inheritAttribute(
  240. context, SegmentTemplate.fromInheritance_, 'k');
  241. let numChunks = 0;
  242. if (k) {
  243. numChunks = parseInt(k, 10);
  244. }
  245. return {
  246. segmentDuration: segmentInfo.segmentDuration,
  247. timescale: segmentInfo.timescale,
  248. startNumber: segmentInfo.startNumber,
  249. scaledPresentationTimeOffset: segmentInfo.scaledPresentationTimeOffset,
  250. unscaledPresentationTimeOffset:
  251. segmentInfo.unscaledPresentationTimeOffset,
  252. timeline: segmentInfo.timeline,
  253. mediaTemplate: media && StringUtils.htmlUnescape(media),
  254. indexTemplate: index,
  255. mimeType: context.representation.mimeType,
  256. codecs: context.representation.codecs,
  257. bandwidth: context.bandwidth,
  258. numChunks: numChunks,
  259. };
  260. }
  261. /**
  262. * Verifies a SegmentTemplate info object.
  263. *
  264. * @param {shaka.dash.DashParser.Context} context
  265. * @param {shaka.dash.SegmentTemplate.SegmentTemplateInfo} info
  266. * @private
  267. */
  268. static checkSegmentTemplateInfo_(context, info) {
  269. let n = 0;
  270. n += info.indexTemplate ? 1 : 0;
  271. n += info.timeline ? 1 : 0;
  272. n += info.segmentDuration ? 1 : 0;
  273. if (n == 0) {
  274. shaka.log.error(
  275. 'SegmentTemplate does not contain any segment information:',
  276. 'the SegmentTemplate must contain either an index URL template',
  277. 'a SegmentTimeline, or a segment duration.',
  278. context.representation);
  279. throw new shaka.util.Error(
  280. shaka.util.Error.Severity.CRITICAL,
  281. shaka.util.Error.Category.MANIFEST,
  282. shaka.util.Error.Code.DASH_NO_SEGMENT_INFO);
  283. } else if (n != 1) {
  284. shaka.log.warning(
  285. 'SegmentTemplate containes multiple segment information sources:',
  286. 'the SegmentTemplate should only contain an index URL template,',
  287. 'a SegmentTimeline or a segment duration.',
  288. context.representation);
  289. if (info.indexTemplate) {
  290. shaka.log.info('Using the index URL template by default.');
  291. info.timeline = null;
  292. info.segmentDuration = null;
  293. } else {
  294. goog.asserts.assert(info.timeline, 'There should be a timeline');
  295. shaka.log.info('Using the SegmentTimeline by default.');
  296. info.segmentDuration = null;
  297. }
  298. }
  299. if (!info.indexTemplate && !info.mediaTemplate) {
  300. shaka.log.error(
  301. 'SegmentTemplate does not contain sufficient segment information:',
  302. 'the SegmentTemplate\'s media URL template is missing.',
  303. context.representation);
  304. throw new shaka.util.Error(
  305. shaka.util.Error.Severity.CRITICAL,
  306. shaka.util.Error.Category.MANIFEST,
  307. shaka.util.Error.Code.DASH_NO_SEGMENT_INFO);
  308. }
  309. }
  310. /**
  311. * Generates a SegmentIndex from an index URL template.
  312. *
  313. * @param {shaka.dash.DashParser.Context} context
  314. * @param {shaka.dash.DashParser.RequestSegmentCallback} requestSegment
  315. * @param {shaka.media.InitSegmentReference} init
  316. * @param {shaka.dash.SegmentTemplate.SegmentTemplateInfo} info
  317. * @return {!Promise.<shaka.media.SegmentIndex>}
  318. * @private
  319. */
  320. static generateSegmentIndexFromIndexTemplate_(
  321. context, requestSegment, init, info) {
  322. const MpdUtils = shaka.dash.MpdUtils;
  323. const ManifestParserUtils = shaka.util.ManifestParserUtils;
  324. goog.asserts.assert(info.indexTemplate, 'must be using index template');
  325. const filledTemplate = MpdUtils.fillUriTemplate(
  326. info.indexTemplate, context.representation.id,
  327. null, null, context.bandwidth || null, null);
  328. const resolvedUris = ManifestParserUtils.resolveUris(
  329. context.representation.getBaseUris(), [filledTemplate]);
  330. return shaka.dash.SegmentBase.generateSegmentIndexFromUris(
  331. context, requestSegment, init, resolvedUris, 0, null,
  332. info.scaledPresentationTimeOffset);
  333. }
  334. /**
  335. * Generates a SegmentIndex from fixed-duration segments.
  336. *
  337. * @param {shaka.dash.DashParser.Context} context
  338. * @param {shaka.dash.SegmentTemplate.SegmentTemplateInfo} info
  339. * @param {number} segmentLimit The maximum number of segments to generate.
  340. * @param {shaka.media.InitSegmentReference} initSegmentReference
  341. * @param {!Object.<string, number>} periodDurationMap
  342. * @param {shaka.extern.aesKey|undefined} aesKey
  343. * @param {?number} lastSegmentNumber
  344. * @param {number} segmentSequenceCadence
  345. * @return {!Promise.<shaka.media.SegmentIndex>}
  346. * @private
  347. */
  348. static generateSegmentIndexFromDuration_(
  349. context, info, segmentLimit, initSegmentReference, periodDurationMap,
  350. aesKey, lastSegmentNumber, segmentSequenceCadence) {
  351. goog.asserts.assert(info.mediaTemplate,
  352. 'There should be a media template with duration');
  353. const MpdUtils = shaka.dash.MpdUtils;
  354. const ManifestParserUtils = shaka.util.ManifestParserUtils;
  355. const presentationTimeline = context.presentationTimeline;
  356. // Capture values that could change as the parsing context moves on to
  357. // other parts of the manifest.
  358. const periodStart = context.periodInfo.start;
  359. const periodId = context.period.id;
  360. const initialPeriodDuration = context.periodInfo.duration;
  361. // For multi-period live streams the period duration may not be known until
  362. // the following period appears in an updated manifest. periodDurationMap
  363. // provides the updated period duration.
  364. const getPeriodEnd = () => {
  365. const periodDuration =
  366. (periodId != null && periodDurationMap[periodId]) ||
  367. initialPeriodDuration;
  368. const periodEnd = periodDuration ?
  369. (periodStart + periodDuration) : Infinity;
  370. return periodEnd;
  371. };
  372. const segmentDuration = info.segmentDuration;
  373. goog.asserts.assert(
  374. segmentDuration != null, 'Segment duration must not be null!');
  375. const startNumber = info.startNumber;
  376. const timescale = info.timescale;
  377. const template = info.mediaTemplate;
  378. const bandwidth = context.bandwidth || null;
  379. const id = context.representation.id;
  380. const getBaseUris = context.representation.getBaseUris;
  381. const urlParams = context.urlParams;
  382. const timestampOffset = periodStart - info.scaledPresentationTimeOffset;
  383. // Computes the range of presentation timestamps both within the period and
  384. // available. This is an intersection of the period range and the
  385. // availability window.
  386. const computeAvailablePeriodRange = () => {
  387. return [
  388. Math.max(
  389. presentationTimeline.getSegmentAvailabilityStart(),
  390. periodStart),
  391. Math.min(
  392. presentationTimeline.getSegmentAvailabilityEnd(),
  393. getPeriodEnd()),
  394. ];
  395. };
  396. // Computes the range of absolute positions both within the period and
  397. // available. The range is inclusive. These are the positions for which we
  398. // will generate segment references.
  399. const computeAvailablePositionRange = () => {
  400. // In presentation timestamps.
  401. const availablePresentationTimes = computeAvailablePeriodRange();
  402. goog.asserts.assert(availablePresentationTimes.every(isFinite),
  403. 'Available presentation times must be finite!');
  404. goog.asserts.assert(availablePresentationTimes.every((x) => x >= 0),
  405. 'Available presentation times must be positive!');
  406. goog.asserts.assert(segmentDuration != null,
  407. 'Segment duration must not be null!');
  408. // In period-relative timestamps.
  409. const availablePeriodTimes =
  410. availablePresentationTimes.map((x) => x - periodStart);
  411. // These may sometimes be reversed ([1] <= [0]) if the period is
  412. // completely unavailable. The logic will still work if this happens,
  413. // because we will simply generate no references.
  414. // In period-relative positions (0-based).
  415. const availablePeriodPositions = [
  416. Math.ceil(availablePeriodTimes[0] / segmentDuration),
  417. Math.ceil(availablePeriodTimes[1] / segmentDuration) - 1,
  418. ];
  419. // For Low Latency we can request the partial current position.
  420. if (context.representation.availabilityTimeOffset) {
  421. availablePeriodPositions[1]++;
  422. }
  423. // In absolute positions.
  424. const availablePresentationPositions =
  425. availablePeriodPositions.map((x) => x + startNumber);
  426. return availablePresentationPositions;
  427. };
  428. // For Live, we must limit the initial SegmentIndex in size, to avoid
  429. // consuming too much CPU or memory for content with gigantic
  430. // timeShiftBufferDepth (which can have values up to and including
  431. // Infinity).
  432. const range = computeAvailablePositionRange();
  433. const minPosition = context.dynamic ?
  434. Math.max(range[0], range[1] - segmentLimit + 1) :
  435. range[0];
  436. const maxPosition = lastSegmentNumber || range[1];
  437. const references = [];
  438. const createReference = (position) => {
  439. // These inner variables are all scoped to the inner loop, and can be used
  440. // safely in the callback below.
  441. goog.asserts.assert(segmentDuration != null,
  442. 'Segment duration must not be null!');
  443. // Relative to the period start.
  444. const positionWithinPeriod = position - startNumber;
  445. const segmentPeriodTime = positionWithinPeriod * segmentDuration;
  446. // What will appear in the actual segment files. The media timestamp is
  447. // what is expected in the $Time$ template.
  448. const segmentMediaTime = segmentPeriodTime +
  449. info.scaledPresentationTimeOffset;
  450. // Relative to the presentation.
  451. const segmentStart = segmentPeriodTime + periodStart;
  452. const trueSegmentEnd = segmentStart + segmentDuration;
  453. // Cap the segment end at the period end so that references from the
  454. // next period will fit neatly after it.
  455. const segmentEnd = Math.min(trueSegmentEnd, getPeriodEnd());
  456. // This condition will be true unless the segmentStart was >= periodEnd.
  457. // If we've done the position calculations correctly, this won't happen.
  458. goog.asserts.assert(segmentStart < segmentEnd,
  459. 'Generated a segment outside of the period!');
  460. const partialSegmentRefs = [];
  461. const numChunks = info.numChunks;
  462. if (numChunks) {
  463. const partialDuration = (segmentEnd - segmentStart) / numChunks;
  464. for (let i = 0; i < numChunks; i++) {
  465. const start = segmentStart + partialDuration * i;
  466. const end = start + partialDuration;
  467. const subNumber = i + 1;
  468. const getPartialUris = () => {
  469. let time = segmentMediaTime * timescale;
  470. if ('BigInt' in window && time > Number.MAX_SAFE_INTEGER) {
  471. time = BigInt(segmentMediaTime) * BigInt(timescale);
  472. }
  473. const mediaUri = MpdUtils.fillUriTemplate(
  474. template, id, position, subNumber, bandwidth, time);
  475. return ManifestParserUtils.resolveUris(
  476. getBaseUris(), [mediaUri], urlParams());
  477. };
  478. const partial = new shaka.media.SegmentReference(
  479. start,
  480. end,
  481. getPartialUris,
  482. /* startByte= */ 0,
  483. /* endByte= */ null,
  484. initSegmentReference,
  485. timestampOffset,
  486. /* appendWindowStart= */ periodStart,
  487. /* appendWindowEnd= */ getPeriodEnd(),
  488. /* partialReferences= */ [],
  489. /* tilesLayout= */ '',
  490. /* tileDuration= */ null,
  491. /* syncTime= */ null,
  492. shaka.media.SegmentReference.Status.AVAILABLE,
  493. aesKey);
  494. partial.codecs = context.representation.codecs;
  495. partial.mimeType = context.representation.mimeType;
  496. if (segmentSequenceCadence == 0) {
  497. if (i > 0) {
  498. partial.markAsNonIndependent();
  499. }
  500. } else if ((i % segmentSequenceCadence) != 0) {
  501. partial.markAsNonIndependent();
  502. }
  503. partialSegmentRefs.push(partial);
  504. }
  505. }
  506. const getUris = () => {
  507. if (numChunks) {
  508. return [];
  509. }
  510. let time = segmentMediaTime * timescale;
  511. if ('BigInt' in window && time > Number.MAX_SAFE_INTEGER) {
  512. time = BigInt(segmentMediaTime) * BigInt(timescale);
  513. }
  514. const mediaUri = MpdUtils.fillUriTemplate(
  515. template, id, position, /* subNumber= */ null, bandwidth, time);
  516. return ManifestParserUtils.resolveUris(
  517. getBaseUris(), [mediaUri], urlParams());
  518. };
  519. const ref = new shaka.media.SegmentReference(
  520. segmentStart,
  521. segmentEnd,
  522. getUris,
  523. /* startByte= */ 0,
  524. /* endByte= */ null,
  525. initSegmentReference,
  526. timestampOffset,
  527. /* appendWindowStart= */ periodStart,
  528. /* appendWindowEnd= */ getPeriodEnd(),
  529. partialSegmentRefs,
  530. /* tilesLayout= */ '',
  531. /* tileDuration= */ null,
  532. /* syncTime= */ null,
  533. shaka.media.SegmentReference.Status.AVAILABLE,
  534. aesKey,
  535. partialSegmentRefs.length > 0);
  536. ref.codecs = context.representation.codecs;
  537. ref.mimeType = context.representation.mimeType;
  538. ref.bandwidth = context.bandwidth;
  539. // This is necessary information for thumbnail streams:
  540. ref.trueEndTime = trueSegmentEnd;
  541. return ref;
  542. };
  543. for (let position = minPosition; position <= maxPosition; ++position) {
  544. const reference = createReference(position);
  545. references.push(reference);
  546. }
  547. /** @type {shaka.media.SegmentIndex} */
  548. const segmentIndex = new shaka.media.SegmentIndex(references);
  549. // If the availability timeline currently ends before the period, we will
  550. // need to add references over time.
  551. const willNeedToAddReferences =
  552. presentationTimeline.getSegmentAvailabilityEnd() < getPeriodEnd();
  553. // When we start a live stream with a period that ends within the
  554. // availability window we will not need to add more references, but we will
  555. // need to evict old references.
  556. const willNeedToEvictReferences = presentationTimeline.isLive();
  557. if (willNeedToAddReferences || willNeedToEvictReferences) {
  558. // The period continues to get longer over time, so check for new
  559. // references once every |segmentDuration| seconds.
  560. // We clamp to |minPosition| in case the initial range was reversed and no
  561. // references were generated. Otherwise, the update would start creating
  562. // negative positions for segments in periods which begin in the future.
  563. let nextPosition = Math.max(minPosition, maxPosition + 1);
  564. let updateTime = segmentDuration;
  565. // For low latency we need to evict very frequently.
  566. if (context.representation.availabilityTimeOffset) {
  567. updateTime = 0.1;
  568. }
  569. segmentIndex.updateEvery(updateTime, () => {
  570. // Evict any references outside the window.
  571. const availabilityStartTime =
  572. presentationTimeline.getSegmentAvailabilityStart();
  573. segmentIndex.evict(availabilityStartTime);
  574. // Compute any new references that need to be added.
  575. const [_, maxPosition] = computeAvailablePositionRange();
  576. const references = [];
  577. while (nextPosition <= maxPosition) {
  578. const reference = createReference(nextPosition);
  579. references.push(reference);
  580. nextPosition++;
  581. }
  582. // The timer must continue firing until the entire period is
  583. // unavailable, so that all references will be evicted.
  584. if (availabilityStartTime > getPeriodEnd() && !references.length) {
  585. // Signal stop.
  586. return null;
  587. }
  588. return references;
  589. });
  590. }
  591. return Promise.resolve(segmentIndex);
  592. }
  593. /**
  594. * Creates an init segment reference from a context object.
  595. *
  596. * @param {shaka.dash.DashParser.Context} context
  597. * @param {shaka.extern.aesKey|undefined} aesKey
  598. * @return {shaka.media.InitSegmentReference}
  599. * @private
  600. */
  601. static createInitSegment_(context, aesKey) {
  602. const MpdUtils = shaka.dash.MpdUtils;
  603. const ManifestParserUtils = shaka.util.ManifestParserUtils;
  604. const SegmentTemplate = shaka.dash.SegmentTemplate;
  605. let initialization = context.representation.initialization;
  606. if (!initialization) {
  607. initialization = MpdUtils.inheritAttribute(
  608. context, SegmentTemplate.fromInheritance_, 'initialization');
  609. }
  610. if (!initialization) {
  611. return null;
  612. }
  613. initialization = shaka.util.StringUtils.htmlUnescape(initialization);
  614. const repId = context.representation.id;
  615. const bandwidth = context.bandwidth || null;
  616. const getBaseUris = context.representation.getBaseUris;
  617. const urlParams = context.urlParams;
  618. const getUris = () => {
  619. goog.asserts.assert(initialization, 'Should have returned earler');
  620. const filledTemplate = MpdUtils.fillUriTemplate(
  621. initialization, repId, null, null, bandwidth, null);
  622. const resolvedUris = ManifestParserUtils.resolveUris(
  623. getBaseUris(), [filledTemplate], urlParams());
  624. return resolvedUris;
  625. };
  626. const qualityInfo = shaka.dash.SegmentBase.createQualityInfo(context);
  627. const ref = new shaka.media.InitSegmentReference(
  628. getUris,
  629. /* startByte= */ 0,
  630. /* endByte= */ null,
  631. qualityInfo,
  632. /* timescale= */ null,
  633. /* segmentData= */ null,
  634. aesKey);
  635. ref.codecs = context.representation.codecs;
  636. ref.mimeType = context.representation.mimeType;
  637. return ref;
  638. }
  639. };
  640. /**
  641. * A SegmentIndex that returns segments references on demand from
  642. * a segment timeline.
  643. *
  644. * @extends shaka.media.SegmentIndex
  645. * @implements {shaka.util.IReleasable}
  646. * @implements {Iterable.<!shaka.media.SegmentReference>}
  647. *
  648. * @private
  649. *
  650. */
  651. shaka.dash.TimelineSegmentIndex = class extends shaka.media.SegmentIndex {
  652. /**
  653. *
  654. * @param {!shaka.dash.SegmentTemplate.SegmentTemplateInfo} templateInfo
  655. * @param {?string} representationId
  656. * @param {number} bandwidth
  657. * @param {function():Array.<string>} getBaseUris
  658. * @param {function():string} urlParams
  659. * @param {number} periodStart
  660. * @param {number} periodEnd
  661. * @param {shaka.media.InitSegmentReference} initSegmentReference
  662. * @param {boolean} shouldFit
  663. * @param {shaka.extern.aesKey|undefined} aesKey
  664. * @param {number} segmentSequenceCadence
  665. */
  666. constructor(templateInfo, representationId, bandwidth, getBaseUris,
  667. urlParams, periodStart, periodEnd, initSegmentReference, shouldFit,
  668. aesKey, segmentSequenceCadence) {
  669. super([]);
  670. /** @private {?shaka.dash.SegmentTemplate.SegmentTemplateInfo} */
  671. this.templateInfo_ = templateInfo;
  672. /** @private {?string} */
  673. this.representationId_ = representationId;
  674. /** @private {number} */
  675. this.bandwidth_ = bandwidth;
  676. /** @private {function():Array.<string>} */
  677. this.getBaseUris_ = getBaseUris;
  678. /** @private {function():string} */
  679. this.urlParams_ = urlParams;
  680. /** @private {number} */
  681. this.periodStart_ = periodStart;
  682. /** @private {number} */
  683. this.periodEnd_ = periodEnd;
  684. /** @private {shaka.media.InitSegmentReference} */
  685. this.initSegmentReference_ = initSegmentReference;
  686. /** @private {shaka.extern.aesKey|undefined} */
  687. this.aesKey_ = aesKey;
  688. /** @private {number} */
  689. this.segmentSequenceCadence_ = segmentSequenceCadence;
  690. if (shouldFit) {
  691. this.fitTimeline();
  692. }
  693. }
  694. /**
  695. * @override
  696. */
  697. getNumReferences() {
  698. if (this.templateInfo_) {
  699. return this.templateInfo_.timeline.length;
  700. } else {
  701. return 0;
  702. }
  703. }
  704. /**
  705. * @override
  706. */
  707. release() {
  708. super.release();
  709. this.templateInfo_ = null;
  710. // We cannot release other fields, as segment index can
  711. // be recreated using only template info.
  712. }
  713. /**
  714. * @override
  715. */
  716. evict(time) {
  717. if (!this.templateInfo_) {
  718. return;
  719. }
  720. shaka.log.debug(`${this.representationId_} Evicting at ${time}`);
  721. let numToEvict = 0;
  722. const timeline = this.templateInfo_.timeline;
  723. for (let i = 0; i < timeline.length; i += 1) {
  724. const range = timeline[i];
  725. const end = range.end + this.periodStart_;
  726. const start = range.start + this.periodStart_;
  727. if (end <= time) {
  728. shaka.log.debug(`Evicting ${start} - ${end}`);
  729. numToEvict += 1;
  730. } else {
  731. break;
  732. }
  733. }
  734. if (numToEvict > 0) {
  735. this.templateInfo_.timeline = timeline.slice(numToEvict);
  736. if (this.references.length >= numToEvict) {
  737. this.references = this.references.slice(numToEvict);
  738. }
  739. this.numEvicted_ += numToEvict;
  740. if (this.getNumReferences() === 0) {
  741. this.release();
  742. }
  743. }
  744. }
  745. /**
  746. * Merge new template info
  747. * @param {shaka.dash.SegmentTemplate.SegmentTemplateInfo} info
  748. * @param {number} periodStart
  749. * @param {number} periodEnd
  750. * @param {boolean} shouldFit
  751. * @param {shaka.media.InitSegmentReference} initSegmentReference
  752. */
  753. appendTemplateInfo(info, periodStart, periodEnd, shouldFit,
  754. initSegmentReference) {
  755. this.updateInitSegmentReference(initSegmentReference);
  756. if (!this.templateInfo_) {
  757. this.templateInfo_ = info;
  758. this.periodStart_ = periodStart;
  759. this.periodEnd_ = periodEnd;
  760. } else {
  761. const currentTimeline = this.templateInfo_.timeline;
  762. if (this.templateInfo_.mediaTemplate !== info.mediaTemplate) {
  763. this.templateInfo_.mediaTemplate = info.mediaTemplate;
  764. }
  765. // Append timeline
  766. let newEntries;
  767. if (currentTimeline.length) {
  768. const lastCurrentEntry = currentTimeline[currentTimeline.length - 1];
  769. newEntries = info.timeline.filter((entry) => {
  770. return entry.end > lastCurrentEntry.end;
  771. });
  772. } else {
  773. newEntries = info.timeline.slice();
  774. }
  775. if (newEntries.length > 0) {
  776. shaka.log.debug(`Appending ${newEntries.length} entries`);
  777. this.templateInfo_.timeline.push(...newEntries);
  778. }
  779. if (this.periodEnd_ !== periodEnd) {
  780. this.periodEnd_ = periodEnd;
  781. }
  782. }
  783. if (shouldFit) {
  784. this.fitTimeline();
  785. }
  786. }
  787. /**
  788. * Updates the init segment reference and propagates the update to all
  789. * references.
  790. * @param {shaka.media.InitSegmentReference} initSegmentReference
  791. */
  792. updateInitSegmentReference(initSegmentReference) {
  793. if (this.initSegmentReference_ === initSegmentReference) {
  794. return;
  795. }
  796. this.initSegmentReference_ = initSegmentReference;
  797. for (const reference of this.references) {
  798. if (reference) {
  799. reference.updateInitSegmentReference(initSegmentReference);
  800. }
  801. }
  802. }
  803. /**
  804. *
  805. * @param {number} time
  806. */
  807. isBeforeFirstEntry(time) {
  808. const hasTimeline = this.templateInfo_ &&
  809. this.templateInfo_.timeline && this.templateInfo_.timeline.length;
  810. if (hasTimeline) {
  811. const timeline = this.templateInfo_.timeline;
  812. return time < timeline[0].start + this.periodStart_;
  813. } else {
  814. return false;
  815. }
  816. }
  817. /**
  818. * Fit timeline entries to period boundaries
  819. */
  820. fitTimeline() {
  821. if (this.getIsImmutable()) {
  822. return;
  823. }
  824. const timeline = this.templateInfo_.timeline;
  825. while (timeline.length) {
  826. const lastTimePeriod = timeline[timeline.length - 1];
  827. if (lastTimePeriod.start >= this.periodEnd_) {
  828. timeline.pop();
  829. } else {
  830. break;
  831. }
  832. }
  833. this.evict(this.periodStart_);
  834. // Do NOT adjust last range to match period end! With high precision
  835. // timestamps several recalculations may give wrong results on less precise
  836. // platforms. To mitigate that, we're using cached |periodEnd_| value in
  837. // find/get() methods whenever possible.
  838. }
  839. /**
  840. * @override
  841. */
  842. find(time) {
  843. shaka.log.debug(`Find ${time}`);
  844. if (this.isBeforeFirstEntry(time)) {
  845. return this.numEvicted_;
  846. }
  847. if (!this.templateInfo_) {
  848. return null;
  849. }
  850. const timeline = this.templateInfo_.timeline;
  851. // Early exit if the time isn't within this period
  852. if (time < this.periodStart_ || time >= this.periodEnd_) {
  853. return null;
  854. }
  855. const lastIndex = timeline.length - 1;
  856. for (let i = 0; i < timeline.length; i++) {
  857. const range = timeline[i];
  858. const start = range.start + this.periodStart_;
  859. // A rounding error can cause /time/ to equal e.endTime or fall in between
  860. // the references by a fraction of a second. To account for this, we use
  861. // the start of the next segment as /end/, unless this is the last
  862. // reference, in which case we use the period end as the /end/
  863. let end;
  864. if (i < lastIndex) {
  865. end = timeline[i + 1].start + this.periodStart_;
  866. } else if (this.periodEnd_ === Infinity) {
  867. end = range.end + this.periodStart_;
  868. } else {
  869. end = this.periodEnd_;
  870. }
  871. if ((time >= start) && (time < end)) {
  872. return i + this.numEvicted_;
  873. }
  874. }
  875. return null;
  876. }
  877. /**
  878. * @override
  879. */
  880. get(position) {
  881. const correctedPosition = position - this.numEvicted_;
  882. if (correctedPosition < 0 ||
  883. correctedPosition >= this.getNumReferences() || !this.templateInfo_) {
  884. return null;
  885. }
  886. let ref = this.references[correctedPosition];
  887. if (!ref) {
  888. const range = this.templateInfo_.timeline[correctedPosition];
  889. const segmentReplacement = range.segmentPosition;
  890. const timeReplacement = this.templateInfo_
  891. .unscaledPresentationTimeOffset + range.unscaledStart;
  892. const timestampOffset = this.periodStart_ -
  893. this.templateInfo_.scaledPresentationTimeOffset;
  894. const trueSegmentEnd = this.periodStart_ + range.end;
  895. let segmentEnd = trueSegmentEnd;
  896. if (correctedPosition === this.getNumReferences() - 1 &&
  897. this.periodEnd_ !== Infinity) {
  898. segmentEnd = this.periodEnd_;
  899. }
  900. const codecs = this.templateInfo_.codecs;
  901. const mimeType = this.templateInfo_.mimeType;
  902. const bandwidth = this.templateInfo_.bandwidth;
  903. const partialSegmentRefs = [];
  904. const partialDuration = (range.end - range.start) / range.partialSegments;
  905. for (let i = 0; i < range.partialSegments; i++) {
  906. const start = range.start + partialDuration * i;
  907. const end = start + partialDuration;
  908. const subNumber = i + 1;
  909. let uris = null;
  910. const getPartialUris = () => {
  911. if (!this.templateInfo_) {
  912. return [];
  913. }
  914. if (uris == null) {
  915. uris = shaka.dash.TimelineSegmentIndex.createUris_(
  916. this.templateInfo_.mediaTemplate,
  917. this.representationId_,
  918. segmentReplacement,
  919. this.bandwidth_,
  920. timeReplacement,
  921. subNumber,
  922. this.getBaseUris_,
  923. this.urlParams_);
  924. }
  925. return uris;
  926. };
  927. const partial = new shaka.media.SegmentReference(
  928. this.periodStart_ + start,
  929. this.periodStart_ + end,
  930. getPartialUris,
  931. /* startByte= */ 0,
  932. /* endByte= */ null,
  933. this.initSegmentReference_,
  934. timestampOffset,
  935. this.periodStart_,
  936. this.periodEnd_,
  937. /* partialReferences= */ [],
  938. /* tilesLayout= */ '',
  939. /* tileDuration= */ null,
  940. /* syncTime= */ null,
  941. shaka.media.SegmentReference.Status.AVAILABLE,
  942. this.aesKey_);
  943. partial.codecs = codecs;
  944. partial.mimeType = mimeType;
  945. partial.bandwidth = bandwidth;
  946. if (this.segmentSequenceCadence_ == 0) {
  947. if (i > 0) {
  948. partial.markAsNonIndependent();
  949. }
  950. } else if ((i % this.segmentSequenceCadence_) != 0) {
  951. partial.markAsNonIndependent();
  952. }
  953. partialSegmentRefs.push(partial);
  954. }
  955. const createUrisCb = () => {
  956. if (range.partialSegments > 0 || !this.templateInfo_) {
  957. return [];
  958. }
  959. return shaka.dash.TimelineSegmentIndex
  960. .createUris_(
  961. this.templateInfo_.mediaTemplate,
  962. this.representationId_,
  963. segmentReplacement,
  964. this.bandwidth_,
  965. timeReplacement,
  966. /* subNumber= */ null,
  967. this.getBaseUris_,
  968. this.urlParams_,
  969. );
  970. };
  971. ref = new shaka.media.SegmentReference(
  972. this.periodStart_ + range.start,
  973. segmentEnd,
  974. createUrisCb,
  975. /* startByte= */ 0,
  976. /* endByte= */ null,
  977. this.initSegmentReference_,
  978. timestampOffset,
  979. this.periodStart_,
  980. this.periodEnd_,
  981. partialSegmentRefs,
  982. /* tilesLayout= */ '',
  983. /* tileDuration= */ null,
  984. /* syncTime= */ null,
  985. shaka.media.SegmentReference.Status.AVAILABLE,
  986. this.aesKey_,
  987. /* allPartialSegments= */ range.partialSegments > 0);
  988. ref.codecs = codecs;
  989. ref.mimeType = mimeType;
  990. ref.trueEndTime = trueSegmentEnd;
  991. ref.bandwidth = bandwidth;
  992. this.references[correctedPosition] = ref;
  993. }
  994. return ref;
  995. }
  996. /**
  997. * @override
  998. */
  999. forEachTopLevelReference(fn) {
  1000. this.fitTimeline();
  1001. for (let i = 0; i < this.getNumReferences(); i++) {
  1002. const reference = this.get(i + this.numEvicted_);
  1003. if (reference) {
  1004. fn(reference);
  1005. }
  1006. }
  1007. }
  1008. /**
  1009. * Fill in a specific template with values to get the segment uris
  1010. *
  1011. * @return {!Array.<string>}
  1012. * @private
  1013. */
  1014. static createUris_(mediaTemplate, repId, segmentReplacement,
  1015. bandwidth, timeReplacement, subNumber, getBaseUris, urlParams) {
  1016. const mediaUri = shaka.dash.MpdUtils.fillUriTemplate(
  1017. mediaTemplate, repId,
  1018. segmentReplacement, subNumber, bandwidth || null, timeReplacement);
  1019. return shaka.util.ManifestParserUtils
  1020. .resolveUris(getBaseUris(), [mediaUri], urlParams())
  1021. .map((g) => {
  1022. return g.toString();
  1023. });
  1024. }
  1025. };
  1026. /**
  1027. * @typedef {{
  1028. * timescale: number,
  1029. * segmentDuration: ?number,
  1030. * startNumber: number,
  1031. * scaledPresentationTimeOffset: number,
  1032. * unscaledPresentationTimeOffset: number,
  1033. * timeline: Array.<shaka.media.PresentationTimeline.TimeRange>,
  1034. * mediaTemplate: ?string,
  1035. * indexTemplate: ?string,
  1036. * mimeType: string,
  1037. * codecs: string,
  1038. * bandwidth: number,
  1039. * numChunks: number
  1040. * }}
  1041. *
  1042. * @description
  1043. * Contains information about a SegmentTemplate.
  1044. *
  1045. * @property {number} timescale
  1046. * The time-scale of the representation.
  1047. * @property {?number} segmentDuration
  1048. * The duration of the segments in seconds, if given.
  1049. * @property {number} startNumber
  1050. * The start number of the segments; 1 or greater.
  1051. * @property {number} scaledPresentationTimeOffset
  1052. * The presentation time offset of the representation, in seconds.
  1053. * @property {number} unscaledPresentationTimeOffset
  1054. * The presentation time offset of the representation, in timescale units.
  1055. * @property {Array.<shaka.media.PresentationTimeline.TimeRange>} timeline
  1056. * The timeline of the representation, if given. Times in seconds.
  1057. * @property {?string} mediaTemplate
  1058. * The media URI template, if given.
  1059. * @property {?string} indexTemplate
  1060. * The index URI template, if given.
  1061. * @property {string} mimeType
  1062. * The mimeType.
  1063. * @property {string} codecs
  1064. * The codecs.
  1065. * @property {number} bandwidth
  1066. * The bandwidth.
  1067. * @property {number} numChunks
  1068. * The number of chunks in each segment.
  1069. */
  1070. shaka.dash.SegmentTemplate.SegmentTemplateInfo;