mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-25 17:45:03 +03:00
1e16366eb3
Change-Id: I65df78f278e5a02c81aabb03fe3c0b81ff3ed605
792 lines
22 KiB
JavaScript
792 lines
22 KiB
JavaScript
/**
|
|
* @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.media.Stream');
|
|
|
|
goog.require('shaka.asserts');
|
|
goog.require('shaka.log');
|
|
goog.require('shaka.media.IStream');
|
|
goog.require('shaka.media.SourceBufferManager');
|
|
goog.require('shaka.media.StreamInfo');
|
|
goog.require('shaka.player.Defaults');
|
|
goog.require('shaka.util.FakeEvent');
|
|
goog.require('shaka.util.FakeEventTarget');
|
|
goog.require('shaka.util.IBandwidthEstimator');
|
|
goog.require('shaka.util.PublicPromise');
|
|
goog.require('shaka.util.TypedBind');
|
|
|
|
|
|
|
|
/**
|
|
* @event shaka.media.Stream.EndedEvent
|
|
* @description Fired when the stream ends.
|
|
* @property {string} type 'ended'
|
|
* @property {boolean} bubbles false
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
* Creates a Stream, which presents audio and video streams via an
|
|
* HTMLVideoElement.
|
|
*
|
|
* @param {!shaka.util.FakeEventTarget} parent The parent for event bubbling.
|
|
* @param {!HTMLVideoElement} video
|
|
* @param {!MediaSource} mediaSource The MediaSource, which must be in the
|
|
* 'open' state.
|
|
* @param {string} fullMimeType The full MIME type of the SourceBuffer to
|
|
* create, which |mediaSource| must support.
|
|
* @param {!shaka.util.IBandwidthEstimator} estimator A bandwidth estimator to
|
|
* attach to all segment requests.
|
|
*
|
|
* @fires shaka.media.IStream.AdaptationEvent
|
|
* @fires shaka.media.Stream.EndedEvent
|
|
* @fires shaka.player.Player.ErrorEvent
|
|
*
|
|
* @throws {QuotaExceededError} if another SourceBuffer is not allowed.
|
|
*
|
|
* @struct
|
|
* @constructor
|
|
* @implements {shaka.media.IStream}
|
|
* @extends {shaka.util.FakeEventTarget}
|
|
*/
|
|
shaka.media.Stream = function(
|
|
parent, video, mediaSource, fullMimeType, estimator) {
|
|
shaka.util.FakeEventTarget.call(this, parent);
|
|
|
|
/** @private {!HTMLVideoElement} */
|
|
this.video_ = video;
|
|
|
|
/** @private {!shaka.media.SourceBufferManager} */
|
|
this.sbm_ = new shaka.media.SourceBufferManager(
|
|
mediaSource, fullMimeType, estimator);
|
|
|
|
/** @private {!shaka.util.IBandwidthEstimator} */
|
|
this.estimator_ = estimator;
|
|
|
|
/** @private {shaka.media.StreamInfo} */
|
|
this.streamInfo_ = null;
|
|
|
|
/** @private {shaka.media.SegmentIndex} */
|
|
this.segmentIndex_ = null;
|
|
|
|
/** @private {ArrayBuffer} */
|
|
this.initData_ = null;
|
|
|
|
/** @private {boolean} */
|
|
this.switched_ = false;
|
|
|
|
/** @private {?number} */
|
|
this.updateTimer_ = null;
|
|
|
|
/** @private {boolean} */
|
|
this.resyncing_ = false;
|
|
|
|
/** @private {?number} */
|
|
this.timestampCorrection_ = null;
|
|
|
|
/** @private {boolean} */
|
|
this.started_ = false;
|
|
|
|
/** @private {!shaka.util.PublicPromise} */
|
|
this.startedPromise_ = new shaka.util.PublicPromise();
|
|
|
|
/** @private {boolean} */
|
|
this.proceeded_ = false;
|
|
|
|
/** @private {boolean} */
|
|
this.ended_ = false;
|
|
|
|
/** @private {number} */
|
|
this.initialBufferSizeSeconds_ = 0;
|
|
|
|
/** @private {number} */
|
|
this.bufferSizeSeconds_ = shaka.player.Defaults.STREAM_BUFFER_SIZE;
|
|
|
|
/**
|
|
* Work-around for MSE issues where the stream can get stuck after clearing
|
|
* the buffer or starting mid-stream (as is done for live). Nudging the
|
|
* playhead seems to get the browser's media pipeline moving again.
|
|
* @private {boolean}
|
|
*/
|
|
this.needsNudge_ = false;
|
|
|
|
// For debugging purposes:
|
|
if (!COMPILED) {
|
|
/** @private {boolean} */
|
|
this.fetching_ = false;
|
|
|
|
/** @private {string} */
|
|
this.mimeType_ = fullMimeType.split(';')[0];
|
|
shaka.asserts.assert(this.mimeType_.length > 0);
|
|
}
|
|
};
|
|
goog.inherits(shaka.media.Stream, shaka.util.FakeEventTarget);
|
|
|
|
|
|
/**
|
|
* A tiny amount of time, in seconds, used to nudge the video element. This
|
|
* is used when the buffer has been cleared to get the media pipeline unstuck.
|
|
*
|
|
* @see http://crbug.com/478151
|
|
*
|
|
* @private
|
|
* @const {number}
|
|
*/
|
|
shaka.media.Stream.NUDGE_ = 0.001;
|
|
|
|
|
|
/**
|
|
* <p>
|
|
* Configures the Stream options.
|
|
* </p>
|
|
*
|
|
* The following configuration options are supported:
|
|
* <ul>
|
|
* <li>
|
|
* <b>initialStreamBufferSize</b>: number <br>
|
|
* Sets the amount of content, in seconds, that the Stream must buffer to
|
|
* ensure smooth playback.
|
|
*
|
|
* <li>
|
|
* <b>streamBufferSize</b>: number <br>
|
|
* Sets the maximum amount of content, in seconds, that the Stream will
|
|
* buffer ahead of the playhead. If 'initialStreamBufferSize' is larger,
|
|
* then that value is used.
|
|
*
|
|
* <li>
|
|
* <b>segmentRequestTimeout</b>: number <br>
|
|
* Sets the segment request timeout in seconds.
|
|
* </ul>
|
|
*
|
|
* @example
|
|
* stream.configure({'streamBufferSize': 20});
|
|
*
|
|
* @param {!Object.<string, *>} config A configuration object, which contains
|
|
* the configuration options as key-value pairs. All fields should have
|
|
* already been validated.
|
|
* @override
|
|
*/
|
|
shaka.media.Stream.prototype.configure = function(config) {
|
|
if (config['initialStreamBufferSize'] != null) {
|
|
this.initialBufferSizeSeconds_ = Number(config['initialStreamBufferSize']);
|
|
}
|
|
|
|
if (config['streamBufferSize'] != null) {
|
|
this.bufferSizeSeconds_ = Number(config['streamBufferSize']);
|
|
}
|
|
|
|
if (config['segmentRequestTimeout'] != null) {
|
|
this.sbm_.setSegmentRequestTimeout(Number(config['segmentRequestTimeout']));
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @override
|
|
* @suppress {checkTypes} to set otherwise non-nullable types to null.
|
|
*/
|
|
shaka.media.Stream.prototype.destroy = function() {
|
|
this.cancelUpdateTimer_();
|
|
|
|
this.startedPromise_.destroy();
|
|
this.startedPromise_ = null;
|
|
|
|
this.streamInfo_ = null;
|
|
this.estimator_ = null;
|
|
|
|
this.sbm_.destroy();
|
|
this.sbm_ = null;
|
|
|
|
this.video_ = null;
|
|
|
|
this.parent = null;
|
|
};
|
|
|
|
|
|
/** @override */
|
|
shaka.media.Stream.prototype.getStreamInfo = function() {
|
|
return this.streamInfo_;
|
|
};
|
|
|
|
|
|
/** @override */
|
|
shaka.media.Stream.prototype.getSegmentIndex = function() {
|
|
return this.segmentIndex_;
|
|
};
|
|
|
|
|
|
/**
|
|
* <p>
|
|
* Returns a promise that the Stream will resolve after it has buffered
|
|
* 'initialStreamBufferSize' seconds of content
|
|
* (see {@link shaka.media.Stream#configure}).
|
|
* </p>
|
|
*
|
|
* <p>
|
|
* The Stream will not modify its underlying SourceBuffer while it's in the
|
|
* 'waiting' state (see {@link shaka.media.IStream}).
|
|
* </p>
|
|
*
|
|
* <p>
|
|
* The caller should apply the timestamp correction to the Stream's current
|
|
* StreamInfo's SegmentIndex and future StreamInfo's passed to switch() before
|
|
* it resolves |proceed|.
|
|
* </p>
|
|
*
|
|
* @override
|
|
*/
|
|
shaka.media.Stream.prototype.started = function(proceed) {
|
|
if (!this.proceeded_) {
|
|
proceed.then(
|
|
function() {
|
|
shaka.asserts.assert(this.started_);
|
|
shaka.asserts.assert(!this.proceeded_);
|
|
shaka.asserts.assert(!this.ended_);
|
|
shaka.log.debug(this.logPrefix_(), 'proceeding...');
|
|
this.proceeded_ = true;
|
|
if (!this.updateTimer_) {
|
|
this.setUpdateTimer_(0);
|
|
}
|
|
}.bind(this)).catch(
|
|
function(error) {
|
|
// The caller should never reject |proceed| unless it is destroyed.
|
|
shaka.asserts.assert(error.type == 'destroy');
|
|
});
|
|
}
|
|
return this.startedPromise_;
|
|
};
|
|
|
|
|
|
/**
|
|
* <p>
|
|
* Returns true if the stream has ended; otherwise, returns false.
|
|
* </p>
|
|
*
|
|
* <p>
|
|
* The Stream will not modify its underlying SourceBuffer while it's in the
|
|
* 'ended' state.
|
|
* </p>
|
|
*
|
|
* @override
|
|
*/
|
|
shaka.media.Stream.prototype.hasEnded = function() {
|
|
return this.ended_;
|
|
};
|
|
|
|
|
|
/**
|
|
* <p>
|
|
* Starts presenting the specified stream asynchronously. The stream's MIME
|
|
* type must be compatible with the Stream's underlying SourceBuffer, i.e., the
|
|
* stream must have the same content type, container, and codec as the
|
|
* SourceBuffer, but the stream's codec's profile may vary.
|
|
* </p>
|
|
*
|
|
* <p>
|
|
* Note: |opt_clearBufferOffset| is relative to the video's playhead.
|
|
* </p>
|
|
*
|
|
* @override
|
|
*/
|
|
shaka.media.Stream.prototype.switch = function(
|
|
streamInfo, clearBuffer, opt_clearBufferOffset) {
|
|
shaka.asserts.assert(
|
|
streamInfo.mimeType == this.mimeType_,
|
|
'The stream\'s type must match the Stream\'s SourceBuffer\'s type.');
|
|
|
|
if (streamInfo == this.streamInfo_) {
|
|
shaka.log.debug(this.logPrefix_(), 'already using stream', streamInfo);
|
|
return;
|
|
}
|
|
|
|
var async = [
|
|
streamInfo.segmentIndexSource.create(),
|
|
streamInfo.segmentInitSource.create()
|
|
];
|
|
|
|
Promise.all(async).then(shaka.util.TypedBind(this,
|
|
/** @param {!Array} results */
|
|
function(results) {
|
|
if (!this.video_) {
|
|
// We got destroyed.
|
|
return;
|
|
}
|
|
|
|
var previousStreamInfo = this.streamInfo_;
|
|
|
|
this.streamInfo_ = streamInfo;
|
|
this.segmentIndex_ = results[0];
|
|
this.initData_ = results[1];
|
|
this.switched_ = true;
|
|
|
|
if (this.resyncing_) {
|
|
// resync() was called while creating the SegmentIndex and init data.
|
|
// resync() will set the update timer if needed.
|
|
return;
|
|
}
|
|
|
|
if (!previousStreamInfo) {
|
|
// Call onUpdate_() asynchronously so it can more easily assert that
|
|
// it was called at an appopriate time.
|
|
this.setUpdateTimer_(0);
|
|
} else if (clearBuffer) {
|
|
this.resync_(true /* forceClear */, opt_clearBufferOffset);
|
|
} else {
|
|
shaka.asserts.assert((this.updateTimer_ != null) || this.fetching_);
|
|
}
|
|
})
|
|
).catch(shaka.util.TypedBind(this,
|
|
/** @param {*} error */
|
|
function(error) {
|
|
if (error.type == 'aborted') {
|
|
return;
|
|
}
|
|
|
|
if (this.proceeded_) {
|
|
var event = shaka.util.FakeEvent.createErrorEvent(error);
|
|
this.dispatchEvent(event);
|
|
} else {
|
|
this.startedPromise_.reject(error);
|
|
}
|
|
})
|
|
);
|
|
};
|
|
|
|
|
|
/**
|
|
* Resynchronizes the Stream's current position to the video's playhead.
|
|
* This must be called when the user seeks.
|
|
*
|
|
* @override
|
|
*/
|
|
shaka.media.Stream.prototype.resync = function() {
|
|
shaka.log.debug(this.logPrefix_(), 'resync');
|
|
return this.resync_(false /* forceClear */);
|
|
};
|
|
|
|
|
|
/**
|
|
* @param {boolean} forceClear
|
|
* @param {number=} opt_clearBufferOffset if |forceClear| and
|
|
* |opt_clearBufferOffset| are truthy, clear the stream buffer from the
|
|
* given offset (relative to the video's current time) to the end of the
|
|
* stream.
|
|
* @private
|
|
*/
|
|
shaka.media.Stream.prototype.resync_ = function(
|
|
forceClear, opt_clearBufferOffset) {
|
|
if (!this.streamInfo_ || this.resyncing_) {
|
|
return;
|
|
}
|
|
|
|
// We should either be between updates, fetching a segment, or waiting to
|
|
// proceed after startup.
|
|
shaka.asserts.assert(
|
|
(this.updateTimer_ != null) ||
|
|
this.fetching_ ||
|
|
(this.started_ && !this.proceeded_),
|
|
'Unexpected call to resync_().');
|
|
|
|
shaka.asserts.assert(!opt_clearBufferOffset || opt_clearBufferOffset > 0);
|
|
|
|
this.resyncing_ = true;
|
|
this.cancelUpdateTimer_();
|
|
|
|
this.sbm_.abort().then(shaka.util.TypedBind(this,
|
|
function() {
|
|
shaka.log.v1(this.logPrefix_(), 'abort done.');
|
|
shaka.asserts.assert((this.updateTimer_ == null) && this.resyncing_);
|
|
shaka.asserts.assert(!this.fetching_);
|
|
|
|
// Clear the source buffer if we are seeking outside of the currently
|
|
// buffered range. This seems to make the browser's eviction policy
|
|
// saner and fixes "dead-zone" issues such as #15 and #26. If seeking
|
|
// within the buffered range, we avoid clearing so that we don't
|
|
// re-download content.
|
|
var currentTime = this.video_.currentTime;
|
|
if (forceClear ||
|
|
!this.sbm_.isBuffered(currentTime) ||
|
|
!this.sbm_.isInserted(currentTime)) {
|
|
shaka.log.debug(this.logPrefix_(), 'clear required.');
|
|
|
|
if (opt_clearBufferOffset) {
|
|
return this.sbm_.clearAfter(
|
|
this.video_.currentTime + opt_clearBufferOffset);
|
|
} else {
|
|
shaka.log.debug(this.logPrefix_(), 'nudge needed!');
|
|
this.needsNudge_ = true;
|
|
return this.sbm_.clear();
|
|
}
|
|
} else {
|
|
shaka.log.debug(this.logPrefix_(), 'no clear required.');
|
|
return Promise.resolve();
|
|
}
|
|
})
|
|
).then(shaka.util.TypedBind(this,
|
|
function() {
|
|
shaka.asserts.assert((this.updateTimer_ == null) && this.resyncing_);
|
|
shaka.asserts.assert(!this.fetching_);
|
|
this.resyncing_ = false;
|
|
this.setUpdateTimer_(0);
|
|
})
|
|
).catch(shaka.util.TypedBind(this,
|
|
function(error) {
|
|
shaka.asserts.assert(error.type != 'aborted');
|
|
this.resyncing_ = false;
|
|
|
|
if (this.proceeded_) {
|
|
var event = shaka.util.FakeEvent.createErrorEvent(error);
|
|
this.dispatchEvent(event);
|
|
} else {
|
|
this.startedPromise_.reject(error);
|
|
}
|
|
})
|
|
);
|
|
};
|
|
|
|
|
|
/** @override */
|
|
shaka.media.Stream.prototype.isBuffered = function(time) {
|
|
return this.sbm_.isBuffered(time) && this.sbm_.isInserted(time);
|
|
};
|
|
|
|
|
|
/**
|
|
* Does nothing since audio and video streams are always enabled.
|
|
*
|
|
* @override
|
|
*/
|
|
shaka.media.Stream.prototype.setEnabled = function(enabled) {};
|
|
|
|
|
|
/**
|
|
* Always returns true since audio and video streams are always enabled.
|
|
*
|
|
* @override
|
|
*/
|
|
shaka.media.Stream.prototype.getEnabled = function() {
|
|
return true;
|
|
};
|
|
|
|
|
|
/**
|
|
* Gets the Stream's buffering goal, which is the amount of content, in
|
|
* seconds, that the Stream attempts to buffer ahead of the Stream's current
|
|
* position.
|
|
*
|
|
* @return {number} The buffering goal.
|
|
* @private
|
|
*/
|
|
shaka.media.Stream.prototype.getBufferingGoal_ = function() {
|
|
return this.started_ ?
|
|
Math.max(this.initialBufferSizeSeconds_, this.bufferSizeSeconds_) :
|
|
this.initialBufferSizeSeconds_;
|
|
};
|
|
|
|
|
|
/**
|
|
* Update callback.
|
|
*
|
|
* @private
|
|
*/
|
|
shaka.media.Stream.prototype.onUpdate_ = function() {
|
|
shaka.log.v2(this.logPrefix_(), 'onUpdate_');
|
|
|
|
shaka.asserts.assert(this.streamInfo_);
|
|
shaka.asserts.assert(this.segmentIndex_);
|
|
shaka.asserts.assert((this.updateTimer_ != null) && !this.resyncing_);
|
|
shaka.asserts.assert(!this.fetching_);
|
|
|
|
if (this.started_ && !this.proceeded_) {
|
|
shaka.log.v1(this.logPrefix_(), 'waiting to proceed...');
|
|
this.updateTimer_ = null;
|
|
return;
|
|
}
|
|
|
|
// We should only have one buffered range at any given time, so the next
|
|
// segment we need is after the last one we inserted. However, non-ideal
|
|
// content and/or browser eviction policy may induce multiple buffered
|
|
// ranges in some cases.
|
|
if (this.proceeded_ &&
|
|
!this.ended_ &&
|
|
this.sbm_.detectMultipleBufferedRanges()) {
|
|
// Try to recover by clearing the buffer.
|
|
this.resync_(true /* clearBuffer */);
|
|
return;
|
|
}
|
|
|
|
this.updateTimer_ = null;
|
|
|
|
// Retain local copies since switch() may be called while fetching.
|
|
var streamInfo = /** @type {!shaka.media.StreamInfo} */ (this.streamInfo_);
|
|
var segmentIndex =
|
|
/** @type {!shaka.media.SegmentIndex} */ (this.segmentIndex_);
|
|
|
|
var currentTime = this.video_.currentTime;
|
|
|
|
// During startup, if there's a non-zero timestamp correction then the
|
|
// playhead will not align with the start of the first buffered range, as we
|
|
// do not apply the timestamp correction to the SegmentIndexes until after
|
|
// stream startup. So, we must call bufferedAheadOf() with an offset to
|
|
// ensure we compute the correct size of the initial buffered range.
|
|
var bufferStart = this.started_ ?
|
|
currentTime :
|
|
currentTime + (this.timestampCorrection_ || 0);
|
|
if (this.sbm_.bufferedAheadOf(bufferStart) >= this.getBufferingGoal_()) {
|
|
shaka.log.v1(this.logPrefix_(), 'buffering goal reached.');
|
|
this.startIfNeeded_();
|
|
// TODO: trigger onUpdate_ when playback rate changes (assuming changed
|
|
// through Player.setPlaybackRate).
|
|
var rate = Math.abs(this.video_.playbackRate) || 1;
|
|
this.setUpdateTimer_(1000 / rate);
|
|
return;
|
|
}
|
|
|
|
var reference = this.getNext_(currentTime, segmentIndex);
|
|
if (!reference) {
|
|
shaka.log.v1(
|
|
this.logPrefix_(), 'new segment is not needed or is not available.');
|
|
|
|
// We haven't hit our buffering goal yet, but there's nothing left to
|
|
// buffer.
|
|
this.startIfNeeded_();
|
|
|
|
if (this.proceeded_ && !this.ended_) {
|
|
shaka.asserts.assert(this.started_);
|
|
shaka.log.debug(this.logPrefix_(), 'stream has ended.');
|
|
this.ended_ = true;
|
|
this.fireEndedEvent_();
|
|
}
|
|
|
|
// Check again in a second: the SegmentIndex might be generating
|
|
// SegmentReferences or there might be a manifest update.
|
|
this.setUpdateTimer_(1000);
|
|
return;
|
|
}
|
|
|
|
// Fetch and append the next segment. We only fetch a single segment at a
|
|
// time because fetching multiple segments can cause buffering when bandwidth
|
|
// is limited. If we are behind our buffering goal by more than one segment,
|
|
// we should still be able to catch up by requesting single segments.
|
|
if (!COMPILED) {
|
|
this.fetching_ = true;
|
|
}
|
|
|
|
if (this.initData_) {
|
|
shaka.log.v1(this.logPrefix_(), 'appending initialization data...');
|
|
}
|
|
shaka.log.v1(this.logPrefix_(), 'fetching segment', reference);
|
|
var fetch = this.sbm_.fetch(
|
|
reference, streamInfo.timestampOffset, this.initData_);
|
|
|
|
this.initData_ = null;
|
|
if (this.switched_) {
|
|
this.switched_ = false;
|
|
this.fireAdaptationEvent_(streamInfo);
|
|
}
|
|
this.ended_ = false;
|
|
|
|
fetch.then(shaka.util.TypedBind(this,
|
|
/** @param {?number} timestampCorrection */
|
|
function(timestampCorrection) {
|
|
shaka.log.v1(this.logPrefix_(), 'fetch done.');
|
|
shaka.asserts.assert((this.updateTimer_ == null) && !this.resyncing_);
|
|
shaka.asserts.assert(this.fetching_);
|
|
|
|
if (!COMPILED) {
|
|
this.fetching_ = false;
|
|
}
|
|
|
|
if (this.timestampCorrection_ == null) {
|
|
shaka.asserts.assert(!this.started_);
|
|
shaka.asserts.assert(timestampCorrection != null);
|
|
this.timestampCorrection_ = timestampCorrection;
|
|
}
|
|
|
|
if (this.needsNudge_ && this.sbm_.bufferedAheadOf(currentTime) > 0) {
|
|
shaka.log.debug(this.logPrefix_(), 'applying nudge...');
|
|
this.needsNudge_ = false;
|
|
this.video_.currentTime += shaka.media.Stream.NUDGE_;
|
|
}
|
|
|
|
this.setUpdateTimer_(0);
|
|
})
|
|
).catch(shaka.util.TypedBind(this,
|
|
/** @param {*} error */
|
|
function(error) {
|
|
if (!COMPILED) {
|
|
this.fetching_ = false;
|
|
}
|
|
|
|
if (error.type == 'aborted') {
|
|
// We were aborted from either destroy() or resync().
|
|
shaka.log.v1(this.logPrefix_(), 'fetch aborted.');
|
|
shaka.asserts.assert(!this.sbm_ || this.resyncing_);
|
|
return;
|
|
}
|
|
|
|
var recoverableErrors = [0, 404, 410];
|
|
if (error.type == 'net' &&
|
|
recoverableErrors.indexOf(error.xhr.status) != -1 &&
|
|
this.streamInfo_ /* not yet destroyed */) {
|
|
shaka.log.info(
|
|
this.logPrefix_(), 'retrying segment request in 5 seconds...');
|
|
// Depending on application policy, this could be recoverable,
|
|
// so set a timer on the supposition that the app might not end
|
|
// playback.
|
|
this.setUpdateTimer_(5000);
|
|
}
|
|
|
|
// We should not reject |startedPromise_| here since we may still be
|
|
// able to complete startup.
|
|
var event = shaka.util.FakeEvent.createErrorEvent(error);
|
|
this.dispatchEvent(event);
|
|
})
|
|
);
|
|
};
|
|
|
|
|
|
/**
|
|
* Finds the next segment that starts at or after |currentTime| from
|
|
* |segmentIndex| that has not been inserted.
|
|
*
|
|
* @param {number} currentTime The time in seconds.
|
|
* @param {!shaka.media.SegmentIndex} segmentIndex
|
|
* @return {shaka.media.SegmentReference}
|
|
* @private
|
|
*/
|
|
shaka.media.Stream.prototype.getNext_ = function(
|
|
currentTime, segmentIndex) {
|
|
var last = this.sbm_.getLastInserted();
|
|
if (last != null) {
|
|
return last.endTime != null ? segmentIndex.find(last.endTime) : null;
|
|
} else {
|
|
// Return the last SegmentReference if no segments have been inserted so
|
|
// that we will always compute a timestamp correction and resolve started().
|
|
return segmentIndex.find(currentTime) ||
|
|
(segmentIndex.length() ? segmentIndex.last() : null);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Corrects the SBM and resolves |startedPromise_| if needed.
|
|
*
|
|
* @private
|
|
*/
|
|
shaka.media.Stream.prototype.startIfNeeded_ = function() {
|
|
if (this.started_ || (this.timestampCorrection_ == null)) {
|
|
// We've already started or we haven't started yet.
|
|
return;
|
|
}
|
|
shaka.asserts.assert(!this.proceeded_);
|
|
shaka.asserts.assert(!this.ended_);
|
|
|
|
shaka.log.info(this.logPrefix_(), 'stream has started.');
|
|
this.started_ = true;
|
|
this.sbm_.correct(this.timestampCorrection_);
|
|
this.startedPromise_.resolve(this.timestampCorrection_);
|
|
};
|
|
|
|
|
|
/**
|
|
* Fires an AdaptationEvent for the given StreamInfo.
|
|
*
|
|
* @param {!shaka.media.StreamInfo} streamInfo
|
|
* @private
|
|
*/
|
|
shaka.media.Stream.prototype.fireAdaptationEvent_ = function(streamInfo) {
|
|
var event = shaka.media.Stream.createAdaptationEvent_(streamInfo);
|
|
this.dispatchEvent(event);
|
|
};
|
|
|
|
|
|
/**
|
|
* Fires an EndedEvent
|
|
*
|
|
* @private
|
|
*/
|
|
shaka.media.Stream.prototype.fireEndedEvent_ = function() {
|
|
shaka.asserts.assert(this.ended_);
|
|
var event = shaka.util.FakeEvent.create({ type: 'ended' });
|
|
this.dispatchEvent(event);
|
|
};
|
|
|
|
|
|
/**
|
|
* Sets the update timer.
|
|
*
|
|
* @param {number} ms The timeout in milliseconds.
|
|
* @private
|
|
*/
|
|
shaka.media.Stream.prototype.setUpdateTimer_ = function(ms) {
|
|
shaka.asserts.assert(this.updateTimer_ == null);
|
|
this.updateTimer_ = window.setTimeout(this.onUpdate_.bind(this), ms);
|
|
};
|
|
|
|
|
|
/**
|
|
* Cancels the update timer if it is running.
|
|
*
|
|
* @private
|
|
*/
|
|
shaka.media.Stream.prototype.cancelUpdateTimer_ = function() {
|
|
if (this.updateTimer_ != null) {
|
|
window.clearTimeout(this.updateTimer_);
|
|
this.updateTimer_ = null;
|
|
}
|
|
};
|
|
|
|
|
|
if (!COMPILED) {
|
|
/**
|
|
* Returns a string with the form 'Stream MIME_TYPE:' for logging purposes.
|
|
*
|
|
* @return {string}
|
|
* @private
|
|
*/
|
|
shaka.media.Stream.prototype.logPrefix_ = function() {
|
|
return 'Stream ' + this.mimeType_ + ':';
|
|
};
|
|
}
|
|
|
|
|
|
/**
|
|
* Creates an event object for an AdaptationEvent using the given StreamInfo.
|
|
*
|
|
* @param {!shaka.media.StreamInfo} streamInfo
|
|
* @return {!Event}
|
|
* @private
|
|
*/
|
|
shaka.media.Stream.createAdaptationEvent_ = function(streamInfo) {
|
|
var contentType = streamInfo.mimeType.split('/')[0];
|
|
var size = (contentType != 'video') ? null : {
|
|
'width': streamInfo.width,
|
|
'height': streamInfo.height
|
|
};
|
|
var event = shaka.util.FakeEvent.create({
|
|
'type': 'adaptation',
|
|
'bubbles': true,
|
|
'contentType': contentType,
|
|
'size': size,
|
|
'bandwidth': streamInfo.bandwidth
|
|
});
|
|
return event;
|
|
};
|
|
|