Files
shaka-player/lib/dash/live_segment_index.js
T
Timothy Drews 34915527f0 Complete stream rework: refactor seek ranges.
* Move seek range updates into SegmentIndex implementations
  and StreamVideoSource.
* Remove unnecessary sub-class hooks from StreamVideoSource.
* Remove internal LiveSegmentIndex timer: do SegmentReference eviction
  and generation on demand.
* Rework Stream startup again: consider Streams as started when they
  have buffered some minimum amount of content. As part of this, only
  apply a timestamp correction to the SBM when the streams have started.
* Defer stream switches until all SegmentIndexes have been corrected.
* Partially revert optimistic "bufferedAhead" calculations in Stream:
  check if we've buffered enough content at least every second.
* Make LiveSegmentIndex more robust against a "wrong"
  @availabilityStartTime.
* Fix a bug where a positive timestamp correction would trigger an extra
  Stream resync.
* Fix a bug in evict_() where the first segment would never get evicted.
* Check stream started conditions in onUpdate_ instead of after a segment
  has been inserted.
* Tidy-up immediate/clearBuffer naming and other comments.

Closes #51
Closes #109

Change-Id: Ic6f0b96065d3c5dfb5c03ca1c52363bd77b22875
2015-07-06 14:20:01 -07:00

404 lines
13 KiB
JavaScript

/**
* 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.
*
* @fileoverview Implements a SegmentIndex that supports live DASH content.
*/
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 LiveSegmentIndex.
*
* 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;
/**
* @const {number}
* @protected
*/
this.manifestCreationTime = manifestCreationTime;
/**
* 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) {
this.merge(segmentIndex);
if (this.originalPresentationTime_ == null) {
this.initializeSeekWindow();
}
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);
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);
if (this.originalPresentationTime_ != null) {
shaka.asserts.assert(this.originalLiveEdge_ != null);
shaka.asserts.assert(this.seekStartTime_ != null);
this.originalLiveEdge_ += delta;
this.seekStartTime_ += delta;
if (this.originalLiveEdge_ > this.originalPresentationTime_) {
// A timestamp correction should be less than the duration of any one
// segment in the stream. So, the live-edge should not surpass the
// current presentation time, but if it does then try to recover.
shaka.log.warning(
'Timestamp correction (' + timestampCorrection + ')',
'is unreasonably large for live content.',
'The content may have errors in it.');
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 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;
}
}
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;
shaka.asserts.assert(currentLiveEdge >= 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_);
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) {
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.');
}
};