/**
 * @file segment-loader.js
 */
'use strict';

Object.defineProperty(exports, '__esModule', {
  value: true
});

var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();

var _get = function get(_x4, _x5, _x6) { var _again = true; _function: while (_again) { var object = _x4, property = _x5, receiver = _x6; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x4 = parent; _x5 = property; _x6 = receiver; _again = true; desc = parent = undefined; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } };

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }

function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

var _playlist = require('./playlist');

var _playlist2 = _interopRequireDefault(_playlist);

var _videoJs = require('video.js');

var _videoJs2 = _interopRequireDefault(_videoJs);

var _sourceUpdater = require('./source-updater');

var _sourceUpdater2 = _interopRequireDefault(_sourceUpdater);

var _config = require('./config');

var _config2 = _interopRequireDefault(_config);

var _globalWindow = require('global/window');

var _globalWindow2 = _interopRequireDefault(_globalWindow);

var _videojsContribMediaSourcesEs5RemoveCuesFromTrackJs = require('videojs-contrib-media-sources/es5/remove-cues-from-track.js');

var _videojsContribMediaSourcesEs5RemoveCuesFromTrackJs2 = _interopRequireDefault(_videojsContribMediaSourcesEs5RemoveCuesFromTrackJs);

var _binUtils = require('./bin-utils');

var _mediaSegmentRequest = require('./media-segment-request');

var _ranges = require('./ranges');

var _playlistSelectors = require('./playlist-selectors');

// in ms
var CHECK_BUFFER_DELAY = 500;

/**
 * Determines if we should call endOfStream on the media source based
 * on the state of the buffer or if appened segment was the final
 * segment in the playlist.
 *
 * @param {Object} playlist a media playlist object
 * @param {Object} mediaSource the MediaSource object
 * @param {Number} segmentIndex the index of segment we last appended
 * @returns {Boolean} do we need to call endOfStream on the MediaSource
 */
var detectEndOfStream = function detectEndOfStream(playlist, mediaSource, segmentIndex) {
  if (!playlist || !mediaSource) {
    return false;
  }

  var segments = playlist.segments;

  // determine a few boolean values to help make the branch below easier
  // to read
  var appendedLastSegment = segmentIndex === segments.length;

  // if we've buffered to the end of the video, we need to call endOfStream
  // so that MediaSources can trigger the `ended` event when it runs out of
  // buffered data instead of waiting for me
  return playlist.endList && mediaSource.readyState === 'open' && appendedLastSegment;
};

var finite = function finite(num) {
  return typeof num === 'number' && isFinite(num);
};

var illegalMediaSwitch = function illegalMediaSwitch(loaderType, startingMedia, newSegmentMedia) {
  // Although these checks should most likely cover non 'main' types, for now it narrows
  // the scope of our checks.
  if (loaderType !== 'main' || !startingMedia || !newSegmentMedia) {
    return null;
  }

  if (!newSegmentMedia.containsAudio && !newSegmentMedia.containsVideo) {
    return 'Neither audio nor video found in segment.';
  }

  if (startingMedia.containsVideo && !newSegmentMedia.containsVideo) {
    return 'Only audio found in segment when we expected video.' + ' We can\'t switch to audio only from a stream that had video.' + ' To get rid of this message, please add codec information to the manifest.';
  }

  if (!startingMedia.containsVideo && newSegmentMedia.containsVideo) {
    return 'Video found in segment when we expected only audio.' + ' We can\'t switch to a stream with video from an audio only stream.' + ' To get rid of this message, please add codec information to the manifest.';
  }

  return null;
};

exports.illegalMediaSwitch = illegalMediaSwitch;
/**
 * An object that manages segment loading and appending.
 *
 * @class SegmentLoader
 * @param {Object} options required and optional options
 * @extends videojs.EventTarget
 */

var SegmentLoader = (function (_videojs$EventTarget) {
  _inherits(SegmentLoader, _videojs$EventTarget);

  function SegmentLoader(settings) {
    var _this = this;

    var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];

    _classCallCheck(this, SegmentLoader);

    _get(Object.getPrototypeOf(SegmentLoader.prototype), 'constructor', this).call(this);
    // check pre-conditions
    if (!settings) {
      throw new TypeError('Initialization settings are required');
    }
    if (typeof settings.currentTime !== 'function') {
      throw new TypeError('No currentTime getter specified');
    }
    if (!settings.mediaSource) {
      throw new TypeError('No MediaSource specified');
    }
    // public properties
    this.state = 'INIT';
    this.bandwidth = settings.bandwidth;
    this.throughput = { rate: 0, count: 0 };
    this.roundTrip = NaN;
    this.resetStats_();
    this.mediaIndex = null;

    // private settings
    this.hasPlayed_ = settings.hasPlayed;
    this.currentTime_ = settings.currentTime;
    this.seekable_ = settings.seekable;
    this.seeking_ = settings.seeking;
    this.duration_ = settings.duration;
    this.mediaSource_ = settings.mediaSource;
    this.hls_ = settings.hls;
    this.loaderType_ = settings.loaderType;
    this.startingMedia_ = void 0;
    this.segmentMetadataTrack_ = settings.segmentMetadataTrack;
    this.goalBufferLength_ = settings.goalBufferLength;

    // private instance variables
    this.checkBufferTimeout_ = null;
    this.error_ = void 0;
    this.currentTimeline_ = -1;
    this.pendingSegment_ = null;
    this.mimeType_ = null;
    this.sourceUpdater_ = null;
    this.xhrOptions_ = null;

    // Fragmented mp4 playback
    this.activeInitSegmentId_ = null;
    this.initSegments_ = {};

    this.decrypter_ = settings.decrypter;

    // Manages the tracking and generation of sync-points, mappings
    // between a time in the display time and a segment index within
    // a playlist
    this.syncController_ = settings.syncController;
    this.syncPoint_ = {
      segmentIndex: 0,
      time: 0
    };

    this.syncController_.on('syncinfoupdate', function () {
      return _this.trigger('syncinfoupdate');
    });

    this.mediaSource_.addEventListener('sourceopen', function () {
      return _this.ended_ = false;
    });

    // ...for determining the fetch location
    this.fetchAtBuffer_ = false;

    if (options.debug) {
      this.logger_ = _videoJs2['default'].log.bind(_videoJs2['default'], 'segment-loader', this.loaderType_, '->');
    }
  }

  /**
   * reset all of our media stats
   *
   * @private
   */

  _createClass(SegmentLoader, [{
    key: 'resetStats_',
    value: function resetStats_() {
      this.mediaBytesTransferred = 0;
      this.mediaRequests = 0;
      this.mediaRequestsAborted = 0;
      this.mediaRequestsTimedout = 0;
      this.mediaRequestsErrored = 0;
      this.mediaTransferDuration = 0;
      this.mediaSecondsLoaded = 0;
    }

    /**
     * dispose of the SegmentLoader and reset to the default state
     */
  }, {
    key: 'dispose',
    value: function dispose() {
      this.state = 'DISPOSED';
      this.pause();
      this.abort_();
      if (this.sourceUpdater_) {
        this.sourceUpdater_.dispose();
      }
      this.resetStats_();
    }

    /**
     * abort anything that is currently doing on with the SegmentLoader
     * and reset to a default state
     */
  }, {
    key: 'abort',
    value: function abort() {
      if (this.state !== 'WAITING') {
        if (this.pendingSegment_) {
          this.pendingSegment_ = null;
        }
        return;
      }

      this.abort_();

      // We aborted the requests we were waiting on, so reset the loader's state to READY
      // since we are no longer "waiting" on any requests. XHR callback is not always run
      // when the request is aborted. This will prevent the loader from being stuck in the
      // WAITING state indefinitely.
      this.state = 'READY';

      // don't wait for buffer check timeouts to begin fetching the
      // next segment
      if (!this.paused()) {
        this.monitorBuffer_();
      }
    }

    /**
     * abort all pending xhr requests and null any pending segements
     *
     * @private
     */
  }, {
    key: 'abort_',
    value: function abort_() {
      if (this.pendingSegment_) {
        this.pendingSegment_.abortRequests();
      }

      // clear out the segment being processed
      this.pendingSegment_ = null;
    }

    /**
     * set an error on the segment loader and null out any pending segements
     *
     * @param {Error} error the error to set on the SegmentLoader
     * @return {Error} the error that was set or that is currently set
     */
  }, {
    key: 'error',
    value: function error(_error) {
      if (typeof _error !== 'undefined') {
        this.error_ = _error;
      }

      this.pendingSegment_ = null;
      return this.error_;
    }
  }, {
    key: 'endOfStream',
    value: function endOfStream() {
      this.ended_ = true;
      this.pause();
      this.trigger('ended');
    }

    /**
     * Indicates which time ranges are buffered
     *
     * @return {TimeRange}
     *         TimeRange object representing the current buffered ranges
     */
  }, {
    key: 'buffered_',
    value: function buffered_() {
      if (!this.sourceUpdater_) {
        return _videoJs2['default'].createTimeRanges();
      }

      return this.sourceUpdater_.buffered();
    }

    /**
     * Gets and sets init segment for the provided map
     *
     * @param {Object} map
     *        The map object representing the init segment to get or set
     * @param {Boolean=} set
     *        If true, the init segment for the provided map should be saved
     * @return {Object}
     *         map object for desired init segment
     */
  }, {
    key: 'initSegment',
    value: function initSegment(map) {
      var set = arguments.length <= 1 || arguments[1] === undefined ? false : arguments[1];

      if (!map) {
        return null;
      }

      var id = (0, _binUtils.initSegmentId)(map);
      var storedMap = this.initSegments_[id];

      if (set && !storedMap && map.bytes) {
        this.initSegments_[id] = storedMap = {
          resolvedUri: map.resolvedUri,
          byterange: map.byterange,
          bytes: map.bytes
        };
      }

      return storedMap || map;
    }

    /**
     * Returns true if all configuration required for loading is present, otherwise false.
     *
     * @return {Boolean} True if the all configuration is ready for loading
     * @private
     */
  }, {
    key: 'couldBeginLoading_',
    value: function couldBeginLoading_() {
      return this.playlist_ && (
      // the source updater is created when init_ is called, so either having a
      // source updater or being in the INIT state with a mimeType is enough
      // to say we have all the needed configuration to start loading.
      this.sourceUpdater_ || this.mimeType_ && this.state === 'INIT') && !this.paused();
    }

    /**
     * load a playlist and start to fill the buffer
     */
  }, {
    key: 'load',
    value: function load() {
      // un-pause
      this.monitorBuffer_();

      // if we don't have a playlist yet, keep waiting for one to be
      // specified
      if (!this.playlist_) {
        return;
      }

      // not sure if this is the best place for this
      this.syncController_.setDateTimeMapping(this.playlist_);

      // if all the configuration is ready, initialize and begin loading
      if (this.state === 'INIT' && this.couldBeginLoading_()) {
        return this.init_();
      }

      // if we're in the middle of processing a segment already, don't
      // kick off an additional segment request
      if (!this.couldBeginLoading_() || this.state !== 'READY' && this.state !== 'INIT') {
        return;
      }

      this.state = 'READY';
    }

    /**
     * Once all the starting parameters have been specified, begin
     * operation. This method should only be invoked from the INIT
     * state.
     *
     * @private
     */
  }, {
    key: 'init_',
    value: function init_() {
      this.state = 'READY';
      this.sourceUpdater_ = new _sourceUpdater2['default'](this.mediaSource_, this.mimeType_);
      this.resetEverything();
      return this.monitorBuffer_();
    }

    /**
     * set a playlist on the segment loader
     *
     * @param {PlaylistLoader} media the playlist to set on the segment loader
     */
  }, {
    key: 'playlist',
    value: function playlist(newPlaylist) {
      var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];

      if (!newPlaylist) {
        return;
      }

      var oldPlaylist = this.playlist_;
      var segmentInfo = this.pendingSegment_;

      this.playlist_ = newPlaylist;
      this.xhrOptions_ = options;

      // when we haven't started playing yet, the start of a live playlist
      // is always our zero-time so force a sync update each time the playlist
      // is refreshed from the server
      if (!this.hasPlayed_()) {
        newPlaylist.syncInfo = {
          mediaSequence: newPlaylist.mediaSequence,
          time: 0
        };
      }

      // in VOD, this is always a rendition switch (or we updated our syncInfo above)
      // in LIVE, we always want to update with new playlists (including refreshes)
      this.trigger('syncinfoupdate');

      // if we were unpaused but waiting for a playlist, start
      // buffering now
      if (this.state === 'INIT' && this.couldBeginLoading_()) {
        return this.init_();
      }

      if (!oldPlaylist || oldPlaylist.uri !== newPlaylist.uri) {
        if (this.mediaIndex !== null) {
          // we must "resync" the segment loader when we switch renditions and
          // the segment loader is already synced to the previous rendition
          this.resyncLoader();
        }

        // the rest of this function depends on `oldPlaylist` being defined
        return;
      }

      // we reloaded the same playlist so we are in a live scenario
      // and we will likely need to adjust the mediaIndex
      var mediaSequenceDiff = newPlaylist.mediaSequence - oldPlaylist.mediaSequence;

      this.logger_('mediaSequenceDiff', mediaSequenceDiff);

      // update the mediaIndex on the SegmentLoader
      // this is important because we can abort a request and this value must be
      // equal to the last appended mediaIndex
      if (this.mediaIndex !== null) {
        this.mediaIndex -= mediaSequenceDiff;
      }

      // update the mediaIndex on the SegmentInfo object
      // this is important because we will update this.mediaIndex with this value
      // in `handleUpdateEnd_` after the segment has been successfully appended
      if (segmentInfo) {
        segmentInfo.mediaIndex -= mediaSequenceDiff;

        // we need to update the referenced segment so that timing information is
        // saved for the new playlist's segment, however, if the segment fell off the
        // playlist, we can leave the old reference and just lose the timing info
        if (segmentInfo.mediaIndex >= 0) {
          segmentInfo.segment = newPlaylist.segments[segmentInfo.mediaIndex];
        }
      }

      this.syncController_.saveExpiredSegmentInfo(oldPlaylist, newPlaylist);
    }

    /**
     * Prevent the loader from fetching additional segments. If there
     * is a segment request outstanding, it will finish processing
     * before the loader halts. A segment loader can be unpaused by
     * calling load().
     */
  }, {
    key: 'pause',
    value: function pause() {
      if (this.checkBufferTimeout_) {
        _globalWindow2['default'].clearTimeout(this.checkBufferTimeout_);

        this.checkBufferTimeout_ = null;
      }
    }

    /**
     * Returns whether the segment loader is fetching additional
     * segments when given the opportunity. This property can be
     * modified through calls to pause() and load().
     */
  }, {
    key: 'paused',
    value: function paused() {
      return this.checkBufferTimeout_ === null;
    }

    /**
     * create/set the following mimetype on the SourceBuffer through a
     * SourceUpdater
     *
     * @param {String} mimeType the mime type string to use
     */
  }, {
    key: 'mimeType',
    value: function mimeType(_mimeType) {
      if (this.mimeType_) {
        return;
      }

      this.mimeType_ = _mimeType;
      // if we were unpaused but waiting for a sourceUpdater, start
      // buffering now
      if (this.state === 'INIT' && this.couldBeginLoading_()) {
        this.init_();
      }
    }

    /**
     * Delete all the buffered data and reset the SegmentLoader
     */
  }, {
    key: 'resetEverything',
    value: function resetEverything() {
      this.ended_ = false;
      this.resetLoader();
      this.remove(0, this.duration_());
      this.trigger('reseteverything');
    }

    /**
     * Force the SegmentLoader to resync and start loading around the currentTime instead
     * of starting at the end of the buffer
     *
     * Useful for fast quality changes
     */
  }, {
    key: 'resetLoader',
    value: function resetLoader() {
      this.fetchAtBuffer_ = false;
      this.resyncLoader();
    }

    /**
     * Force the SegmentLoader to restart synchronization and make a conservative guess
     * before returning to the simple walk-forward method
     */
  }, {
    key: 'resyncLoader',
    value: function resyncLoader() {
      this.mediaIndex = null;
      this.syncPoint_ = null;
      this.abort();
    }

    /**
     * Remove any data in the source buffer between start and end times
     * @param {Number} start - the start time of the region to remove from the buffer
     * @param {Number} end - the end time of the region to remove from the buffer
     */
  }, {
    key: 'remove',
    value: function remove(start, end) {
      if (this.sourceUpdater_) {
        this.sourceUpdater_.remove(start, end);
      }
      (0, _videojsContribMediaSourcesEs5RemoveCuesFromTrackJs2['default'])(start, end, this.segmentMetadataTrack_);
    }

    /**
     * (re-)schedule monitorBufferTick_ to run as soon as possible
     *
     * @private
     */
  }, {
    key: 'monitorBuffer_',
    value: function monitorBuffer_() {
      if (this.checkBufferTimeout_) {
        _globalWindow2['default'].clearTimeout(this.checkBufferTimeout_);
      }

      this.checkBufferTimeout_ = _globalWindow2['default'].setTimeout(this.monitorBufferTick_.bind(this), 1);
    }

    /**
     * As long as the SegmentLoader is in the READY state, periodically
     * invoke fillBuffer_().
     *
     * @private
     */
  }, {
    key: 'monitorBufferTick_',
    value: function monitorBufferTick_() {
      if (this.state === 'READY') {
        this.fillBuffer_();
      }

      if (this.checkBufferTimeout_) {
        _globalWindow2['default'].clearTimeout(this.checkBufferTimeout_);
      }

      this.checkBufferTimeout_ = _globalWindow2['default'].setTimeout(this.monitorBufferTick_.bind(this), CHECK_BUFFER_DELAY);
    }

    /**
     * fill the buffer with segements unless the sourceBuffers are
     * currently updating
     *
     * Note: this function should only ever be called by monitorBuffer_
     * and never directly
     *
     * @private
     */
  }, {
    key: 'fillBuffer_',
    value: function fillBuffer_() {
      if (this.sourceUpdater_.updating()) {
        return;
      }

      if (!this.syncPoint_) {
        this.syncPoint_ = this.syncController_.getSyncPoint(this.playlist_, this.duration_(), this.currentTimeline_, this.currentTime_());
      }

      // see if we need to begin loading immediately
      var segmentInfo = this.checkBuffer_(this.buffered_(), this.playlist_, this.mediaIndex, this.hasPlayed_(), this.currentTime_(), this.syncPoint_);

      if (!segmentInfo) {
        return;
      }

      var isEndOfStream = detectEndOfStream(this.playlist_, this.mediaSource_, segmentInfo.mediaIndex);

      if (isEndOfStream) {
        this.endOfStream();
        return;
      }

      if (segmentInfo.mediaIndex === this.playlist_.segments.length - 1 && this.mediaSource_.readyState === 'ended' && !this.seeking_()) {
        return;
      }

      // We will need to change timestampOffset of the sourceBuffer if either of
      // the following conditions are true:
      // - The segment.timeline !== this.currentTimeline
      //   (we are crossing a discontinuity somehow)
      // - The "timestampOffset" for the start of this segment is less than
      //   the currently set timestampOffset
      if (segmentInfo.timeline !== this.currentTimeline_ || segmentInfo.startOfSegment !== null && segmentInfo.startOfSegment < this.sourceUpdater_.timestampOffset()) {
        this.syncController_.reset();
        segmentInfo.timestampOffset = segmentInfo.startOfSegment;
      }

      this.loadSegment_(segmentInfo);
    }

    /**
     * Determines what segment request should be made, given current playback
     * state.
     *
     * @param {TimeRanges} buffered - the state of the buffer
     * @param {Object} playlist - the playlist object to fetch segments from
     * @param {Number} mediaIndex - the previous mediaIndex fetched or null
     * @param {Boolean} hasPlayed - a flag indicating whether we have played or not
     * @param {Number} currentTime - the playback position in seconds
     * @param {Object} syncPoint - a segment info object that describes the
     * @returns {Object} a segment request object that describes the segment to load
     */
  }, {
    key: 'checkBuffer_',
    value: function checkBuffer_(buffered, playlist, mediaIndex, hasPlayed, currentTime, syncPoint) {
      var lastBufferedEnd = 0;
      var startOfSegment = undefined;

      if (buffered.length) {
        lastBufferedEnd = buffered.end(buffered.length - 1);
      }

      var bufferedTime = Math.max(0, lastBufferedEnd - currentTime);

      if (!playlist.segments.length) {
        return null;
      }

      // if there is plenty of content buffered, and the video has
      // been played before relax for awhile
      if (bufferedTime >= this.goalBufferLength_()) {
        return null;
      }

      // if the video has not yet played once, and we already have
      // one segment downloaded do nothing
      if (!hasPlayed && bufferedTime >= 1) {
        return null;
      }

      this.logger_('checkBuffer_', 'mediaIndex:', mediaIndex, 'hasPlayed:', hasPlayed, 'currentTime:', currentTime, 'syncPoint:', syncPoint, 'fetchAtBuffer:', this.fetchAtBuffer_, 'bufferedTime:', bufferedTime);

      // When the syncPoint is null, there is no way of determining a good
      // conservative segment index to fetch from
      // The best thing to do here is to get the kind of sync-point data by
      // making a request
      if (syncPoint === null) {
        mediaIndex = this.getSyncSegmentCandidate_(playlist);
        this.logger_('getSync', 'mediaIndex:', mediaIndex);
        return this.generateSegmentInfo_(playlist, mediaIndex, null, true);
      }

      // Under normal playback conditions fetching is a simple walk forward
      if (mediaIndex !== null) {
        this.logger_('walkForward', 'mediaIndex:', mediaIndex + 1);
        var segment = playlist.segments[mediaIndex];

        if (segment && segment.end) {
          startOfSegment = segment.end;
        } else {
          startOfSegment = lastBufferedEnd;
        }
        return this.generateSegmentInfo_(playlist, mediaIndex + 1, startOfSegment, false);
      }

      // There is a sync-point but the lack of a mediaIndex indicates that
      // we need to make a good conservative guess about which segment to
      // fetch
      if (this.fetchAtBuffer_) {
        // Find the segment containing the end of the buffer
        var mediaSourceInfo = _playlist2['default'].getMediaInfoForTime(playlist, lastBufferedEnd, syncPoint.segmentIndex, syncPoint.time);

        mediaIndex = mediaSourceInfo.mediaIndex;
        startOfSegment = mediaSourceInfo.startTime;
      } else {
        // Find the segment containing currentTime
        var mediaSourceInfo = _playlist2['default'].getMediaInfoForTime(playlist, currentTime, syncPoint.segmentIndex, syncPoint.time);

        mediaIndex = mediaSourceInfo.mediaIndex;
        startOfSegment = mediaSourceInfo.startTime;
      }
      this.logger_('getMediaIndexForTime', 'mediaIndex:', mediaIndex, 'startOfSegment:', startOfSegment);

      return this.generateSegmentInfo_(playlist, mediaIndex, startOfSegment, false);
    }

    /**
     * The segment loader has no recourse except to fetch a segment in the
     * current playlist and use the internal timestamps in that segment to
     * generate a syncPoint. This function returns a good candidate index
     * for that process.
     *
     * @param {Object} playlist - the playlist object to look for a
     * @returns {Number} An index of a segment from the playlist to load
     */
  }, {
    key: 'getSyncSegmentCandidate_',
    value: function getSyncSegmentCandidate_(playlist) {
      var _this2 = this;

      if (this.currentTimeline_ === -1) {
        return 0;
      }

      var segmentIndexArray = playlist.segments.map(function (s, i) {
        return {
          timeline: s.timeline,
          segmentIndex: i
        };
      }).filter(function (s) {
        return s.timeline === _this2.currentTimeline_;
      });

      if (segmentIndexArray.length) {
        return segmentIndexArray[Math.min(segmentIndexArray.length - 1, 1)].segmentIndex;
      }

      return Math.max(playlist.segments.length - 1, 0);
    }
  }, {
    key: 'generateSegmentInfo_',
    value: function generateSegmentInfo_(playlist, mediaIndex, startOfSegment, isSyncRequest) {
      if (mediaIndex < 0 || mediaIndex >= playlist.segments.length) {
        return null;
      }

      var segment = playlist.segments[mediaIndex];

      return {
        requestId: 'segment-loader-' + Math.random(),
        // resolve the segment URL relative to the playlist
        uri: segment.resolvedUri,
        // the segment's mediaIndex at the time it was requested
        mediaIndex: mediaIndex,
        // whether or not to update the SegmentLoader's state with this
        // segment's mediaIndex
        isSyncRequest: isSyncRequest,
        startOfSegment: startOfSegment,
        // the segment's playlist
        playlist: playlist,
        // unencrypted bytes of the segment
        bytes: null,
        // when a key is defined for this segment, the encrypted bytes
        encryptedBytes: null,
        // The target timestampOffset for this segment when we append it
        // to the source buffer
        timestampOffset: null,
        // The timeline that the segment is in
        timeline: segment.timeline,
        // The expected duration of the segment in seconds
        duration: segment.duration,
        // retain the segment in case the playlist updates while doing an async process
        segment: segment
      };
    }

    /**
     * Determines if the network has enough bandwidth to complete the current segment
     * request in a timely manner. If not, the request will be aborted early and bandwidth
     * updated to trigger a playlist switch.
     *
     * @param {Object} stats
     *        Object containing stats about the request timing and size
     * @return {Boolean} True if the request was aborted, false otherwise
     * @private
     */
  }, {
    key: 'abortRequestEarly_',
    value: function abortRequestEarly_(stats) {
      if (this.hls_.tech_.paused() ||
      // Don't abort if the current playlist is on the lowestEnabledRendition
      // TODO: Replace using timeout with a boolean indicating whether this playlist is
      //       the lowestEnabledRendition.
      !this.xhrOptions_.timeout ||
      // Don't abort if we have no bandwidth information to estimate segment sizes
      !this.playlist_.attributes.BANDWIDTH) {
        return false;
      }

      // Wait at least 1 second since the first byte of data has been received before
      // using the calculated bandwidth from the progress event to allow the bitrate
      // to stabilize
      if (Date.now() - (stats.firstBytesReceivedAt || Date.now()) < 1000) {
        return false;
      }

      var currentTime = this.currentTime_();
      var measuredBandwidth = stats.bandwidth;
      var segmentDuration = this.pendingSegment_.duration;

      var requestTimeRemaining = _playlist2['default'].estimateSegmentRequestTime(segmentDuration, measuredBandwidth, this.playlist_, stats.bytesReceived);

      // Subtract 1 from the timeUntilRebuffer so we still consider an early abort
      // if we are only left with less than 1 second when the request completes.
      // A negative timeUntilRebuffering indicates we are already rebuffering
      var timeUntilRebuffer = (0, _ranges.timeUntilRebuffer)(this.buffered_(), currentTime, this.hls_.tech_.playbackRate()) - 1;

      // Only consider aborting early if the estimated time to finish the download
      // is larger than the estimated time until the player runs out of forward buffer
      if (requestTimeRemaining <= timeUntilRebuffer) {
        return false;
      }

      var switchCandidate = (0, _playlistSelectors.minRebufferMaxBandwidthSelector)({
        master: this.hls_.playlists.master,
        currentTime: currentTime,
        bandwidth: measuredBandwidth,
        duration: this.duration_(),
        segmentDuration: segmentDuration,
        timeUntilRebuffer: timeUntilRebuffer,
        currentTimeline: this.currentTimeline_,
        syncController: this.syncController_
      });

      if (!switchCandidate) {
        return;
      }

      var rebufferingImpact = requestTimeRemaining - timeUntilRebuffer;

      var timeSavedBySwitching = rebufferingImpact - switchCandidate.rebufferingImpact;

      var minimumTimeSaving = 0.5;

      // If we are already rebuffering, increase the amount of variance we add to the
      // potential round trip time of the new request so that we are not too aggressive
      // with switching to a playlist that might save us a fraction of a second.
      if (timeUntilRebuffer <= _ranges.TIME_FUDGE_FACTOR) {
        minimumTimeSaving = 1;
      }

      if (!switchCandidate.playlist || switchCandidate.playlist.uri === this.playlist_.uri || timeSavedBySwitching < minimumTimeSaving) {
        return false;
      }

      // set the bandwidth to that of the desired playlist being sure to scale by
      // BANDWIDTH_VARIANCE and add one so the playlist selector does not exclude it
      // don't trigger a bandwidthupdate as the bandwidth is artifial
      this.bandwidth = switchCandidate.playlist.attributes.BANDWIDTH * _config2['default'].BANDWIDTH_VARIANCE + 1;
      this.abort();
      this.trigger('earlyabort');
      return true;
    }

    /**
     * XHR `progress` event handler
     *
     * @param {Event}
     *        The XHR `progress` event
     * @param {Object} simpleSegment
     *        A simplified segment object copy
     * @private
     */
  }, {
    key: 'handleProgress_',
    value: function handleProgress_(event, simpleSegment) {
      if (!this.pendingSegment_ || simpleSegment.requestId !== this.pendingSegment_.requestId || this.abortRequestEarly_(simpleSegment.stats)) {
        return;
      }

      this.trigger('progress');
    }

    /**
     * load a specific segment from a request into the buffer
     *
     * @private
     */
  }, {
    key: 'loadSegment_',
    value: function loadSegment_(segmentInfo) {
      this.state = 'WAITING';
      this.pendingSegment_ = segmentInfo;
      this.trimBackBuffer_(segmentInfo);

      segmentInfo.abortRequests = (0, _mediaSegmentRequest.mediaSegmentRequest)(this.hls_.xhr, this.xhrOptions_, this.decrypter_, this.createSimplifiedSegmentObj_(segmentInfo),
      // progress callback
      this.handleProgress_.bind(this), this.segmentRequestFinished_.bind(this));
    }

    /**
     * trim the back buffer so that we don't have too much data
     * in the source buffer
     *
     * @private
     *
     * @param {Object} segmentInfo - the current segment
     */
  }, {
    key: 'trimBackBuffer_',
    value: function trimBackBuffer_(segmentInfo) {
      var seekable = this.seekable_();
      var currentTime = this.currentTime_();
      var removeToTime = 0;

      // Chrome has a hard limit of 150MB of
      // buffer and a very conservative "garbage collector"
      // We manually clear out the old buffer to ensure
      // we don't trigger the QuotaExceeded error
      // on the source buffer during subsequent appends

      // If we have a seekable range use that as the limit for what can be removed safely
      // otherwise remove anything older than 30 seconds before the current play head
      if (seekable.length && seekable.start(0) > 0 && seekable.start(0) < currentTime) {
        removeToTime = seekable.start(0);
      } else {
        removeToTime = currentTime - 30;
      }

      if (removeToTime > 0) {
        this.remove(0, removeToTime);
      }
    }

    /**
     * created a simplified copy of the segment object with just the
     * information necessary to perform the XHR and decryption
     *
     * @private
     *
     * @param {Object} segmentInfo - the current segment
     * @returns {Object} a simplified segment object copy
     */
  }, {
    key: 'createSimplifiedSegmentObj_',
    value: function createSimplifiedSegmentObj_(segmentInfo) {
      var segment = segmentInfo.segment;
      var simpleSegment = {
        resolvedUri: segment.resolvedUri,
        byterange: segment.byterange,
        requestId: segmentInfo.requestId
      };

      if (segment.key) {
        // if the media sequence is greater than 2^32, the IV will be incorrect
        // assuming 10s segments, that would be about 1300 years
        var iv = segment.key.iv || new Uint32Array([0, 0, 0, segmentInfo.mediaIndex + segmentInfo.playlist.mediaSequence]);

        simpleSegment.key = {
          resolvedUri: segment.key.resolvedUri,
          iv: iv
        };
      }

      if (segment.map) {
        simpleSegment.map = this.initSegment(segment.map);
      }

      return simpleSegment;
    }

    /**
     * Handle the callback from the segmentRequest function and set the
     * associated SegmentLoader state and errors if necessary
     *
     * @private
     */
  }, {
    key: 'segmentRequestFinished_',
    value: function segmentRequestFinished_(error, simpleSegment) {
      // every request counts as a media request even if it has been aborted
      // or canceled due to a timeout
      this.mediaRequests += 1;

      if (simpleSegment.stats) {
        this.mediaBytesTransferred += simpleSegment.stats.bytesReceived;
        this.mediaTransferDuration += simpleSegment.stats.roundTripTime;
      }

      // The request was aborted and the SegmentLoader has already been reset
      if (!this.pendingSegment_) {
        this.mediaRequestsAborted += 1;
        return;
      }

      // the request was aborted and the SegmentLoader has already started
      // another request. this can happen when the timeout for an aborted
      // request triggers due to a limitation in the XHR library
      // do not count this as any sort of request or we risk double-counting
      if (simpleSegment.requestId !== this.pendingSegment_.requestId) {
        return;
      }

      // an error occurred from the active pendingSegment_ so reset everything
      if (error) {
        this.pendingSegment_ = null;
        this.state = 'READY';

        // the requests were aborted just record the aborted stat and exit
        // this is not a true error condition and nothing corrective needs
        // to be done
        if (error.code === _mediaSegmentRequest.REQUEST_ERRORS.ABORTED) {
          this.mediaRequestsAborted += 1;
          return;
        }

        this.pause();

        // the error is really just that at least one of the requests timed-out
        // set the bandwidth to a very low value and trigger an ABR switch to
        // take emergency action
        if (error.code === _mediaSegmentRequest.REQUEST_ERRORS.TIMEOUT) {
          this.mediaRequestsTimedout += 1;
          this.bandwidth = 1;
          this.roundTrip = NaN;
          this.trigger('bandwidthupdate');
          return;
        }

        // if control-flow has arrived here, then the error is real
        // emit an error event to blacklist the current playlist
        this.mediaRequestsErrored += 1;
        this.error(error);
        this.trigger('error');
        return;
      }

      // the response was a success so set any bandwidth stats the request
      // generated for ABR purposes
      this.bandwidth = simpleSegment.stats.bandwidth;
      this.roundTrip = simpleSegment.stats.roundTripTime;

      // if this request included an initialization segment, save that data
      // to the initSegment cache
      if (simpleSegment.map) {
        simpleSegment.map = this.initSegment(simpleSegment.map, true);
      }

      this.processSegmentResponse_(simpleSegment);
    }

    /**
     * Move any important data from the simplified segment object
     * back to the real segment object for future phases
     *
     * @private
     */
  }, {
    key: 'processSegmentResponse_',
    value: function processSegmentResponse_(simpleSegment) {
      var segmentInfo = this.pendingSegment_;

      segmentInfo.bytes = simpleSegment.bytes;
      if (simpleSegment.map) {
        segmentInfo.segment.map.bytes = simpleSegment.map.bytes;
      }

      segmentInfo.endOfAllRequests = simpleSegment.endOfAllRequests;
      this.handleSegment_();
    }

    /**
     * append a decrypted segement to the SourceBuffer through a SourceUpdater
     *
     * @private
     */
  }, {
    key: 'handleSegment_',
    value: function handleSegment_() {
      var _this3 = this;

      if (!this.pendingSegment_) {
        this.state = 'READY';
        return;
      }

      var segmentInfo = this.pendingSegment_;
      var segment = segmentInfo.segment;
      var timingInfo = this.syncController_.probeSegmentInfo(segmentInfo);

      // When we have our first timing info, determine what media types this loader is
      // dealing with. Although we're maintaining extra state, it helps to preserve the
      // separation of segment loader from the actual source buffers.
      if (typeof this.startingMedia_ === 'undefined' && timingInfo && (
      // Guard against cases where we're not getting timing info at all until we are
      // certain that all streams will provide it.
      timingInfo.containsAudio || timingInfo.containsVideo)) {
        this.startingMedia_ = {
          containsAudio: timingInfo.containsAudio,
          containsVideo: timingInfo.containsVideo
        };
      }

      var illegalMediaSwitchError = illegalMediaSwitch(this.loaderType_, this.startingMedia_, timingInfo);

      if (illegalMediaSwitchError) {
        this.error({
          message: illegalMediaSwitchError
        });
        this.trigger('error');
        return;
      }

      this.state = 'APPENDING';

      if (segmentInfo.isSyncRequest) {
        this.trigger('syncinfoupdate');
        this.pendingSegment_ = null;
        this.state = 'READY';
        return;
      }

      if (segmentInfo.timestampOffset !== null && segmentInfo.timestampOffset !== this.sourceUpdater_.timestampOffset()) {
        this.sourceUpdater_.timestampOffset(segmentInfo.timestampOffset);
        // fired when a timestamp offset is set in HLS (can also identify discontinuities)
        this.trigger('timestampoffset');
      }

      // if the media initialization segment is changing, append it
      // before the content segment
      if (segment.map) {
        (function () {
          var initId = (0, _binUtils.initSegmentId)(segment.map);

          if (!_this3.activeInitSegmentId_ || _this3.activeInitSegmentId_ !== initId) {
            var initSegment = _this3.initSegment(segment.map);

            _this3.sourceUpdater_.appendBuffer(initSegment.bytes, function () {
              _this3.activeInitSegmentId_ = initId;
            });
          }
        })();
      }

      segmentInfo.byteLength = segmentInfo.bytes.byteLength;
      if (typeof segment.start === 'number' && typeof segment.end === 'number') {
        this.mediaSecondsLoaded += segment.end - segment.start;
      } else {
        this.mediaSecondsLoaded += segment.duration;
      }

      this.sourceUpdater_.appendBuffer(segmentInfo.bytes, this.handleUpdateEnd_.bind(this));
    }

    /**
     * callback to run when appendBuffer is finished. detects if we are
     * in a good state to do things with the data we got, or if we need
     * to wait for more
     *
     * @private
     */
  }, {
    key: 'handleUpdateEnd_',
    value: function handleUpdateEnd_() {
      this.logger_('handleUpdateEnd_', 'segmentInfo:', this.pendingSegment_);

      if (!this.pendingSegment_) {
        this.state = 'READY';
        if (!this.paused()) {
          this.monitorBuffer_();
        }
        return;
      }

      var segmentInfo = this.pendingSegment_;
      var segment = segmentInfo.segment;
      var isWalkingForward = this.mediaIndex !== null;

      this.pendingSegment_ = null;
      this.recordThroughput_(segmentInfo);
      this.addSegmentMetadataCue_(segmentInfo);

      this.state = 'READY';

      this.mediaIndex = segmentInfo.mediaIndex;
      this.fetchAtBuffer_ = true;
      this.currentTimeline_ = segmentInfo.timeline;

      // We must update the syncinfo to recalculate the seekable range before
      // the following conditional otherwise it may consider this a bad "guess"
      // and attempt to resync when the post-update seekable window and live
      // point would mean that this was the perfect segment to fetch
      this.trigger('syncinfoupdate');

      // If we previously appended a segment that ends more than 3 targetDurations before
      // the currentTime_ that means that our conservative guess was too conservative.
      // In that case, reset the loader state so that we try to use any information gained
      // from the previous request to create a new, more accurate, sync-point.
      if (segment.end && this.currentTime_() - segment.end > segmentInfo.playlist.targetDuration * 3) {
        this.resetEverything();
        return;
      }

      // Don't do a rendition switch unless we have enough time to get a sync segment
      // and conservatively guess
      if (isWalkingForward) {
        this.trigger('bandwidthupdate');
      }
      this.trigger('progress');

      // any time an update finishes and the last segment is in the
      // buffer, end the stream. this ensures the "ended" event will
      // fire if playback reaches that point.
      var isEndOfStream = detectEndOfStream(segmentInfo.playlist, this.mediaSource_, segmentInfo.mediaIndex + 1);

      if (isEndOfStream) {
        this.endOfStream();
      }

      if (!this.paused()) {
        this.monitorBuffer_();
      }
    }

    /**
     * Records the current throughput of the decrypt, transmux, and append
     * portion of the semgment pipeline. `throughput.rate` is a the cumulative
     * moving average of the throughput. `throughput.count` is the number of
     * data points in the average.
     *
     * @private
     * @param {Object} segmentInfo the object returned by loadSegment
     */
  }, {
    key: 'recordThroughput_',
    value: function recordThroughput_(segmentInfo) {
      var rate = this.throughput.rate;
      // Add one to the time to ensure that we don't accidentally attempt to divide
      // by zero in the case where the throughput is ridiculously high
      var segmentProcessingTime = Date.now() - segmentInfo.endOfAllRequests + 1;
      // Multiply by 8000 to convert from bytes/millisecond to bits/second
      var segmentProcessingThroughput = Math.floor(segmentInfo.byteLength / segmentProcessingTime * 8 * 1000);

      // This is just a cumulative moving average calculation:
      //   newAvg = oldAvg + (sample - oldAvg) / (sampleCount + 1)
      this.throughput.rate += (segmentProcessingThroughput - rate) / ++this.throughput.count;
    }

    /**
     * A debugging logger noop that is set to console.log only if debugging
     * is enabled globally
     *
     * @private
     */
  }, {
    key: 'logger_',
    value: function logger_() {}

    /**
     * Adds a cue to the segment-metadata track with some metadata information about the
     * segment
     *
     * @private
     * @param {Object} segmentInfo
     *        the object returned by loadSegment
     * @method addSegmentMetadataCue_
     */
  }, {
    key: 'addSegmentMetadataCue_',
    value: function addSegmentMetadataCue_(segmentInfo) {
      if (!this.segmentMetadataTrack_) {
        return;
      }

      var segment = segmentInfo.segment;
      var start = segment.start;
      var end = segment.end;

      // Do not try adding the cue if the start and end times are invalid.
      if (!finite(start) || !finite(end)) {
        return;
      }

      (0, _videojsContribMediaSourcesEs5RemoveCuesFromTrackJs2['default'])(start, end, this.segmentMetadataTrack_);

      var Cue = _globalWindow2['default'].WebKitDataCue || _globalWindow2['default'].VTTCue;
      var value = {
        uri: segmentInfo.uri,
        timeline: segmentInfo.timeline,
        playlist: segmentInfo.playlist.uri,
        start: start,
        end: end
      };
      var data = JSON.stringify(value);
      var cue = new Cue(start, end, data);

      // Attach the metadata to the value property of the cue to keep consistency between
      // the differences of WebKitDataCue in safari and VTTCue in other browsers
      cue.value = value;

      this.segmentMetadataTrack_.addCue(cue);
    }
  }]);

  return SegmentLoader;
})(_videoJs2['default'].EventTarget);

exports['default'] = SegmentLoader;