From 8923834d7f2afe4da5201b51ff68a7eff06269cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Tyczy=C5=84ski?= Date: Mon, 9 Dec 2024 12:46:45 +0100 Subject: [PATCH] fix: Codec switch reload - apply boundaries correctly (#7700) Fixes #7595 --- lib/media/media_source_engine.js | 119 ++++++------------------------- lib/media/streaming_engine.js | 10 ++- 2 files changed, 32 insertions(+), 97 deletions(-) diff --git a/lib/media/media_source_engine.js b/lib/media/media_source_engine.js index 06e934ff74..61792b52ee 100644 --- a/lib/media/media_source_engine.js +++ b/lib/media/media_source_engine.js @@ -466,12 +466,12 @@ shaka.media.MediaSourceEngine = class { cleanup.push(this.textEngine_.destroy()); } + await Promise.all(cleanup); + for (const contentType in this.transmuxers_) { - cleanup.push(this.transmuxers_[contentType].destroy()); + this.transmuxers_[contentType].destroy(); } - - await Promise.all(cleanup); if (this.eventManager_) { this.eventManager_.release(); this.eventManager_ = null; @@ -692,7 +692,8 @@ shaka.media.MediaSourceEngine = class { * @return {boolean} */ isStreamingAllowed() { - return this.streamingAllowed_ && !this.usingRemotePlayback_; + return this.streamingAllowed_ && !this.usingRemotePlayback_ && + !this.reloadingMediaSource_; } /** @@ -1859,11 +1860,13 @@ shaka.media.MediaSourceEngine = class { /** @type {!Array.} */ const allWaiters = []; + /** @type {!Array.} */ + const contentTypes = Object.keys(this.sourceBuffers_); // 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 (const contentType in this.sourceBuffers_) { + for (const contentType of contentTypes) { const ready = new shaka.util.PublicPromise(); const operation = { start: () => ready.resolve(), @@ -1893,7 +1896,7 @@ shaka.media.MediaSourceEngine = class { // assert at the end of destroy passes. In compiled mode, the queues // are wiped in destroy. if (goog.DEBUG) { - for (const contentType in this.sourceBuffers_) { + for (const contentType of contentTypes) { if (this.queues_[contentType].length) { goog.asserts.assert( this.queues_[contentType].length == 1, @@ -1910,7 +1913,7 @@ shaka.media.MediaSourceEngine = class { if (goog.DEBUG) { // If we did it correctly, nothing is updating. - for (const contentType in this.sourceBuffers_) { + for (const contentType of contentTypes) { goog.asserts.assert( this.sourceBuffers_[contentType].updating == false, 'SourceBuffers should not be updating after a blocking op!'); @@ -1930,7 +1933,7 @@ shaka.media.MediaSourceEngine = class { null); } finally { // Unblock the queues. - for (const contentType in this.sourceBuffers_) { + for (const contentType of contentTypes) { this.popFromQueue_(contentType); } } @@ -1942,6 +1945,7 @@ shaka.media.MediaSourceEngine = class { * @private */ popFromQueue_(contentType) { + goog.asserts.assert(this.queues_[contentType], 'Queue should exist'); // Remove the in-progress operation, which is now complete. this.queues_[contentType].shift(); this.startOperation_(contentType); @@ -2099,48 +2103,6 @@ shaka.media.MediaSourceEngine = class { null); } - /** - * Returns the source buffer parameters - * @param {shaka.util.ManifestParserUtils.ContentType} contentType - * @return {?shaka.media.MediaSourceEngine.SourceBufferParams} - * @private - */ - getSourceBufferParams_(contentType) { - if (!this.sourceBuffers_[contentType]) { - return null; - } - return { - timestampOffset: this.sourceBuffers_[contentType].timestampOffset, - appendWindowStart: this.sourceBuffers_[contentType].appendWindowStart, - appendWindowEnd: this.sourceBuffers_[contentType].appendWindowEnd, - }; - } - - /** - * Restore source buffer parameters - * @param {shaka.util.ManifestParserUtils.ContentType} contentType - * @param {?shaka.media.MediaSourceEngine.SourceBufferParams} params - * @private - */ - restoreSourceBufferParams_(contentType, params) { - if (!params) { - return; - } - - if (!this.sourceBuffers_[contentType]) { - shaka.log.warning('Attempted to restore a non-existent source buffer'); - return; - } - - this.sourceBuffers_[contentType].timestampOffset = - params.timestampOffset; - // `end` needs to be set before `start` - this.sourceBuffers_[contentType].appendWindowEnd = - params.appendWindowEnd; - this.sourceBuffers_[contentType].appendWindowStart = - params.appendWindowStart; - } - /** * Resets the MediaSource and re-adds source buffers due to codec mismatch * @@ -2149,7 +2111,6 @@ shaka.media.MediaSourceEngine = class { * @private */ async reset_(streamsByType) { - const Functional = shaka.util.Functional; const ContentType = shaka.util.ManifestParserUtils.ContentType; this.reloadingMediaSource_ = true; this.needSplitMuxedContent_ = false; @@ -2171,36 +2132,19 @@ shaka.media.MediaSourceEngine = class { try { this.eventManager_.removeAll(); - const cleanup = []; for (const contentType in this.transmuxers_) { - cleanup.push(this.transmuxers_[contentType].destroy()); - } - for (const contentType in this.queues_) { - // Make a local copy of the queue and the first item. - const q = this.queues_[contentType]; - const 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 (const item of q.slice(1)) { - item.p.reject(shaka.util.Destroyer.destroyedError()); - } + this.transmuxers_[contentType].destroy(); } for (const contentType in this.sourceBuffers_) { const sourceBuffer = this.sourceBuffers_[contentType]; try { this.mediaSource_.removeSourceBuffer(sourceBuffer); - } catch (e) {} + } catch (e) { + shaka.log.debug('Exception on removeSourceBuffer', e); + } } - await Promise.all(cleanup); this.transmuxers_ = {}; + this.sourceBuffers_ = {}; const previousDuration = this.mediaSource_.duration; this.mediaSourceOpen_ = new shaka.util.PublicPromise(); @@ -2231,23 +2175,17 @@ shaka.media.MediaSourceEngine = class { onSourceBufferAdded); for (const contentType of streamsByType.keys()) { - const previousParams = this.getSourceBufferParams_(contentType); const stream = streamsByType.get(contentType); // eslint-disable-next-line no-await-in-loop await this.initSourceBuffer_(contentType, stream, stream.codecs); - if (this.needSplitMuxedContent_) { - this.queues_[ContentType.AUDIO] = []; - this.queues_[ContentType.VIDEO] = []; - } else { - this.queues_[contentType] = []; - } - - this.restoreSourceBufferParams_(contentType, previousParams); } const audio = streamsByType.get(ContentType.AUDIO); if (audio && audio.isAudioMuxedInVideo) { this.needSplitMuxedContent_ = true; } + if (this.needSplitMuxedContent_ && !this.queues_[ContentType.AUDIO]) { + this.queues_[ContentType.AUDIO] = []; + } // Fake a seek to catchup the playhead. this.video_.currentTime = currentTime; @@ -2393,13 +2331,12 @@ shaka.media.MediaSourceEngine = class { * Returns true if it's necessary codec switch to load the new stream. * * @param {shaka.util.ManifestParserUtils.ContentType} contentType - * @param {shaka.extern.Stream} stream * @param {string} refMimeType * @param {string} refCodecs * @return {boolean} * @private */ - isCodecSwitchNecessary_(contentType, stream, refMimeType, refCodecs) { + isCodecSwitchNecessary_(contentType, refMimeType, refCodecs) { if (contentType == shaka.util.ManifestParserUtils.ContentType.TEXT) { return false; } @@ -2443,13 +2380,12 @@ shaka.media.MediaSourceEngine = class { * new stream. * * @param {shaka.util.ManifestParserUtils.ContentType} contentType - * @param {shaka.extern.Stream} stream * @param {string} mimeType * @param {string} codecs * @return {boolean} */ - isResetMediaSourceNecessary(contentType, stream, mimeType, codecs) { - if (!this.isCodecSwitchNecessary_(contentType, stream, mimeType, codecs)) { + isResetMediaSourceNecessary(contentType, mimeType, codecs) { + if (!this.isCodecSwitchNecessary_(contentType, mimeType, codecs)) { return false; } @@ -2540,12 +2476,3 @@ shaka.media.MediaSourceEngine.SourceBufferMode_ = { * Called when an embedded 'emsg' box should trigger a manifest update. */ shaka.media.MediaSourceEngine.PlayerInterface; - -/** - * @typedef {{ - * timestampOffset: number, - * appendWindowStart: number, - * appendWindowEnd: number - * }} - */ -shaka.media.MediaSourceEngine.SourceBufferParams; diff --git a/lib/media/streaming_engine.js b/lib/media/streaming_engine.js index c08cd05941..dd8a794bde 100644 --- a/lib/media/streaming_engine.js +++ b/lib/media/streaming_engine.js @@ -2011,7 +2011,7 @@ shaka.media.StreamingEngine = class { const isResetMediaSourceNecessary = mediaState.lastCodecs && mediaState.lastMimeType && this.playerInterface_.mediaSourceEngine.isResetMediaSourceNecessary( - mediaState.type, mediaState.stream, mimeType, fullCodecs); + mediaState.type, mimeType, fullCodecs); if (isResetMediaSourceNecessary) { let otherState = null; if (mediaState.type === ContentType.VIDEO) { @@ -2025,6 +2025,10 @@ shaka.media.StreamingEngine = class { // Then clear our cache of the last init segment, since MSE will be // reloaded and no init segment will be there post-reload. otherState.lastInitSegmentReference = null; + // Clear cache of append window start and end, since they will need + // to be reapplied post-reload by streaming engine. + otherState.lastAppendWindowStart = null; + otherState.lastAppendWindowEnd = null; // Now force the existing buffer to be cleared. It is not necessary // to perform the MSE clear operation, but this has the side-effect // that our state for that stream will then match MSE's post-reload @@ -2729,6 +2733,8 @@ shaka.media.StreamingEngine = class { const audioMediaState = this.mediaStates_.get(ContentType.AUDIO); if (audioMediaState) { audioMediaState.lastInitSegmentReference = null; + audioMediaState.lastAppendWindowStart = null; + audioMediaState.lastAppendWindowEnd = null; if (clearBuffer) { this.forceClearBuffer_(audioMediaState); } @@ -2740,6 +2746,8 @@ shaka.media.StreamingEngine = class { const videoMediaState = this.mediaStates_.get(ContentType.VIDEO); if (videoMediaState) { videoMediaState.lastInitSegmentReference = null; + videoMediaState.lastAppendWindowStart = null; + videoMediaState.lastAppendWindowEnd = null; if (clearBuffer) { this.forceClearBuffer_(videoMediaState); }