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