UNPKG

shaka-player

Version:
444 lines (381 loc) 16.9 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.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).'); };