Source: dash/dynamic_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.DynamicLiveSegmentIndex');

goog.require('shaka.asserts');
goog.require('shaka.dash.LiveSegmentIndex');
goog.require('shaka.dash.MpdUtils');
goog.require('shaka.media.SegmentIndex');
goog.require('shaka.media.SegmentReference');
goog.require('shaka.util.ArrayUtils');



/**
 * Creates a SegmentIndex that supports live DASH content by generating
 * SegmentReferences as needed and automatically evicting SegmentReferences
 * that are no longer available.
 *
 * If the SegmentIndex's corresponding stream is not available yet then the
 * SegmentIndex will be inactive: it will not contain any SegmentReferences nor
 * will it generate any new SegmentReferences. An inactive SegmentIndex can be
 * activated by integrating an active SegmentIndex into it.
 *
 * @param {!shaka.dash.mpd.Mpd} mpd
 * @param {!shaka.dash.mpd.Period} period
 * @param {!shaka.dash.mpd.Representation} representation
 * @param {number} manifestCreationTime The time, in seconds, when the manifest
 *     was created.
 * @param {shaka.util.FailoverUri.NetworkCallback} networkCallback
 * @throws {Error} If the SegmentIndex's corresponding stream is available but
 *     the initial SegmentReferences could not be generated.
 * @constructor
 * @struct
 * @extends {shaka.dash.LiveSegmentIndex}
 */
shaka.dash.DynamicLiveSegmentIndex = function(
    mpd, period, representation, manifestCreationTime, networkCallback) {
  shaka.asserts.assert(mpd.availabilityStartTime != null);
  shaka.asserts.assert(period.start != null);
  shaka.asserts.assert(representation.segmentTemplate);
  shaka.asserts.assert(representation.segmentTemplate.segmentDuration);
  shaka.asserts.assert(representation.segmentTemplate.timescale > 0);

  // Alias.
  var DynamicLiveSegmentIndex = shaka.dash.DynamicLiveSegmentIndex;

  var earliestSegmentNumber = 1;
  var numSegments = 0;
  var pair = DynamicLiveSegmentIndex.computeAvailableSegmentRange_(
      mpd, period, representation, manifestCreationTime);
  if (pair) {
    earliestSegmentNumber = pair.earliest;
    numSegments = pair.current - pair.earliest + 1;
  }

  var references = shaka.dash.MpdUtils.generateSegmentReferences(
      networkCallback, representation, earliestSegmentNumber, numSegments);
  if (references == null) {
    var error = new Error('Failed to generate SegmentReferences.');
    error.type = 'stream';
    throw error;
  }
  shaka.asserts.assert(references.length == 0 ||
                       references[references.length - 1].endTime != null);

  shaka.dash.LiveSegmentIndex.call(
      this, references, mpd, period, manifestCreationTime);

  /** @private {!shaka.dash.mpd.Representation} */
  this.representation_ = representation;

  /**
   * Either the time when the last segment became available, in seconds, or
   * null if this SegmentIndex is inactive.
   *
   * @private {?number}
   */
  this.latestAvailableSegmentEndTime_ =
      this.length() > 0 ?
      mpd.availabilityStartTime + period.start + this.last().endTime :
      null;
  shaka.asserts.assert(this.latestAvailableSegmentEndTime_ <=
                       manifestCreationTime);

  /**
   * Either the time when the last segment became available when the manifest
   * was created, in seconds, or null if this SegmentIndex is inactive.
   *
   * @private {?number}
   */
  this.originalLatestAvailableSegmentEndTime_ =
      this.latestAvailableSegmentEndTime_;

  /**
   * Either the segment number (one-based) of the next new SegmentReference, or
   * null if this SegmentIndex is inactive.
   *
   * @private {?number}
   */
  this.nextSegmentNumber_ = pair ? pair.current + 1 : null;

  /** @private {shaka.util.FailoverUri.NetworkCallback} */
  this.networkCallback_ = networkCallback;
};
goog.inherits(shaka.dash.DynamicLiveSegmentIndex,
              shaka.dash.LiveSegmentIndex);


/**
 * Computes the segment numbers of the earliest segment and the current
 * segment, both relative to the start of |period|. Assumes the MPD is dynamic
 * and the Representation has a SegmentTemplate that specifies a segment
 * duration.
 *
 * The earliest segment is the segment with the smallest start time that is
 * still available from the media server. The current segment is the segment
 * with the largest start time that is available from the media server and that
 * also respects the 'suggestedPresentationDelay' attribute.
 *
 * @param {!shaka.dash.mpd.Mpd} mpd
 * @param {!shaka.dash.mpd.Period} period
 * @param {!shaka.dash.mpd.Representation} representation
 * @param {number} manifestCreationTime The time, in seconds, when the manifest
 *     was created.
 * @return {?{earliest: number, current: number}} Two segment numbers
 *     (both one-based), or null if the stream is not available yet.
 * @private
 */
shaka.dash.DynamicLiveSegmentIndex.computeAvailableSegmentRange_ =
    function(mpd, period, representation, manifestCreationTime) {
  shaka.asserts.assert(period.start != null);
  shaka.asserts.assert(representation.segmentTemplate);
  shaka.asserts.assert(representation.segmentTemplate.segmentDuration);
  shaka.asserts.assert(representation.segmentTemplate.timescale > 0);

  if (mpd.availabilityStartTime > manifestCreationTime) {
    shaka.log.warning('The stream is not available yet!', representation);
    return null;
  }

  var suggestedPresentationDelay = mpd.suggestedPresentationDelay || 0;
  var timeShiftBufferDepth = mpd.timeShiftBufferDepth || 0;

  // The following diagram shows the relationship between the values we use to
  // compute the current segment number; descriptions of each value are given
  // within the code. The diagram depicts the media presentation timeline. 0
  // corresponds to availabilityStartTime + period.start in wall-clock time,
  // and currentPresentationTime corresponds to |manifestCreationTime_| in
  // wall-clock time.
  //
  // Legend:
  // CPT: currentPresentationTime
  // EAT: earliestAvailableSegmentStartTime
  // LAT: latestAvailableSegmentStartTime
  // BAT: bestAvailableSegmentStartTime
  // SD:  scaledSegmentDuration.
  // SPD: suggestedPresentationDelay
  // TSB: timeShiftBufferDepth
  //
  // Time:
  //   <---|-----------------+--------+-----------------+----------|--------->
  //       0                EAT      BAT               LAT        CPT
  //                                                      |---SD---|
  //                                      |-----SPD-----|
  //                      |---SD---|---SD---|<--------TSB--------->|
  // Segments:
  //   <---1--------2--------3--------4--------5--------6--------7--------8-->
  //       |---SD---|---SD---| ...

  var segmentTemplate = representation.segmentTemplate;

  var scaledSegmentDuration =
      segmentTemplate.segmentDuration / segmentTemplate.timescale;

  // The current presentation time, which is the amount of time since the start
  // of the Period.
  var currentPresentationTime =
      manifestCreationTime - (mpd.availabilityStartTime + period.start);
  if (currentPresentationTime < 0) {
    shaka.log.warning('The Period is not available yet!', representation);
    return null;
  }

  // Compute the segment start time of the earliest available segment, i.e.,
  // the segment that starts furthest from the present but is still available).
  // The MPD spec. indicates that
  //
  // SegmentAvailabilityStartTime =
  //   MpdAvailabilityStartTime + PeriodStart + SegmentStart + SegmentDuration
  //
  // SegmentAvailabilityEndTime =
  //   SegmentAvailabilityStartTime + SegmentDuration + TimeShiftBufferDepth
  //
  // So let SegmentAvailabilityEndTime equal the current time and compute
  // SegmentStart, which yields the start time that a segment would need to
  // have to have an availability end time equal to the current time.
  //
  // TODO: Take into account @availabilityTimeOffset.
  var earliestAvailableTimestamp = currentPresentationTime -
                                   (2 * scaledSegmentDuration) -
                                   timeShiftBufferDepth;
  if (earliestAvailableTimestamp < 0) {
    shaka.log.v1(
        'The earliest available segment is not available yet.', representation);
    earliestAvailableTimestamp = 0;
  }

  // Now round up to the nearest segment boundary, since the segment
  // corresponding to |earliestAvailableTimestamp| is not available.
  var earliestAvailableSegmentStartTime =
      Math.ceil(earliestAvailableTimestamp / scaledSegmentDuration) *
      scaledSegmentDuration;

  // Compute the segment start time of the latest available segment, i.e., the
  // segment that starts closest to the present but is available.
  //
  // Using the above formulas, let SegmentAvailabilityStartTime equal the
  // current time and compute SegmentStart, which yields the start time that
  // a segment would need to have to have an availability start time
  // equal to the current time.
  var latestAvailableTimestamp = currentPresentationTime -
                                 scaledSegmentDuration;
  if (latestAvailableTimestamp < 0) {
    shaka.log.error(
        'The current segment is not available yet!', representation);
    return null;
  }

  // Now round down to the nearest segment boundary, since the segment
  // corresponding to |latestAvailableTimestamp| may not yet be available.
  var latestAvailableSegmentStartTime =
      Math.floor(latestAvailableTimestamp / scaledSegmentDuration) *
      scaledSegmentDuration;

  // Now compute the start time of the "best" available segment by offsetting
  // by @suggestedPresentationDelay.
  var bestAvailableTimestamp = latestAvailableSegmentStartTime -
                               suggestedPresentationDelay;
  if (bestAvailableTimestamp < 0) {
    shaka.log.v1('bestAvailableTimestamp < 0');
    bestAvailableTimestamp = 0;
    // Don't return; taking into account @suggestedPresentationDelay is only a
    // recommendation. The first segment /might/ be available.
  }

  var bestAvailableSegmentStartTime =
      Math.floor(bestAvailableTimestamp / scaledSegmentDuration) *
      scaledSegmentDuration;

  // Now take the larger of |bestAvailableSegmentStartTime| and
  // |earliestAvailableSegmentStartTime|.
  var currentSegmentStartTime;
  if (bestAvailableSegmentStartTime >= earliestAvailableSegmentStartTime) {
    currentSegmentStartTime = bestAvailableSegmentStartTime;
    shaka.log.debug('The best available segment is still available.');
  } else {
    // @suggestedPresentationDelay is large compared to @timeShiftBufferDepth,
    // so we can't start as far back as we'd like.
    currentSegmentStartTime = earliestAvailableSegmentStartTime;
    shaka.log.debug('The best available segment is no longer available.');
  }

  // Now compute the segment numbers.
  var earliestSegmentNumber =
      (earliestAvailableSegmentStartTime / scaledSegmentDuration) + 1;
  shaka.asserts.assert(
      earliestSegmentNumber == Math.round(earliestSegmentNumber),
      'earliestSegmentNumber should be an integer.');

  var currentSegmentNumber =
      (currentSegmentStartTime / scaledSegmentDuration) + 1;
  shaka.asserts.assert(
      currentSegmentNumber == Math.round(currentSegmentNumber),
      'currentSegmentNumber should be an integer.');

  shaka.log.v1('earliestSegmentNumber', earliestSegmentNumber);
  shaka.log.v1('currentSegmentNumber', currentSegmentNumber);

  return { earliest: earliestSegmentNumber, current: currentSegmentNumber };
};


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


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


/**
 * Integrates |segmentIndex| into this SegmentIndex, but only if this
 * SegmentIndex is inactive and |segmentIndex| is an active
 * DynamicLiveSegmentIndex.
 *
 * @override
 */
shaka.dash.DynamicLiveSegmentIndex.prototype.integrate = function(
    segmentIndex) {
  if (this.latestAvailableSegmentEndTime_ != null) {
    // There's no need to integrate |segmentIndex| since we are already
    // generating SegmentReferences.
    shaka.log.debug('Ignoring SegmentIndex integration.', this);
    return false;
  }

  if (!(segmentIndex instanceof shaka.dash.DynamicLiveSegmentIndex)) {
    // The SegmentIndex's corresponding Representation changed, or we were
    // called with an incorrect SegmentIndex, either way, don't do anything.
    shaka.log.warning('Cannot integrate SegmentIndex:',
                      'Only a DynamicLiveSegmentIndex can be integrated into',
                      'another DynamicLiveSegmentIndex.',
                      this);
    return false;
  }

  var other = /** @type {!shaka.dash.DynamicLiveSegmentIndex} */ (segmentIndex);
  if (other.latestAvailableSegmentEndTime_ == null) {
    // The stream still isn't available.
    return false;
  }

  this.latestAvailableSegmentEndTime_ =
      other.latestAvailableSegmentEndTime_;
  this.originalLatestAvailableSegmentEndTime_ =
      other.originalLatestAvailableSegmentEndTime_;
  this.nextSegmentNumber_ = other.nextSegmentNumber_;
  this.manifestCreationTime = other.manifestCreationTime;
  this.duration = other.duration;

  this.merge(segmentIndex);
  this.generateSegmentReferences_(shaka.util.Clock.now() / 1000.0);
  this.initializeSeekWindow();

  return true;
};


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

  if (this.latestAvailableSegmentEndTime_ != null) {
    shaka.asserts.assert(
        this.originalLatestAvailableSegmentEndTime_ != null);
    this.latestAvailableSegmentEndTime_ += delta;
    this.originalLatestAvailableSegmentEndTime_ += delta;
  }

  return delta;
};


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


/**
 * @param {number} wallTime The current wall-clock time in seconds.
 * @private
 */
shaka.dash.DynamicLiveSegmentIndex.prototype.generateSegmentReferences_ =
    function(wallTime) {
  if (this.latestAvailableSegmentEndTime_ == null ||
      this.originalLatestAvailableSegmentEndTime_ == null ||
      this.nextSegmentNumber_ == null) {
    return;
  }

  var manifestAge = wallTime - this.manifestCreationTime;

  // Compute the number of seconds that have elapsed between the time when the
  // last segment was generated and the current wall-clock time.
  var elapsed = (this.originalLatestAvailableSegmentEndTime_ + manifestAge) -
                this.latestAvailableSegmentEndTime_;
  shaka.asserts.assert(elapsed >= 0);

  // Determine the number of new SegmentReferences to generate.
  var segmentTemplate = this.representation_.segmentTemplate;
  var scaledSegmentDuration =
      (segmentTemplate.segmentDuration / segmentTemplate.timescale);
  var numNewSegments = Math.floor(elapsed / scaledSegmentDuration);

  if (numNewSegments == 0) {
    return;
  }

  // Generate and correct the new SegmentReferences.
  var newReferences = shaka.dash.MpdUtils.generateSegmentReferences(
      this.networkCallback_, this.representation_,
      this.nextSegmentNumber_, numNewSegments);

  // |newReferences| should never be null since generateSegmentReferences()
  // should have been called at least once successfully with |representation_|.
  shaka.asserts.assert(newReferences);
  newReferences =
      /** @type {!Array.<!shaka.media.SegmentReference>} */ (newReferences);

  Array.prototype.push.apply(
      this.references,
      shaka.media.SegmentReference.shift(
          newReferences, this.timestampCorrection));
  this.assertCorrectReferences();

  this.latestAvailableSegmentEndTime_ +=
      numNewSegments * scaledSegmentDuration;
  this.nextSegmentNumber_ += numNewSegments;

  shaka.log.debug('Generated', numNewSegments, 'SegmentReference(s).');
};