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