/**
 * @file sync-controller.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(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; _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 { _x = parent; _x2 = property; _x3 = 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 _muxJsLibMp4Probe = require('mux.js/lib/mp4/probe');

var _muxJsLibMp4Probe2 = _interopRequireDefault(_muxJsLibMp4Probe);

var _muxJsLibToolsTsInspectorJs = require('mux.js/lib/tools/ts-inspector.js');

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

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

var _videoJs2 = _interopRequireDefault(_videoJs);

var c = 'console';
// temporary, switchable debug logging
var log = function log() {
  if (window.logit) {
    window[c].log.apply(window[c], arguments);
  }
};

var syncPointStrategies = [
// Stategy "VOD": Handle the VOD-case where the sync-point is *always*
//                the equivalence display-time 0 === segment-index 0
{
  name: 'VOD',
  run: function run(syncController, playlist, duration, currentTimeline) {
    if (duration !== Infinity) {
      var syncPoint = {
        time: 0,
        segmentIndex: 0
      };

      return syncPoint;
    }
    return null;
  }
},
// Stategy "ProgramDateTime": We have a program-date-time tag in this playlist
{
  name: 'ProgramDateTime',
  run: function run(syncController, playlist, duration, currentTimeline) {
    if (syncController.datetimeToDisplayTime && playlist.dateTimeObject) {
      var playlistTime = playlist.dateTimeObject.getTime() / 1000;
      var playlistStart = playlistTime + syncController.datetimeToDisplayTime;
      var syncPoint = {
        time: playlistStart,
        segmentIndex: 0
      };

      return syncPoint;
    }
    return null;
  }
},
// Stategy "Segment": We have a known time mapping for a timeline and a
//                    segment in the current timeline with timing data
{
  name: 'Segment',
  run: function run(syncController, playlist, duration, currentTimeline) {
    var segments = playlist.segments;

    for (var i = segments.length - 1; i >= 0; i--) {
      var segment = segments[i];

      if (segment.timeline === currentTimeline && typeof segment.start !== 'undefined') {
        var syncPoint = {
          time: segment.start,
          segmentIndex: i
        };

        return syncPoint;
      }
    }
    return null;
  }
},

// Stategy "Discontinuity": We have a discontinuity with a known
//                          display-time
{
  name: 'Discontinuity',
  run: function run(syncController, playlist, duration, currentTimeline) {
    if (playlist.discontinuityStarts.length) {
      for (var i = 0; i < playlist.discontinuityStarts.length; i++) {
        var segmentIndex = playlist.discontinuityStarts[i];
        var discontinuity = playlist.discontinuitySequence + i + 1;

        if (syncController.discontinuities[discontinuity]) {
          var syncPoint = {
            time: syncController.discontinuities[discontinuity].time,
            segmentIndex: segmentIndex
          };

          return syncPoint;
        }
      }
    }
    return null;
  }
},
// Stategy "Playlist": We have a playlist with a known mapping of
//                     segment index to display time
{
  name: 'Playlist',
  run: function run(syncController, playlist, duration, currentTimeline) {
    if (playlist.syncInfo) {
      var syncPoint = {
        time: playlist.syncInfo.time,
        segmentIndex: playlist.syncInfo.mediaSequence - playlist.mediaSequence
      };

      return syncPoint;
    }
    return null;
  }
}];

exports.syncPointStrategies = syncPointStrategies;

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

  function SyncController() {
    _classCallCheck(this, SyncController);

    _get(Object.getPrototypeOf(SyncController.prototype), 'constructor', this).call(this);
    // Segment Loader state variables...
    // ...for synching across variants
    this.inspectCache_ = undefined;

    // ...for synching across variants
    this.timelines = [];
    this.discontinuities = [];
    this.datetimeToDisplayTime = null;
  }

  /**
   * Find a sync-point for the playlist specified
   *
   * A sync-point is defined as a known mapping from display-time to
   * a segment-index in the current playlist.
   *
   * @param {Playlist} media - The playlist that needs a sync-point
   * @param {Number} duration - Duration of the MediaSource (Infinite if playing a live source)
   * @param {Number} currentTimeline - The last timeline from which a segment was loaded
   * @returns {Object} - A sync-point object
   */

  _createClass(SyncController, [{
    key: 'getSyncPoint',
    value: function getSyncPoint(playlist, duration, currentTimeline) {
      // Try to find a sync-point in by utilizing various strategies...
      for (var i = 0; i < syncPointStrategies.length; i++) {
        var strategy = syncPointStrategies[i];
        var syncPoint = strategy.run(this, playlist, duration, currentTimeline);

        if (syncPoint) {
          log('syncPoint found via <' + strategy.name + '>:', syncPoint);
          return syncPoint;
        }
      }
      // Otherwise, signal that we need to attempt to get a sync-point
      // manually by fetching a segment in the playlist and constructing
      // a sync-point from that information
      return null;
    }

    /**
     * Save any meta-data present on the segments when segments leave
     * the live window to the playlist to allow for synchronization at the
     * playlist level later.
     *
     * @param {Playlist} oldPlaylist - The previous active playlist
     * @param {Playlist} newPlaylist - The updated and most current playlist
     */
  }, {
    key: 'saveExpiredSegmentInfo',
    value: function saveExpiredSegmentInfo(oldPlaylist, newPlaylist) {
      var mediaSequenceDiff = newPlaylist.mediaSequence - oldPlaylist.mediaSequence;

      // When a segment expires from the playlist and it has a start time
      // save that information as a possible sync-point reference in future
      for (var i = mediaSequenceDiff - 1; i >= 0; i--) {
        var lastRemovedSegment = oldPlaylist.segments[i];

        if (typeof lastRemovedSegment.start !== 'undefined') {
          newPlaylist.syncInfo = {
            mediaSequence: oldPlaylist.mediaSequence + i,
            time: lastRemovedSegment.start
          };
          log('playlist sync:', newPlaylist.syncInfo);
          this.trigger('syncinfoupdate');
          break;
        }
      }
    }

    /**
     * Save the mapping from playlist's ProgramDateTime to display. This should
     * only ever happen once at the start of playback.
     *
     * @param {Playlist} playlist - The currently active playlist
     */
  }, {
    key: 'setDateTimeMapping',
    value: function setDateTimeMapping(playlist) {
      if (!this.datetimeToDisplayTime && playlist.dateTimeObject) {
        var playlistTimestamp = playlist.dateTimeObject.getTime() / 1000;

        this.datetimeToDisplayTime = -playlistTimestamp;
      }
    }

    /**
     * Reset the state of the inspection cache when we do a rendition
     * switch
     */
  }, {
    key: 'reset',
    value: function reset() {
      this.inspectCache_ = undefined;
    }

    /**
     * Probe or inspect a fmp4 or an mpeg2-ts segment to determine the start
     * and end of the segment in it's internal "media time". Used to generate
     * mappings from that internal "media time" to the display time that is
     * shown on the player.
     *
     * @param {SegmentInfo} segmentInfo - The current active request information
     */
  }, {
    key: 'probeSegmentInfo',
    value: function probeSegmentInfo(segmentInfo) {
      var segment = segmentInfo.segment;
      var timingInfo = undefined;

      if (segment.map) {
        timingInfo = this.probeMp4Segment_(segmentInfo);
      } else {
        timingInfo = this.probeTsSegment_(segmentInfo);
      }

      if (timingInfo) {
        if (this.calculateSegmentTimeMapping_(segmentInfo, timingInfo)) {
          this.saveDiscontinuitySyncInfo_(segmentInfo);
        }
      }
    }

    /**
     * Probe an fmp4 or an mpeg2-ts segment to determine the start of the segment
     * in it's internal "media time".
     *
     * @private
     * @param {SegmentInfo} segmentInfo - The current active request information
     * @return {object} The start and end time of the current segment in "media time"
     */
  }, {
    key: 'probeMp4Segment_',
    value: function probeMp4Segment_(segmentInfo) {
      var segment = segmentInfo.segment;
      var timescales = _muxJsLibMp4Probe2['default'].timescale(segment.map.bytes);
      var startTime = _muxJsLibMp4Probe2['default'].startTime(timescales, segmentInfo.bytes);

      if (segmentInfo.timestampOffset !== null) {
        segmentInfo.timestampOffset -= startTime;
      }

      return {
        start: startTime,
        end: startTime + segment.duration
      };
    }

    /**
     * Probe an mpeg2-ts segment to determine the start and end of the segment
     * in it's internal "media time".
     *
     * @private
     * @param {SegmentInfo} segmentInfo - The current active request information
     * @return {object} The start and end time of the current segment in "media time"
     */
  }, {
    key: 'probeTsSegment_',
    value: function probeTsSegment_(segmentInfo) {
      var timeInfo = (0, _muxJsLibToolsTsInspectorJs.inspect)(segmentInfo.bytes, this.inspectCache_);
      var segmentStartTime = undefined;
      var segmentEndTime = undefined;

      if (!timeInfo) {
        return null;
      }

      if (timeInfo.video && timeInfo.video.length === 2) {
        this.inspectCache_ = timeInfo.video[1].dts;
        segmentStartTime = timeInfo.video[0].dtsTime;
        segmentEndTime = timeInfo.video[1].dtsTime;
      } else if (timeInfo.audio && timeInfo.audio.length === 2) {
        this.inspectCache_ = timeInfo.audio[1].dts;
        segmentStartTime = timeInfo.audio[0].dtsTime;
        segmentEndTime = timeInfo.audio[1].dtsTime;
      }

      return {
        start: segmentStartTime,
        end: segmentEndTime
      };
    }

    /**
     * Use the "media time" for a segment to generate a mapping to "display time" and
     * save that display time to the segment.
     *
     * @private
     * @param {SegmentInfo} segmentInfo - The current active request information
     * @param {object} timingInfo - The start and end time of the current segment in "media time"
     */
  }, {
    key: 'calculateSegmentTimeMapping_',
    value: function calculateSegmentTimeMapping_(segmentInfo, timingInfo) {
      var segment = segmentInfo.segment;
      var mappingObj = this.timelines[segmentInfo.timeline];

      if (segmentInfo.timestampOffset !== null) {
        log('tsO:', segmentInfo.timestampOffset);

        mappingObj = {
          time: segmentInfo.timestampOffset,
          mapping: segmentInfo.timestampOffset - timingInfo.start
        };
        this.timelines[segmentInfo.timeline] = mappingObj;

        segment.start = segmentInfo.timestampOffset;
        segment.end = timingInfo.end + mappingObj.mapping;
      } else if (mappingObj) {
        segment.start = timingInfo.start + mappingObj.mapping;
        segment.end = timingInfo.end + mappingObj.mapping;
      } else {
        return false;
      }
      this.trigger('syncinfoupdate');
      return true;
    }

    /**
     * Each time we have discontinuity in the playlist, attempt to calculate the location
     * in display of the start of the discontinuity and save that. We also save an accuracy
     * value so that we save values with the most accuracy (closest to 0.)
     *
     * @private
     * @param {SegmentInfo} segmentInfo - The current active request information
     */
  }, {
    key: 'saveDiscontinuitySyncInfo_',
    value: function saveDiscontinuitySyncInfo_(segmentInfo) {
      var playlist = segmentInfo.playlist;
      var segment = segmentInfo.segment;

      // If the current segment is a discontinuity then we know exactly where
      // the start of the range and it's accuracy is 0 (greater accuracy values
      // mean more approximation)
      if (segment.discontinuity) {
        this.discontinuities[segment.timeline] = {
          time: segment.start,
          accuracy: 0
        };
      } else if (playlist.discontinuityStarts.length) {
        // Search for future discontinuities that we can provide better timing
        // information for and save that information for sync purposes
        for (var i = 0; i < playlist.discontinuityStarts.length; i++) {
          var segmentIndex = playlist.discontinuityStarts[i];
          var discontinuity = playlist.discontinuitySequence + i + 1;
          var accuracy = segmentIndex - segmentInfo.mediaIndex;

          if (accuracy > 0 && (!this.discontinuities[discontinuity] || this.discontinuities[discontinuity].accuracy > accuracy)) {

            this.discontinuities[discontinuity] = {
              time: segment.end + (0, _playlist.sumDurations)(playlist, segmentInfo.mediaIndex + 1, segmentIndex),
              accuracy: accuracy
            };
          }
        }
      }
    }
  }]);

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

exports['default'] = SyncController;