UNPKG

shaka-player

Version:
476 lines (401 loc) 15 kB
/** * @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.'); } };