fix: re-open the MediaSource if readyState is not open when the init() method is called. (#7783)

Builds on top of @tykus160's observation in
https://github.com/shaka-project/shaka-player/issues/4903 where
`MediaSource.readyState` was either in a `closed` or `ended` state when
the `MediaSourceEngine.init()` logic is executed.

This fix will simply re-open the `MediaSource` if non-open, resulting in
fewer scenarios where the `MEDIA_SOURCE_OPERATION_THREW` error:
https://github.com/shaka-project/shaka-player/blob/de0f33c2623b057e80b7cafd53e19fac2f984961/lib/media/media_source_engine.js#L648-L651

is thrown because of an
[`InvalidStateError`](https://developer.mozilla.org/en-US/docs/Web/API/MediaSource/addSourceBuffer#exceptions).
This commit is contained in:
Julian Domingo
2024-12-19 18:37:01 -08:00
committed by GitHub
parent de0f33c262
commit 6610fa3e57
2 changed files with 50 additions and 1 deletions
+19 -1
View File
@@ -551,6 +551,13 @@ shaka.media.MediaSourceEngine = class {
const ContentType = shaka.util.ManifestParserUtils.ContentType;
await this.mediaSourceOpen_;
if (this.ended() || this.closed()) {
shaka.log.alwaysError('Expected MediaSource to be open during init(); ' +
'reopening the media source.');
this.mediaSourceOpen_ = new shaka.util.PublicPromise();
this.mediaSource_ = this.createMediaSource(this.mediaSourceOpen_);
await this.mediaSourceOpen_;
}
this.sequenceMode_ = sequenceMode;
this.manifestType_ = manifestType;
@@ -724,6 +731,17 @@ shaka.media.MediaSourceEngine = class {
return this.mediaSource_ ? this.mediaSource_.readyState == 'ended' : true;
}
/**
* @return {boolean} True if the MediaSource is in an "closed" state, or if
* the object has been destroyed.
*/
closed() {
if (this.reloadingMediaSource_) {
return false;
}
return this.mediaSource_ ? this.mediaSource_.readyState == 'closed' : true;
}
/**
* Gets the first timestamp in buffer for the given content type.
*
@@ -1608,7 +1626,7 @@ shaka.media.MediaSourceEngine = class {
// don't call it again. Also do not call if readyState is
// 'closed' (not attached to video element) since it is not a
// valid operation.
if (this.ended() || this.mediaSource_.readyState === 'closed') {
if (this.ended() || this.closed()) {
return;
}
// Tizen won't let us pass undefined, but it will let us omit the
+31
View File
@@ -165,6 +165,10 @@ describe('MediaSourceEngine', () => {
videoSourceBuffer = createMockSourceBuffer();
mockMediaSource = createMockMediaSource();
mockMediaSource.addSourceBuffer.and.callFake((mimeType) => {
if (mockMediaSource.readyState !== 'open') {
// https://w3c.github.io/media-source/#addsourcebuffer-method
throw new Error('InvalidStateError');
}
const type = mimeType.split('/')[0];
const buffer = type == 'audio' ? audioSourceBuffer : videoSourceBuffer;
@@ -200,6 +204,7 @@ describe('MediaSourceEngine', () => {
createMediaSourceSpy = jasmine.createSpy('createMediaSource');
createMediaSourceSpy.and.callFake((p) => {
p.resolve();
mockMediaSource.readyState = 'open';
return mockMediaSource;
});
// eslint-disable-next-line no-restricted-syntax
@@ -400,6 +405,32 @@ describe('MediaSourceEngine', () => {
expect(shaka.text.TextEngine).not.toHaveBeenCalled();
});
it('creates SourceBuffers when MediaSource readyState is closed',
async () => {
const initObject = new Map();
initObject.set(ContentType.AUDIO, fakeAudioStream);
initObject.set(ContentType.VIDEO, fakeVideoStream);
await mediaSourceEngine.open();
mockMediaSource.readyState = 'closed';
await expectAsync(
mediaSourceEngine.init(initObject, false)).not.toBeRejected();
});
it('creates SourceBuffers when MediaSource readyState is ended',
async () => {
const initObject = new Map();
initObject.set(ContentType.AUDIO, fakeAudioStream);
initObject.set(ContentType.VIDEO, fakeVideoStream);
await mediaSourceEngine.open();
mockMediaSource.readyState = 'ended';
await expectAsync(
mediaSourceEngine.init(initObject, false)).not.toBeRejected();
});
it('creates TextEngines for text types', async () => {
const initObject = new Map();
initObject.set(ContentType.TEXT, fakeTextStream);