Skip to content

Commit

Permalink
fix: re-open the MediaSource if readyState is not open when the `…
Browse files Browse the repository at this point in the history
…init()` method is called. (#7783)

Builds on top of @tykus160's observation in
#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).
  • Loading branch information
JulianDomingo authored Dec 20, 2024
1 parent de0f33c commit 6610fa3
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 1 deletion.
20 changes: 19 additions & 1 deletion lib/media/media_source_engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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
Expand Down
31 changes: 31 additions & 0 deletions test/media/media_source_engine_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 6610fa3

Please sign in to comment.