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