Source: lib/util/state_history.js

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

goog.provide('shaka.util.StateHistory');

goog.require('goog.asserts');
goog.require('shaka.log');


/**
 * This class is used to track the time spent in arbitrary states. When told of
 * a state, it will assume that state was active until a new state is provided.
 * When provided with identical states back-to-back, the existing entry will be
 * updated.
 *
 * @final
 */
shaka.util.StateHistory = class {
  /** */
  constructor() {
    /**
     * The state that we think is still the current change. It is "open" for
     * updating.
     *
     * @private {?shaka.extern.StateChange}
     */
    this.open_ = null;

    /**
     * The stats that are "closed" for updating. The "open" state becomes closed
     * once we move to a new state.
     *
     * @private {!Array.<shaka.extern.StateChange>}
     */
    this.closed_ = [];
  }

  /**
   * @param {string} state
   * @return {boolean} True if this changed the state
   */
  update(state) {
    // |open_| will only be |null| when we first call |update|.
    if (this.open_ == null) {
      this.start_(state);
      return true;
    } else {
      return this.update_(state);
    }
  }

  /**
   * Go through all entries in the history and count how much time was spend in
   * the given state.
   *
   * @param {string} state
   * @return {number}
   */
  getTimeSpentIn(state) {
    let sum = 0;

    if (this.open_ && this.open_.state == state) {
      sum += this.open_.duration;
    }

    for (const entry of this.closed_) {
      sum += entry.state == state ? entry.duration : 0;
    }

    return sum;
  }

  /**
   * Get a copy of each state change entry in the history. A copy of each entry
   * is created to break the reference to the internal data.
   *
   * @return {!Array.<shaka.extern.StateChange>}
   */
  getCopy() {
    const clone = (entry) => {
      return {
        timestamp: entry.timestamp,
        state: entry.state,
        duration: entry.duration,
      };
    };

    const copy = [];
    for (const entry of this.closed_) {
      copy.push(clone(entry));
    }
    if (this.open_) {
      copy.push(clone(this.open_));
    }

    return copy;
  }

  /**
   * @param {string} state
   * @private
   */
  start_(state) {
    goog.asserts.assert(
        this.open_ == null,
        'There must be no open entry in order when we start');
    shaka.log.v1('Changing Player state to', state);

    this.open_ = {
      timestamp: this.getNowInSeconds_(),
      state: state,
      duration: 0,
    };
  }

  /**
   * @param {string} state
   * @return {boolean} True if this changed the state
   * @private
   */
  update_(state) {
    goog.asserts.assert(
        this.open_,
        'There must be an open entry in order to update it');

    const currentTimeSeconds = this.getNowInSeconds_();

    // Always update the duration so that it can always be as accurate as
    // possible.
    this.open_.duration = currentTimeSeconds - this.open_.timestamp;

    // If the state has not changed, there is no need to add a new entry.
    if (this.open_.state == state) {
      return false;
    }

    // We have changed states, so "close" the open state.
    shaka.log.v1('Changing Player state to', state);
    this.closed_.push(this.open_);
    this.open_ = {
      timestamp: currentTimeSeconds,
      state: state,
      duration: 0,
    };
    return true;
  }

  /**
   * Get the system time in seconds.
   *
   * @return {number}
   * @private
   */
  getNowInSeconds_() {
    return Date.now() / 1000;
  }
};