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