Source: dash/live_segment_index.js

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

goog.provide('shaka.dash.LiveSegmentIndex');

goog.require('shaka.asserts');
goog.require('shaka.media.SegmentIndex');
goog.require('shaka.media.SegmentReference');
goog.require('shaka.util.ArrayUtils');
goog.require('shaka.util.Clock');



/**
 * Creates a SegmentIndex that supports live DASH content.
 *
 * A LiveSegmentIndex automatically evicts SegmentReferences that are no longer
 * available. However, it does not generate any new SegmentReferences.
 * Additional SegmentReferences can be added to the SegmentIndex by integrating
 * another SegmentIndex into it.
 *
 * @param {!Array.<!shaka.media.SegmentReference>} references The set of
 *     SegmentReferences. The live-edge is the start time of the last
 *     SegmentReference.
 * @param {!shaka.dash.mpd.Mpd} mpd
 * @param {!shaka.dash.mpd.Period} period
 * @param {number} manifestCreationTime The time, in seconds, when the manifest
 *     was created.
 * @constructor
 * @struct
 * @extends {shaka.media.SegmentIndex}
 */
shaka.dash.LiveSegmentIndex = function(
    references, mpd, period, manifestCreationTime) {
  shaka.asserts.assert(mpd.availabilityStartTime != null);
  shaka.asserts.assert(period.start != null);

  shaka.media.SegmentIndex.call(this, references);

  /** @protected {!shaka.dash.mpd.Mpd} */
  this.mpd = mpd;

  /** @protected {!shaka.dash.mpd.Period} */
  this.period = period;

  /** @protected {number} */
  this.manifestCreationTime = manifestCreationTime;

  /** @protected {?number} */
  this.duration = this.mpd.mediaPresentationDuration ||
      this.mpd.periods.reduce(function(all, part) {
        if (part.duration == null) {
          return NaN;
        } else {
          return all + part.duration;
        }
      }, 0) || 0;

  /**
   * Either the current presentation time when the manifest was created, in
   * seconds, or null if this SegmentIndex has never contained any
   * SegmentReferences.
   *
   * @private {?number}
   */
  this.originalPresentationTime_ = null;

  /**
   * Either the time of the live-edge when the manifest was created, in
   * seconds, or null if this SegmentIndex has never contained any
   * SegmentReferences.
   *
   * The original live-edge is the same as the original latest available
   * segment start time. The live-edge is not taken to be the end time of the
   * last SegmentReference (i.e., the latest available segment end time), as if
   * it were, there wouldn't be any content in front of the playhead during
   * stream startup.
   *
   * @private {?number}
   */
  this.originalLiveEdge_ = null;

  /**
   * Either the seek start time, in seconds, or null if this SegmentIndex has
   * never contained any SegmentReferences.
   *
   * The seek start time moves "continuously" from the start of the earliest
   * available segment to the end of the earliest available segment. It is not
   * taken as the earliest available segment start time directly because if it
   * were, it would end up moving stepwise, which is undesirable.
   *
   * @private {?number}
   */
  this.seekStartTime_ = null;

  this.initializeSeekWindow();
};
goog.inherits(shaka.dash.LiveSegmentIndex, shaka.media.SegmentIndex);


/**
 * @override
 * @suppress {checkTypes} to set otherwise non-nullable types to null.
 */
shaka.dash.LiveSegmentIndex.prototype.destroy = function() {
  this.mpd = null;
  this.period = null;
  shaka.media.SegmentIndex.prototype.destroy.call(this);
};


/** @override */
shaka.dash.LiveSegmentIndex.prototype.find = function(time) {
  return this.findInternal(time, shaka.util.Clock.now() / 1000.0);
};


/**
 * Finds a SegmentReference for the specified time.
 *
 * @param {number} targetTime The time in seconds.
 * @param {number} wallTime The current wall-clock time in seconds.
 * @return {shaka.media.SegmentReference}
 * @protected
 */
shaka.dash.LiveSegmentIndex.prototype.findInternal = function(
    targetTime, wallTime) {
  this.evict_(wallTime);
  return shaka.media.SegmentIndex.prototype.find.call(this, targetTime);
};


/** @override */
shaka.dash.LiveSegmentIndex.prototype.integrate = function(segmentIndex) {
  if (!(segmentIndex instanceof shaka.dash.LiveSegmentIndex)) {
    shaka.log.warning('Cannot integrate SegmentIndex:',
                      'Only a LiveSegmentIndex can be integrated into',
                      'another LiveSegmentIndex.',
                      this);
    return false;
  }
  var temp = /** @type {shaka.dash.LiveSegmentIndex} */ (segmentIndex);

  this.merge(segmentIndex);
  this.duration = Math.max(this.duration, temp.duration);

  if (this.originalPresentationTime_ == null) {
    this.manifestCreationTime = temp.manifestCreationTime;
    this.initializeSeekWindow();
  } else {
    this.evictEnd_();
  }
  return true;
};


/**
 * Initializes the seek window, if possible, during construction or after
 * integrating a SegmentIndex.
 *
 * @protected
 */
shaka.dash.LiveSegmentIndex.prototype.initializeSeekWindow = function() {
  shaka.asserts.assert(this.originalPresentationTime_ == null);
  shaka.asserts.assert(this.originalLiveEdge_ == null);
  shaka.asserts.assert(this.seekStartTime_ == null);

  this.evictEnd_();
  if (this.length() == 0) {
    return;
  }

  this.setOriginalPresentationTime_();
  this.originalLiveEdge_ = this.last().startTime;
  this.seekStartTime_ = this.first().startTime;

  shaka.log.v1('originalPresentationTime_', this.originalPresentationTime_);
  shaka.log.v1('originalLiveEdge_', this.originalLiveEdge_);
  shaka.log.v1('seekStartTime_', this.seekStartTime_);

  if (!COMPILED) {
    var delta = (shaka.util.Clock.now() / 1000.0) - this.manifestCreationTime;
    var currentPresentationTime = this.originalPresentationTime_ + delta;
    if (this.originalLiveEdge_ > currentPresentationTime) {
      shaka.log.error(
          'The live-edge (' + this.originalLiveEdge_ + ')',
          'should not be greater than',
          'the current presentation time (' + currentPresentationTime + ')');
    }
  }
};


/**
 * Sets the original presentation time.
 *
 * @private
 */
shaka.dash.LiveSegmentIndex.prototype.setOriginalPresentationTime_ =
    function() {
  shaka.asserts.assert(this.length() > 0);

  var fallback = this.last().endTime != null ?
                 this.last().endTime :
                 this.last().startTime;

  if (this.mpd.availabilityStartTime > this.manifestCreationTime) {
    shaka.log.warning('The stream might not be available yet!', this.period);
    this.originalPresentationTime_ = fallback;
    return;
  }

  var currentPresentationTime =
      this.manifestCreationTime -
      (this.mpd.availabilityStartTime + this.period.start);
  if (currentPresentationTime < 0) {
    shaka.log.warning('The Period might not be available yet!', this.period);
    this.originalPresentationTime_ = fallback;
    return;
  }

  if (currentPresentationTime <
      Math.max(this.last().startTime, this.last().endTime || 0)) {
    // Some SegmentReferences shouldn't be available yet, yet they were
    // included in the MPD; assume that @availabilityStartTime is inaccurate.
    shaka.log.warning(
        '@availabilityStartTime seems to be inaccurate;',
        'some segments may not be available yet:',
        'currentPresentationTime', currentPresentationTime,
        'latestAvailableSegmentEndTime', this.last().endTime);
    this.originalPresentationTime_ = fallback;
    return;
  }

  this.originalPresentationTime_ = currentPresentationTime;
};


/** @override */
shaka.dash.LiveSegmentIndex.prototype.correct = function(timestampCorrection) {
  var delta = shaka.media.SegmentIndex.prototype.correct.call(
      this, timestampCorrection);

  var max = Math.min.apply(null,
      this.references
        .filter(function(a) { return a.endTime != null; })
        .map(function(a) { return a.endTime - a.startTime; }));
  if (Math.abs(delta) > max) {
    // A timestamp correction should be less than the duration of any one
    // segment in the stream.
    shaka.log.warning(
        'Timestamp correction (' + timestampCorrection + ')',
        'is unreasonably large for live content.',
        'The content may have errors in it.');
  }

  if (this.originalPresentationTime_ != null) {
    shaka.asserts.assert(this.originalLiveEdge_ != null);
    shaka.asserts.assert(this.seekStartTime_ != null);

    this.originalLiveEdge_ += delta;
    this.seekStartTime_ += delta;
    this.originalPresentationTime_ += delta;

    shaka.asserts.assert(this.originalLiveEdge_ <=
                         this.originalPresentationTime_);
  }

  return delta;
};


/** @override */
shaka.dash.LiveSegmentIndex.prototype.getSeekRange = function() {
  return this.getSeekRangeInternal(shaka.util.Clock.now() / 1000.0);
};


/**
 * @param {number} wallTime The wall-clock time in seconds.
 * @return {{start: number, end: ?number}}
 * @protected
 */
shaka.dash.LiveSegmentIndex.prototype.getSeekRangeInternal = function(
    wallTime) {
  this.evict_(wallTime);

  if (this.originalPresentationTime_ == null ||
      this.originalLiveEdge_ == null ||
      this.seekStartTime_ == null) {
    return { start: 0, end: 0 };
  }

  var streamEnd = Number.POSITIVE_INFINITY;
  if (this.duration) {
    streamEnd = this.duration;
  }

  var manifestAge = wallTime - this.manifestCreationTime;
  var currentPresentationTime = this.originalPresentationTime_ + manifestAge;

  // Update the seek start time.
  if (this.mpd.timeShiftBufferDepth != null) {
    var seekWindow = currentPresentationTime - this.seekStartTime_;
    shaka.asserts.assert(seekWindow >= 0);
    var seekWindowSurplus = seekWindow - this.mpd.timeShiftBufferDepth;

    // Don't move the seek start time if it results in a seek window less than
    // @timeShiftBufferDepth.
    if (seekWindowSurplus > 0) {
      this.seekStartTime_ += seekWindowSurplus;
    }
  }
  this.seekStartTime_ = Math.min(this.seekStartTime_, streamEnd);

  if (!COMPILED) {
    if (this.length() > 0) {
      shaka.asserts.assert(
          this.seekStartTime_ >= this.first().startTime,
          'The seek start time (' + this.seekStartTime_ + ')' +
          'should not be less than' +
          'the first segment\'s start time (' + this.first().startTime + ')');
    }
  }


  var currentLiveEdge = this.originalLiveEdge_ + manifestAge;
  if (currentLiveEdge < this.seekStartTime_) {
    // The seek window has moved past the last segment; the stream may have
    // stopped broadcasting or the manifest may be malformed (e.g.
    // @availabilityStartTime may be large compared to the segment start/end).
    return { start: this.seekStartTime_, end: this.seekStartTime_ };
  }

  // Compute the seek end time. Allow the seek end time to move into the last
  // segment (in the usual case), so we can play-out the last segment.
  var targetSeekEndTime;
  if (this.length() > 0) {
    targetSeekEndTime =
        this.last().endTime != null ?
        Math.min(currentLiveEdge, this.last().endTime) :
        currentLiveEdge;
  } else {
    targetSeekEndTime = this.seekStartTime_;
  }

  // If we are not receiving any new SegmentReferences then the seek start time
  // may surpass the end time of the last SegmentReference (although, it will
  // never surpass the live-edge). This last SegmentReference may not have been
  // evicted because the seek window is smaller (by two segments) than the
  // available segment range.
  if (!COMPILED) {
    if (this.seekStartTime_ > targetSeekEndTime) {
      shaka.log.debug(
          'The seek start time (' + this.seekStartTime_ + ')',
          'has surpassed the target seek end time (' + targetSeekEndTime + ')');
    }
  }
  var seekEndTime = Math.max(targetSeekEndTime, this.seekStartTime_);
  seekEndTime = Math.min(seekEndTime, streamEnd);

  return { start: this.seekStartTime_, end: seekEndTime };
};


/**
 * Removes all inaccessible SegmentReferences.
 *
 * @param {number} wallTime The current wall-clock time in seconds.
 * @private
 */
shaka.dash.LiveSegmentIndex.prototype.evict_ = function(wallTime) {
  // Always evict segments at the end.
  this.evictEnd_();

  if (this.mpd.timeShiftBufferDepth == null) {
    return;
  }

  if (this.originalPresentationTime_ == null) {
    shaka.asserts.assert(this.length() == 0);
    return;
  }

  var manifestAge = wallTime - this.manifestCreationTime;
  var currentPresentationTime = this.originalPresentationTime_ + manifestAge;

  // The MPD spec. indicates that
  //
  // SegmentAvailabilityEndTime =
  //   MpdAvailabilityStartTime + PeriodStart +
  //   SegmentStart + 2*SegmentDuration + TimeShiftBufferDepth
  //
  // Thus, a segment is still available if the end time of the segment
  // following it plus @timeShiftBufferDepth is greater than or equal to the
  // current presentation time.
  var remove = 0;

  for (var i = 0; i < this.references.length; ++i) {
    /** @type {?number} */
    var nextEndTime = null;

    if (i < this.references.length - 1) {
      nextEndTime = this.references[i + 1].endTime;
    } else {
      // We don't have enough segments to compute an accurate
      // SegmentAvailabilityEndTime, so just assume that the next segment has
      // the same duration as the last one we have.
      var r = this.references[i];
      nextEndTime =
          r.endTime != null ? r.endTime + (r.endTime - r.startTime) : null;
    }

    if ((nextEndTime != null) &&
        (nextEndTime <
         currentPresentationTime - this.mpd.timeShiftBufferDepth)) {
      ++remove;
    } else {
      break;
    }
  }

  if (remove > 0) {
    this.references.splice(0, remove);
    shaka.log.debug(
        'Evicted', remove, 'SegmentReference(s),',
        this.references.length, 'SegmentReference(s) remain.');
  }
};


/**
 * Evicts segments that are past the end of the stream.
 *
 * @private
 */
shaka.dash.LiveSegmentIndex.prototype.evictEnd_ = function() {
  if (!this.duration) {
    return;
  }

  // Check to remove references past |duration|.
  var end = 0;
  for (var i = this.references.length - 1; i >= 0; --i) {
    var startTime = this.references[i].startTime;
    if (startTime > this.duration) {
      ++end;
    } else {
      break;
    }
  }

  if (end > 0) {
    this.references.splice(-end);
    shaka.log.warning(
        'Segments found after stream end, evicted', end,
        'SegmentReference(s),', this.references.length,
        'SegmentReference(s) remain.');
  }
};