Source: lib/media/region_observer.js

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

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

goog.require('shaka.media.IPlayheadObserver');
goog.require('shaka.media.RegionTimeline');


/**
 * The region observer watches a region timeline and playhead, and fires events
 * (onEnter, onExit, and onSkip) as the playhead moves.
 *
 * @implements {shaka.media.IPlayheadObserver}
 * @final
 */
shaka.media.RegionObserver = class {
  /**
   * Create a region observer for the given timeline. The observer does not
   * own the timeline, only uses it. This means that the observer should NOT
   * destroy the timeline.
   *
   * @param {!shaka.media.RegionTimeline} timeline
   */
  constructor(timeline) {
    /** @private {shaka.media.RegionTimeline} */
    this.timeline_ = timeline;

    /**
     * A mapping between a region and where we previously were relative to it.
     * When the value here differs from what we calculate, it means we moved and
     * should fire an event.
     *
     * @private {!Map.<shaka.extern.TimelineRegionInfo,
     *                 shaka.media.RegionObserver.RelativePosition_>}
     */
    this.oldPosition_ = new Map();

    /** @private {shaka.media.RegionObserver.EventListener} */
    this.onEnter_ = (region, seeking) => {};
    /** @private {shaka.media.RegionObserver.EventListener} */
    this.onExit_ = (region, seeking) => {};
    /** @private {shaka.media.RegionObserver.EventListener} */
    this.onSkip_ = (region, seeking) => {};

    // To make the rules easier to read, alias all the relative positions.
    const RelativePosition = shaka.media.RegionObserver.RelativePosition_;
    const BEFORE_THE_REGION = RelativePosition.BEFORE_THE_REGION;
    const IN_THE_REGION = RelativePosition.IN_THE_REGION;
    const AFTER_THE_REGION = RelativePosition.AFTER_THE_REGION;

    /**
     * A read-only collection of rules for what to do when we change position
     * relative to a region.
     *
     * @private {!Iterable.<shaka.media.RegionObserver.Rule_>}
     */
    this.rules_ = [
      {
        weWere: null,
        weAre: IN_THE_REGION,
        invoke: (region, seeking) => this.onEnter_(region, seeking),
      },
      {
        weWere: BEFORE_THE_REGION,
        weAre: IN_THE_REGION,
        invoke: (region, seeking) => this.onEnter_(region, seeking),
      },
      {
        weWere: AFTER_THE_REGION,
        weAre: IN_THE_REGION,
        invoke: (region, seeking) => this.onEnter_(region, seeking),
      },
      {
        weWere: IN_THE_REGION,
        weAre: BEFORE_THE_REGION,
        invoke: (region, seeking) => this.onExit_(region, seeking),
      },
      {
        weWere: IN_THE_REGION,
        weAre: AFTER_THE_REGION,
        invoke: (region, seeking) => this.onExit_(region, seeking),
      },
      {
        weWere: BEFORE_THE_REGION,
        weAre: AFTER_THE_REGION,
        invoke: (region, seeking) => this.onSkip_(region, seeking),
      },
      {
        weWere: AFTER_THE_REGION,
        weAre: BEFORE_THE_REGION,
        invoke: (region, seeking) => this.onSkip_(region, seeking),
      },
    ];
  }

  /** @override */
  release() {
    this.timeline_ = null;

    // Clear our maps so that we are not holding onto any more information than
    // needed.
    this.oldPosition_.clear();

    // Clear the callbacks so that we don't hold onto any references external
    // to this class.
    this.onEnter_ = (region, seeking) => {};
    this.onExit_ = (region, seeking) => {};
    this.onSkip_ = (region, seeking) => {};
  }

  /** @override */
  poll(positionInSeconds, wasSeeking) {
    const RegionObserver = shaka.media.RegionObserver;

    for (const region of this.timeline_.regions()) {
      const previousPosition = this.oldPosition_.get(region);
      const currentPosition = RegionObserver.determinePositionRelativeTo_(
          region, positionInSeconds);

      // We will only use |previousPosition| and |currentPosition|, so we can
      // update our state now.
      this.oldPosition_.set(region, currentPosition);

      for (const rule of this.rules_) {
        if (rule.weWere == previousPosition && rule.weAre == currentPosition) {
          rule.invoke(region, wasSeeking);
        }
      }
    }
  }

  /**
   * Set all the listeners. This overrides any previous calls to |setListeners|.
   *
   * @param {shaka.media.RegionObserver.EventListener} onEnter
   *    The callback for when we move from outside a region to inside a region.
   * @param {shaka.media.RegionObserver.EventListener} onExit
   *    The callback for when we move from inside a region to outside a region.
   * @param {shaka.media.RegionObserver.EventListener} onSkip
   *    The callback for when we move from before to after a region or from
   *    after to before a region.
   */
  setListeners(onEnter, onExit, onSkip) {
    this.onEnter_ = onEnter;
    this.onExit_ = onExit;
    this.onSkip_ = onSkip;
  }

  /**
   * Get the relative position of the playhead to |region| when the playhead is
   * at |seconds|. We treat the region's start and end times as inclusive
   * bounds.
   *
   * @param {shaka.extern.TimelineRegionInfo} region
   * @param {number} seconds
   * @return {shaka.media.RegionObserver.RelativePosition_}
   * @private
   */
  static determinePositionRelativeTo_(region, seconds) {
    const RelativePosition = shaka.media.RegionObserver.RelativePosition_;

    if (seconds < region.startTime) {
      return RelativePosition.BEFORE_THE_REGION;
    }

    if (seconds > region.endTime) {
      return RelativePosition.AFTER_THE_REGION;
    }

    return RelativePosition.IN_THE_REGION;
  }
};

/**
 * An enum of relative positions between the playhead and a region. Each is
 * phrased so that it works in "The playhead is X" where "X" is any value in
 * the enum.
 *
 * @enum {number}
 * @private
 */
shaka.media.RegionObserver.RelativePosition_ = {
  BEFORE_THE_REGION: 1,
  IN_THE_REGION: 2,
  AFTER_THE_REGION: 3,
};

/**
 * All region observer events (onEnter, onExit, and onSkip) will be passed the
 * region that the playhead is interacting with and whether or not the playhead
 * moving is part of a seek event.
 *
 * @typedef {function(shaka.extern.TimelineRegionInfo, boolean)}
 */
shaka.media.RegionObserver.EventListener;

/**
 * @typedef {{
 *    weWere: ?shaka.media.RegionObserver.RelativePosition_,
 *    weAre: ?shaka.media.RegionObserver.RelativePosition_,
 *    invoke: shaka.media.RegionObserver.EventListener
 * }}
 *
 * @private
 */
shaka.media.RegionObserver.Rule_;