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