Files
shaka-player/lib/media/source_buffer_manager.js
T
Timothy Drews 952057a00c Refactor segment metadata fetching.
Move segment metadata fetching into StreamInfo so that segment metadata
can be retreived outside of Stream. This enables other components to
access segment indexes.

Change-Id: I322bcf7cdabc7daecb5d554834275c1b6d1775ee
2015-02-25 01:00:00 +00:00

570 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 from the current representation
* have been inserted into the source buffer.
* These segments may or may not have been evicted by the browser.
* @private {!Array.<boolean>}
*/
this.inserted_ = [];
/** @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
};
/**
* A fudge factor to apply to buffered ranges to account for rounding error.
* @const {number}
* @private
*/
shaka.media.SourceBufferManager.FUDGE_FACTOR_ = 1 / 60;
/**
* 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.inserted_ = null;
this.sourceBuffer_ = null;
this.mediaSource_ = null;
};
/**
* Checks if the given segment index has been inserted.
* @param {number} index
* @return {boolean} True if the segment has been inserted.
*/
shaka.media.SourceBufferManager.prototype.isInserted = function(index) {
return this.inserted_[index];
};
/**
* Checks if the given timestamp is buffered according to the SourceBuffer.
* @param {number} timestamp
* @return {boolean} True if the timestamp is buffered.
*/
shaka.media.SourceBufferManager.prototype.isBuffered = function(timestamp) {
var b = this.sourceBuffer_.buffered;
for (var i = 0; i < b.length; ++i) {
var start = b.start(i) - shaka.media.SourceBufferManager.FUDGE_FACTOR_;
var end = b.end(i) + shaka.media.SourceBufferManager.FUDGE_FACTOR_;
if (timestamp >= start && timestamp <= end) {
return true;
}
}
return false;
};
/**
* 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.inserted_.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 |inserted_| immediately since any inserted segments will be
// gone soon.
this.inserted_ = [];
this.state_ = SBM.State_.CLEARING;
this.promise_ = new shaka.util.PublicPromise();
return this.promise_;
};
/**
* Resets the index of inserted segments without removing any media from the
* underlying SourceBuffer. This should be called when switching
* representations.
*/
shaka.media.SourceBufferManager.prototype.reset = function() {
this.inserted_ = [];
};
/**
* 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 |inserted_|.
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 |inserted_|. Note that if we abort an append then there may be
// segments in the underlying source buffer that are not indicated in
// |inserted_|. However, this should not cause any harm.
for (var i = 0; i < this.references_.length; ++i) {
var r = this.references_[i];
this.inserted_[r.index] = true;
}
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_ = [];
};