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