Source: lib/media/segment_index.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.media.MetaSegmentIndex');
  7. goog.provide('shaka.media.SegmentIndex');
  8. goog.provide('shaka.media.SegmentIterator');
  9. goog.require('goog.asserts');
  10. goog.require('shaka.media.SegmentReference');
  11. goog.require('shaka.util.IReleasable');
  12. goog.require('shaka.util.Timer');
  13. /**
  14. * SegmentIndex.
  15. *
  16. * @implements {shaka.util.IReleasable}
  17. * @implements {Iterable.<!shaka.media.SegmentReference>}
  18. * @export
  19. */
  20. shaka.media.SegmentIndex = class {
  21. /**
  22. * @param {!Array.<!shaka.media.SegmentReference>} references The list of
  23. * SegmentReferences, which must be sorted first by their start times
  24. * (ascending) and second by their end times (ascending).
  25. */
  26. constructor(references) {
  27. if (goog.DEBUG) {
  28. shaka.media.SegmentIndex.assertCorrectReferences_(references);
  29. }
  30. /** @protected {!Array.<!shaka.media.SegmentReference>} */
  31. this.references = references;
  32. /** @private {shaka.util.Timer} */
  33. this.timer_ = null;
  34. /**
  35. * The number of references that have been removed from the front of the
  36. * array. Used to create stable positions in the find/get APIs.
  37. *
  38. * @protected {number}
  39. */
  40. this.numEvicted = 0;
  41. /** @private {boolean} */
  42. this.immutable_ = false;
  43. }
  44. /**
  45. * @override
  46. * @export
  47. */
  48. release() {
  49. if (this.immutable_) {
  50. return;
  51. }
  52. this.references = [];
  53. if (this.timer_) {
  54. this.timer_.stop();
  55. }
  56. this.timer_ = null;
  57. }
  58. /**
  59. * Marks the index as immutable. Segments cannot be added or removed after
  60. * this point. This doesn't affect the references themselves. This also
  61. * makes the destroy/release methods do nothing.
  62. *
  63. * This is mainly for testing.
  64. *
  65. * @export
  66. */
  67. markImmutable() {
  68. this.immutable_ = true;
  69. }
  70. /**
  71. * Iterates over all top-level segment references in this segment index.
  72. * @param {function(!shaka.media.SegmentReference)} fn
  73. */
  74. forEachTopLevelReference(fn) {
  75. for (const reference of this.references) {
  76. fn(reference);
  77. }
  78. }
  79. /**
  80. * Return the earliest reference, or null if empty.
  81. * @return {shaka.media.SegmentReference}
  82. */
  83. earliestReference() {
  84. return this.references[0] || null;
  85. }
  86. /**
  87. * Drop the first N references.
  88. * Used in early HLS synchronization, and does not count as eviction.
  89. * @param {number} n
  90. */
  91. dropFirstReferences(n) {
  92. this.references.splice(0, n);
  93. }
  94. /**
  95. * Finds the position of the segment for the given time, in seconds, relative
  96. * to the start of the presentation. Returns the position of the segment
  97. * with the largest end time if more than one segment is known for the given
  98. * time.
  99. *
  100. * @param {number} time
  101. * @return {?number} The position of the segment, or null if the position of
  102. * the segment could not be determined.
  103. * @export
  104. */
  105. find(time) {
  106. // For live streams, searching from the end is faster. For VOD, it balances
  107. // out either way. In both cases, references.length is small enough that
  108. // the difference isn't huge.
  109. const lastReferenceIndex = this.references.length - 1;
  110. for (let i = lastReferenceIndex; i >= 0; --i) {
  111. const r = this.references[i];
  112. const start = r.startTime;
  113. // A rounding error can cause /time/ to equal e.endTime or fall in between
  114. // the references by a fraction of a second. To account for this, we use
  115. // the start of the next segment as /end/, unless this is the last
  116. // reference, in which case we use its end time as /end/.
  117. const end = i < lastReferenceIndex ?
  118. this.references[i + 1].startTime : r.endTime;
  119. // Note that a segment ends immediately before the end time.
  120. if ((time >= start) && (time < end)) {
  121. return i + this.numEvicted;
  122. }
  123. }
  124. if (this.references.length && time < this.references[0].startTime) {
  125. return this.numEvicted;
  126. }
  127. return null;
  128. }
  129. /**
  130. * Gets the SegmentReference for the segment at the given position.
  131. *
  132. * @param {number} position The position of the segment as returned by find().
  133. * @return {shaka.media.SegmentReference} The SegmentReference, or null if
  134. * no such SegmentReference exists.
  135. * @export
  136. */
  137. get(position) {
  138. if (this.references.length == 0) {
  139. return null;
  140. }
  141. const index = position - this.numEvicted;
  142. if (index < 0 || index >= this.references.length) {
  143. return null;
  144. }
  145. return this.references[index];
  146. }
  147. /**
  148. * Offset all segment references by a fixed amount.
  149. *
  150. * @param {number} offset The amount to add to each segment's start and end
  151. * times.
  152. * @export
  153. */
  154. offset(offset) {
  155. if (!this.immutable_) {
  156. for (const ref of this.references) {
  157. ref.startTime += offset;
  158. ref.endTime += offset;
  159. ref.trueEndTime += offset;
  160. for (const partial of ref.partialReferences) {
  161. partial.startTime += offset;
  162. partial.endTime += offset;
  163. partial.trueEndTime += offset;
  164. }
  165. }
  166. }
  167. }
  168. /**
  169. * Merges the given SegmentReferences. Supports extending the original
  170. * references only. Will replace old references with equivalent new ones, and
  171. * keep any unique old ones.
  172. *
  173. * Used, for example, by the DASH and HLS parser, where manifests may not list
  174. * all available references, so we must keep available references in memory to
  175. * fill the availability window.
  176. *
  177. * @param {!Array.<!shaka.media.SegmentReference>} references The list of
  178. * SegmentReferences, which must be sorted first by their start times
  179. * (ascending) and second by their end times (ascending).
  180. */
  181. merge(references) {
  182. if (goog.DEBUG) {
  183. shaka.media.SegmentIndex.assertCorrectReferences_(references);
  184. }
  185. if (this.immutable_) {
  186. return;
  187. }
  188. if (!references.length) {
  189. return;
  190. }
  191. // Partial segments are used for live edge, and should be removed when they
  192. // get older. Remove the old SegmentReferences after the first new
  193. // reference's start time.
  194. this.references = this.references.filter((r) => {
  195. return r.startTime < references[0].startTime;
  196. });
  197. this.references.push(...references);
  198. if (goog.DEBUG) {
  199. shaka.media.SegmentIndex.assertCorrectReferences_(this.references);
  200. }
  201. }
  202. /**
  203. * Merges the given SegmentReferences and evicts the ones that end before the
  204. * given time. Supports extending the original references only.
  205. * Will not replace old references or interleave new ones.
  206. * Used, for example, by the DASH and HLS parser, where manifests may not list
  207. * all available references, so we must keep available references in memory to
  208. * fill the availability window.
  209. *
  210. * @param {!Array.<!shaka.media.SegmentReference>} references The list of
  211. * SegmentReferences, which must be sorted first by their start times
  212. * (ascending) and second by their end times (ascending).
  213. * @param {number} windowStart The start of the availability window to filter
  214. * out the references that are no longer available.
  215. * @export
  216. */
  217. mergeAndEvict(references, windowStart) {
  218. // Filter out the references that are no longer available to avoid
  219. // repeatedly evicting them and messing up eviction count.
  220. references = references.filter((r) => {
  221. return r.endTime > windowStart &&
  222. (this.references.length == 0 ||
  223. r.endTime > this.references[0].startTime);
  224. });
  225. const oldFirstRef = this.references[0];
  226. this.merge(references);
  227. const newFirstRef = this.references[0];
  228. if (oldFirstRef) {
  229. // We don't compare the actual ref, since the object could legitimately be
  230. // replaced with an equivalent. Even the URIs could change due to
  231. // load-balancing actions taken by the server. However, if the time
  232. // changes, its not an equivalent reference.
  233. goog.asserts.assert(oldFirstRef.startTime == newFirstRef.startTime,
  234. 'SegmentIndex.merge should not change the first reference time!');
  235. }
  236. this.evict(windowStart);
  237. }
  238. /**
  239. * Removes all SegmentReferences that end before the given time.
  240. *
  241. * @param {number} time The time in seconds.
  242. * @export
  243. */
  244. evict(time) {
  245. if (this.immutable_) {
  246. return;
  247. }
  248. const oldSize = this.references.length;
  249. this.references = this.references.filter((ref) => ref.endTime > time);
  250. const newSize = this.references.length;
  251. const diff = oldSize - newSize;
  252. // Tracking the number of evicted refs will keep their "positions" stable
  253. // for the caller.
  254. this.numEvicted += diff;
  255. }
  256. /**
  257. * Drops references that start after windowEnd, or end before windowStart,
  258. * and contracts the last reference so that it ends at windowEnd.
  259. *
  260. * Do not call on the last period of a live presentation (unknown duration).
  261. * It is okay to call on the other periods of a live presentation, where the
  262. * duration is known and another period has been added.
  263. *
  264. * @param {number} windowStart
  265. * @param {?number} windowEnd
  266. * @param {boolean=} isNew Whether this is a new SegmentIndex and we shouldn't
  267. * update the number of evicted elements.
  268. * @export
  269. */
  270. fit(windowStart, windowEnd, isNew = false) {
  271. goog.asserts.assert(windowEnd != null,
  272. 'Content duration must be known for static content!');
  273. goog.asserts.assert(windowEnd != Infinity,
  274. 'Content duration must be finite for static content!');
  275. if (this.immutable_) {
  276. return;
  277. }
  278. // Trim out references we will never use.
  279. while (this.references.length) {
  280. const lastReference = this.references[this.references.length - 1];
  281. if (lastReference.startTime >= windowEnd) {
  282. this.references.pop();
  283. } else {
  284. break;
  285. }
  286. }
  287. while (this.references.length) {
  288. const firstReference = this.references[0];
  289. if (firstReference.endTime <= windowStart) {
  290. this.references.shift();
  291. if (!isNew) {
  292. this.numEvicted++;
  293. }
  294. } else {
  295. break;
  296. }
  297. }
  298. if (this.references.length == 0) {
  299. return;
  300. }
  301. // Adjust the last SegmentReference.
  302. const lastReference = this.references[this.references.length - 1];
  303. this.references[this.references.length - 1] =
  304. new shaka.media.SegmentReference(
  305. lastReference.startTime,
  306. /* endTime= */ windowEnd,
  307. lastReference.getUrisInner,
  308. lastReference.startByte,
  309. lastReference.endByte,
  310. lastReference.initSegmentReference,
  311. lastReference.timestampOffset,
  312. lastReference.appendWindowStart,
  313. lastReference.appendWindowEnd,
  314. lastReference.partialReferences,
  315. lastReference.tilesLayout,
  316. lastReference.tileDuration,
  317. lastReference.syncTime,
  318. lastReference.status,
  319. lastReference.hlsAes128Key,
  320. );
  321. }
  322. /**
  323. * Updates the references every so often. Stops when the references list
  324. * returned by the callback is null.
  325. *
  326. * @param {number} interval The interval in seconds.
  327. * @param {function():Array.<shaka.media.SegmentReference>} updateCallback
  328. * @export
  329. */
  330. updateEvery(interval, updateCallback) {
  331. goog.asserts.assert(!this.timer_, 'SegmentIndex timer already started!');
  332. if (this.immutable_) {
  333. return;
  334. }
  335. if (this.timer_) {
  336. this.timer_.stop();
  337. }
  338. this.timer_ = new shaka.util.Timer(() => {
  339. const references = updateCallback();
  340. if (references) {
  341. this.references.push(...references);
  342. } else {
  343. this.timer_.stop();
  344. this.timer_ = null;
  345. }
  346. });
  347. this.timer_.tickEvery(interval);
  348. }
  349. /** @return {!shaka.media.SegmentIterator} */
  350. [Symbol.iterator]() {
  351. const iter = this.getIteratorForTime(0);
  352. goog.asserts.assert(iter != null, 'Iterator for 0 should never be null!');
  353. return iter;
  354. }
  355. /**
  356. * Returns a new iterator that initially points to the segment that contains
  357. * the given time. Like the normal iterator, next() must be called first to
  358. * get to the first element. Returns null if we do not find a segment at the
  359. * requested time.
  360. *
  361. * @param {number} time
  362. * @return {?shaka.media.SegmentIterator}
  363. * @export
  364. */
  365. getIteratorForTime(time) {
  366. let index = this.find(time);
  367. if (index == null) {
  368. return null;
  369. } else {
  370. index--;
  371. }
  372. // +1 so we can get the element we'll eventually point to so we can see if
  373. // we need to use a partial segment index.
  374. const ref = this.get(index + 1);
  375. let partialSegmentIndex = -1;
  376. if (ref && ref.hasPartialSegments()) {
  377. // Look for a partial SegmentReference.
  378. for (let i = ref.partialReferences.length - 1; i >= 0; --i) {
  379. const r = ref.partialReferences[i];
  380. // Note that a segment ends immediately before the end time.
  381. if ((time >= r.startTime) && (time < r.endTime)) {
  382. // Call to next() should move the partial segment, not the full
  383. // segment.
  384. index++;
  385. partialSegmentIndex = i - 1;
  386. break;
  387. }
  388. }
  389. }
  390. return new shaka.media.SegmentIterator(this, index, partialSegmentIndex);
  391. }
  392. /**
  393. * @return {boolean}
  394. */
  395. isEmpty() {
  396. return this.references.length == 0;
  397. }
  398. /**
  399. * Create a SegmentIndex for a single segment of the given start time and
  400. * duration at the given URIs.
  401. *
  402. * @param {number} startTime
  403. * @param {number} duration
  404. * @param {!Array.<string>} uris
  405. * @return {!shaka.media.SegmentIndex}
  406. * @export
  407. */
  408. static forSingleSegment(startTime, duration, uris) {
  409. const reference = new shaka.media.SegmentReference(
  410. /* startTime= */ startTime,
  411. /* endTime= */ startTime + duration,
  412. /* getUris= */ () => uris,
  413. /* startByte= */ 0,
  414. /* endByte= */ null,
  415. /* initSegmentReference= */ null,
  416. /* presentationTimeOffset= */ startTime,
  417. /* appendWindowStart= */ startTime,
  418. /* appendWindowEnd= */ startTime + duration);
  419. return new shaka.media.SegmentIndex([reference]);
  420. }
  421. };
  422. if (goog.DEBUG) {
  423. /**
  424. * Asserts that the given SegmentReferences are sorted.
  425. *
  426. * @param {!Array.<shaka.media.SegmentReference>} references
  427. * @private
  428. */
  429. shaka.media.SegmentIndex.assertCorrectReferences_ = (references) => {
  430. goog.asserts.assert(references.every((r2, i) => {
  431. if (i == 0) {
  432. return true;
  433. }
  434. const r1 = references[i - 1];
  435. if (r1.startTime < r2.startTime) {
  436. return true;
  437. } else if (r1.startTime > r2.startTime) {
  438. return false;
  439. } else {
  440. if (r1.endTime <= r2.endTime) {
  441. return true;
  442. } else {
  443. return false;
  444. }
  445. }
  446. }), 'SegmentReferences are incorrect');
  447. };
  448. }
  449. /**
  450. * An iterator over a SegmentIndex's references.
  451. *
  452. * @implements {Iterator.<shaka.media.SegmentReference>}
  453. * @export
  454. */
  455. shaka.media.SegmentIterator = class {
  456. /**
  457. * @param {shaka.media.SegmentIndex} segmentIndex
  458. * @param {number} index
  459. * @param {number} partialSegmentIndex
  460. */
  461. constructor(segmentIndex, index, partialSegmentIndex) {
  462. /** @private {shaka.media.SegmentIndex} */
  463. this.segmentIndex_ = segmentIndex;
  464. /** @private {number} */
  465. this.currentPosition_ = index;
  466. /** @private {number} */
  467. this.currentPartialPosition_ = partialSegmentIndex;
  468. }
  469. /**
  470. * @return {number}
  471. * @export
  472. */
  473. currentPosition() {
  474. return this.currentPosition_;
  475. }
  476. /**
  477. * @return {shaka.media.SegmentReference}
  478. * @export
  479. */
  480. current() {
  481. let ref = this.segmentIndex_.get(this.currentPosition_);
  482. // When we advance past the end of partial references in next(), then add
  483. // new references in merge(), the pointers may not make sense any more.
  484. // This adjusts the invalid pointer values to point to the next newly added
  485. // segment or partial segment.
  486. if (ref && ref.hasPartialSegments() && ref.getUris().length &&
  487. this.currentPartialPosition_ >= ref.partialReferences.length) {
  488. this.currentPosition_++;
  489. this.currentPartialPosition_ = 0;
  490. ref = this.segmentIndex_.get(this.currentPosition_);
  491. }
  492. // If the regular segment contains partial segments, get the current
  493. // partial SegmentReference.
  494. if (ref && ref.hasPartialSegments()) {
  495. const partial = ref.partialReferences[this.currentPartialPosition_];
  496. return partial;
  497. }
  498. return ref;
  499. }
  500. /**
  501. * @override
  502. * @export
  503. */
  504. next() {
  505. const ref = this.segmentIndex_.get(this.currentPosition_);
  506. if (ref && ref.hasPartialSegments()) {
  507. // If the regular segment contains partial segments, move to the next
  508. // partial SegmentReference.
  509. this.currentPartialPosition_++;
  510. // If the current regular segment has been published completely (has a
  511. // valid Uri), and we've reached the end of its partial segments list,
  512. // move to the next regular segment.
  513. // If the Partial Segments list is still on the fly, do not move to
  514. // the next regular segment.
  515. if (ref.getUris().length &&
  516. this.currentPartialPosition_ == ref.partialReferences.length) {
  517. this.currentPosition_++;
  518. this.currentPartialPosition_ = 0;
  519. }
  520. } else {
  521. // If the regular segment doesn't contain partial segments, move to the
  522. // next regular segment.
  523. this.currentPosition_++;
  524. this.currentPartialPosition_ = 0;
  525. }
  526. const res = this.current();
  527. return {
  528. 'value': res,
  529. 'done': !res,
  530. };
  531. }
  532. };
  533. /**
  534. * A meta-SegmentIndex composed of multiple other SegmentIndexes.
  535. * Used in constructing multi-Period Streams for DASH.
  536. *
  537. * @extends shaka.media.SegmentIndex
  538. * @implements {shaka.util.IReleasable}
  539. * @implements {Iterable.<!shaka.media.SegmentReference>}
  540. * @export
  541. */
  542. shaka.media.MetaSegmentIndex = class extends shaka.media.SegmentIndex {
  543. /** */
  544. constructor() {
  545. super([]);
  546. /** @private {!Array.<!shaka.media.SegmentIndex>} */
  547. this.indexes_ = [];
  548. }
  549. /**
  550. * Append a SegmentIndex to this MetaSegmentIndex. This effectively stitches
  551. * the underlying Stream onto the end of the multi-Period Stream represented
  552. * by this MetaSegmentIndex.
  553. *
  554. * @param {!shaka.media.SegmentIndex} segmentIndex
  555. */
  556. appendSegmentIndex(segmentIndex) {
  557. goog.asserts.assert(
  558. this.indexes_.length == 0 || segmentIndex.numEvicted == 0,
  559. 'Should not append a new segment index with already-evicted segments');
  560. this.indexes_.push(segmentIndex);
  561. }
  562. /**
  563. * Create a clone of this MetaSegmentIndex containing all the same indexes.
  564. *
  565. * @return {!shaka.media.MetaSegmentIndex}
  566. */
  567. clone() {
  568. const clone = new shaka.media.MetaSegmentIndex();
  569. // Be careful to clone the Array. We don't want to share the reference with
  570. // our clone and affect each other accidentally.
  571. clone.indexes_ = this.indexes_.slice();
  572. return clone;
  573. }
  574. /**
  575. * @override
  576. * @export
  577. */
  578. release() {
  579. for (const index of this.indexes_) {
  580. index.release();
  581. }
  582. this.indexes_ = [];
  583. }
  584. /**
  585. * @override
  586. * @export
  587. */
  588. find(time) {
  589. let numPassedInEarlierIndexes = 0;
  590. for (const index of this.indexes_) {
  591. const position = index.find(time);
  592. if (position != null) {
  593. return position + numPassedInEarlierIndexes;
  594. }
  595. numPassedInEarlierIndexes += index.numEvicted + index.references.length;
  596. }
  597. return null;
  598. }
  599. /**
  600. * @override
  601. * @export
  602. */
  603. get(position) {
  604. let numPassedInEarlierIndexes = 0;
  605. let sawSegments = false;
  606. for (const index of this.indexes_) {
  607. goog.asserts.assert(
  608. !sawSegments || index.numEvicted == 0,
  609. 'Should not see evicted segments after available segments');
  610. const reference = index.get(position - numPassedInEarlierIndexes);
  611. if (reference) {
  612. return reference;
  613. }
  614. numPassedInEarlierIndexes += index.numEvicted + index.references.length;
  615. sawSegments = sawSegments || index.references.length != 0;
  616. }
  617. return null;
  618. }
  619. /**
  620. * @override
  621. * @export
  622. */
  623. offset(offset) {
  624. // offset() is only used by HLS, and MetaSegmentIndex is only used for DASH.
  625. goog.asserts.assert(
  626. false, 'offset() should not be used in MetaSegmentIndex!');
  627. }
  628. /**
  629. * @override
  630. * @export
  631. */
  632. merge(references) {
  633. // merge() is only used internally by the DASH and HLS parser on
  634. // SegmentIndexes, but never on MetaSegmentIndex.
  635. goog.asserts.assert(
  636. false, 'merge() should not be used in MetaSegmentIndex!');
  637. }
  638. /**
  639. * @override
  640. * @export
  641. */
  642. evict(time) {
  643. // evict() is only used internally by the DASH and HLS parser on
  644. // SegmentIndexes, but never on MetaSegmentIndex.
  645. goog.asserts.assert(
  646. false, 'evict() should not be used in MetaSegmentIndex!');
  647. }
  648. /**
  649. * @override
  650. * @export
  651. */
  652. mergeAndEvict(references, windowStart) {
  653. // mergeAndEvict() is only used internally by the DASH and HLS parser on
  654. // SegmentIndexes, but never on MetaSegmentIndex.
  655. goog.asserts.assert(
  656. false, 'mergeAndEvict() should not be used in MetaSegmentIndex!');
  657. }
  658. /**
  659. * @override
  660. * @export
  661. */
  662. fit(windowStart, windowEnd) {
  663. // fit() is only used internally by manifest parsers on SegmentIndexes, but
  664. // never on MetaSegmentIndex.
  665. goog.asserts.assert(false, 'fit() should not be used in MetaSegmentIndex!');
  666. }
  667. /**
  668. * @override
  669. * @export
  670. */
  671. updateEvery(interval, updateCallback) {
  672. // updateEvery() is only used internally by the DASH parser on
  673. // SegmentIndexes, but never on MetaSegmentIndex.
  674. goog.asserts.assert(
  675. false, 'updateEvery() should not be used in MetaSegmentIndex!');
  676. }
  677. };