diff --git a/lib/media/media_source_engine.js b/lib/media/media_source_engine.js index 61792b52ee..b921dbf7b8 100644 --- a/lib/media/media_source_engine.js +++ b/lib/media/media_source_engine.js @@ -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 diff --git a/test/media/media_source_engine_unit.js b/test/media/media_source_engine_unit.js index d579a317a0..67c4c81313 100644 --- a/test/media/media_source_engine_unit.js +++ b/test/media/media_source_engine_unit.js @@ -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);