Skip to content

Commit

Permalink
fix: Codec switch reload - apply boundaries correctly (#7700)
Browse files Browse the repository at this point in the history
Fixes #7595
  • Loading branch information
tykus160 authored and joeyparrish committed Dec 12, 2024
1 parent 06efdb1 commit 8923834
Show file tree
Hide file tree
Showing 2 changed files with 32 additions and 97 deletions.
119 changes: 23 additions & 96 deletions lib/media/media_source_engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -692,7 +692,8 @@ shaka.media.MediaSourceEngine = class {
* @return {boolean}
*/
isStreamingAllowed() {
return this.streamingAllowed_ && !this.usingRemotePlayback_;
return this.streamingAllowed_ && !this.usingRemotePlayback_ &&
!this.reloadingMediaSource_;
}

/**
Expand Down Expand Up @@ -1859,11 +1860,13 @@ shaka.media.MediaSourceEngine = class {

/** @type {!Array.<!shaka.util.PublicPromise>} */
const allWaiters = [];
/** @type {!Array.<!shaka.util.ManifestParserUtils.ContentType>} */
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(),
Expand Down Expand Up @@ -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,
Expand All @@ -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!');
Expand All @@ -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);
}
}
Expand All @@ -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);
Expand Down Expand Up @@ -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
*
Expand All @@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
10 changes: 9 additions & 1 deletion lib/media/streaming_engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
}
Expand Down

0 comments on commit 8923834

Please sign in to comment.