/**
 * @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(_x2, _x3, _x4) { var _again = true; _function: while (_again) { var object = _x2, property = _x3, receiver = _x4; _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 { _x2 = parent; _x3 = property; _x4 = 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 _ranges = require('./ranges');

var _ranges2 = _interopRequireDefault(_ranges);

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

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

var _videoJs2 = _interopRequireDefault(_videoJs);

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

var _sourceUpdater2 = _interopRequireDefault(_sourceUpdater);

var _muxJsLibMp4Probe = require('mux.js/lib/mp4/probe');

var _muxJsLibMp4Probe2 = _interopRequireDefault(_muxJsLibMp4Probe);

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

var _config2 = _interopRequireDefault(_config);

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

var _globalWindow2 = _interopRequireDefault(_globalWindow);

var _webworkify = require('webworkify');

var _webworkify2 = _interopRequireDefault(_webworkify);

var _decrypterWorkerJs = require('./decrypter-worker.js');

var _decrypterWorkerJs2 = _interopRequireDefault(_decrypterWorkerJs);

// in ms
var CHECK_BUFFER_DELAY = 500;

/**
 * Updates segment with information about its end-point in time and, optionally,
 * the segment duration if we have enough information to determine a segment duration
 * accurately.
 *
 * @param {Object} playlist a media playlist object
 * @param {Number} segmentIndex the index of segment we last appended
 * @param {Number} segmentEnd the known of the segment referenced by segmentIndex
 */
var updateSegmentMetadata = function updateSegmentMetadata(playlist, segmentIndex, segmentEnd) {
  if (!playlist) {
    return false;
  }

  var segment = playlist.segments[segmentIndex];
  var previousSegment = playlist.segments[segmentIndex - 1];

  if (segmentEnd && segment) {
    segment.end = segmentEnd;

    // fix up segment durations based on segment end data
    if (!previousSegment) {
      // first segment is always has a start time of 0 making its duration
      // equal to the segment end
      segment.duration = segment.end;
    } else if (previousSegment.end) {
      segment.duration = segment.end - previousSegment.end;
    }
    return true;
  }
  return false;
};

/**
 * 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) {
    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;
};

/**
 * Turns segment byterange into a string suitable for use in
 * HTTP Range requests
 */
var byterangeStr = function byterangeStr(byterange) {
  var byterangeStart = undefined;
  var byterangeEnd = undefined;

  // `byterangeEnd` is one less than `offset + length` because the HTTP range
  // header uses inclusive ranges
  byterangeEnd = byterange.offset + byterange.length - 1;
  byterangeStart = byterange.offset;
  return 'bytes=' + byterangeStart + '-' + byterangeEnd;
};

/**
 * Defines headers for use in the xhr request for a particular segment.
 */
var segmentXhrHeaders = function segmentXhrHeaders(segment) {
  var headers = {};

  if ('byterange' in segment) {
    headers.Range = byterangeStr(segment.byterange);
  }
  return headers;
};

/**
 * Returns a unique string identifier for a media initialization
 * segment.
 */
var initSegmentId = function initSegmentId(initSegment) {
  var byterange = initSegment.byterange || {
    length: Infinity,
    offset: 0
  };

  return [byterange.length, byterange.offset, initSegment.resolvedUri].join(',');
};

/**
 * 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(options) {
    var _this = this;

    _classCallCheck(this, SegmentLoader);

    _get(Object.getPrototypeOf(SegmentLoader.prototype), 'constructor', this).call(this);
    var settings = undefined;

    // check pre-conditions
    if (!options) {
      throw new TypeError('Initialization options are required');
    }
    if (typeof options.currentTime !== 'function') {
      throw new TypeError('No currentTime getter specified');
    }
    if (!options.mediaSource) {
      throw new TypeError('No MediaSource specified');
    }
    settings = _videoJs2['default'].mergeOptions(_videoJs2['default'].options.hls, options);

    // public properties
    this.state = 'INIT';
    this.bandwidth = settings.bandwidth;
    this.throughput = { rate: 0, count: 0 };
    this.roundTrip = NaN;
    this.resetStats_();

    // private settings
    this.hasPlayed_ = settings.hasPlayed;
    this.currentTime_ = settings.currentTime;
    this.seekable_ = settings.seekable;
    this.seeking_ = settings.seeking;
    this.setCurrentTime_ = settings.setCurrentTime;
    this.mediaSource_ = settings.mediaSource;

    this.hls_ = settings.hls;

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

    this.activeInitSegmentId_ = null;
    this.initSegments_ = {};

    this.decrypter_ = (0, _webworkify2['default'])(_decrypterWorkerJs2['default']);
    this.decrypter_.onmessage = function (event) {
      if (event.data.action === 'done') {
        var segmentInfo = _this.pendingSegment_;

        if (segmentInfo) {
          var bytes = new Uint8Array(event.data.bytes, event.data.byteOffset, event.data.byteLength);

          segmentInfo.bytes = bytes;
          _this.handleSegment_();
        }
      }
    };
  }

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

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

    /**
     * dispose of the SegmentLoader and reset to the default state
     */
  }, {
    key: 'dispose',
    value: function dispose() {
      this.state = 'DISPOSED';
      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') {
        return;
      }

      this.abort_();

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

    /**
     * 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_;
    }

    /**
     * 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;
      }

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

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

      this.state = 'READY';
      this.fillBuffer_();
    }

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

      if (!media) {
        return;
      }

      this.playlist_ = media;
      this.xhrOptions_ = options;

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

    /**
     * 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;
    }

    /**
     * setter for expired time on the SegmentLoader
     *
     * @param {Number} expired the exired time to set
     */
  }, {
    key: 'expired',
    value: function expired(_expired) {
      this.expired_ = _expired;
    }

    /**
     * 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.playlist_ && this.state === 'INIT' && !this.paused()) {
        this.init_();
      }
    }

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

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

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

    /**
     * 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} currentTime - the playback position in seconds
     * @param {Boolean} hasPlayed - if the player has played before
     * @param {Number} expired - the seconds expired off the playlist
     * @param {Number} timeCorrection - correction value to add to current time when
     *  when determining media index to use
     * @returns {Object} a segment info object that describes the
     * request that should be made or null if no request is necessary
     */
  }, {
    key: 'checkBuffer_',
    value: function checkBuffer_(buffered, playlist, currentTime, hasPlayed, expired, timeCorrection) {
      var currentBuffered = _ranges2['default'].findRange(buffered, currentTime);

      // There are times when MSE reports the first segment as starting a
      // little after 0-time so add a fudge factor to try and fix those cases
      // or we end up fetching the same first segment over and over
      if (currentBuffered.length === 0 && currentTime === 0) {
        currentBuffered = _ranges2['default'].findRange(buffered, currentTime + _ranges2['default'].TIME_FUDGE_FACTOR);
      }

      var bufferedTime = undefined;
      var currentBufferedEnd = undefined;
      var mediaIndex = undefined;

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

      if (currentBuffered.length === 0) {
        // find the segment containing currentTime
        mediaIndex = (0, _playlist.getMediaIndexForTime_)(playlist, currentTime + timeCorrection, expired);
      } else {
        // find the segment adjacent to the end of the current
        // buffered region
        currentBufferedEnd = currentBuffered.end(0);
        bufferedTime = Math.max(0, currentBufferedEnd - currentTime);

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

        // if there is plenty of content buffered, and the video has
        // been played before relax for awhile
        if (hasPlayed && bufferedTime >= _config2['default'].GOAL_BUFFER_LENGTH) {
          return null;
        }
        mediaIndex = (0, _playlist.getMediaIndexForTime_)(playlist, currentBufferedEnd + timeCorrection, expired);
      }

      return mediaIndex;
    }

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

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

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

    /**
     * fill the buffer with segements unless the
     * sourceBuffers are currently updating
     *
     * @private
     */
  }, {
    key: 'fillBuffer_',
    value: function fillBuffer_() {
      if (this.sourceUpdater_.updating()) {
        return;
      }

      var buffered = this.sourceUpdater_.buffered();
      var playlist = this.playlist_;
      var currentTime = this.currentTime_();
      var hasPlayed = this.hasPlayed_();
      var expired = this.expired_;
      var timeCorrection = this.timeCorrection_;

      // see if we need to begin loading immediately
      var requestIndex = this.checkBuffer_(buffered, playlist, currentTime, hasPlayed, expired, timeCorrection);

      if (requestIndex === null) {
        return;
      }

      var isEndOfStream = detectEndOfStream(playlist, this.mediaSource_, requestIndex);

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

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

      if (requestIndex < 0 || requestIndex >= playlist.segments.length) {
        return;
      }

      var segment = this.playlist_.segments[requestIndex];

      var request = {
        // resolve the segment URL relative to the playlist
        uri: segment.resolvedUri,
        // the segment's mediaIndex at the time it was requested
        mediaIndex: requestIndex,
        // 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 state of the buffer before a segment is appended will be
        // stored here so that the actual segment duration can be
        // determined after it has been appended
        buffered: null,
        // The target timestampOffset for this segment when we append it
        // to the source buffer
        timestampOffset: NaN,
        // The timeline that the segment is in
        timeline: segment.timeline,
        // The expected duration of the segment in seconds
        duration: segment.duration
      };

      var startOfSegment = (0, _playlist.duration)(playlist, playlist.mediaSequence + request.mediaIndex, expired);

      //   (we are crossing a discontinuity somehow)
      // - The "timestampOffset" for the start of this segment is less than
      //   the currently set timestampOffset
      request.timestampOffset = this.sourceUpdater_.timestampOffset();
      if (segment.timeline !== this.currentTimeline_ || startOfSegment < this.sourceUpdater_.timestampOffset()) {
        request.timestampOffset = startOfSegment;
      }

      // Sanity check the segment-index determining logic by calcuating the
      // percentage of the chosen segment that is buffered. If more than 90%
      // of the segment is buffered then fetching it will likely not help in
      // any way
      var percentBuffered = _ranges2['default'].getSegmentBufferedPercent(startOfSegment, segment.duration, currentTime, buffered);

      if (percentBuffered >= 90) {
        // Increment the timeCorrection_ variable to push the fetcher forward
        // in time and hopefully skip any gaps or flaws in our understanding
        // of the media
        var correctionApplied = this.incrementTimeCorrection_(playlist.targetDuration / 2, 1);

        if (correctionApplied && !this.paused()) {
          this.fillBuffer_();
        }

        return;
      }

      this.loadSegment_(request);
    }

    /**
     * trim the back buffer so we only remove content
     * on segment boundaries
     *
     * @private
     *
     * @param {Object} segmentInfo - the current segment
     * @returns {Number} removeToTime - the end point in time, in seconds
     * that the the buffer should be trimmed.
     */
  }, {
    key: 'trimBuffer_',
    value: function trimBuffer_(segmentInfo) {
      var seekable = this.seekable_();
      var currentTime = this.currentTime_();
      var removeToTime = undefined;

      // 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 1 minute before the current play head
      if (seekable.length && seekable.start(0) > 0 && seekable.start(0) < currentTime) {
        return seekable.start(0);
      }

      removeToTime = currentTime - 60;

      if (!this.playlist_.endList) {
        return removeToTime;
      }

      // If we are going to remove time from the front of the buffer, make
      // sure we aren't discarding a partial segment to avoid throwing
      // PLAYER_ERR_TIMEOUT while trying to read a partially discarded segment
      for (var i = 1; i <= segmentInfo.playlist.segments.length; i++) {
        // Loop through the segments and calculate the duration to compare
        // against the removeToTime
        var removeDuration = (0, _playlist.duration)(segmentInfo.playlist, segmentInfo.playlist.mediaSequence + i, this.expired_);

        // If we are close to next segment begining, remove to end of previous
        // segment instead
        var previousDuration = (0, _playlist.duration)(segmentInfo.playlist, segmentInfo.playlist.mediaSequence + (i - 1), this.expired_);

        if (removeDuration >= removeToTime) {
          removeToTime = previousDuration;
          break;
        }
      }
      return removeToTime;
    }

    /**
     * load a specific segment from a request into the buffer
     *
     * @private
     */
  }, {
    key: 'loadSegment_',
    value: function loadSegment_(segmentInfo) {
      var segment = undefined;
      var keyXhr = undefined;
      var initSegmentXhr = undefined;
      var segmentXhr = undefined;
      var removeToTime = 0;

      removeToTime = this.trimBuffer_(segmentInfo);

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

      segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];

      // optionally, request the decryption key
      if (segment.key) {
        var keyRequestOptions = _videoJs2['default'].mergeOptions(this.xhrOptions_, {
          uri: segment.key.resolvedUri,
          responseType: 'arraybuffer'
        });

        keyXhr = this.hls_.xhr(keyRequestOptions, this.handleResponse_.bind(this));
      }

      // optionally, request the associated media init segment
      if (segment.map && !this.initSegments_[initSegmentId(segment.map)]) {
        var initSegmentOptions = _videoJs2['default'].mergeOptions(this.xhrOptions_, {
          uri: segment.map.resolvedUri,
          responseType: 'arraybuffer',
          headers: segmentXhrHeaders(segment.map)
        });

        initSegmentXhr = this.hls_.xhr(initSegmentOptions, this.handleResponse_.bind(this));
      }
      this.pendingSegment_ = segmentInfo;

      var segmentRequestOptions = _videoJs2['default'].mergeOptions(this.xhrOptions_, {
        uri: segmentInfo.uri,
        responseType: 'arraybuffer',
        headers: segmentXhrHeaders(segment)
      });

      segmentXhr = this.hls_.xhr(segmentRequestOptions, this.handleResponse_.bind(this));

      this.xhr_ = {
        keyXhr: keyXhr,
        initSegmentXhr: initSegmentXhr,
        segmentXhr: segmentXhr,
        abort: function abort() {
          if (this.segmentXhr) {
            // Prevent error handler from running.
            this.segmentXhr.onreadystatechange = null;
            this.segmentXhr.abort();
            this.segmentXhr = null;
          }
          if (this.initSegmentXhr) {
            // Prevent error handler from running.
            this.initSegmentXhr.onreadystatechange = null;
            this.initSegmentXhr.abort();
            this.initSegmentXhr = null;
          }
          if (this.keyXhr) {
            // Prevent error handler from running.
            this.keyXhr.onreadystatechange = null;
            this.keyXhr.abort();
            this.keyXhr = null;
          }
        }
      };

      this.state = 'WAITING';
    }

    /**
     * triggered when a segment response is received
     *
     * @private
     */
  }, {
    key: 'handleResponse_',
    value: function handleResponse_(error, request) {
      var segmentInfo = undefined;
      var segment = undefined;
      var keyXhrRequest = undefined;
      var view = undefined;

      // timeout of previously aborted request
      if (!this.xhr_ || request !== this.xhr_.segmentXhr && request !== this.xhr_.keyXhr && request !== this.xhr_.initSegmentXhr) {
        return;
      }

      segmentInfo = this.pendingSegment_;
      segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];

      // if a request times out, reset bandwidth tracking
      if (request.timedout) {
        this.abort_();
        this.bandwidth = 1;
        this.roundTrip = NaN;
        this.state = 'READY';
        return this.trigger('progress');
      }

      // trigger an event for other errors
      if (!request.aborted && error) {
        // abort will clear xhr_
        keyXhrRequest = this.xhr_.keyXhr;
        this.abort_();
        this.error({
          status: request.status,
          message: request === keyXhrRequest ? 'HLS key request error at URL: ' + segment.key.uri : 'HLS segment request error at URL: ' + segmentInfo.uri,
          code: 2,
          xhr: request
        });
        this.state = 'READY';
        this.pause();
        return this.trigger('error');
      }

      // stop processing if the request was aborted
      if (!request.response) {
        this.abort_();
        return;
      }

      if (request === this.xhr_.segmentXhr) {
        // the segment request is no longer outstanding
        this.xhr_.segmentXhr = null;
        segmentInfo.startOfAppend = Date.now();

        // calculate the download bandwidth based on segment request
        this.roundTrip = request.roundTripTime;
        this.bandwidth = request.bandwidth;
        this.mediaBytesTransferred += request.bytesReceived || 0;
        this.mediaRequests += 1;
        this.mediaTransferDuration += request.roundTripTime || 0;

        if (segment.key) {
          segmentInfo.encryptedBytes = new Uint8Array(request.response);
        } else {
          segmentInfo.bytes = new Uint8Array(request.response);
        }
      }

      if (request === this.xhr_.keyXhr) {
        keyXhrRequest = this.xhr_.segmentXhr;
        // the key request is no longer outstanding
        this.xhr_.keyXhr = null;

        if (request.response.byteLength !== 16) {
          this.abort_();
          this.error({
            status: request.status,
            message: 'Invalid HLS key at URL: ' + segment.key.uri,
            code: 2,
            xhr: request
          });
          this.state = 'READY';
          this.pause();
          return this.trigger('error');
        }

        view = new DataView(request.response);
        segment.key.bytes = new Uint32Array([view.getUint32(0), view.getUint32(4), view.getUint32(8), view.getUint32(12)]);

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

      if (request === this.xhr_.initSegmentXhr) {
        // the init segment request is no longer outstanding
        this.xhr_.initSegmentXhr = null;
        segment.map.bytes = new Uint8Array(request.response);
        this.initSegments_[initSegmentId(segment.map)] = segment.map;
      }

      if (!this.xhr_.segmentXhr && !this.xhr_.keyXhr && !this.xhr_.initSegmentXhr) {
        this.xhr_ = null;
        this.processResponse_();
      }
    }

    /**
     * clear anything that is currently in the buffer and throw it away
     */
  }, {
    key: 'clearBuffer',
    value: function clearBuffer() {
      if (this.sourceUpdater_ && this.sourceUpdater_.buffered().length) {
        this.sourceUpdater_.remove(0, Infinity);
      }
    }

    /**
     * Decrypt the segment that is being loaded if necessary
     *
     * @private
     */
  }, {
    key: 'processResponse_',
    value: function processResponse_() {
      var segmentInfo = undefined;
      var segment = undefined;

      this.state = 'DECRYPTING';

      segmentInfo = this.pendingSegment_;
      segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];

      // some videos don't start from presentation time zero
      // if that is the case, set the timestamp offset on the first
      // segment to adjust them so that it is not necessary to seek
      // before playback can begin
      if (segment.map && isNaN(this.zeroOffset_)) {
        var timescales = _muxJsLibMp4Probe2['default'].timescale(segment.map.bytes);
        var startTime = _muxJsLibMp4Probe2['default'].startTime(timescales, segmentInfo.bytes);

        this.zeroOffset_ = startTime;
        segmentInfo.timestampOffset -= startTime;
      }

      if (segment.key) {
        // this is an encrypted segment
        // incrementally decrypt the segment
        /* eslint-disable no-new, handle-callback-err */
        this.decrypter_.postMessage({
          action: 'decrypt',
          encrypted: {
            bytes: segmentInfo.encryptedBytes.buffer,
            byteOffset: segmentInfo.encryptedBytes.byteOffset,
            byteLength: segmentInfo.encryptedBytes.byteLength
          },
          key: {
            bytes: segment.key.bytes.buffer,
            byteOffset: segment.key.bytes.byteOffset,
            byteLength: segment.key.bytes.byteLength
          },
          iv: {
            bytes: segment.key.iv.buffer,
            byteOffset: segment.key.iv.byteOffset,
            byteLength: segment.key.iv.byteLength
          }
        }, [segmentInfo.encryptedBytes.buffer, segment.key.bytes.buffer]);
        /* eslint-enable */
      } else {
          this.handleSegment_();
        }
    }

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

      var segmentInfo = undefined;
      var segment = undefined;

      this.state = 'APPENDING';
      segmentInfo = this.pendingSegment_;
      segmentInfo.buffered = this.sourceUpdater_.buffered();
      segment = segmentInfo.playlist.segments[segmentInfo.mediaIndex];
      this.currentTimeline_ = segmentInfo.timeline;

      if (segmentInfo.timestampOffset !== this.sourceUpdater_.timestampOffset()) {
        this.sourceUpdater_.timestampOffset(segmentInfo.timestampOffset);
      }

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

          if (!_this2.activeInitSegmentId_ || _this2.activeInitSegmentId_ !== initId) {
            var initSegment = _this2.initSegments_[initId];

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

      segmentInfo.byteLength = segmentInfo.bytes.byteLength;

      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_() {
      var segmentInfo = this.pendingSegment_;
      var currentTime = this.currentTime_();

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

      // add segment metadata if it we have gained information during the
      // last append
      var timelineUpdated = this.updateTimeline_(segmentInfo);

      this.trigger('progress');

      var currentMediaIndex = segmentInfo.mediaIndex;

      currentMediaIndex += segmentInfo.playlist.mediaSequence - this.playlist_.mediaSequence;

      var currentBuffered = _ranges2['default'].findRange(this.sourceUpdater_.buffered(), currentTime);

      // 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_, currentMediaIndex + 1);

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

      // when seeking to the beginning of the seekable range, it's
      // possible that imprecise timing information may cause the seek to
      // end up earlier than the start of the range
      // in that case, seek again
      var seekable = this.seekable_();
      var next = _ranges2['default'].findNextRange(this.sourceUpdater_.buffered(), currentTime);

      if (this.seeking_() && currentBuffered.length === 0) {
        if (seekable.length && currentTime < seekable.start(0)) {

          if (next.length) {
            _videoJs2['default'].log('tried seeking to', currentTime, 'but that was too early, retrying at', next.start(0));
            this.setCurrentTime_(next.start(0) + _ranges2['default'].TIME_FUDGE_FACTOR);
          }
        }
      }

      this.state = 'READY';

      if (timelineUpdated) {
        this.timeCorrection_ = 0;
        if (!this.paused()) {
          this.fillBuffer_();
        }
        return;
      }

      // the last segment append must have been entirely in the
      // already buffered time ranges. adjust the timeCorrection
      // offset to fetch forward until we find a segment that adds
      // to the buffered time ranges and improves subsequent media
      // index calculations.
      var correctionApplied = this.incrementTimeCorrection_(segmentInfo.duration, 4);

      if (correctionApplied && !this.paused()) {
        this.fillBuffer_();
      }
    }

    /**
     * annotate the segment with any start and end time information
     * added by the media processing
     *
     * @private
     * @param {Object} segmentInfo annotate a segment with time info
     */
  }, {
    key: 'updateTimeline_',
    value: function updateTimeline_(segmentInfo) {
      var segment = undefined;
      var segmentEnd = undefined;
      var timelineUpdated = false;
      var playlist = segmentInfo.playlist;
      var currentMediaIndex = segmentInfo.mediaIndex;

      currentMediaIndex += playlist.mediaSequence - this.playlist_.mediaSequence;
      segment = playlist.segments[currentMediaIndex];

      // Update segment meta-data (duration and end-point) based on timeline
      if (segment && segmentInfo && segmentInfo.playlist.uri === this.playlist_.uri) {
        segmentEnd = _ranges2['default'].findSoleUncommonTimeRangesEnd(segmentInfo.buffered, this.sourceUpdater_.buffered());
        timelineUpdated = updateSegmentMetadata(playlist, currentMediaIndex, segmentEnd);
      }

      return timelineUpdated;
    }

    /**
     * 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.startOfAppend + 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;
    }

    /**
     * add a number of seconds to the currentTime when determining which
     * segment to fetch in order to force the fetcher to advance in cases
     * where it may get stuck on the same segment due to buffer gaps or
     * missing segment annotation after a rendition switch (especially
     * during a live stream)
     *
     * @private
     * @param {Number} secondsToIncrement number of seconds to add to the
     * timeCorrection_ variable
     * @param {Number} maxSegmentsToWalk maximum number of times we allow this
     * function to walk forward
     */
  }, {
    key: 'incrementTimeCorrection_',
    value: function incrementTimeCorrection_(secondsToIncrement, maxSegmentsToWalk) {
      // If we have already incremented timeCorrection_ beyond the limit,
      // stop searching for a segment and reset timeCorrection_
      if (this.timeCorrection_ >= this.playlist_.targetDuration * maxSegmentsToWalk) {
        this.timeCorrection_ = 0;
        return false;
      }

      this.timeCorrection_ += secondsToIncrement;
      return true;
    }
  }]);

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

exports['default'] = SegmentLoader;
module.exports = exports['default'];