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