/**
* @license
* Copyright 2016 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.MediaSourceEngine');
goog.require('goog.asserts');
goog.require('shaka.log');
goog.require('shaka.media.TimeRangesUtils');
goog.require('shaka.media.Transmuxer');
goog.require('shaka.text.TextEngine');
goog.require('shaka.util.Error');
goog.require('shaka.util.EventManager');
goog.require('shaka.util.Functional');
goog.require('shaka.util.IDestroyable');
goog.require('shaka.util.ManifestParserUtils');
goog.require('shaka.util.MimeUtils');
goog.require('shaka.util.PublicPromise');
/**
* MediaSourceEngine wraps all operations on MediaSource and SourceBuffers.
* All asynchronous operations return a Promise, and all operations are
* internally synchronized and serialized as needed. Operations that can
* be done in parallel will be done in parallel.
*
* @param {HTMLMediaElement} video The video element, whose source is tied to
* MediaSource during the lifetime of the MediaSourceEngine.
*
* @struct
* @constructor
* @implements {shaka.util.IDestroyable}
*/
shaka.media.MediaSourceEngine = function(video) {
/** @private {HTMLMediaElement} */
this.video_ = video;
/** @private {shakaExtern.TextDisplayer} */
this.textDisplayer_ = null;
/** @private {!Object.<shaka.util.ManifestParserUtils.ContentType,
SourceBuffer>} */
this.sourceBuffers_ = {};
/** @private {shaka.text.TextEngine} */
this.textEngine_ = null;
/**
* @private {!Object.<string,
* !Array.<shaka.media.MediaSourceEngine.Operation>>}
*/
this.queues_ = {};
/** @private {shaka.util.EventManager} */
this.eventManager_ = new shaka.util.EventManager();
/** @private {boolean} */
this.destroyed_ = false;
/** @private {!Object.<string, !shaka.media.Transmuxer>} */
this.transmuxers_ = {};
/** @private {boolean} */
this.useEmbeddedText_ = false;
/** @private {!shaka.util.PublicPromise} */
this.mediaSourceOpen_ = new shaka.util.PublicPromise();
/** @private {MediaSource} */
this.mediaSource_ = this.createMediaSource(this.mediaSourceOpen_);
};
/**
* Create a MediaSource object, attach it to the video element, and return it.
* Resolves the given promise when the MediaSource is ready.
*
* Replaced by unit tests.
*
* @param {!shaka.util.PublicPromise} p
* @return {!MediaSource}
*/
shaka.media.MediaSourceEngine.prototype.createMediaSource = function(p) {
let mediaSource = new MediaSource();
// Set up MediaSource on the video element.
this.eventManager_.listenOnce(mediaSource, 'sourceopen', p.resolve);
this.video_.src = window.URL.createObjectURL(mediaSource);
return mediaSource;
};
/**
* @typedef {{
* start: function(),
* p: !shaka.util.PublicPromise
* }}
*
* @summary An operation in queue.
* @property {function()} start
* The function which starts the operation.
* @property {!shaka.util.PublicPromise} p
* The PublicPromise which is associated with this operation.
*/
shaka.media.MediaSourceEngine.Operation;
/**
* Checks if a certain type is supported.
*
* @param {shakaExtern.Stream} stream
* @return {boolean}
*/
shaka.media.MediaSourceEngine.isStreamSupported = function(stream) {
let fullMimeType = shaka.util.MimeUtils.getFullType(
stream.mimeType, stream.codecs);
let extendedMimeType = shaka.util.MimeUtils.getExtendedType(stream);
return shaka.text.TextEngine.isTypeSupported(fullMimeType) ||
MediaSource.isTypeSupported(extendedMimeType) ||
shaka.media.Transmuxer.isSupported(fullMimeType, stream.type);
};
/**
* Returns true if the browser has the basic APIs we need.
*
* @return {boolean}
*/
shaka.media.MediaSourceEngine.isBrowserSupported = function() {
return !!window.MediaSource && !!MediaSource.isTypeSupported;
};
/**
* Returns a map of MediaSource support for well-known types.
*
* @return {!Object.<string, boolean>}
*/
shaka.media.MediaSourceEngine.probeSupport = function() {
goog.asserts.assert(shaka.media.MediaSourceEngine.isBrowserSupported(),
'Requires basic support');
let support = {};
let testMimeTypes = [
// MP4 types
'video/mp4; codecs="avc1.42E01E"',
'video/mp4; codecs="avc3.42E01E"',
'video/mp4; codecs="hev1.1.6.L93.90"',
'video/mp4; codecs="hvc1.1.6.L93.90"',
'video/mp4; codecs="hev1.2.4.L153.B0"; eotf="smpte2084"', // HDR HEVC
'video/mp4; codecs="hvc1.2.4.L153.B0"; eotf="smpte2084"', // HDR HEVC
'video/mp4; codecs="vp9"',
'video/mp4; codecs="vp09.00.10.08"',
'audio/mp4; codecs="mp4a.40.2"',
'audio/mp4; codecs="ac-3"',
'audio/mp4; codecs="ec-3"',
'audio/mp4; codecs="opus"',
'audio/mp4; codecs="flac"',
// WebM types
'video/webm; codecs="vp8"',
'video/webm; codecs="vp9"',
'video/webm; codecs="vp09.00.10.08"',
'audio/webm; codecs="vorbis"',
'audio/webm; codecs="opus"',
// MPEG2 TS types (video/ is also used for audio: http://goo.gl/tYHXiS)
'video/mp2t; codecs="avc1.42E01E"',
'video/mp2t; codecs="avc3.42E01E"',
'video/mp2t; codecs="hvc1.1.6.L93.90"',
'video/mp2t; codecs="mp4a.40.2"',
'video/mp2t; codecs="ac-3"',
'video/mp2t; codecs="ec-3"',
// WebVTT types
'text/vtt',
'application/mp4; codecs="wvtt"',
// TTML types
'application/ttml+xml',
'application/mp4; codecs="stpp"'
];
testMimeTypes.forEach(function(type) {
support[type] = shaka.text.TextEngine.isTypeSupported(type) ||
MediaSource.isTypeSupported(type) ||
shaka.media.Transmuxer.isSupported(type);
let basicType = type.split(';')[0];
support[basicType] = support[basicType] || support[type];
});
return support;
};
/** @override */
shaka.media.MediaSourceEngine.prototype.destroy = function() {
const Functional = shaka.util.Functional;
this.destroyed_ = true;
let cleanup = [];
for (let contentType in this.queues_) {
// Make a local copy of the queue and the first item.
let q = this.queues_[contentType];
let inProgress = q[0];
// Drop everything else out of the original queue.
this.queues_[contentType] = q.slice(0, 1);
// We will wait for this item to complete/fail.
if (inProgress) {
cleanup.push(inProgress.p.catch(Functional.noop));
}
// The rest will be rejected silently if possible.
for (let i = 1; i < q.length; ++i) {
q[i].p.catch(Functional.noop);
q[i].p.reject();
}
}
if (this.textEngine_) {
cleanup.push(this.textEngine_.destroy());
}
for (let contentType in this.transmuxers_) {
cleanup.push(this.transmuxers_[contentType].destroy());
}
return Promise.all(cleanup).then(function() {
let p = this.eventManager_ ? this.eventManager_.destroy() : null;
if (this.video_) {
this.video_.removeAttribute('src');
this.video_.load();
}
this.eventManager_ = null;
this.video_ = null;
this.mediaSource_ = null;
this.textEngine_ = null;
this.textDisplayer_ = null;
this.sourceBuffers_ = {};
this.transmuxers_ = {};
if (goog.DEBUG) {
for (let contentType in this.queues_) {
goog.asserts.assert(
this.queues_[contentType].length == 0,
contentType + ' queue should be empty after destroy!');
}
}
this.queues_ = {};
return p;
}.bind(this));
};
/**
* @return {!Promise} Resolved when MediaSource is open and attached to the
* media element. This process is actually initiated by the constructor.
*/
shaka.media.MediaSourceEngine.prototype.open = function() {
return this.mediaSourceOpen_;
};
/**
* Initialize MediaSourceEngine.
*
* Note that it is not valid to call this multiple times, except to add or
* reinitialize text streams.
*
* @param {!Object.<shaka.util.ManifestParserUtils.ContentType,
* shakaExtern.Stream>} streamsByType
* A map of content types to streams. All streams must be supported according
* to MediaSourceEngine.isStreamSupported.
* @param {boolean} forceTransmuxTS
* If true, this will transmux TS content even if it is natively supported.
*
* @return {!Promise}
*
* @throws InvalidAccessError if blank MIME types are given
* @throws NotSupportedError if unsupported MIME types are given
* @throws QuotaExceededError if the browser can't support that many buffers
*/
shaka.media.MediaSourceEngine.prototype.init = function(
streamsByType, forceTransmuxTS) {
const ContentType = shaka.util.ManifestParserUtils.ContentType;
return this.mediaSourceOpen_.then(() => {
for (let contentType in streamsByType) {
let stream = streamsByType[contentType];
goog.asserts.assert(
shaka.media.MediaSourceEngine.isStreamSupported(stream),
'Type negotiation should happen before MediaSourceEngine.init!');
let mimeType = shaka.util.MimeUtils.getFullType(
stream.mimeType, stream.codecs);
if (contentType == ContentType.TEXT) {
this.reinitText(mimeType);
} else {
if ((forceTransmuxTS || !MediaSource.isTypeSupported(mimeType)) &&
shaka.media.Transmuxer.isSupported(mimeType, contentType)) {
this.transmuxers_[contentType] = new shaka.media.Transmuxer();
mimeType =
shaka.media.Transmuxer.convertTsCodecs(contentType, mimeType);
}
let sourceBuffer = this.mediaSource_.addSourceBuffer(mimeType);
this.eventManager_.listen(
sourceBuffer, 'error',
this.onError_.bind(this, contentType));
this.eventManager_.listen(
sourceBuffer, 'updateend',
this.onUpdateEnd_.bind(this, contentType));
this.sourceBuffers_[contentType] = sourceBuffer;
this.queues_[contentType] = [];
}
}
});
};
/**
* @param {shakaExtern.TextDisplayer} textDisplayer
*/
shaka.media.MediaSourceEngine.prototype.setTextDisplayer =
function(textDisplayer) {
this.textDisplayer_ = textDisplayer;
};
/**
* Reinitialize the TextEngine for a new text type.
* @param {string} mimeType
*/
shaka.media.MediaSourceEngine.prototype.reinitText = function(mimeType) {
if (!this.textEngine_) {
this.textEngine_ = new shaka.text.TextEngine(this.textDisplayer_);
}
this.textEngine_.initParser(mimeType);
};
/**
* @return {boolean} True if the MediaSource is in an "ended" state, or if the
* object has been destroyed.
*/
shaka.media.MediaSourceEngine.prototype.ended = function() {
return this.mediaSource_ ? this.mediaSource_.readyState == 'ended' : true;
};
/**
* Gets the first timestamp in buffer for the given content type.
*
* @param {shaka.util.ManifestParserUtils.ContentType} contentType
* @return {?number} The timestamp in seconds, or null if nothing is buffered.
*/
shaka.media.MediaSourceEngine.prototype.bufferStart = function(contentType) {
const ContentType = shaka.util.ManifestParserUtils.ContentType;
if (contentType == ContentType.TEXT) {
return this.textEngine_.bufferStart();
}
return shaka.media.TimeRangesUtils.bufferStart(
this.getBuffered_(contentType));
};
/**
* Gets the last timestamp in buffer for the given content type.
*
* @param {shaka.util.ManifestParserUtils.ContentType} contentType
* @return {?number} The timestamp in seconds, or null if nothing is buffered.
*/
shaka.media.MediaSourceEngine.prototype.bufferEnd = function(contentType) {
const ContentType = shaka.util.ManifestParserUtils.ContentType;
if (contentType == ContentType.TEXT) {
return this.textEngine_.bufferEnd();
}
return shaka.media.TimeRangesUtils.bufferEnd(this.getBuffered_(contentType));
};
/**
* Determines if the given time is inside the buffered range of the given
* content type.
*
* @param {shaka.util.ManifestParserUtils.ContentType} contentType
* @param {number} time Playhead time
* @param {number=} smallGapLimit
* @return {boolean}
*/
shaka.media.MediaSourceEngine.prototype.isBuffered = function(
contentType, time, smallGapLimit) {
const ContentType = shaka.util.ManifestParserUtils.ContentType;
if (contentType == ContentType.TEXT) {
return this.textEngine_.isBuffered(time);
} else {
let buffered = this.getBuffered_(contentType);
return shaka.media.TimeRangesUtils.isBuffered(
buffered, time, smallGapLimit);
}
};
/**
* Computes how far ahead of the given timestamp is buffered for the given
* content type.
*
* @param {shaka.util.ManifestParserUtils.ContentType} contentType
* @param {number} time
* @return {number} The amount of time buffered ahead in seconds.
*/
shaka.media.MediaSourceEngine.prototype.bufferedAheadOf =
function(contentType, time) {
const ContentType = shaka.util.ManifestParserUtils.ContentType;
if (contentType == ContentType.TEXT) {
return this.textEngine_.bufferedAheadOf(time);
} else {
let buffered = this.getBuffered_(contentType);
return shaka.media.TimeRangesUtils.bufferedAheadOf(buffered, time);
}
};
/**
* Gets the current buffered ranges.
* @return {shakaExtern.BufferedInfo}
*/
shaka.media.MediaSourceEngine.prototype.getBufferedInfo = function() {
const ContentType = shaka.util.ManifestParserUtils.ContentType;
let getBufferedInfo = shaka.media.TimeRangesUtils.getBufferedInfo;
let textRanges;
if (this.textEngine_ && this.textEngine_.bufferStart() != null) {
textRanges = [{
start: this.textEngine_.bufferStart(),
end: this.textEngine_.bufferEnd()
}];
} else {
textRanges = [];
}
return {
total: getBufferedInfo(this.video_.buffered),
audio: getBufferedInfo(this.getBuffered_(ContentType.AUDIO)),
video: getBufferedInfo(this.getBuffered_(ContentType.VIDEO)),
text: textRanges
};
};
/**
* @param {shaka.util.ManifestParserUtils.ContentType} contentType
* @return {TimeRanges} The buffered ranges for the given content type, or
* null if the buffered ranges could not be obtained.
* @private
*/
shaka.media.MediaSourceEngine.prototype.getBuffered_ = function(contentType) {
try {
return this.sourceBuffers_[contentType].buffered;
} catch (exception) {
if (contentType in this.sourceBuffers_) {
// Note: previous MediaSource errors may cause access to |buffered| to
// throw.
shaka.log.error('failed to get buffered range for ' + contentType,
exception);
}
return null;
}
};
/**
* Enqueue an operation to append data to the SourceBuffer.
* Start and end times are needed for TextEngine, but not for MediaSource.
* Start and end times may be null for initialization segments; if present they
* are relative to the presentation timeline.
*
* @param {shaka.util.ManifestParserUtils.ContentType} contentType
* @param {!ArrayBuffer} data
* @param {?number} startTime relative to the start of the presentation
* @param {?number} endTime relative to the start of the presentation
* @return {!Promise}
*/
shaka.media.MediaSourceEngine.prototype.appendBuffer =
function(contentType, data, startTime, endTime) {
const ContentType = shaka.util.ManifestParserUtils.ContentType;
if (contentType == ContentType.TEXT) {
return this.textEngine_.appendBuffer(data, startTime, endTime);
} else if (this.transmuxers_[contentType]) {
return this.transmuxers_[contentType].transmux(data).then(
function(transmuxedData) {
// For CEA-608/708 CLOSED-CAPTIONS, text data is embedded in the
// video stream, so textEngine may not have been initialized.
if (!this.textEngine_) {
this.reinitText('text/vtt');
}
// This doesn't work for native TS support (ex. Edge/Chromecast),
// since no transmuxing is needed for native TS.
if (this.useEmbeddedText_) {
this.textEngine_.appendCues(transmuxedData.cues);
}
return this.enqueueOperation_(contentType,
this.append_.bind(this, contentType, transmuxedData.data.buffer));
}.bind(this));
} else {
return this.enqueueOperation_(
contentType,
this.append_.bind(this, contentType, data));
}
};
/**
* Set whether to use embedded text cues.
* Used for CEA 608/708 captions data, which is embedded inside the video
* stream.
*
* @param {boolean} useEmbeddedText
*/
shaka.media.MediaSourceEngine.prototype.setUseEmbeddedText =
function(useEmbeddedText) {
this.useEmbeddedText_ = useEmbeddedText;
};
/**
* Get whether we're using the embedded text cues.
*
* @return {boolean}
*/
shaka.media.MediaSourceEngine.prototype.getUseEmbeddedText = function() {
return this.useEmbeddedText_;
};
/**
* Enqueue an operation to remove data from the SourceBuffer.
*
* @param {shaka.util.ManifestParserUtils.ContentType} contentType
* @param {number} startTime relative to the start of the presentation
* @param {number} endTime relative to the start of the presentation
* @return {!Promise}
*/
shaka.media.MediaSourceEngine.prototype.remove =
function(contentType, startTime, endTime) {
// On IE11, this operation would be permitted, but would have no effect!
// See https://github.com/google/shaka-player/issues/251
goog.asserts.assert(endTime < Number.MAX_VALUE,
'remove() with MAX_VALUE or Infinity is not IE-compatible!');
const ContentType = shaka.util.ManifestParserUtils.ContentType;
if (contentType == ContentType.TEXT) {
return this.textEngine_.remove(startTime, endTime);
}
return this.enqueueOperation_(
contentType,
this.remove_.bind(this, contentType, startTime, endTime));
};
/**
* Enqueue an operation to clear the SourceBuffer.
*
* @param {shaka.util.ManifestParserUtils.ContentType} contentType
* @return {!Promise}
*/
shaka.media.MediaSourceEngine.prototype.clear = function(contentType) {
const ContentType = shaka.util.ManifestParserUtils.ContentType;
if (contentType == ContentType.TEXT) {
if (!this.textEngine_) {
return Promise.resolve();
}
return this.textEngine_.remove(0, Infinity);
}
// Note that not all platforms allow clearing to Infinity.
return this.enqueueOperation_(
contentType,
this.remove_.bind(this, contentType, 0, this.mediaSource_.duration));
};
/**
* Enqueue an operation to flush the SourceBuffer.
* This is a workaround for what we believe is a Chromecast bug.
*
* @param {shaka.util.ManifestParserUtils.ContentType} contentType
* @return {!Promise}
*/
shaka.media.MediaSourceEngine.prototype.flush = function(contentType) {
// Flush the pipeline. Necessary on Chromecast, even though we have removed
// everything.
const ContentType = shaka.util.ManifestParserUtils.ContentType;
if (contentType == ContentType.TEXT) {
// Nothing to flush for text.
return Promise.resolve();
}
return this.enqueueOperation_(
contentType,
this.flush_.bind(this, contentType));
};
/**
* Sets the timestamp offset and append window end for the given content type.
*
* @param {shaka.util.ManifestParserUtils.ContentType} contentType
* @param {number} timestampOffset The timestamp offset. Segments which start
* at time t will be inserted at time t + timestampOffset instead. This
* value does not affect segments which have already been inserted.
* @param {number} appendWindowStart The timestamp to set the append window
* start to. Media before this value will be truncated.
* @param {number} appendWindowEnd The timestamp to set the append window end
* to. Media beyond this value will be truncated.
* @return {!Promise}
*/
shaka.media.MediaSourceEngine.prototype.setStreamProperties = function(
contentType, timestampOffset, appendWindowStart, appendWindowEnd) {
const ContentType = shaka.util.ManifestParserUtils.ContentType;
if (contentType == ContentType.TEXT) {
this.textEngine_.setTimestampOffset(timestampOffset);
this.textEngine_.setAppendWindow(appendWindowStart, appendWindowEnd);
return Promise.resolve();
}
return Promise.all([
// Queue an abort() to help MSE splice together overlapping segments.
// We set appendWindowEnd when we change periods in DASH content, and the
// period transition may result in overlap.
//
// An abort() also helps with MPEG2-TS. When we append a TS segment, we
// always enter a PARSING_MEDIA_SEGMENT state and we can't change the
// timestamp offset. By calling abort(), we reset the state so we can
// set it.
this.enqueueOperation_(
contentType,
this.abort_.bind(this, contentType)),
this.enqueueOperation_(
contentType,
this.setTimestampOffset_.bind(this, contentType, timestampOffset)),
this.enqueueOperation_(
contentType,
this.setAppendWindow_.bind(
this, contentType, appendWindowStart, appendWindowEnd))
]);
};
/**
* @param {string=} opt_reason Valid reasons are 'network' and 'decode'.
* @return {!Promise}
* @see http://w3c.github.io/media-source/#idl-def-EndOfStreamError
*/
shaka.media.MediaSourceEngine.prototype.endOfStream = function(opt_reason) {
return this.enqueueBlockingOperation_(function() {
// Chrome won't let us pass undefined, but it will let us omit the
// argument. Firefox does not have this problem.
// TODO: File a bug about this.
if (opt_reason) {
this.mediaSource_.endOfStream(opt_reason);
} else {
this.mediaSource_.endOfStream();
}
}.bind(this));
};
/**
* We only support increasing duration at this time. Decreasing duration
* causes the MSE removal algorithm to run, which results in an 'updateend'
* event. Supporting this scenario would be complicated, and is not currently
* needed.
*
* @param {number} duration
* @return {!Promise}
*/
shaka.media.MediaSourceEngine.prototype.setDuration = function(duration) {
goog.asserts.assert(
isNaN(this.mediaSource_.duration) ||
this.mediaSource_.duration <= duration,
'duration cannot decrease: ' + this.mediaSource_.duration + ' -> ' +
duration);
return this.enqueueBlockingOperation_(function() {
this.mediaSource_.duration = duration;
}.bind(this));
};
/**
* Get the current MediaSource duration.
*
* @return {number}
*/
shaka.media.MediaSourceEngine.prototype.getDuration = function() {
return this.mediaSource_.duration;
};
/**
* Append data to the SourceBuffer.
* @param {shaka.util.ManifestParserUtils.ContentType} contentType
* @param {!ArrayBuffer} data
* @throws QuotaExceededError if the browser's buffer is full
* @private
*/
shaka.media.MediaSourceEngine.prototype.append_ =
function(contentType, data) {
// This will trigger an 'updateend' event.
this.sourceBuffers_[contentType].appendBuffer(data);
};
/**
* Remove data from the SourceBuffer.
* @param {shaka.util.ManifestParserUtils.ContentType} contentType
* @param {number} startTime relative to the start of the presentation
* @param {number} endTime relative to the start of the presentation
* @private
*/
shaka.media.MediaSourceEngine.prototype.remove_ =
function(contentType, startTime, endTime) {
if (endTime <= startTime) {
// Ignore removal of inverted or empty ranges.
// Fake 'updateend' event to resolve the operation.
this.onUpdateEnd_(contentType);
return;
}
// This will trigger an 'updateend' event.
this.sourceBuffers_[contentType].remove(startTime, endTime);
};
/**
* Call abort() on the SourceBuffer.
* This resets MSE's last_decode_timestamp on all track buffers, which should
* trigger the splicing logic for overlapping segments.
* @param {shaka.util.ManifestParserUtils.ContentType} contentType
* @private
*/
shaka.media.MediaSourceEngine.prototype.abort_ = function(contentType) {
// Save the append window, which is reset on abort().
let appendWindowStart = this.sourceBuffers_[contentType].appendWindowStart;
let appendWindowEnd = this.sourceBuffers_[contentType].appendWindowEnd;
// This will not trigger an 'updateend' event, since nothing is happening.
// This is only to reset MSE internals, not to abort an actual operation.
this.sourceBuffers_[contentType].abort();
// Restore the append window.
this.sourceBuffers_[contentType].appendWindowStart = appendWindowStart;
this.sourceBuffers_[contentType].appendWindowEnd = appendWindowEnd;
// Fake an 'updateend' event to resolve the operation.
this.onUpdateEnd_(contentType);
};
/**
* Nudge the playhead to force the media pipeline to be flushed.
* This seems to be necessary on Chromecast to get new content to replace old
* content.
* @param {shaka.util.ManifestParserUtils.ContentType} contentType
* @private
*/
shaka.media.MediaSourceEngine.prototype.flush_ = function(contentType) {
// Never use flush_ if there's data. It causes a hiccup in playback.
goog.asserts.assert(
this.video_.buffered.length == 0,
'MediaSourceEngine.flush_ should only be used after clearing all data!');
// Seeking forces the pipeline to be flushed.
this.video_.currentTime -= 0.001;
// Fake an 'updateend' event to resolve the operation.
this.onUpdateEnd_(contentType);
};
/**
* Set the SourceBuffer's timestamp offset.
* @param {shaka.util.ManifestParserUtils.ContentType} contentType
* @param {number} timestampOffset
* @private
*/
shaka.media.MediaSourceEngine.prototype.setTimestampOffset_ =
function(contentType, timestampOffset) {
// Work around for https://github.com/google/shaka-player/issues/1281:
// TODO(https://goo.gl/3ZTzse): follow up when this is fixed in Edge
if (timestampOffset < 0) {
// Try to prevent rounding errors in Edge from removing the first keyframe.
timestampOffset += 0.001;
}
this.sourceBuffers_[contentType].timestampOffset = timestampOffset;
// Fake an 'updateend' event to resolve the operation.
this.onUpdateEnd_(contentType);
};
/**
* Set the SourceBuffer's append window end.
* @param {shaka.util.ManifestParserUtils.ContentType} contentType
* @param {number} appendWindowStart
* @param {number} appendWindowEnd
* @private
*/
shaka.media.MediaSourceEngine.prototype.setAppendWindow_ =
function(contentType, appendWindowStart, appendWindowEnd) {
// You can't set start > end, so first set start to 0, then set the new end,
// then set the new start. That way, there are no intermediate states which
// are invalid.
this.sourceBuffers_[contentType].appendWindowStart = 0;
this.sourceBuffers_[contentType].appendWindowEnd = appendWindowEnd;
this.sourceBuffers_[contentType].appendWindowStart = appendWindowStart;
// Fake an 'updateend' event to resolve the operation.
this.onUpdateEnd_(contentType);
};
/**
* @param {shaka.util.ManifestParserUtils.ContentType} contentType
* @param {!Event} event
* @private
*/
shaka.media.MediaSourceEngine.prototype.onError_ =
function(contentType, event) {
let operation = this.queues_[contentType][0];
goog.asserts.assert(operation, 'Spurious error event!');
goog.asserts.assert(!this.sourceBuffers_[contentType].updating,
'SourceBuffer should not be updating on error!');
let code = this.video_.error ? this.video_.error.code : 0;
operation.p.reject(new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.MEDIA_SOURCE_OPERATION_FAILED,
code));
// Do not pop from queue. An 'updateend' event will fire next, and to avoid
// synchronizing these two event handlers, we will allow that one to pop from
// the queue as normal. Note that because the operation has already been
// rejected, the call to resolve() in the 'updateend' handler will have no
// effect.
};
/**
* @param {shaka.util.ManifestParserUtils.ContentType} contentType
* @private
*/
shaka.media.MediaSourceEngine.prototype.onUpdateEnd_ = function(contentType) {
let operation = this.queues_[contentType][0];
goog.asserts.assert(operation, 'Spurious updateend event!');
if (!operation) return;
goog.asserts.assert(!this.sourceBuffers_[contentType].updating,
'SourceBuffer should not be updating on updateend!');
operation.p.resolve();
this.popFromQueue_(contentType);
};
/**
* Enqueue an operation and start it if appropriate.
*
* @param {shaka.util.ManifestParserUtils.ContentType} contentType
* @param {function()} start
* @return {!Promise}
* @private
*/
shaka.media.MediaSourceEngine.prototype.enqueueOperation_ =
function(contentType, start) {
if (this.destroyed_) return Promise.reject();
let operation = {
start: start,
p: new shaka.util.PublicPromise()
};
this.queues_[contentType].push(operation);
if (this.queues_[contentType].length == 1) {
try {
operation.start();
} catch (exception) {
if (exception.name == 'QuotaExceededError') {
operation.p.reject(new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.QUOTA_EXCEEDED_ERROR,
contentType));
} else {
operation.p.reject(new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.MEDIA_SOURCE_OPERATION_THREW,
exception));
}
this.popFromQueue_(contentType);
}
}
return operation.p;
};
/**
* Enqueue an operation which must block all other operations on all
* SourceBuffers.
*
* @param {function()} run
* @return {!Promise}
* @private
*/
shaka.media.MediaSourceEngine.prototype.enqueueBlockingOperation_ =
function(run) {
if (this.destroyed_) return Promise.reject();
let allWaiters = [];
// Enqueue a 'wait' operation onto each queue.
// This operation signals its readiness when it starts.
// When all wait operations are ready, the real operation takes place.
for (let contentType in this.sourceBuffers_) {
let ready = new shaka.util.PublicPromise();
let operation = {
start: function(ready) { ready.resolve(); }.bind(null, ready),
p: ready
};
this.queues_[contentType].push(operation);
allWaiters.push(ready);
if (this.queues_[contentType].length == 1) {
operation.start();
}
}
// Return a Promise to the real operation, which waits to begin until there
// are no other in-progress operations on any SourceBuffers.
return Promise.all(allWaiters).then(function() {
if (goog.DEBUG) {
// If we did it correctly, nothing is updating.
for (let contentType in this.sourceBuffers_) {
goog.asserts.assert(
this.sourceBuffers_[contentType].updating == false,
'SourceBuffers should not be updating after a blocking op!');
}
}
let ret;
// Run the real operation, which is synchronous.
try {
run();
} catch (exception) {
ret = Promise.reject(new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.MEDIA_SOURCE_OPERATION_THREW,
exception));
}
// Unblock the queues.
for (let contentType in this.sourceBuffers_) {
this.popFromQueue_(contentType);
}
return ret;
}.bind(this), function() {
// One of the waiters failed, which means we've been destroyed.
goog.asserts.assert(this.destroyed_, 'Should be destroyed by now');
// We haven't popped from the queue. Canceled waiters have been removed by
// destroy. What's left now should just be resolved waiters. In uncompiled
// mode, we will maintain good hygiene and make sure the assert at the end
// of destroy passes. In compiled mode, the queues are wiped in destroy.
if (goog.DEBUG) {
for (let contentType in this.sourceBuffers_) {
if (this.queues_[contentType].length) {
goog.asserts.assert(
this.queues_[contentType].length == 1,
'Should be at most one item in queue!');
goog.asserts.assert(
allWaiters.indexOf(this.queues_[contentType][0].p) != -1,
'The item in queue should be one of our waiters!');
this.queues_[contentType].shift();
}
}
}
return Promise.reject();
}.bind(this));
};
/**
* Pop from the front of the queue and start a new operation.
* @param {shaka.util.ManifestParserUtils.ContentType} contentType
* @private
*/
shaka.media.MediaSourceEngine.prototype.popFromQueue_ = function(contentType) {
// Remove the in-progress operation, which is now complete.
this.queues_[contentType].shift();
// Retrieve the next operation, if any, from the queue and start it.
let next = this.queues_[contentType][0];
if (next) {
try {
next.start();
} catch (exception) {
next.p.reject(new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MEDIA,
shaka.util.Error.Code.MEDIA_SOURCE_OPERATION_THREW,
exception));
this.popFromQueue_(contentType);
}
}
};