Source: lib/media/region_timeline.js

/*! @license
 * Shaka Player
 * Copyright 2016 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

goog.provide('shaka.media.RegionTimeline');

goog.require('shaka.util.FakeEvent');
goog.require('shaka.util.FakeEventTarget');
goog.require('shaka.util.IReleasable');
goog.require('shaka.util.Timer');


/**
 * The region timeline is a set of unique timeline region info entries. When
 * a new entry is added, the 'regionadd' event will be fired.  When an entry is
 * deleted, the 'regionremove' event will be fired.
 *
 * @implements {shaka.util.IReleasable}
 * @template T
 * @final
 */
shaka.media.RegionTimeline = class extends shaka.util.FakeEventTarget {
  /**
   * @param {!function():{start: number, end: number}} getSeekRange
   */
  constructor(getSeekRange) {
    super();

    /** @private {!Map<string, T>} */
    this.regions_ = new Map();

    /** @private {!function():{start: number, end: number}} */
    this.getSeekRange_ = getSeekRange;

    /**
     * Make sure all of the regions we're tracking are within the
     * seek range or further in the future. We don't want to store
     * regions that fall before the start of the seek range.
     *
     * @private {shaka.util.Timer}
     */
    this.filterTimer_ = null;
  }

  /** @override */
  release() {
    this.regions_.clear();
    this.releaseTimer_();
    super.release();
  }

  /**
   * @param {T} region
   */
  addRegion(region) {
    const key = this.generateKey_(region);

    // Make sure we don't add duplicate regions. We keep track of this here
    // instead of making the parser track it.
    if (!this.regions_.has(key)) {
      this.regions_.set(key, region);
      const event = new shaka.util.FakeEvent('regionadd', new Map([
        ['region', region],
      ]));
      this.dispatchEvent(event);
      this.setupTimer_();
    }
  }

  /**
   * @private
   */
  filterBySeekRange_() {
    const seekRange = this.getSeekRange_();
    for (const [key, region] of this.regions_) {
      // Only consider the seek range start here.
      // Future regions might become relevant eventually,
      // but regions that are in the past and can't ever be
      // seeked to will never come up again, and there's no
      // reason to store or process them.
      if (region.endTime < seekRange.start) {
        this.regions_.delete(key);
        const event = new shaka.util.FakeEvent('regionremove', new Map([
          ['region', region],
        ]));
        this.dispatchEvent(event);
      }
    }
    if (!this.regions_.size) {
      this.releaseTimer_();
    }
  }

  /** @private */
  setupTimer_() {
    if (this.filterTimer_) {
      return;
    }
    this.filterTimer_ = new shaka.util.Timer(() => {
      this.filterBySeekRange_();
    }).tickEvery(
        /* seconds= */ shaka.media.RegionTimeline.REGION_FILTER_INTERVAL);
  }

  /** @private */
  releaseTimer_() {
    if (this.filterTimer_) {
      this.filterTimer_.stop();
      this.filterTimer_ = null;
    }
  }

  /**
   * Get an iterable for all the regions in the timeline. This will allow
   * others to see what regions are in the timeline while not being able to
   * change the collection.
   *
   * @return {!Iterable<T>}
   */
  regions() {
    return this.regions_.values();
  }

  /**
   * @param {T} region
   * @return {string}
   * @private
   */
  generateKey_(region) {
    return `${region.schemeIdUri}_${region.id}_` +
      `${region.startTime.toFixed(1)}_${region.endTime.toFixed(1)}`;
  }
};

/** @const {number} */
shaka.media.RegionTimeline.REGION_FILTER_INTERVAL = 2; // in seconds