Files
shaka-player/lib/media/source_buffer_manager.js
T
Timothy Drews 36fec68e75 Create 'media' namespace for stream generic code.
* Move internal non-DASH specific code into the 'media' namespace.
* Remove DASH references from generic stream code.

The documentation is not changed as a follow-up patch will factor out
DASH functionality from StreamVideoSource back into a new
DashVideoSource class.

b/18903621

Change-Id: I78d6e4f2824d4983619f17872828d95655fcfe50
2015-01-26 15:08:48 -08:00

547 lines
16 KiB
JavaScript

/**
* Copyright 2014 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 Manages a SourceBuffer an provides an enhanced interface
* based on Promises.
*/
goog.provide('shaka.media.SourceBufferManager');
goog.require('shaka.asserts');
goog.require('shaka.media.SegmentRange');
goog.require('shaka.util.EventManager');
goog.require('shaka.util.IBandwidthEstimator');
goog.require('shaka.util.PublicPromise');
goog.require('shaka.util.RangeRequest');
/**
* Creates a SourceBufferManager (SBM).
*
* The SBM manages access to a SourceBuffer object through a fetch operation
* and a clear operation. It also maintains a "virtual source buffer" to keep
* track of which segments have been appended to the actual underlying source
* buffer. The SBM uses this virtual source buffer because it cannot rely on
* the browser to tell it what is in the underlying SourceBuffer because the
* segment index may use PTS (presentation timestamps) and the browser may use
* DTS (decoding timestamps) or vice-versa.
*
* @param {!MediaSource} mediaSource The SourceBuffer's parent MediaSource.
* @param {!SourceBuffer} sourceBuffer
* @param {shaka.util.IBandwidthEstimator} estimator A bandwidth estimator to
* attach to all requests.
* @struct
* @constructor
*/
shaka.media.SourceBufferManager = function(
mediaSource, sourceBuffer, estimator) {
/** @private {!MediaSource} */
this.mediaSource_ = mediaSource;
/** @private {!SourceBuffer} */
this.sourceBuffer_ = sourceBuffer;
/** @private {shaka.util.IBandwidthEstimator} */
this.estimator_ = estimator;
/** @private {!shaka.util.EventManager} */
this.eventManager_ = new shaka.util.EventManager();
/**
* An array that indicates which segments are buffered.
* @private {!Array.<boolean>}
*/
this.buffered_ = [];
/** @private {shaka.media.SourceBufferManager.State_} */
this.state_ = shaka.media.SourceBufferManager.State_.IDLE;
/** @private {Promise} */
this.promise_ = null;
/** @private {Promise} */
this.abortPromise_ = null;
/**
* The current SegmentReferences being fetched or appended.
* @private {!Array.<!shaka.media.SegmentReference>}
*/
this.references_ = [];
/**
* The current request while fetching.
* @private {shaka.util.RangeRequest}
*/
this.request_ = null;
/**
* The current segment data being fetched or appended.
* @private {!Array.<!ArrayBuffer>}
*/
this.segments_ = [];
this.eventManager_.listen(
this.sourceBuffer_,
'updateend',
this.onSourceBufferUpdateEnd_.bind(this));
};
/**
* SBM states.
* @enum
* @private
*/
shaka.media.SourceBufferManager.State_ = {
IDLE: 0,
REQUESTING: 1,
APPENDING: 2,
CLEARING: 3,
ABORTING: 4
};
/**
* Destroys the SourceBufferManager.
* @suppress {checkTypes} to set otherwise non-nullable types to null.
*/
shaka.media.SourceBufferManager.prototype.destroy = function() {
this.abort();
this.state_ = null;
this.segments_ = null;
this.request_ = null;
this.references_ = null;
this.abortPromise_ = null;
this.promise_ = null;
this.eventManager_.destroy();
this.eventManager_ = null;
this.buffered_ = null;
this.sourceBuffer_ = null;
this.mediaSource_ = null;
};
/**
* Checks if the given segment is buffered.
*
* @param {number} index The segment's index.
* @return {boolean} True if the segment is buffered.
*/
shaka.media.SourceBufferManager.prototype.isBuffered = function(index) {
return this.buffered_[index] == true;
};
/**
* Fetches the segments specified by the given SegmentRange and appends the
* retrieved segment data to the underlying SourceBuffer. This cannot be called
* if another operation is in progress.
*
* @param {!shaka.media.SegmentRange} segmentRange
* @param {!ArrayBuffer=} opt_initSegment Optional initialization segment that
* will be appended to the underlying SourceBuffer before the retrieved
* segment data.
*
* @return {!Promise}
*/
shaka.media.SourceBufferManager.prototype.fetch = function(
segmentRange, opt_initSegment) {
shaka.log.v1('fetch');
// Alias.
var SBM = shaka.media.SourceBufferManager;
// Check state.
shaka.asserts.assert(this.state_ == SBM.State_.IDLE);
if (this.state_ != SBM.State_.IDLE) {
var error = new Error('Cannot fetch: previous operation not complete.');
error.type = 'stream';
return Promise.reject(error);
}
shaka.asserts.assert(this.promise_ == null);
shaka.asserts.assert(this.references_.length == 0);
shaka.asserts.assert(this.request_ == null);
shaka.asserts.assert(this.segments_.length == 0);
this.state_ = SBM.State_.REQUESTING;
this.promise_ = new shaka.util.PublicPromise();
this.references_ = segmentRange.references;
if (opt_initSegment) {
this.segments_.push(opt_initSegment);
}
// If the segments are all located at the same URL then only a single request
// is required.
var singleLocation = true;
var firstUrl = this.references_[0].url.toString();
for (var i = 1; i < this.references_.length; ++i) {
if (this.references_[i].url.toString() != firstUrl) {
singleLocation = false;
break;
}
}
// Send the request. If this.abort() is called before |this.request_|'s
// promise is resolved then |this.request_|'s promise will be rejected via a
// call to this.request_.abort().
var p = singleLocation ?
this.fetchFromSingleUrl_() :
this.fetchFromMultipleUrls_();
p.then(shaka.util.TypedBind(this,
function() {
shaka.log.debug('Estimated bandwidth:',
(this.estimator_.getBandwidth() / 1e6).toFixed(2), 'Mbps');
this.sourceBuffer_.appendBuffer(this.segments_.shift());
this.state_ = SBM.State_.APPENDING;
this.request_ = null;
})
).catch(shaka.util.TypedBind(this,
/** @param {!Error} error */
function(error) {
if (error.type != 'aborted') {
this.rejectPromise_(error);
}
})
);
return this.promise_;
};
/**
* Returns a promise to fetch one or more segments from the same location. The
* promise will resolve once the request completes. This synchronously sets
* |request_| to the request in progress.
*
* @return {!Promise}
* @private
*/
shaka.media.SourceBufferManager.prototype.fetchFromSingleUrl_ = function() {
shaka.log.v1('fetchFromSingleUrl_');
shaka.asserts.assert(this.references_.length > 0);
shaka.asserts.assert(this.request_ == null);
this.request_ = new shaka.util.RangeRequest(
this.references_[0].url.toString(),
this.references_[0].startByte,
this.references_[this.references_.length - 1].endByte);
this.request_.estimator = this.estimator_;
return this.request_.send().then(this.appendSegment_.bind(this));
};
/**
* Returns a promise to fetch multiple segments from different locations. The
* promise will resolve once the last request completes. This synchronously
* sets |request_| to the first request and then asynchronously sets |request_|
* to the request in progress.
*
* @return {!Promise}
* @private
*/
shaka.media.SourceBufferManager.prototype.fetchFromMultipleUrls_ = function() {
shaka.log.v1('fetchFromMultipleUrls_');
shaka.asserts.assert(this.references_.length > 0);
shaka.asserts.assert(this.request_ == null);
/**
* Requests the segment specified by |reference|.
* @param {!shaka.media.SegmentReference} reference
* @this {shaka.media.SourceBufferManager}
* @return {!Promise.<!ArrayBuffer>}
*/
var requestSegment = function(reference) {
this.request_ = new shaka.util.RangeRequest(
reference.url.toString(),
reference.startByte,
reference.endByte);
this.request_.estimator = this.estimator_;
return this.request_.send();
};
// Request the first segment.
var p = shaka.util.TypedBind(this, requestSegment)(this.references_[0]);
// Request the subsequent segments.
var appendSegment = this.appendSegment_.bind(this);
for (var i = 1; i < this.references_.length; ++i) {
var requestNextSegment = requestSegment.bind(this, this.references_[i]);
p = p.then(appendSegment).then(requestNextSegment);
}
p = p.then(shaka.util.TypedBind(this, this.appendSegment_));
return p;
};
/**
* Appends |data| to |segments_|.
*
* @param {!ArrayBuffer} data
* @return {!Promise}
* @private
*/
shaka.media.SourceBufferManager.prototype.appendSegment_ = function(data) {
this.segments_.push(data);
return Promise.resolve();
};
/**
* Resets the virtual source buffer and clears all media from the underlying
* SourceBuffer. The returned promise will resolve immediately if there is no
* media within the underlying SourceBuffer. This cannot be called if another
* operation is in progress.
*
* @return {!Promise}
*/
shaka.media.SourceBufferManager.prototype.clear = function() {
shaka.log.v1('clear');
// Alias.
var SBM = shaka.media.SourceBufferManager;
// Check state.
shaka.asserts.assert(this.state_ == SBM.State_.IDLE);
if (this.state_ != SBM.State_.IDLE) {
var error = new Error('Cannot clear: previous operation not complete.');
error.type = 'stream';
return Promise.reject(error);
}
shaka.asserts.assert(this.promise_ == null);
shaka.asserts.assert(this.references_.length == 0);
shaka.asserts.assert(this.request_ == null);
shaka.asserts.assert(this.segments_.length == 0);
if (this.sourceBuffer_.buffered.length == 0) {
shaka.log.v1('Nothing to clear.');
shaka.asserts.assert(this.buffered_.length == 0);
return Promise.resolve();
}
try {
// This will trigger an 'updateend' event.
this.sourceBuffer_.remove(0, Number.POSITIVE_INFINITY);
} catch (exception) {
shaka.log.debug('Failed to clear buffer:', exception);
return Promise.reject(exception);
}
// Clear |buffered_| immediately since any buffered segments will be
// gone soon.
this.buffered_ = [];
this.state_ = SBM.State_.CLEARING;
this.promise_ = new shaka.util.PublicPromise();
return this.promise_;
};
/**
* Resets the virtual source buffer without removing any media from the
* underlying SourceBuffer. This can be called at any time.
*/
shaka.media.SourceBufferManager.prototype.reset = function() {
this.buffered_ = [];
};
/**
* Aborts the current operation if one exists. This should not be called
* if the current operation is an abort operation. The returned promise
* will never be rejected.
*
* @return {!Promise}
*/
shaka.media.SourceBufferManager.prototype.abort = function() {
shaka.log.v1('abort');
// Alias.
var SBM = shaka.media.SourceBufferManager;
shaka.asserts.assert(this.abortPromise_ == null);
shaka.asserts.assert(this.state_ != SBM.State_.ABORTING);
switch (this.state_) {
case SBM.State_.IDLE:
return Promise.resolve();
case SBM.State_.REQUESTING:
shaka.log.info('Aborting request...');
shaka.asserts.assert(this.request_);
this.state_ = SBM.State_.ABORTING;
// We do not need to wait for |request_| to completely stop. It is
// enough to know that no SourceBuffer operations are in progress when
// the abort promise is resolved.
// Create a new promise where resolveAbortPromise_() will look for it.
this.abortPromise_ = new shaka.util.PublicPromise();
// Keep a local reference since resolveAbortPromise_() will nullify it.
var p = this.abortPromise_;
// Abort the request.
this.request_.abort();
// Reject the original promise and resolve the abort promise.
this.resolveAbortPromise_();
// Return the local reference to the abort promise.
return p;
case SBM.State_.APPENDING:
case SBM.State_.CLEARING:
shaka.log.info('Aborting append/clear...');
this.state_ = SBM.State_.ABORTING;
this.abortPromise_ = new shaka.util.PublicPromise();
// If |mediaSource_| is open and aborting will not cause an exception,
// call abort() on |sourceBuffer_|. This will trigger an 'updateend'
// event if updating (e.g., appending or removing).
if (this.mediaSource_.readyState == 'open') {
this.sourceBuffer_.abort();
}
shaka.asserts.assert(this.sourceBuffer_.updating == false);
return this.abortPromise_;
case SBM.State_.ABORTING:
// This case should not happen, but handle it just in case it occurs in
// production.
shaka.log.error('Already aborting!');
shaka.asserts.assert(this.abortPromise_);
return /** @type {!Promise} */ (this.abortPromise_);
}
shaka.asserts.unreachable();
};
/**
* |sourceBuffer_|'s 'updateend' callback.
*
* @param {!Event} event
*
* @private
*/
shaka.media.SourceBufferManager.prototype.onSourceBufferUpdateEnd_ =
function(event) {
shaka.log.v1('onSourceBufferUpdateEnd_');
// Alias.
var SBM = shaka.media.SourceBufferManager;
shaka.asserts.assert(this.sourceBuffer_.updating == false);
shaka.asserts.assert(this.state_ == SBM.State_.APPENDING ||
this.state_ == SBM.State_.CLEARING ||
this.state_ == SBM.State_.ABORTING);
shaka.asserts.assert(this.promise_);
shaka.asserts.assert(this.request_ == null);
switch (this.state_) {
case SBM.State_.APPENDING:
// A segment has been appended so update |buffered_|.
shaka.asserts.assert(this.references_.length > 0);
if (this.segments_.length > 0) {
// Append the next segment.
try {
this.sourceBuffer_.appendBuffer(this.segments_.shift());
} catch (exception) {
shaka.log.debug('Failed to append buffer:', exception);
this.rejectPromise_(exception);
}
return;
}
// Update |buffered_|. Note that if we abort an append then there may be
// segments in the underlying source buffer that are not indicated in
// |buffered_|. However, this should not cause any harm.
for (var i = 0; i < this.references_.length; ++i) {
var r = this.references_[i];
this.buffered_[r.index] = true;
// To solve bug #18597156, where buffered ranges manifest a gap after
// seeking. All data after the gap is unusable, so always treat the
// next segment as one you don't have. If you don't seek, or if you
// only seek forward, this has no effect.
this.buffered_[r.index + 1] = false;
}
this.references_ = [];
// Fall-through.
case SBM.State_.CLEARING:
this.state_ = SBM.State_.IDLE;
this.promise_.resolve();
this.promise_ = null;
break;
case SBM.State_.ABORTING:
this.resolveAbortPromise_();
break;
default:
shaka.asserts.unreachable();
}
};
/**
* Resolves |abortPromise_|, and then calls rejectPromise_().
*
* @private
*/
shaka.media.SourceBufferManager.prototype.resolveAbortPromise_ = function() {
shaka.log.v1('resolveAbortPromise_');
shaka.asserts.assert(this.abortPromise_);
this.abortPromise_.resolve();
this.abortPromise_ = null;
var error = new Error('Current operation aborted.');
error.type = 'aborted';
this.rejectPromise_(error);
};
/**
* Rejects |promise_| and puts the SBM into the IDLE state.
*
* @param {!Error} error
*
* @private
*/
shaka.media.SourceBufferManager.prototype.rejectPromise_ = function(error) {
shaka.log.v1('rejectPromise_');
shaka.asserts.assert(this.promise_);
shaka.asserts.assert(this.abortPromise_ == null);
this.promise_.reject(error);
this.state_ = shaka.media.SourceBufferManager.State_.IDLE;
this.promise_ = null;
this.references_ = [];
this.request_ = null;
this.segments_ = [];
};