UNPKG

shaka-player

Version:
1,124 lines (984 loc) 37.7 kB
/** * @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.IClosedCaptionParser'); 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.Platform'); 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. * @param {!shaka.media.IClosedCaptionParser} closedCaptionParser * The closed caption parser that should be used to parser closed captions * from the video stream. MediaSourceEngine takes ownership of the parser. * When MediaSourceEngine is destroyed, it will destroy the parser. * @param {!shaka.extern.TextDisplayer} textDisplayer * The text displayer that will be used with the text engine. * MediaSourceEngine takes ownership of the displayer. When MediaSourceEngine * is destroyed, it will destroy the displayer. * @struct * @constructor * @implements {shaka.util.IDestroyable} */ shaka.media.MediaSourceEngine = function( video, closedCaptionParser, textDisplayer) { /** @private {HTMLMediaElement} */ this.video_ = video; /** @private {shaka.extern.TextDisplayer} */ this.textDisplayer_ = textDisplayer; /** @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 {shaka.media.IClosedCaptionParser} */ this.captionParser_ = closedCaptionParser; /** @private {!shaka.util.PublicPromise} */ this.mediaSourceOpen_ = new shaka.util.PublicPromise(); /** @private {MediaSource} */ this.mediaSource_ = this.createMediaSource(this.mediaSourceOpen_); /** @private {string} */ this.url_ = ''; }; /** * Internal reference to window.URL.createObjectURL function to avoid * compatibility issues with other libraries and frameworks such as React * Native. For use in unit tests only, not meant for external use. * * @type {function(?):string} */ shaka.media.MediaSourceEngine.createObjectURL = window.URL.createObjectURL; /** * 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', () => this.onSourceOpen_(p)); // Store the object URL for releasing it later. this.url_ = shaka.media.MediaSourceEngine.createObjectURL(mediaSource); this.video_.src = this.url_; return mediaSource; }; /** * @param {!shaka.util.PublicPromise} p * @private */ shaka.media.MediaSourceEngine.prototype.onSourceOpen_ = function(p) { // Release the object URL that was previously created, to prevent memory // leak. // createObjectURL creates a strong reference to the MediaSource object // inside the browser. Setting the src of the video then creates another // reference within the video element. revokeObjectURL will remove the // strong reference to the MediaSource object, and allow it to be // garbage-collected later. URL.revokeObjectURL(this.url_); p.resolve(); }; /** * @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 {shaka.extern.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 a map of MediaSource support for well-known types. * * @return {!Object.<string, boolean>} */ shaka.media.MediaSourceEngine.probeSupport = function() { const 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"', 'video/mp4; codecs="av01.0.01M.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: https://bit.ly/TsMse) '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"', ]; let support = {}; for (let type of testMimeTypes) { if (shaka.util.Platform.supportsMediaSource()) { // Our TextEngine is only effective for MSE platforms at the moment. if (shaka.text.TextEngine.isTypeSupported(type)) { support[type] = true; } else { support[type] = MediaSource.isTypeSupported(type) || shaka.media.Transmuxer.isSupported(type); } } else { support[type] = shaka.util.Platform.supportsMediaType(type); } const 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.reject(); } } if (this.textEngine_) { cleanup.push(this.textEngine_.destroy()); } if (this.textDisplayer_) { cleanup.push(this.textDisplayer_.destroy()); } for (let contentType in this.transmuxers_) { cleanup.push(this.transmuxers_[contentType].destroy()); } return Promise.all(cleanup).then(() => { if (this.eventManager_) { this.eventManager_.release(); this.eventManager_ = null; } if (this.video_) { // "unload" the video element. this.video_.removeAttribute('src'); this.video_.load(); this.video_ = null; } this.mediaSource_ = null; this.textEngine_ = null; this.textDisplayer_ = null; this.sourceBuffers_ = {}; this.transmuxers_ = {}; this.captionParser_ = null; 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 {!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 {!Map.<shaka.util.ManifestParserUtils.ContentType, * shaka.extern.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 = async function( streamsByType, forceTransmuxTS) { const ContentType = shaka.util.ManifestParserUtils.ContentType; await this.mediaSourceOpen_; streamsByType.forEach((stream, 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] = []; } }); }; /** * 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); } }; /** * Fill in the given buffered info object with the buffered info that media * source knows about. * * @param {shaka.extern.BufferedInfo} info */ shaka.media.MediaSourceEngine.prototype.getBufferedInfo = function(info) { const ContentType = shaka.util.ManifestParserUtils.ContentType; const getBufferedInfo = shaka.media.TimeRangesUtils.getBufferedInfo; info.total = getBufferedInfo(this.video_.buffered); info.audio = getBufferedInfo(this.getBuffered_(ContentType.AUDIO)); info.video = getBufferedInfo(this.getBuffered_(ContentType.VIDEO)); info.text = []; if (this.textEngine_) { const start = this.textEngine_.bufferStart(); const end = this.textEngine_.bufferEnd(); if (start != null && end != null) { info.text.push({start: start, end: end}); } } }; /** * @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 * @param {?boolean} hasClosedCaptions True if the buffer contains CEA closed * captions * @return {!Promise} */ shaka.media.MediaSourceEngine.prototype.appendBuffer = function(contentType, data, startTime, endTime, hasClosedCaptions) { 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 HLS 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 (transmuxedData.captions && transmuxedData.captions.length) { const videoOffset = this.sourceBuffers_[ContentType.VIDEO].timestampOffset; this.textEngine_.storeAndAppendClosedCaptions( transmuxedData.captions, startTime, endTime, videoOffset); } return this.enqueueOperation_(contentType, this.append_.bind(this, contentType, transmuxedData.data.buffer)); }.bind(this)); } else if (hasClosedCaptions && window.muxjs) { if (!this.textEngine_) { this.reinitText('text/vtt'); } // If it is the init segment for closed captions, initialize the closed // caption parser. if (startTime == null && endTime == null) { this.captionParser_.init(data); } else { this.captionParser_.parseFrom(data, (captions) => { if (captions.length) { const videoOffset = this.sourceBuffers_[ContentType.VIDEO].timestampOffset; this.textEngine_.storeAndAppendClosedCaptions( captions, startTime, endTime, videoOffset); } }); } return this.enqueueOperation_( contentType, this.append_.bind(this, contentType, data)); } else { return this.enqueueOperation_( contentType, this.append_.bind(this, contentType, data)); } }; /** * Set the selected closed captions Id and language. * * @param {string} id */ shaka.media.MediaSourceEngine.prototype.setSelectedClosedCaptionId = function(id) { const VIDEO = shaka.util.ManifestParserUtils.ContentType.VIDEO; const videoBufferEndTime = this.bufferEnd(VIDEO) || 0; this.textEngine_.setSelectedClosedCaptionId(id, videoBufferEndTime); }; /** * 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(); } // CaptionParser tracks the latest timestamp and uses this to filter // for duplicate captions. We do this ourselves, so we must reset // the CaptionParser when we seek. The best indicator of an // unbuffered seek in MediaSourceEngine is clear(). This causes a // small glitch when we change languages (which also calls clear()), // where the first caption in the new language may be missing. // TODO: Ask mux.js for a switch to remove this timestamp-tracking // feature so that we can do away with these hacks. this.captionParser_.reset(); 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. For future appends, frames/samples with timestamps less than * this value will be dropped. * @param {number} appendWindowEnd The timestamp to set the append window end * to. For future appends, frames/samples with timestamps greater than this * value will be dropped. * @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=} 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(reason) { return this.enqueueBlockingOperation_(function() { // If endOfStream() has already been called on the media source, // don't call it again. if (this.ended()) { return; } // Tizen and IE11 won't let us pass undefined, but it will let us omit the // argument. if (reason) { this.mediaSource_.endOfStream(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://bit.ly/2ttKiBU): 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(error) { // 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.includes(this.queues_[contentType][0].p), 'The item in queue should be one of our waiters!'); this.queues_[contentType].shift(); } } } throw error; }.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); } } }; /** * @return {!shaka.extern.TextDisplayer} */ shaka.media.MediaSourceEngine.prototype.getTextDisplayer = function() { goog.asserts.assert( this.textDisplayer_, 'TextDisplayer should only be null when this is destroyed'); return this.textDisplayer_; }; /** * @param {!shaka.extern.TextDisplayer} textDisplayer */ shaka.media.MediaSourceEngine.prototype.setTextDisplayer = function(textDisplayer) { const oldTextDisplayer = this.textDisplayer_; this.textDisplayer_ = textDisplayer; if (oldTextDisplayer) { textDisplayer.setTextVisibility(oldTextDisplayer.isTextVisible()); oldTextDisplayer.destroy(); } if (this.textEngine_) { this.textEngine_.setDisplayer(textDisplayer); } };