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