Source: player/player.js

/**
 * @license
 * Copyright 2015 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.player.Player');

goog.require('shaka.asserts');
goog.require('shaka.log');
goog.require('shaka.media.EmeManager');
goog.require('shaka.player.AudioTrack');
goog.require('shaka.player.Defaults');
goog.require('shaka.player.Restrictions');
goog.require('shaka.player.Stats');
goog.require('shaka.player.TextStyle');
goog.require('shaka.player.TextTrack');
goog.require('shaka.player.VideoTrack');
goog.require('shaka.timer');
goog.require('shaka.util.EventManager');
goog.require('shaka.util.FakeEvent');
goog.require('shaka.util.FakeEventTarget');
goog.require('shaka.util.MapUtils');


/**
 * @event shaka.player.Player.ErrorEvent
 * @description Fired when a playback error occurs.
 *     Bubbles up through the Player.
 * @property {string} type 'error'
 * @property {boolean} bubbles true
 * @property {!Error} detail An object which contains details on the error.
 *    The error's 'type' property will help you identify the specific error
 *    condition and display an appropriate message or error indicator to the
 *    user.  The error's 'message' property contains English text which can
 *    be useful during debugging.
 * @export
 */


/**
 * @event shaka.player.Player.BufferingEvent
 * @description Fired when the player's buffering state changes.
 * @property {string} type 'bufferingStart' or 'bufferingEnd'
 * @export
 */



/**
 * Creates a Player.
 *
 * @param {!HTMLVideoElement} video The video element.
 *
 * @fires shaka.media.IStream.AdaptationEvent
 * @fires shaka.player.StreamVideoSource.SeekRangeChangedEvent
 * @fires shaka.player.Player.BufferingEvent
 * @fires shaka.player.Player.ErrorEvent
 *
 * @constructor
 * @struct
 * @extends {shaka.util.FakeEventTarget}
 * @export
 */
shaka.player.Player = function(video) {
  shaka.util.FakeEventTarget.call(this, null);

  /**
   * The video element.
   * @private {!HTMLVideoElement}
   */
  this.video_ = video;

  /**
   * The video source object.
   * @private {shaka.player.IVideoSource}
   */
  this.videoSource_ = null;

  /** @private {!shaka.util.EventManager} */
  this.eventManager_ = new shaka.util.EventManager();

  /** @private {shaka.media.EmeManager} */
  this.emeManager_ = null;

  /** @private {?number} */
  this.rewindTimer_ = null;

  /** @private {number} */
  this.seekRangeStart_ = 0;

  /** @private {?number} */
  this.watchdogTimer_ = null;

  /** @private {boolean} */
  this.buffering_ = false;

  /** @private {!shaka.player.Stats} */
  this.stats_ = new shaka.player.Stats;

  /** @private {!Object.<string, *>} */
  this.videoSourceConfig_ = {
    'enableAdaptation': true,
    'streamBufferSize': shaka.player.Defaults.STREAM_BUFFER_SIZE,
    'liveStreamEndTimeout': shaka.player.Defaults.STREAM_BUFFER_SIZE,
    'licenseRequestTimeout': shaka.player.Defaults.LICENSE_REQUEST_TIMEOUT,
    'mpdRequestTimeout': shaka.player.Defaults.MPD_REQUEST_TIMEOUT,
    'segmentRequestTimeout': shaka.player.Defaults.SEGMENT_REQUEST_TIMEOUT,
    'preferredLanguage': shaka.player.Defaults.PREFERRED_LANGUAGE,
    'restrictions': new shaka.player.Restrictions()
  };

  /** @private {number} */
  this.playbackRate_ = 1.0;

  /** @private {?number} */
  this.playbackStartTime_ = null;
};
goog.inherits(shaka.player.Player, shaka.util.FakeEventTarget);


/**
 * @define {string} A version number taken from git at compile time.
 */
goog.define('GIT_VERSION', 'v1.6.2-debug');


/**
 * @const {string}
 * @export
 */
shaka.player.Player.version = GIT_VERSION;


/**
 * Determines if the browser has all of the necessary APIs to support the Shaka
 * Player.  This check may not pass if polyfills have not been installed.
 *
 * @return {boolean}
 * @export
 */
shaka.player.Player.isBrowserSupported = function() {
  return true &&
      // MSE is needed for adaptive streaming.
      !!window.MediaSource &&
      // EME is needed for protected content.
      !!window.MediaKeys &&
      // Indicates recent EME APIs.
      !!window.navigator &&
      !!window.navigator.requestMediaKeySystemAccess &&
      !!window.MediaKeySystemAccess &&
      !!window.MediaKeySystemAccess.prototype.getConfiguration &&
      // Promises are used frequently for asynchronous operations.
      !!window.Promise &&
      // Fullscreen API.
      !!Element.prototype.requestFullscreen &&
      !!document.exitFullscreen &&
      'fullscreenElement' in document &&
      // Uint8Array is used frequently for parsing binary data
      !!window.Uint8Array;
};


/**
 * Determines if the specified MIME type and codec is supported by the browser.
 *
 * @param {string} fullMimeType A MIME type, which should include codec info.
 * @return {boolean} true if the type is supported.
 * @export
 */
shaka.player.Player.isTypeSupported = function(fullMimeType) {
  var supported;

  if (fullMimeType == 'text/vtt') {
    supported = !!window.VTTCue;
  } else {
    supported = MediaSource.isTypeSupported(fullMimeType);
  }

  shaka.log.info('+', fullMimeType, supported ? 'is' : 'is not', 'supported');
  return supported;
};


/**
 * Sets style attributes for text tracks.
 *
 * @param {!shaka.player.TextStyle} style
 * @export
 */
shaka.player.Player.setTextStyle = function(style) {
  var element = document.getElementById(shaka.player.Player.STYLE_ELEMENT_ID_);
  if (!element) {
    element = document.createElement('style');
    element.id = shaka.player.Player.STYLE_ELEMENT_ID_;
    document.head.appendChild(element);
  }
  var sheet = element.sheet;

  while (sheet.cssRules.length) {
    sheet.deleteRule(0);
  }
  sheet.insertRule('::cue { ' + style.toCSS() + ' }', 0);
};


/**
 * Destroys the player.
 * @return {!Promise} A promise, resolved when destroy has finished.
 * @suppress {checkTypes} to set otherwise non-nullable types to null.
 * @export
 */
shaka.player.Player.prototype.destroy = function() {
  return this.unload().then(shaka.util.TypedBind(this, function() {
    this.eventManager_.destroy();
    this.eventManager_ = null;

    this.video_ = null;
  })).catch(function() {});
};


/**
 * Stop playback and unload the current video source.  Makes the player ready
 * for reuse.  Also resets any statistics gathered.
 *
 * MediaKeys must be unloaded asynchronously, but all other resources are
 * removed synchronously.
 *
 * @return {!Promise} A promise, resolved when MediaKeys is removed.
 * @export
 */
shaka.player.Player.prototype.unload = function() {
  if (!this.videoSource_) {
    // Nothing to unload.
    return Promise.resolve();
  }

  if (this.buffering_) {
    this.endBufferingState_();
  }
  // Stop playback.
  this.video_.pause();

  // Stop listening for events and timers.
  this.eventManager_.removeAll();
  this.cancelWatchdogTimer_();
  this.cancelRewindTimer_();

  // Release all EME resources.
  if (this.emeManager_) {
    this.emeManager_.destroy();
    this.emeManager_ = null;
  }

  // Remove the video source.
  this.video_.src = '';
  // Only clear mediaKeys after clearing the source.
  var p = this.video_.setMediaKeys(null);

  if (this.videoSource_) {
    this.videoSource_.destroy();
    this.videoSource_ = null;
  }

  // Reset state.
  this.buffering_ = false;
  this.stats_ = new shaka.player.Stats();

  return p;
};


/**
 * Loads the specified video source and starts playback.  If a video source has
 * already been loaded, this calls unload() for you before loading the new
 * source.
 *
 * @param {!shaka.player.IVideoSource} videoSource The IVideoSource object. The
 *     Player takes ownership of |videoSource|.
 * @return {!Promise}
 * @export
 */
shaka.player.Player.prototype.load = function(videoSource) {
  var p = this.unload();
  shaka.asserts.assert(this.videoSource_ == null);
  shaka.asserts.assert(this.emeManager_ == null);

  if (this.video_.autoplay) {
    shaka.timer.begin('load');
    this.eventManager_.listen(this.video_, 'timeupdate',
                              this.onFirstTimestamp_.bind(this));
  }

  videoSource.configure(this.videoSourceConfig_);
  videoSource.setPlaybackStartTime(this.playbackStartTime_);
  this.playbackStartTime_ = null;

  var loaded = p.then(shaka.util.TypedBind(this,
      function() {
        return videoSource.load();
      }));
  loaded.catch(shaka.util.TypedBind(this,
      /** @param {*} error */
      function(error) {
        // Clean up the local copy, since we haven't set this.videoSource_ yet.
        videoSource.destroy();
        // Pass the error along.
        return Promise.reject(error);
      }));
  return loaded.then(shaka.util.TypedBind(this,
      function() {
        if (!this.video_) return this.rejectDestroyed_();
        this.videoSource_ = videoSource;
        this.eventManager_.listen(this.videoSource_, 'seekrangechanged',
                                  this.onSeekRangeChanged_.bind(this));
        this.emeManager_ = new shaka.media.EmeManager(
            this, this.video_, this.videoSource_);
        return this.emeManager_.initialize();
      })
  ).then(shaka.util.TypedBind(this,
      function() {
        if (!this.video_) return this.rejectDestroyed_();
        this.setVideoEventListeners_();
        return this.videoSource_.attach(this, this.video_);
      })
  ).then(shaka.util.TypedBind(this,
      function() {
        if (!this.video_) return this.rejectDestroyed_();
        this.startWatchdogTimer_();
      })
  ).catch(shaka.util.TypedBind(this,
      /** @param {*} error */
      function(error) {
        if (!this.video_) return this.rejectDestroyed_();

        if (error.type != 'destroy') {
          // Even though we return a rejected promise, we still want to
          // dispatch an error event to ensure that the application is aware of
          // all errors from the player.
          var event = shaka.util.FakeEvent.createErrorEvent(error);
          this.dispatchEvent(event);
        }

        return this.unload().then(function() {
          return Promise.reject(error);
        });
      })
  );
};


/**
 * @return {!Promise}
 * @private
 */
shaka.player.Player.prototype.rejectDestroyed_ = function() {
  var error = new Error('Player destroyed');
  error.type = 'destroy';
  return Promise.reject(error);
};


/**
 * Sets the video's event listeners.
 *
 * @private
 */
shaka.player.Player.prototype.setVideoEventListeners_ = function() {
  this.eventManager_.listen(this.video_, 'error', this.onError_.bind(this));
  this.eventManager_.listen(this.video_, 'playing', this.onPlaying_.bind(this));
  this.eventManager_.listen(this.video_, 'pause', this.onPause_.bind(this));
};


/**
 * Time update event handler.  Will be removed once the first update is seen.
 *
 * @param {!Event} event
 * @private
 */
shaka.player.Player.prototype.onFirstTimestamp_ = function(event) {
  shaka.timer.end('load');
  this.stats_.logPlaybackLatency(shaka.timer.get('load'));
  this.eventManager_.unlisten(this.video_, 'timeupdate');
};


/**
 * Video error event handler.
 *
 * @param {!Event} event
 * @private
 */
shaka.player.Player.prototype.onError_ = function(event) {
  if (!this.video_.error) {
    // This occurred during testing with prefixed EME.  Ignore errors we can't
    // interpret, on the assumption that this is a browser bug.
    shaka.log.debug('Uninterpretable error event!', event);
    return;
  }

  var code = this.video_.error.code;
  if (code == MediaError['MEDIA_ERR_ABORTED']) {
    // Ignore this error code, which should only occur when navigating away or
    // deliberately stopping playback of HTTP content.
    return;
  }

  shaka.log.debug('onError_', event, code);
  var message = shaka.player.Player.MEDIA_ERROR_MAP_[code] ||
                'Unknown playback error.';

  var error = new Error(message);
  error.type = 'playback';
  var errorEvent = shaka.util.FakeEvent.createErrorEvent(error);
  this.dispatchEvent(errorEvent);
};


/**
 * Video playing event handler.  Fires any time the video starts playing.
 *
 * @param {!Event} event
 * @private
 */
shaka.player.Player.prototype.onPlaying_ = function(event) {
  shaka.log.debug('onPlaying_', event);
  shaka.timer.begin('playing');

  // Start rewind timer if playback rate should be negative and a rewind timer
  // is not already set.
  if (!this.rewindTimer_ && this.playbackRate_ < 0) {
    this.video_.playbackRate = 0;
    this.onRewindTimer_(
        this.video_.currentTime, Date.now(), this.playbackRate_);
  }
  if (this.buffering_) {
    this.endBufferingState_();
  }
};


/**
 * Video pause event handler.  Fires any time the video stops for any reason,
 * including before a 'seeking' or 'ended' event.
 *
 * @param {!Event} event
 * @private
 */
shaka.player.Player.prototype.onPause_ = function(event) {
  shaka.log.debug('onPause_', event);
  shaka.timer.end('playing');
  var elapsed = shaka.timer.get('playing');
  if (!isNaN(elapsed)) {
    this.stats_.logPlayTime(elapsed);
  }
  this.cancelRewindTimer_();
};


/**
 * Handler for seek range events.
 *
 * @param {!Event} event
 * @private
 */
shaka.player.Player.prototype.onSeekRangeChanged_ = function(event) {
  this.seekRangeStart_ = event['start'];
};


/**
 * Gets updated stats about the player.
 *
 * @return {!shaka.player.Stats}
 * @export
 */
shaka.player.Player.prototype.getStats = function() {
  if (!this.video_.paused) {
    // Update play time, which is still progressing.
    shaka.timer.end('playing');
    var elapsed = shaka.timer.get('playing');
    if (!isNaN(elapsed)) {
      this.stats_.logPlayTime(elapsed);
      shaka.timer.begin('playing');
    }
  }
  this.stats_.updateVideoStats(this.video_);
  return this.stats_;
};


/**
 * Gets the available video tracks.
 *
 * @return {!Array.<!shaka.player.VideoTrack>}
 * @export
 */
shaka.player.Player.prototype.getVideoTracks = function() {
  if (!this.videoSource_) return [];
  return this.videoSource_.getVideoTracks();
};


/**
 * Gets the available audio tracks.
 *
 * @return {!Array.<!shaka.player.AudioTrack>}
 * @export
 */
shaka.player.Player.prototype.getAudioTracks = function() {
  if (!this.videoSource_) return [];
  return this.videoSource_.getAudioTracks();
};


/**
 * Gets the available text tracks.
 *
 * @return {!Array.<!shaka.player.TextTrack>}
 * @export
 */
shaka.player.Player.prototype.getTextTracks = function() {
  if (!this.videoSource_) return [];
  return this.videoSource_.getTextTracks();
};


/**
 * Select a video track by ID.  This can interfere with automatic bitrate
 * adaptation, so you should disable adaptation, via
 * {@link shaka.player.Player#configure}, if you intend to use manual video
 * track selection.
 *
 * @param {number} id The |id| field of the desired VideoTrack object.
 * @param {boolean=} opt_clearBuffer If true (and by default), removes the
 *     previous stream's content before switching to the new stream.
 *
 * @return {boolean} True if the specified VideoTrack was found.
 * @export
 */
shaka.player.Player.prototype.selectVideoTrack = function(id, opt_clearBuffer) {
  if (!this.videoSource_) return false;
  var clearBuffer = (opt_clearBuffer == undefined) ? true : opt_clearBuffer;
  return this.videoSource_.selectVideoTrack(id, clearBuffer);
};


/**
 * Select an audio track by ID.
 *
 * @param {number} id The |id| field of the desired AudioTrack object.
 * @param {boolean=} opt_clearBuffer If true (and by default), removes the
 *     previous stream's content before switching to the new stream.
 * @param {number=} opt_clearBufferOffset if |clearBuffer| and
 *     |opt_clearBufferOffset| are truthy, clear the stream buffer from the
 *     given offset (relative to the audio's current time) to the end of the
 *     stream.
 *
 * @return {boolean} True if the specified AudioTrack was found.
 * @export
 */
shaka.player.Player.prototype.selectAudioTrack = function(id, opt_clearBuffer,
    opt_clearBufferOffset) {
  if (!this.videoSource_) return false;
  var clearBuffer = (opt_clearBuffer == undefined) ? true : opt_clearBuffer;
  return this.videoSource_.selectAudioTrack(id, clearBuffer,
      opt_clearBufferOffset);
};


/**
 * Select a text track by ID.
 *
 * @param {number} id The |id| field of the desired TextTrack object.
 *
 * @return {boolean} True if the specified TextTrack was found.
 * @export
 */
shaka.player.Player.prototype.selectTextTrack = function(id) {
  if (!this.videoSource_) return false;
  return this.videoSource_.selectTextTrack(id, false);
};


/**
 * Enable or disable the text track.  Has no effect if called before
 * load() resolves.
 *
 * @param {boolean} enabled
 * @export
 */
shaka.player.Player.prototype.enableTextTrack = function(enabled) {
  if (!this.videoSource_) return;
  this.videoSource_.enableTextTrack(enabled);
};


/**
 * @param {number} rate The playback rate.
 *     Negative values will rewind the video.
 *     Positive values less than 1.0 will trigger slow-motion playback.
 *     Positive values greater than 1.0 will trigger fast-forward.
 *     0.0 is similar to pausing the video.
 *     Some UAs will not play audio at rates less than 0.25 or 0.5 or greater
 *     than 4.0 or 5.0, but this behavior is not specified.
 *     No audio will be played while rewinding.
 * @export
 */
shaka.player.Player.prototype.setPlaybackRate = function(rate) {
  // Cancel any rewind we might be in the middle of.
  this.cancelRewindTimer_();

  if (rate >= 0) {
    // Slow-mo or fast-forward are handled natively by the UA.
    this.video_.playbackRate = rate;
  // Only rewind when not paused.
  } else if (!this.video_.paused) {
    // Rewind is not supported by any UA to date (2015), so we fake it.
    // http://crbug.com/33099
    this.video_.playbackRate = 0;
    this.onRewindTimer_(this.video_.currentTime, Date.now(), rate);
  }
  this.playbackRate_ = rate;
};


/**
 * Returns the current playbackRate.
 * @return {number}
 * @export
 */
shaka.player.Player.prototype.getPlaybackRate = function() {
  return this.playbackRate_;
};


/**
 * @param {number} startTime Desired time (in seconds) for playback
 *      to begin from.
 * @export
 */
shaka.player.Player.prototype.setPlaybackStartTime = function(startTime) {
  this.playbackStartTime_ = startTime;
};


/**
 * @return {boolean}
 * @export
 */
shaka.player.Player.prototype.isLive = function() {
  return this.videoSource_ ? this.videoSource_.isLive() : false;
};


/**
 * <p>
 * Configures the Player. Configuration options are set via key-value pairs. The
 * default options are defined in {@link shaka.player.Defaults}.
 * </p>
 *
 * The following configuration options are supported:
 * <ul>
 * <li>
 *   <b>enableAdaptation</b>: boolean <br>
 *   Enables or disables automatic bitrate adaptation.
 *
 * <li>
 *   <b>streamBufferSize</b>: number <br>
 *   Sets the maximum amount of content, in seconds, that audio and video
 *   streams will buffer ahead of the playhead.  For DASH streams, this will
 *   be overridden if 'minBufferTime' is larger.
 *
 * <li>
 *  <b>liveStreamEndTimeout</b>: number <br>
 *  Sets the amount of time that the player will wait after the last segment
 *  to determine if a live stream has ended.
 *
 * <li>
 *   <b>licenseRequestTimeout</b>: number <br>
 *   Sets the license request timeout in seconds. A value of zero indicates
 *   no timeout.
 *
 * <li>
 *   <b>mpdRequestTimeout</b>: number <br>
 *   Sets the MPD request timeout in seconds. A value of zero indicates
 *   no timeout.
 *
 * <li>
 *   <b>segmentRequestTimeout</b>: number <br>
 *   Sets the segment request timeout in seconds. A value of zero indicates
 *   no timeout.
 *
 * <li>
 *   <b>preferredLanguage</b>: string <br>
 *   Sets the preferred language (the default is 'en'). This affects which
 *   audio and video tracks are initially chosen. <br>
 *   See {@link https://tools.ietf.org/html/rfc5646 IETF RFC 5646}. <br>
 *   See {@link http://www.iso.org/iso/home/standards/language_codes.htm
 *        ISO 639}.
 *
 * <li>
 *   <b>restrictions</b>: shaka.player.Restrictions <br>
 *   Sets the video track restrictions. For example, if minBandwidth = 200000
 *   and maxBandwidth = 700000 then the player will only permit switching to
 *   video tracks with bandwidths between 200000 and 700000.
 *
 * <li>
 *   <b>disableCacheBustingEvenThoughItMayAffectBandwidthEstimation</b>: boolean
 *   <br>
 *   Disables all use of cache-busting parameters, even though it may affect
 *   bandwidth estimation.  This is a stop-gap measure for Shaka v1, since many
 *   people have had issues with the cache-busting parameters we typically add
 *   to network requests.  Shaka v2 will be cache-friendly by default.
 *   <br>
 *   Please note that Shaka v1's bandwidth estimation algorithm can be adversely
 *   affected by caching, in particular when seeking in low-bandwidth
 *   environments, such as mobile devices.
 *   <br>
 *   If cache-busting is a problem for your application or your CDN, use this
 *   parameter to disable it.
 *
 * </ul>
 *
 * @example
 *     player.configure({'enableAdaptation': false});
 *     player.configure({'preferredLanguage': 'en',
 *                       'streamBufferSize': 15});
 *
 * @param {Object.<string, *>} config A configuration object, which contains
 *     the configuration options as key-value pairs.
 * @throws TypeError if a configuration option has the wrong type.
 * @throws RangeError if a configuration option is out of range.
 * @export
 */
shaka.player.Player.prototype.configure = function(config) {
  if (!config) return;

  // Alias.
  var MapUtils = shaka.util.MapUtils;

  var enableAdaptation = MapUtils.getBoolean(config, 'enableAdaptation');
  if (enableAdaptation != null) {
    this.videoSourceConfig_['enableAdaptation'] = enableAdaptation;
  }

  var streamBufferSize = MapUtils.getNumber(config, 'streamBufferSize', 0);
  if (streamBufferSize != null) {
    this.videoSourceConfig_['streamBufferSize'] = streamBufferSize;
  }

  var liveStreamEndedTimeout =
      MapUtils.getNumber(config, 'liveStreamEndTimeout', 0);
  if (liveStreamEndedTimeout != null) {
    this.videoSourceConfig_['liveStreamEndTimeout'] = liveStreamEndedTimeout;
  }

  var licenseRequestTimeout =
      MapUtils.getNumber(config, 'licenseRequestTimeout', 0);
  if (licenseRequestTimeout != null) {
    this.videoSourceConfig_['licenseRequestTimeout'] = licenseRequestTimeout;
  }

  var mpdRequestTimeout = MapUtils.getNumber(config, 'mpdRequestTimeout', 0);
  if (mpdRequestTimeout != null) {
    this.videoSourceConfig_['mpdRequestTimeout'] = mpdRequestTimeout;
  }

  var segmentRequestTimeout =
      MapUtils.getNumber(config, 'segmentRequestTimeout', 0);
  if (segmentRequestTimeout != null) {
    this.videoSourceConfig_['segmentRequestTimeout'] = segmentRequestTimeout;
  }

  var preferredLanguage = MapUtils.getString(config, 'preferredLanguage');
  if (preferredLanguage != null) {
    this.videoSourceConfig_['preferredLanguage'] = preferredLanguage;
  }

  var restrictions = MapUtils.getAsInstanceType(
      config, 'restrictions', shaka.player.Restrictions);
  if (restrictions != null) {
    this.videoSourceConfig_['restrictions'] = restrictions.clone();
  }

  var disableCacheBusting = MapUtils.getBoolean(config,
      'disableCacheBustingEvenThoughItMayAffectBandwidthEstimation');
  if (disableCacheBusting != null) {
    shaka.util.AjaxRequest.enableCacheBusting = !disableCacheBusting;
  }

  if (this.videoSource_) {
    this.videoSource_.configure(this.videoSourceConfig_);
  }
};


/**
 * Gets the Player's configuration.
 *
 * @return {!Object.<string, *>} A configuration object.
 * @see {@link shaka.player.Player#configure}
 * @export
 */
shaka.player.Player.prototype.getConfiguration = function() {
  return this.videoSourceConfig_;
};


/**
 * Cancels the rewind timer, if any.
 * @private
 */
shaka.player.Player.prototype.cancelRewindTimer_ = function() {
  if (this.rewindTimer_) {
    window.clearTimeout(this.rewindTimer_);
    this.rewindTimer_ = null;
  }
};


/**
 * Starts the watchdog timer.
 * @private
 */
shaka.player.Player.prototype.startWatchdogTimer_ = function() {
  this.cancelWatchdogTimer_();
  this.watchdogTimer_ =
      window.setTimeout(this.onWatchdogTimer_.bind(this), 100);
};


/**
 * Cancels the watchdog timer, if any.
 * @private
 */
shaka.player.Player.prototype.cancelWatchdogTimer_ = function() {
  if (this.watchdogTimer_) {
    window.clearTimeout(this.watchdogTimer_);
    this.watchdogTimer_ = null;
  }
};


/**
 * Called on a recurring timer to simulate rewind.
 * @param {number} startVideoTime
 * @param {number} startWallTime
 * @param {number} rate
 * @private
 */
shaka.player.Player.prototype.onRewindTimer_ =
    function(startVideoTime, startWallTime, rate) {
  shaka.asserts.assert(rate < 0);
  shaka.asserts.assert(!this.video_.paused);

  this.rewindTimer_ = null;

  var offset = ((Date.now() - startWallTime) / 1000) * rate;

  // For live content the seek start time may increase over time, so to avoid
  // any races between this function and onSeekRangeChanged_() use a larger
  // fudge factor.
  var fudge = this.isLive() ? 1 : 0.05;

  if (this.video_.currentTime < this.seekRangeStart_ + fudge) {
    // Hit the beginning (or near enough), so pause.
    this.video_.pause();
  } else {
    var goal = Math.max(this.seekRangeStart_, startVideoTime + offset);
    this.video_.currentTime = goal;
    var callback = this.onRewindTimer_.bind(
        this, startVideoTime, startWallTime, rate);
    this.rewindTimer_ = window.setTimeout(
        callback, shaka.player.Player.REWIND_UPDATE_INTERVAL_ * 1000);
  }
};


/**
 * Called to enter a buffering state.
 * @private
 */
shaka.player.Player.prototype.enterBufferingState_ = function() {
  this.buffering_ = true;
  this.video_.pause();
  this.stats_.logBufferingEvent();
  shaka.timer.begin('buffering');
  shaka.log.debug('Buffering...');
  this.dispatchEvent(shaka.util.FakeEvent.create({type: 'bufferingStart'}));
};


/**
 * Called to leave a buffering state, either due to unloading a video source,
 * unpausing a video, or because of the watchdog's decision.
 * @private
 */
shaka.player.Player.prototype.endBufferingState_ = function() {
  shaka.asserts.assert(this.buffering_);
  shaka.log.debug('Buffering complete.');
  shaka.timer.end('buffering');
  this.stats_.logBufferingTime(shaka.timer.get('buffering'));
  this.buffering_ = false;
  this.dispatchEvent(shaka.util.FakeEvent.create({type: 'bufferingEnd'}));
};


/**
 * Called on a recurring timer to detect buffering events.
 * @private
 */
shaka.player.Player.prototype.onWatchdogTimer_ = function() {
  this.startWatchdogTimer_();

  if (this.video_.ended || this.video_.seeking) return;

  var bufferedProp = this.video_.buffered;
  // Counter-intuitively, the play head can advance audio-only while the video
  // is buffering.  |bufferedProp| will show the intersection of buffered
  // ranges for both audio and video, so it's an accurate way to determine if
  // we are buffering or not.  The 'stalled', 'waiting', and 'suspended' events
  // do not work for this purpose as of Chrome 38.  Nor will video.readyState.
  var bufferEnd =
      bufferedProp.length ? bufferedProp.end(bufferedProp.length - 1) : 0;
  var buffered = Math.max(bufferEnd - this.video_.currentTime, 0);

  var fudgeFactor = shaka.player.Player.BUFFERED_FUDGE_FACTOR_;
  var threshold = shaka.player.Player.UNDERFLOW_THRESHOLD_;

  var durationProp = this.video_.duration;
  var d = isNaN(durationProp) ? 0 : Math.max(durationProp - fudgeFactor, 0);
  var atEnd = (bufferEnd >= d) || (this.video_.currentTime >= d);

  if (!this.buffering_) {
    // If there are no buffered ranges but the playhead is at the end of
    // the video then we shouldn't enter a buffering state.
    if (!this.video_.paused && !atEnd && buffered < threshold) {
      this.enterBufferingState_();
    }
  } else {
    var bufferingGoal = this.videoSource_.getBufferingGoal();
    if (atEnd || buffered > bufferingGoal) {
      this.endBufferingState_();
      this.video_.play();
    }
  }
};


/**
 * The threshold for underflow, in seconds.  If there is less than this amount
 * of data buffered, we will consider the player to be out of data.
 *
 * @private {number}
 * @const
 */
shaka.player.Player.UNDERFLOW_THRESHOLD_ = 0.5;


/**
 * A fudge factor to apply to buffered ranges and durations to determine if the
 * video has buffered all available content.
 *
 * @private {number}
 * @const
 */
shaka.player.Player.BUFFERED_FUDGE_FACTOR_ = 0.05;


/**
 * The number of seconds for each rewind update interval.
 *
 * @private {number}
 * @const
 */
shaka.player.Player.REWIND_UPDATE_INTERVAL_ = 0.25;


/**
 * A map of MediaError codes to error messages.  The JS interpreter won't take
 * a symbolic name as a key, so the symbolic names for these error codes appear
 * in comments after the number.
 *
 * @private {!Object.<number, string>}
 * @const
 */
shaka.player.Player.MEDIA_ERROR_MAP_ = {
  // This should not occur for DASH sources, but may occur for HTTP sources.
  2: // MediaError.MEDIA_ERR_NETWORK
      'A network failure occured while loading media content.',

  3: // MediaError.MEDIA_ERR_DECODE
      'The browser failed to decode the media content.',

  // This is also unlikely for DASH sources, but HTTP sources do not check
  // browser support before beginning playback.
  4: // MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED
      'The browser does not support the media content.'
};


/**
 * The ID of a style element used to control text styles.
 *
 * @private {string}
 * @const
 */
shaka.player.Player.STYLE_ELEMENT_ID_ = 'ShakaPlayerTextStyle';