diff --git a/externs/shaka/manifest_parser.js b/externs/shaka/manifest_parser.js index 442136ad0a..8c924f08d1 100644 --- a/externs/shaka/manifest_parser.js +++ b/externs/shaka/manifest_parser.js @@ -111,7 +111,8 @@ shaka.extern.ManifestParser = class { * onError: function(!shaka.util.Error), * isLowLatencyMode: function():boolean, * isAutoLowLatencyMode: function():boolean, - * enableLowLatencyMode: function() + * enableLowLatencyMode: function(), + * updateDuration: function() * }} * * @description @@ -147,6 +148,8 @@ shaka.extern.ManifestParser = class { * Return true if auto low latency streaming mode is enabled. * @property {function()} enableLowLatencyMode * Enable low latency streaming mode. + * @property {function()} updateDuration + * Update the presentation duration based on PresentationTimeline. * @exportDoc */ shaka.extern.ManifestParser.PlayerInterface; diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index c25c693c23..1f7347ce39 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -284,6 +284,21 @@ shaka.hls.HlsParser = class { } await Promise.all(updates); + + this.notifySegments_(); + + // If any hasEndList is false, the stream is still live. + const stillLive = streamInfos.some((s) => s.hasEndList == false); + if (!stillLive) { + // Convert the presentation to VOD and set the duration. + const PresentationType = shaka.hls.HlsParser.PresentationType_; + this.setPresentationType_(PresentationType.VOD); + + const maxTimestamps = streamInfos.map((s) => s.maxTimestamp); + // The duration is the minimum of the end times of all streams. + this.presentationTimeline_.setDuration(Math.min(...maxTimestamps)); + this.playerInterface_.updateDuration(); + } } /** @@ -294,7 +309,6 @@ shaka.hls.HlsParser = class { * @private */ async updateStream_(streamInfo) { - const PresentationType = shaka.hls.HlsParser.PresentationType_; const manifestUri = streamInfo.absoluteMediaPlaylistUri; const uriObj = new goog.Uri(manifestUri); if (this.lowLatencyMode_ && streamInfo.canSkipSegments) { @@ -327,6 +341,7 @@ shaka.hls.HlsParser = class { streamInfo.verbatimMediaPlaylistUri, playlist, stream.type, stream.mimeType, streamInfo.mediaSequenceToStartTime, mediaVariables, stream.codecs); + this.segmentsToNotifyByStream_.push(segments); stream.segmentIndex.mergeAndEvict( segments, this.presentationTimeline_.getSegmentAvailabilityStart()); @@ -347,10 +362,10 @@ shaka.hls.HlsParser = class { shaka.hls.Utils.getFirstTagWithName(playlist.tags, 'EXT-X-ENDLIST'); if (endListTag) { - // Convert the presentation to VOD and set the duration to the last - // segment's end time. - this.setPresentationType_(PresentationType.VOD); - this.presentationTimeline_.setDuration(newestSegment.endTime); + // Flag this for later. We don't convert the whole presentation into VOD + // until we've seen the ENDLIST tag for all active playlists. + streamInfo.hasEndList = true; + streamInfo.maxTimestamp = newestSegment.endTime; } } @@ -1707,6 +1722,7 @@ shaka.hls.HlsParser = class { maxTimestamp: lastEndTime, mediaSequenceToStartTime, canSkipSegments, + hasEndList: false, }; } @@ -2557,8 +2573,11 @@ shaka.hls.HlsParser = class { try { await this.update(); - const delay = this.updatePlaylistDelay_; - this.updatePlaylistTimer_.tickAfter(/* seconds= */ delay); + // This may have converted to VOD, in which case we stop updating. + if (this.isLive_()) { + const delay = this.updatePlaylistDelay_; + this.updatePlaylistTimer_.tickAfter(/* seconds= */ delay); + } } catch (error) { // Detect a call to stop() during this.update() if (!this.playerInterface_) { @@ -2749,7 +2768,8 @@ shaka.hls.HlsParser = class { * absoluteMediaPlaylistUri: string, * maxTimestamp: number, * mediaSequenceToStartTime: !Map., - * canSkipSegments: boolean + * canSkipSegments: boolean, + * hasEndList: boolean * }} * * @description @@ -2772,6 +2792,8 @@ shaka.hls.HlsParser = class { * @property {boolean} canSkipSegments * True if the server supports delta playlist updates, and we can send a * request for a playlist that can skip older media segments. + * @property {boolean} hasEndList + * True if the stream has an EXT-X-ENDLIST tag. */ shaka.hls.HlsParser.StreamInfo; diff --git a/lib/media/media_source_engine.js b/lib/media/media_source_engine.js index 7768abed2d..c303adce02 100644 --- a/lib/media/media_source_engine.js +++ b/lib/media/media_source_engine.js @@ -849,21 +849,25 @@ shaka.media.MediaSourceEngine = class { } /** - * We only support increasing duration at this time. Decreasing duration - * causes the MSE removal algorithm to run, which results in an 'updateend' - * event. Supporting this scenario would be complicated, and is not currently - * needed. - * * @param {number} duration * @return {!Promise} */ async setDuration(duration) { - goog.asserts.assert( - isNaN(this.mediaSource_.duration) || - this.mediaSource_.duration <= duration, - 'duration cannot decrease: ' + this.mediaSource_.duration + ' -> ' + - duration); await this.enqueueBlockingOperation_(() => { + // Reducing the duration causes the MSE removal algorithm to run, which + // triggers an 'updateend' event to fire. To handle this scenario, we + // have to insert a dummy operation into the beginning of each queue, + // which the 'updateend' handler will remove. + if (duration < this.mediaSource_.duration) { + for (const contentType in this.sourceBuffers_) { + const dummyOperation = { + start: () => {}, + p: new shaka.util.PublicPromise(), + }; + this.queues_[contentType].unshift(dummyOperation); + } + } + this.mediaSource_.duration = duration; }); } diff --git a/lib/media/streaming_engine.js b/lib/media/streaming_engine.js index c3b570f13f..8c0bf9cba3 100644 --- a/lib/media/streaming_engine.js +++ b/lib/media/streaming_engine.js @@ -774,7 +774,7 @@ shaka.media.StreamingEngine = class { this.manifest_.sequenceMode); this.destroyer_.ensureNotDestroyed(); - this.setDuration_(); + this.updateDuration(); for (const type of streamsByType.keys()) { const stream = streamsByType.get(type); @@ -824,9 +824,8 @@ shaka.media.StreamingEngine = class { /** * Sets the MediaSource's duration. - * @private */ - setDuration_() { + updateDuration() { const duration = this.manifest_.presentationTimeline.getDuration(); if (duration < Infinity) { this.playerInterface_.mediaSourceEngine.setDuration(duration); diff --git a/lib/offline/storage.js b/lib/offline/storage.js index 0b8a027e48..2a507fc9ae 100644 --- a/lib/offline/storage.js +++ b/lib/offline/storage.js @@ -1169,6 +1169,7 @@ shaka.offline.Storage = class { isLowLatencyMode: () => false, isAutoLowLatencyMode: () => false, enableLowLatencyMode: () => {}, + updateDuration: () => {}, }; parser.configure(config.manifest); diff --git a/lib/player.js b/lib/player.js index 96fc116f28..30ec14b24e 100644 --- a/lib/player.js +++ b/lib/player.js @@ -1770,6 +1770,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget { enableLowLatencyMode: () => { this.configure('streaming.lowLatencyMode', true); }, + updateDuration: () => { + if (this.streamingEngine_) { + this.streamingEngine_.updateDuration(); + } + }, }; const startTime = Date.now() / 1000; diff --git a/test/dash/dash_parser_content_protection_unit.js b/test/dash/dash_parser_content_protection_unit.js index 2514c45fea..98c0f622fb 100644 --- a/test/dash/dash_parser_content_protection_unit.js +++ b/test/dash/dash_parser_content_protection_unit.js @@ -43,6 +43,7 @@ describe('DashParser ContentProtection', () => { isLowLatencyMode: () => false, isAutoLowLatencyMode: () => false, enableLowLatencyMode: () => {}, + updateDuration: () => {}, }; const actual = await dashParser.start( diff --git a/test/dash/dash_parser_live_unit.js b/test/dash/dash_parser_live_unit.js index ca1acceb8e..f5b768ab78 100644 --- a/test/dash/dash_parser_live_unit.js +++ b/test/dash/dash_parser_live_unit.js @@ -36,6 +36,7 @@ describe('DashParser Live', () => { isLowLatencyMode: () => false, isAutoLowLatencyMode: () => false, enableLowLatencyMode: () => {}, + updateDuration: () => {}, }; }); diff --git a/test/dash/dash_parser_manifest_unit.js b/test/dash/dash_parser_manifest_unit.js index 1232d992e3..03f9882584 100644 --- a/test/dash/dash_parser_manifest_unit.js +++ b/test/dash/dash_parser_manifest_unit.js @@ -52,6 +52,7 @@ describe('DashParser Manifest', () => { isLowLatencyMode: () => false, isAutoLowLatencyMode: () => false, enableLowLatencyMode: () => {}, + updateDuration: () => {}, }; }); diff --git a/test/dash/dash_parser_segment_base_unit.js b/test/dash/dash_parser_segment_base_unit.js index ca054fe47d..9adf96dc09 100644 --- a/test/dash/dash_parser_segment_base_unit.js +++ b/test/dash/dash_parser_segment_base_unit.js @@ -38,6 +38,7 @@ describe('DashParser SegmentBase', () => { isLowLatencyMode: () => false, isAutoLowLatencyMode: () => false, enableLowLatencyMode: () => {}, + updateDuration: () => {}, }; }); diff --git a/test/dash/dash_parser_segment_list_unit.js b/test/dash/dash_parser_segment_list_unit.js index 1b95d6a076..827a4ec66c 100644 --- a/test/dash/dash_parser_segment_list_unit.js +++ b/test/dash/dash_parser_segment_list_unit.js @@ -348,6 +348,7 @@ describe('DashParser SegmentList', () => { isLowLatencyMode: () => false, isAutoLowLatencyMode: () => false, enableLowLatencyMode: () => {}, + updateDuration: () => {}, }; const manifest = await dashParser.start('dummy://foo', playerInterface); const stream = manifest.variants[0].video; diff --git a/test/dash/dash_parser_segment_template_unit.js b/test/dash/dash_parser_segment_template_unit.js index f68265bdb3..af0a1b6266 100644 --- a/test/dash/dash_parser_segment_template_unit.js +++ b/test/dash/dash_parser_segment_template_unit.js @@ -47,6 +47,7 @@ describe('DashParser SegmentTemplate', () => { isLowLatencyMode: () => false, isAutoLowLatencyMode: () => false, enableLowLatencyMode: () => {}, + updateDuration: () => {}, }; }); diff --git a/test/hls/hls_live_unit.js b/test/hls/hls_live_unit.js index 655a0317c8..a19dbbae09 100644 --- a/test/hls/hls_live_unit.js +++ b/test/hls/hls_live_unit.js @@ -77,6 +77,7 @@ describe('HlsParser live', () => { isLowLatencyMode: () => false, isAutoLowLatencyMode: () => false, enableLowLatencyMode: () => {}, + updateDuration: () => {}, }; parser = new shaka.hls.HlsParser(); @@ -100,13 +101,11 @@ describe('HlsParser live', () => { /** * @param {string} master * @param {string} initialMedia - * @param {!Array} initialReferences - * @param {string} updatedMedia - * @param {!Array} updatedReferences + * @param {Array=} initialReferences + * @return {!Promise.} */ - async function testUpdate( - master, initialMedia, initialReferences, updatedMedia, - updatedReferences) { + async function testInitialManifest( + master, initialMedia, initialReferences=null) { fakeNetEngine .setResponseText('test:/master', master) .setResponseText('test:/video', initialMedia) @@ -118,32 +117,52 @@ describe('HlsParser live', () => { .setResponseValue('test:/main2.mp4', segmentData) .setResponseValue('test:/main3.mp4', segmentData) .setResponseValue('test:/main4.mp4', segmentData) + .setResponseValue('test:/partial.mp4', segmentData) + .setResponseValue('test:/partial2.mp4', segmentData) .setResponseValue('test:/selfInit.mp4', selfInitializingSegmentData); const manifest = await parser.start('test:/master', playerInterface); - await Promise.all(manifest.variants.map(async (variant) => { - await variant.video.createSegmentIndex(); - ManifestParser.verifySegmentIndex(variant.video, initialReferences); - if (variant.audio) { - await variant.audio.createSegmentIndex(); - ManifestParser.verifySegmentIndex(variant.audio, initialReferences); - } - })); + if (initialReferences) { + await Promise.all(manifest.variants.map(async (variant) => { + await variant.video.createSegmentIndex(); + + // The compiler doesn't count null checks done outside this callback, + // so we need an assertion here. + goog.asserts.assert(initialReferences != null, 'references non-null'); + ManifestParser.verifySegmentIndex(variant.video, initialReferences); + + if (variant.audio) { + await variant.audio.createSegmentIndex(); + ManifestParser.verifySegmentIndex(variant.audio, initialReferences); + } + })); + } + + return manifest; + } + /** + * @param {shaka.extern.Manifest} manifest + * @param {string} updatedMedia + * @param {Array=} updatedReferences + */ + async function testUpdate(manifest, updatedMedia, updatedReferences=null) { // Replace the entries with the updated values. fakeNetEngine .setResponseText('test:/video', updatedMedia) .setResponseText('test:/redirected/video', updatedMedia) .setResponseText('test:/video2', updatedMedia) - .setResponseText('test:/audio', updatedMedia) - .setResponseText('test:/video?_HLS_skip=YES', updatedMedia); + .setResponseText('test:/audio', updatedMedia); await delayForUpdatePeriod(); - for (const variant of manifest.variants) { - ManifestParser.verifySegmentIndex(variant.video, updatedReferences); - if (variant.audio) { - ManifestParser.verifySegmentIndex(variant.audio, updatedReferences); + + if (updatedReferences) { + for (const variant of manifest.variants) { + ManifestParser.verifySegmentIndex(variant.video, updatedReferences); + if (variant.audio) { + ManifestParser.verifySegmentIndex(variant.audio, updatedReferences); + } } } } @@ -170,13 +189,8 @@ describe('HlsParser live', () => { ].join(''); it('treats already ended presentation like VOD', async () => { - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', media + '#EXT-X-ENDLIST') - .setResponseValue('test:/init.mp4', initSegmentData) - .setResponseValue('test:/main.mp4', segmentData); - - const manifest = await parser.start('test:/master', playerInterface); + const manifest = await testInitialManifest( + master, media + '#EXT-X-ENDLIST'); expect(manifest.presentationTimeline.isLive()).toBe(false); expect(manifest.presentationTimeline.isInProgress()).toBe(false); }); @@ -188,8 +202,8 @@ describe('HlsParser live', () => { const ref2 = makeReference( 'test:/main2.mp4', 2, 4, /* syncTime= */ null); - await testUpdate( - master, media, [ref1], mediaWithAdditionalSegment, [ref1, ref2]); + const manifest = await testInitialManifest(master, media, [ref1]); + await testUpdate(manifest, mediaWithAdditionalSegment, [ref1, ref2]); }); it('updates all variants', async () => { @@ -205,9 +219,9 @@ describe('HlsParser live', () => { const ref2 = makeReference( 'test:/main2.mp4', 2, 4, /* syncTime= */ null); - await testUpdate( - masterWithTwoVariants, media, [ref1], mediaWithAdditionalSegment, - [ref1, ref2]); + const manifest = await testInitialManifest( + masterWithTwoVariants, media, [ref1]); + await testUpdate(manifest, mediaWithAdditionalSegment, [ref1, ref2]); }); it('updates all streams', async () => { @@ -228,9 +242,9 @@ describe('HlsParser live', () => { const ref2 = makeReference( 'test:/main2.mp4', 2, 4, /* syncTime= */ null); - await testUpdate( - masterWithAudio, media, [ref1], mediaWithAdditionalSegment, - [ref1, ref2]); + const manifest = await testInitialManifest( + masterWithAudio, media, [ref1]); + await testUpdate(manifest, mediaWithAdditionalSegment, [ref1, ref2]); }); it('handles multiple updates', async () => { @@ -253,72 +267,28 @@ describe('HlsParser live', () => { const ref3 = makeReference( 'test:/main3.mp4', 4, 6, /* syncTime= */ null); - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', media) - .setResponseValue('test:/init.mp4', initSegmentData) - .setResponseValue('test:/main.mp4', segmentData); - - const manifest = await parser.start('test:/master', playerInterface); - - const video = manifest.variants[0].video; - await video.createSegmentIndex(); - ManifestParser.verifySegmentIndex(video, [ref1]); - - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', updatedMedia1); - - await delayForUpdatePeriod(); - ManifestParser.verifySegmentIndex(video, [ref1, ref2]); - - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', updatedMedia2); - - await delayForUpdatePeriod(); - ManifestParser.verifySegmentIndex(video, [ref1, ref2, ref3]); + const manifest = await testInitialManifest(master, media, [ref1]); + await testUpdate(manifest, updatedMedia1, [ref1, ref2]); + await testUpdate(manifest, updatedMedia2, [ref1, ref2, ref3]); }); it('converts presentation to VOD when it is finished', async () => { - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', media) - .setResponseValue('test:/init.mp4', initSegmentData) - .setResponseValue('test:/main.mp4', segmentData); - - const manifest = await parser.start('test:/master', playerInterface); - + const manifest = await testInitialManifest(master, media); expect(manifest.presentationTimeline.isLive()).toBe(true); - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', - mediaWithAdditionalSegment + '#EXT-X-ENDLIST\n'); - await delayForUpdatePeriod(); + await testUpdate( + manifest, mediaWithAdditionalSegment + '#EXT-X-ENDLIST\n'); expect(manifest.presentationTimeline.isLive()).toBe(false); }); it('starts presentation as VOD when ENDLIST is present', async () => { - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', media + '#EXT-X-ENDLIST') - .setResponseValue('test:/init.mp4', initSegmentData) - .setResponseValue('test:/main.mp4', segmentData); - - const manifest = await parser.start('test:/master', playerInterface); + const manifest = await testInitialManifest( + master, media + '#EXT-X-ENDLIST'); expect(manifest.presentationTimeline.isLive()).toBe(false); }); it('does not throw when interrupted by stop', async () => { - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', media) - .setResponseValue('test:/init.mp4', initSegmentData) - .setResponseValue('test:/main.mp4', segmentData); - - const manifest = await parser.start('test:/master', playerInterface); - + const manifest = await testInitialManifest(master, media); expect(manifest.presentationTimeline.isLive()).toBe(true); // Block the next request so that update() is still happening when we @@ -336,6 +306,74 @@ describe('HlsParser live', () => { // Wait for stop to complete. await stopPromise; }); + + it('calls notifySegments on each update', async () => { + const manifest = await testInitialManifest(master, media); + const notifySegmentsSpy = spyOn( + manifest.presentationTimeline, 'notifySegments').and.callThrough(); + + // Trigger an update. + await delayForUpdatePeriod(); + + expect(notifySegmentsSpy).toHaveBeenCalled(); + notifySegmentsSpy.calls.reset(); + + // Trigger another update. + await delayForUpdatePeriod(); + + expect(notifySegmentsSpy).toHaveBeenCalled(); + }); + + it('converts to VOD only after all playlists end', async () => { + const master = [ + '#EXTM3U\n', + '#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="eng",', + 'URI="audio"\n', + '#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",AUDIO="aud1",', + 'RESOLUTION=960x540,FRAME-RATE=60\n', + 'video\n', + ].join(''); + + const mediaWithEndList = media + '#EXT-X-ENDLIST'; + + const manifest = await testInitialManifest(master, media); + expect(manifest.presentationTimeline.isLive()).toBe(true); + + // Update video only. + fakeNetEngine.setResponseText('test:/video', mediaWithEndList); + await delayForUpdatePeriod(); + + // Audio hasn't "ended" yet, so we're still live. + expect(manifest.presentationTimeline.isLive()).toBe(true); + + // Update audio. + fakeNetEngine.setResponseText('test:/audio', mediaWithEndList); + await delayForUpdatePeriod(); + + // Now both have "ended", so we're no longer live. + expect(manifest.presentationTimeline.isLive()).toBe(false); + }); + + it('stops updating after all playlists end', async () => { + const manifest = await testInitialManifest(master, media); + expect(manifest.presentationTimeline.isLive()).toBe(true); + + fakeNetEngine.request.calls.reset(); + await testUpdate( + manifest, mediaWithAdditionalSegment + '#EXT-X-ENDLIST\n'); + + // We saw one request for the video playlist, which signalled "ENDLIST". + fakeNetEngine.expectRequest( + 'test:/video', + shaka.net.NetworkingEngine.RequestType.MANIFEST); + expect(manifest.presentationTimeline.isLive()).toBe(false); + + fakeNetEngine.request.calls.reset(); + await delayForUpdatePeriod(); + + // No new updates were requested. + expect(fakeNetEngine.request).not.toHaveBeenCalled(); + }); }); // describe('update') }); // describe('playlist type EVENT') @@ -413,47 +451,27 @@ describe('HlsParser live', () => { ].join(''); it('starts presentation as VOD when ENDLIST is present', async () => { - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', media + '#EXT-X-ENDLIST') - .setResponseValue('test:/init.mp4', initSegmentData) - .setResponseValue('test:/main.mp4', segmentData); - - const manifest = await parser.start('test:/master', playerInterface); + const manifest = await testInitialManifest( + master, media + '#EXT-X-ENDLIST'); expect(manifest.presentationTimeline.isLive()).toBe(false); }); it('does not fail on a missing sequence number', async () => { - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', mediaWithoutSequenceNumber) - .setResponseValue('test:/init.mp4', initSegmentData) - .setResponseValue('test:/main.mp4', segmentData); - - await parser.start('test:/master', playerInterface); + await testInitialManifest(master, mediaWithoutSequenceNumber); }); it('sets presentation delay as configured', async () => { - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', media) - .setResponseValue('test:/init.mp4', initSegmentData) - .setResponseValue('test:/main.mp4', segmentData); config.defaultPresentationDelay = 10; parser.configure(config); - const manifest = await parser.start('test:/master', playerInterface); + + const manifest = await testInitialManifest(master, media); expect(manifest.presentationTimeline.getDelay()).toBe( config.defaultPresentationDelay); }); it('sets 3 times target duration as presentation delay if not configured', async () => { - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', media) - .setResponseValue('test:/init.mp4', initSegmentData) - .setResponseValue('test:/main.mp4', segmentData); - const manifest = await parser.start('test:/master', playerInterface); + const manifest = await testInitialManifest(master, media); expect(manifest.presentationTimeline.getDelay()).toBe(15); }); @@ -469,15 +487,9 @@ describe('HlsParser live', () => { 'main.mp4\n', ].join(''); - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', mediaWithLowLatency) - .setResponseValue('test:/init.mp4', initSegmentData) - .setResponseValue('test:/main.mp4', segmentData); - playerInterface.isLowLatencyMode = () => true; - const manifest = await parser.start('test:/master', playerInterface); + const manifest = await testInitialManifest(master, mediaWithLowLatency); // Presentation delay should be the value of 'PART-HOLD-BACK' if not // configured. expect(manifest.presentationTimeline.getDelay()).toBe(1.8); @@ -485,13 +497,8 @@ describe('HlsParser live', () => { describe('availabilityWindowOverride', () => { async function testWindowOverride(expectedWindow) { - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', mediaWithManySegments) - .setResponseValue('test:/init.mp4', initSegmentData) - .setResponseValue('test:/main.mp4', segmentData); - - const manifest = await parser.start('test:/master', playerInterface); + const manifest = await testInitialManifest( + master, mediaWithManySegments); expect(manifest).toBeTruthy(); const timeline = manifest.presentationTimeline; expect(timeline).toBeTruthy(); @@ -514,13 +521,6 @@ describe('HlsParser live', () => { }); it('sets timestamp offset for segments with discontinuity', async () => { - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', mediaWithDiscontinuity) - .setResponseValue('test:/init.mp4', initSegmentData) - .setResponseValue('test:/main.mp4', segmentData) - .setResponseValue('test:/main2.mp4', segmentData); - const ref1 = makeReference( 'test:/main.mp4', 0, 2, /* syncTime= */ null, /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null, @@ -533,10 +533,7 @@ describe('HlsParser live', () => { /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null, /* timestampOffset= */ 0); - const manifest = await parser.start('test:/master', playerInterface); - const video = manifest.variants[0].video; - await video.createSegmentIndex(); - ManifestParser.verifySegmentIndex(video, [ref1, ref2]); + await testInitialManifest(master, mediaWithDiscontinuity, [ref1, ref2]); }); // Test for https://github.com/shaka-project/shaka-player/issues/4223 @@ -562,14 +559,6 @@ describe('HlsParser live', () => { '#EXT-X-PRELOAD-HINT:TYPE=PART,URI="partial.mp4",BYTERANGE-START=210\n', ].join(''); - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', mediaWithPartialSegments) - .setResponseValue('test:/init.mp4', initSegmentData) - .setResponseValue('test:/main.mp4', segmentData) - .setResponseValue('test:/partial.mp4', segmentData) - .setResponseValue('test:/partial2.mp4', segmentData); - const partialRef = makeReference( 'test:/partial.mp4', 0, 2, /* syncTime= */ null, /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ 199); @@ -597,10 +586,7 @@ describe('HlsParser live', () => { /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null, /* timestampOffset= */ 0, [partialRef3, preloadRef]); - const manifest = await parser.start('test:/master', playerInterface); - const video = manifest.variants[0].video; - await video.createSegmentIndex(); - ManifestParser.verifySegmentIndex(video, [ref, ref2]); + await testInitialManifest(master, mediaWithPartialSegments, [ref, ref2]); }); // Test for https://github.com/shaka-project/shaka-player/issues/4223 @@ -623,14 +609,6 @@ describe('HlsParser live', () => { '#EXT-X-PRELOAD-HINT:TYPE=PART,URI="partial.mp4",BYTERANGE-START=210\n', ].join(''); - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', mediaWithPartialSegments) - .setResponseValue('test:/init.mp4', initSegmentData) - .setResponseValue('test:/main.mp4', segmentData) - .setResponseValue('test:/partial.mp4', segmentData) - .setResponseValue('test:/partial2.mp4', segmentData); - const ref = makeReference( 'test:/main.mp4', 0, 4, /* syncTime= */ null, /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null, @@ -646,10 +624,7 @@ describe('HlsParser live', () => { /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ 209, /* timestampOffset= */ 0, [partialRef]); - const manifest = await parser.start('test:/master', playerInterface); - const video = manifest.variants[0].video; - await video.createSegmentIndex(); - ManifestParser.verifySegmentIndex(video, [ref, ref2]); + await testInitialManifest(master, mediaWithPartialSegments, [ref, ref2]); }); // Test for https://github.com/shaka-project/shaka-player/issues/4185 @@ -667,12 +642,8 @@ describe('HlsParser live', () => { '#EXT-X-PRELOAD-HINT:TYPE=PART,URI="partial.mp4",BYTERANGE-START=210\n', ].join(''); - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', mediaWithPartialSegments); - // If this throws, the test fails. Otherwise, it passes. - await parser.start('test:/master', playerInterface); + await testInitialManifest(master, mediaWithPartialSegments); }); describe('update', () => { @@ -682,8 +653,8 @@ describe('HlsParser live', () => { const ref2 = makeReference( 'test:/main2.mp4', 2, 4, /* syncTime= */ null); - await testUpdate( - master, media, [ref1], mediaWithAdditionalSegment, [ref1, ref2]); + const manifest = await testInitialManifest(master, media, [ref1]); + await testUpdate(manifest, mediaWithAdditionalSegment, [ref1, ref2]); }); it('evicts removed segments', async () => { @@ -692,9 +663,9 @@ describe('HlsParser live', () => { const ref2 = makeReference( 'test:/main2.mp4', 2, 4, /* syncTime= */ null); - await testUpdate( - master, mediaWithAdditionalSegment, [ref1, ref2], - mediaWithRemovedSegment, [ref2]); + const manifest = await testInitialManifest( + master, mediaWithAdditionalSegment, [ref1, ref2]); + await testUpdate(manifest, mediaWithRemovedSegment, [ref2]); }); it('handles updates with redirects', async () => { @@ -719,58 +690,32 @@ describe('HlsParser live', () => { } }); + const manifest = await testInitialManifest(master, media, [oldRef1]); await testUpdate( - master, media, [oldRef1], mediaWithAdditionalSegment, - [newRef1, newRef2]); + manifest, mediaWithAdditionalSegment, [newRef1, newRef2]); }); it('parses start time from mp4 segments', async () => { - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', media) - .setResponseValue('test:/init.mp4', initSegmentData) - .setResponseValue('test:/main.mp4', segmentData); - - const expectedRef = makeReference( + const ref = makeReference( 'test:/main.mp4', 0, 2, /* syncTime= */ null); // In live content, we do not set timestampOffset. - expectedRef.timestampOffset = 0; + ref.timestampOffset = 0; - const manifest = await parser.start('test:/master', playerInterface); - const video = manifest.variants[0].video; - await video.createSegmentIndex(); - ManifestParser.verifySegmentIndex(video, [expectedRef]); + await testInitialManifest(master, media, [ref]); }); it('gets start time on update without segment request', async () => { - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', mediaWithAdditionalSegment) - .setResponseValue('test:/init.mp4', initSegmentData) - .setResponseValue('test:/main.mp4', segmentData); - const ref1 = makeReference( 'test:/main.mp4', 0, 2, /* syncTime= */ null); const ref2 = makeReference( 'test:/main2.mp4', 2, 4, /* syncTime= */ null); - const manifest = await parser.start('test:/master', playerInterface); - const video = manifest.variants[0].video; - await video.createSegmentIndex(); - ManifestParser.verifySegmentIndex(video, [ref1, ref2]); - - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', mediaWithRemovedSegment) - .setResponseValue('test:/init.mp4', initSegmentData) - .setResponseValue('test:/main.mp4', segmentData) - .setResponseValue('test:/main2.mp4', segmentData); + const manifest = await testInitialManifest( + master, mediaWithAdditionalSegment, [ref1, ref2]); fakeNetEngine.request.calls.reset(); - await delayForUpdatePeriod(); - - ManifestParser.verifySegmentIndex(video, [ref2]); + await testUpdate(manifest, mediaWithRemovedSegment, [ref2]); // Only one request was made, and it was for the playlist. // No segment requests were needed to get the start time. @@ -782,37 +727,17 @@ describe('HlsParser live', () => { it('reuses cached timestamp offset for segments with discontinuity', async () => { - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', mediaWithDiscontinuity) - .setResponseValue('test:/init.mp4', initSegmentData) - .setResponseValue('test:/main.mp4', segmentData) - .setResponseValue('test:/main2.mp4', segmentData); - const ref1 = makeReference( 'test:/main.mp4', 0, 2, /* syncTime= */ null); - const ref2 = makeReference( 'test:/main2.mp4', 2, 4, /* syncTime= */ null); - const manifest = - await parser.start('test:/master', playerInterface); - - const video = manifest.variants[0].video; - await video.createSegmentIndex(); - ManifestParser.verifySegmentIndex(video, [ref1, ref2]); - - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', - mediaWithUpdatedDiscontinuitySegment) - .setResponseValue('test:/init.mp4', initSegmentData) - .setResponseValue('test:/main2.mp4', segmentData); + const manifest = await testInitialManifest( + master, mediaWithDiscontinuity, [ref1, ref2]); fakeNetEngine.request.calls.reset(); - await delayForUpdatePeriod(); - - ManifestParser.verifySegmentIndex(video, [ref2]); + await testUpdate( + manifest, mediaWithUpdatedDiscontinuitySegment, [ref2]); // Only one request should be made, and it's for the playlist. // Expect to use the cached timestamp offset for the main2.mp4 @@ -853,19 +778,12 @@ describe('HlsParser live', () => { 'main3.mp4\n', ].join(''); - fakeNetEngine - .setResponseText('test:/master', master) - .setResponseText('test:/video', mediaWithDeltaUpdates) - .setResponseText('test:/video?_HLS_skip=YES', - mediaWithSkippedSegments) - .setResponseValue('test:/init.mp4', initSegmentData) - .setResponseValue('test:/main.mp4', segmentData) - .setResponseValue('test:/main2.mp4', segmentData) - .setResponseValue('test:/main3.mp4', segmentData); + fakeNetEngine.setResponseText( + 'test:/video?_HLS_skip=YES', mediaWithSkippedSegments); playerInterface.isLowLatencyMode = () => true; - await parser.start('test:/master', playerInterface); - // Replace the entries with the updated values. + + await testInitialManifest(master, mediaWithDeltaUpdates); fakeNetEngine.request.calls.reset(); await delayForUpdatePeriod(); @@ -875,7 +793,6 @@ describe('HlsParser live', () => { shaka.net.NetworkingEngine.RequestType.MANIFEST); }); - it('skips older segments', async () => { const mediaWithSkippedSegments = [ '#EXTM3U\n', @@ -896,12 +813,15 @@ describe('HlsParser live', () => { 'test:/main2.mp4', 2, 4, /* syncTime= */ null); const ref3 = makeReference( 'test:/main3.mp4', 4, 6, /* syncTime= */ null); + + const manifest = await testInitialManifest( + master, mediaWithAdditionalSegment, [ref1, ref2]); + // With 'SKIPPED-SEGMENTS', ref1 is skipped from the playlist, // and ref1 should be in the SegmentReferences list. // ref3 should be appended to the SegmentReferences list. await testUpdate( - master, mediaWithAdditionalSegment, [ref1, ref2], - mediaWithSkippedSegments, [ref1, ref2, ref3]); + manifest, mediaWithSkippedSegments, [ref1, ref2, ref3]); }); it('skips older segments with discontinuity', async () => { @@ -959,12 +879,14 @@ describe('HlsParser live', () => { /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null, /* timestampOffset= */ 0); + const manifest = await testInitialManifest( + master, mediaWithDiscontinuity2, [ref1, ref2, ref3]); + // With 'SKIPPED-SEGMENTS', ref1, ref2 are skipped from the playlist, // and ref1,ref2 should be in the SegmentReferences list. // ref3,ref4 should be appended to the SegmentReferences list. await testUpdate( - master, mediaWithDiscontinuity2, [ref1, ref2, ref3], - mediaWithSkippedSegments2, [ref1, ref2, ref3, ref4]); + manifest, mediaWithSkippedSegments2, [ref1, ref2, ref3, ref4]); }); }); // describe('update') }); // describe('playlist type LIVE') diff --git a/test/hls/hls_parser_unit.js b/test/hls/hls_parser_unit.js index 47de05391f..285f967f9c 100644 --- a/test/hls/hls_parser_unit.js +++ b/test/hls/hls_parser_unit.js @@ -98,6 +98,7 @@ describe('HlsParser', () => { isLowLatencyMode: () => false, isAutoLowLatencyMode: () => false, enableLowLatencyMode: () => {}, + updateDuration: () => {}, }; parser = new shaka.hls.HlsParser(); diff --git a/test/media/media_source_engine_unit.js b/test/media/media_source_engine_unit.js index a7a37f02c2..2ef9e190db 100644 --- a/test/media/media_source_engine_unit.js +++ b/test/media/media_source_engine_unit.js @@ -1003,6 +1003,43 @@ describe('MediaSourceEngine', () => { expect(audioSourceBuffer.appendBuffer).toHaveBeenCalledWith(buffer); audioSourceBuffer.updateend(); }); + + it('allows duration to be shrunk', async () => { + // Pretend the initial duration was 100. + mockMediaSource.durationGetter.and.returnValue(100); + + // When duration is shrunk, 'updateend' events are generated. This is + // because reducing the duration triggers the MSE removal algorithm to + // run. + mockMediaSource.durationSetter.and.callFake((duration) => { + expect(duration).toBe(50); + videoSourceBuffer.updateend(); + audioSourceBuffer.updateend(); + }); + + audioSourceBuffer.appendBuffer.and.callFake(() => { + audioSourceBuffer.updateend(); + }); + videoSourceBuffer.appendBuffer.and.callFake(() => { + videoSourceBuffer.updateend(); + }); + + /** @type {!Promise} */ + const p1 = mediaSourceEngine.setDuration(50); + expect(mockMediaSource.durationSetter).not.toHaveBeenCalled(); + + // These operations should be blocked until after duration is shrunk. + // This is tested because shrinking duration generates 'updateend' + // events, and we want to show that the queue still operates correctly. + const a1 = mediaSourceEngine.appendBuffer(ContentType.AUDIO, buffer, null, + /* hasClosedCaptions= */ false); + const a2 = mediaSourceEngine.appendBuffer(ContentType.VIDEO, buffer, null, + /* hasClosedCaptions= */ false); + + await p1; + await a1; + await a2; + }); }); describe('destroy', () => { diff --git a/test/test/util/dash_parser_util.js b/test/test/util/dash_parser_util.js index 32f098c05e..6626ad0bde 100644 --- a/test/test/util/dash_parser_util.js +++ b/test/test/util/dash_parser_util.js @@ -43,6 +43,7 @@ shaka.test.Dash = class { isLowLatencyMode: () => false, isAutoLowLatencyMode: () => false, enableLowLatencyMode: () => {}, + updateDuration: () => {}, }; const manifest = await dashParser.start('dummy://foo', playerInterface); const stream = manifest.variants[0].video; @@ -77,6 +78,7 @@ shaka.test.Dash = class { isLowLatencyMode: () => false, isAutoLowLatencyMode: () => false, enableLowLatencyMode: () => {}, + updateDuration: () => {}, }; const p = dashParser.start('dummy://foo', playerInterface); await expectAsync(p).toBeRejectedWith(