Source: lib/util/state_history.js

/**
 * @license
 * Copyright 2016 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

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

goog.require('goog.asserts');


/**
 * This class is used to track the changes in video state (playing, paused,
 * buffering, and ended) while playing content.
 *
 * @final
 */
shaka.util.StateHistory = class {
  /**
   * @param {!HTMLMediaElement} element
   */
  constructor(element) {
    /** @private {!HTMLMediaElement} */
    this.element_ = element;

    /**
     * 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 {boolean} isBuffering
   */
  update(isBuffering) {
    // |open_| will only be |null| when we first call |update|.
    if (this.open_ == null) {
      this.start_(isBuffering);
    } else {
      this.update_(isBuffering);
    }
  }

  /**
   * 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 {boolean} isBuffering
   * @private
   */
  start_(isBuffering) {
    goog.asserts.assert(
        this.open_ == null,
        'There must be no open entry in order when we start');

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

  /**
   * @param {boolean} isBuffering
   * @private
   */
  update_(isBuffering) {
    goog.asserts.assert(
        this.open_,
        'There must be an open entry in order to update it');

    const currentTimeSeconds = this.getNowInSeconds_();
    const currentState = this.getCurrentState_(isBuffering);

    // 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 == currentState) {
      return;
    }

    // We have changed states, so "close" the open state.
    this.closed_.push(this.open_);
    this.open_ = {
      timestamp: currentTimeSeconds,
      state: currentState,
      duration: 0,
    };
  }

  /**
   * @param {boolean} isBuffering
   * @return {string}
   * @private
   */
  getCurrentState_(isBuffering) {
    if (isBuffering) { return 'buffering'; }
    if (this.element_.ended) { return 'ended'; }
    if (this.element_.paused) { return 'paused'; }
    return 'playing';
  }

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