UNPKG

shaka-player

Version:
1,322 lines (1,123 loc) 48.7 kB
/*! @license * Shaka Player * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ describe('HlsParser live', () => { const ManifestParser = shaka.test.ManifestParser; const master = [ '#EXTM3U\n', '#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",', 'RESOLUTION=960x540,FRAME-RATE=60\n', 'video\n', ].join(''); /** @type {!shaka.test.FakeNetworkingEngine} */ let fakeNetEngine; /** @type {!shaka.hls.HlsParser} */ let parser; /** @type {shaka.extern.ManifestParser.PlayerInterface} */ let playerInterface; /** @type {shaka.extern.ManifestConfiguration} */ let config; /** @type {!Uint8Array} */ let initSegmentData; /** @type {!Uint8Array} */ let segmentData; /** @type {!Uint8Array} */ let selfInitializingSegmentData; beforeEach(() => { // TODO: use StreamGenerator? initSegmentData = new Uint8Array([ 0x00, 0x00, 0x00, 0x30, // size (48) 0x6D, 0x6F, 0x6F, 0x76, // type (moov) 0x00, 0x00, 0x00, 0x28, // trak size (40) 0x74, 0x72, 0x61, 0x6B, // type (trak) 0x00, 0x00, 0x00, 0x20, // mdia size (32) 0x6D, 0x64, 0x69, 0x61, // type (mdia) 0x00, 0x00, 0x00, 0x18, // mdhd size (24) 0x6D, 0x64, 0x68, 0x64, // type (mdhd) 0x00, 0x00, 0x00, 0x00, // version and flags 0x00, 0x00, 0x00, 0x00, // creation time (0) 0x00, 0x00, 0x00, 0x00, // modification time (0) 0x00, 0x00, 0x03, 0xe8, // timescale (1000) ]); segmentData = new Uint8Array([ 0x00, 0x00, 0x00, 0x24, // size (36) 0x6D, 0x6F, 0x6F, 0x66, // type (moof) 0x00, 0x00, 0x00, 0x1C, // traf size (28) 0x74, 0x72, 0x61, 0x66, // type (traf) 0x00, 0x00, 0x00, 0x14, // tfdt size (20) 0x74, 0x66, 0x64, 0x74, // type (tfdt) 0x01, 0x00, 0x00, 0x00, // version and flags 0x00, 0x00, 0x00, 0x00, // baseMediaDecodeTime first 4 bytes 0x00, 0x00, 0x07, 0xd0, // baseMediaDecodeTime last 4 bytes (2000) ]); selfInitializingSegmentData = shaka.util.Uint8ArrayUtils.concat(initSegmentData, segmentData); fakeNetEngine = new shaka.test.FakeNetworkingEngine(); config = shaka.util.PlayerConfiguration.createDefault().manifest; playerInterface = { filter: () => Promise.resolve(), makeTextStreamsForClosedCaptions: (manifest) => {}, networkingEngine: fakeNetEngine, onError: fail, onEvent: fail, onTimelineRegionAdded: fail, isLowLatencyMode: () => false, isAutoLowLatencyMode: () => false, enableLowLatencyMode: () => {}, updateDuration: () => {}, newDrmInfo: (stream) => {}, onManifestUpdated: () => {}, getBandwidthEstimate: () => 1e6, }; parser = new shaka.hls.HlsParser(); parser.configure(config); }); afterEach(() => { // HLS parser stop is synchronous. parser.stop(); }); /** * Gets a spy on the function that sets the update period. * @return {!jasmine.Spy} * @suppress {accessControls} */ function updateTickSpy() { return spyOn(parser.updatePlaylistTimer_, 'tickAfter'); } /** * Trigger a manifest update. * @suppress {accessControls} */ async function delayForUpdatePeriod() { parser.updatePlaylistTimer_.tickNow(); await shaka.test.Util.shortDelay(); // Allow update to finish. } /** * @param {string} master * @param {string} media1 * @param {string} media2 */ function configureNetEngineForInitialManifest(master, media1, media2) { fakeNetEngine .setResponseText('test:/master', master) .setResponseText('test:/video', media1) .setResponseText('test:/redirected/video', media1) .setResponseText('test:/video2', media2) .setResponseText('test:/audio', media1) .setResponseValue('test:/init.mp4', initSegmentData) .setResponseValue('test:/main.mp4', segmentData) .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); } /** * @param {string} master * @param {string} initialMedia * @param {Array=} initialReferences * @return {!Promise.<shaka.extern.Manifest>} */ async function testInitialManifest( master, initialMedia, initialReferences=null) { configureNetEngineForInitialManifest(master, initialMedia, initialMedia); const manifest = await parser.start('test:/master', playerInterface); // Create the segment index for the variants, to finish the lazy-loading. await Promise.all(manifest.variants.map(async (variant) => { await variant.video.createSegmentIndex(); if (initialReferences) { ManifestParser.verifySegmentIndex(variant.video, initialReferences); } if (variant.audio) { await variant.audio.createSegmentIndex(); if (initialReferences) { 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); await delayForUpdatePeriod(); if (updatedReferences) { for (const variant of manifest.variants) { ManifestParser.verifySegmentIndex(variant.video, updatedReferences); if (variant.audio) { ManifestParser.verifySegmentIndex(variant.audio, updatedReferences); } } } } describe('playlist type EVENT', () => { const media = [ '#EXTM3U\n', '#EXT-X-PLAYLIST-TYPE:EVENT\n', '#EXT-X-TARGETDURATION:5\n', '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n', '#EXTINF:2,\n', 'main.mp4\n', ].join(''); const mediaWithAdditionalSegment = [ '#EXTM3U\n', '#EXT-X-PLAYLIST-TYPE:EVENT\n', '#EXT-X-TARGETDURATION:5\n', '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n', '#EXTINF:2,\n', 'main.mp4\n', '#EXTINF:2,\n', 'main2.mp4\n', ].join(''); it('treats already ended presentation like VOD', async () => { const manifest = await testInitialManifest( master, media + '#EXT-X-ENDLIST'); expect(manifest.presentationTimeline.isLive()).toBe(false); expect(manifest.presentationTimeline.isInProgress()).toBe(false); }); describe('update', () => { it('adds new segments when they appear', async () => { const ref1 = makeReference( 'test:/main.mp4', 0, 2, /* syncTime= */ null); const ref2 = makeReference( 'test:/main2.mp4', 2, 4, /* syncTime= */ null); const manifest = await testInitialManifest(master, media, [ref1]); await testUpdate(manifest, mediaWithAdditionalSegment, [ref1, ref2]); }); it('updates all variants', async () => { const secondVariant = [ '#EXT-X-STREAM-INF:BANDWIDTH=300,CODECS="avc1",', 'RESOLUTION=1200x940,FRAME-RATE=60\n', 'video2', ].join(''); const masterWithTwoVariants = master + secondVariant; const ref1 = makeReference( 'test:/main.mp4', 0, 2, /* syncTime= */ null); const ref2 = makeReference( 'test:/main2.mp4', 2, 4, /* syncTime= */ null); const manifest = await testInitialManifest( masterWithTwoVariants, media, [ref1]); await testUpdate(manifest, mediaWithAdditionalSegment, [ref1, ref2]); }); it('updates all streams', async () => { const masterlist = [ '#EXTM3U\n', '#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",AUDIO="aud1",', 'RESOLUTION=960x540,FRAME-RATE=60\n', 'video\n', ].join(''); const audio = [ '#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="eng",', 'URI="audio"\n', ].join(''); const masterWithAudio = masterlist + audio; const ref1 = makeReference( 'test:/main.mp4', 0, 2, /* syncTime= */ null); const ref2 = makeReference( 'test:/main2.mp4', 2, 4, /* syncTime= */ null); const manifest = await testInitialManifest( masterWithAudio, media, [ref1]); await testUpdate(manifest, mediaWithAdditionalSegment, [ref1, ref2]); }); it('handles multiple updates', async () => { const newSegment1 = [ '#EXTINF:2,\n', 'main2.mp4\n', ].join(''); const newSegment2 = [ '#EXTINF:2,\n', 'main3.mp4\n', ].join(''); const updatedMedia1 = media + newSegment1; const updatedMedia2 = updatedMedia1 + newSegment2; const ref1 = makeReference( 'test:/main.mp4', 0, 2, /* syncTime= */ null); const ref2 = makeReference( 'test:/main2.mp4', 2, 4, /* syncTime= */ null); const ref3 = makeReference( 'test:/main3.mp4', 4, 6, /* syncTime= */ null); 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 () => { const manifest = await testInitialManifest(master, media); expect(manifest.presentationTimeline.isLive()).toBe(true); await testUpdate( manifest, mediaWithAdditionalSegment + '#EXT-X-ENDLIST\n'); expect(manifest.presentationTimeline.isLive()).toBe(false); }); it('starts presentation as VOD when ENDLIST is present', async () => { const manifest = await testInitialManifest( master, media + '#EXT-X-ENDLIST'); expect(manifest.presentationTimeline.isLive()).toBe(false); }); it('does not throw when interrupted by stop', async () => { const manifest = await testInitialManifest(master, media); expect(manifest.presentationTimeline.isLive()).toBe(true); // Block the next request so that update() is still happening when we // call stop(). /** @type {!shaka.util.PublicPromise} */ const delay = fakeNetEngine.delayNextRequest(); // Trigger an update. await delayForUpdatePeriod(); // Stop the parser mid-update, but don't wait for stop to complete. const stopPromise = parser.stop(); // Unblock the request. delay.resolve(); // Allow update to finish. await shaka.test.Util.shortDelay(); // 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('fatal error on manifest update request failure when ' + 'raiseFatalErrorOnManifestUpdateRequestFailure is true', async () => { const manifestConfig = shaka.util.PlayerConfiguration.createDefault().manifest; manifestConfig.raiseFatalErrorOnManifestUpdateRequestFailure = true; parser.configure(manifestConfig); const updateTick = updateTickSpy(); await testInitialManifest(master, media); expect(updateTick).toHaveBeenCalledTimes(1); /** @type {!jasmine.Spy} */ const onError = jasmine.createSpy('onError'); playerInterface.onError = shaka.test.Util.spyFunc(onError); const error = new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.NETWORK, shaka.util.Error.Code.BAD_HTTP_STATUS); const operation = shaka.util.AbortableOperation.failed(error); fakeNetEngine.request.and.returnValue(operation); await delayForUpdatePeriod(); expect(onError).toHaveBeenCalledWith(error); expect(updateTick).toHaveBeenCalledTimes(1); }); 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". const type = shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_PLAYLIST; fakeNetEngine.expectRequest( 'test:/video', shaka.net.NetworkingEngine.RequestType.MANIFEST, {type}); 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') describe('playlist type LIVE', () => { const media = [ '#EXTM3U\n', '#EXT-X-TARGETDURATION:5\n', '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n', '#EXT-X-MEDIA-SEQUENCE:0\n', '#EXTINF:2,\n', 'main.mp4\n', ].join(''); const mediaWithoutSequenceNumber = [ '#EXTM3U\n', '#EXT-X-TARGETDURATION:5\n', '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n', '#EXTINF:2,\n', 'main.mp4\n', ].join(''); const mediaWithAdditionalSegment = [ '#EXTM3U\n', '#EXT-X-TARGETDURATION:5\n', '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n', '#EXT-X-MEDIA-SEQUENCE:0\n', '#EXTINF:2,\n', 'main.mp4\n', '#EXTINF:2,\n', 'main2.mp4\n', ].join(''); const mediaWithRemovedSegment = [ '#EXTM3U\n', '#EXT-X-TARGETDURATION:5\n', '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n', '#EXT-X-MEDIA-SEQUENCE:1\n', '#EXTINF:2,\n', 'main2.mp4\n', ].join(''); const mediaWithAdditionalSegment2 = [ '#EXTM3U\n', '#EXT-X-TARGETDURATION:5\n', '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n', '#EXT-X-MEDIA-SEQUENCE:0\n', '#EXTINF:2,\n', 'main3.mp4\n', '#EXTINF:2,\n', 'main4.mp4\n', ].join(''); const mediaWithRemovedSegment2 = [ '#EXTM3U\n', '#EXT-X-TARGETDURATION:5\n', '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n', '#EXT-X-MEDIA-SEQUENCE:1\n', '#EXTINF:2,\n', 'main4.mp4\n', ].join(''); let mediaWithManySegments = [ '#EXTM3U\n', '#EXT-X-TARGETDURATION:5\n', '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n', '#EXT-X-MEDIA-SEQUENCE:0\n', ].join(''); for (let i = 0; i < 1000; i++) { mediaWithManySegments += '#EXTINF:2,\n'; mediaWithManySegments += 'main.mp4\n'; } const mediaWithDiscontinuity = [ '#EXTM3U\n', '#EXT-X-TARGETDURATION:5\n', '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n', '#EXT-X-MEDIA-SEQUENCE:0\n', '#EXT-X-DISCONTINUITY-SEQUENCE:30\n', '#EXTINF:2,\n', 'main.mp4\n', '#EXT-X-DISCONTINUITY\n', '#EXTINF:2,\n', 'main2.mp4\n', ].join(''); const mediaWithUpdatedDiscontinuitySegment = [ '#EXTM3U\n', '#EXT-X-TARGETDURATION:5\n', '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n', '#EXT-X-MEDIA-SEQUENCE:1\n', '#EXT-X-DISCONTINUITY-SEQUENCE:31\n', '#EXTINF:2,\n', 'main2.mp4\n', ].join(''); it('starts presentation as VOD when ENDLIST is present', async () => { 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 () => { await testInitialManifest(master, mediaWithoutSequenceNumber); }); it('sets presentation delay as configured', async () => { config.defaultPresentationDelay = 10; parser.configure(config); 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 () => { const media = [ '#EXTM3U\n', '#EXT-X-TARGETDURATION:5\n', '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n', '#EXT-X-MEDIA-SEQUENCE:0\n', '#EXTINF:2,\n', 'main.mp4\n', '#EXTINF:2,\n', 'main.mp4\n', '#EXTINF:2,\n', 'main.mp4\n', '#EXTINF:2,\n', 'main.mp4\n', '#EXTINF:2,\n', 'main.mp4\n', '#EXTINF:2,\n', 'main.mp4\n', ].join(''); const manifest = await testInitialManifest(master, media); expect(manifest.presentationTimeline.getDelay()).toBe(15); }); it('sets 1 times target duration as presentation delay if there are not enough segments', async () => { // eslint-disable-line max-len const media = [ '#EXTM3U\n', '#EXT-X-TARGETDURATION:5\n', '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n', '#EXT-X-MEDIA-SEQUENCE:0\n', '#EXTINF:2,\n', 'main.mp4\n', '#EXTINF:2,\n', 'main.mp4\n', ].join(''); const manifest = await testInitialManifest(master, media); expect(manifest.presentationTimeline.getDelay()).toBe(5); }); it('sets presentation delay if defined', async () => { const media = [ '#EXTM3U\n', '#EXT-X-SERVER-CONTROL:HOLD-BACK=2\n', '#EXT-X-TARGETDURATION:5\n', '#EXT-X-PART-INF:PART-TARGET=0.5\n', '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n', '#EXT-X-MEDIA-SEQUENCE:0\n', '#EXTINF:2,\n', 'main.mp4\n', ].join(''); const manifest = await testInitialManifest(master, media); // Presentation delay should be the value of 'HOLD-BACK' if not // configured. expect(manifest.presentationTimeline.getDelay()).toBe(2); }); it('sets presentation delay for low latency mode', async () => { const mediaWithLowLatency = [ '#EXTM3U\n', '#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=1.8\n', '#EXT-X-TARGETDURATION:5\n', '#EXT-X-PART-INF:PART-TARGET=0.5\n', '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n', '#EXT-X-MEDIA-SEQUENCE:0\n', '#EXTINF:2,\n', 'main.mp4\n', ].join(''); playerInterface.isLowLatencyMode = () => true; 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); }); describe('availabilityWindowOverride', () => { async function testWindowOverride(expectedWindow) { const manifest = await testInitialManifest( master, mediaWithManySegments); expect(manifest).toBeTruthy(); const timeline = manifest.presentationTimeline; expect(timeline).toBeTruthy(); const start = timeline.getSegmentAvailabilityStart(); const end = timeline.getSegmentAvailabilityEnd(); expect(end - start).toBeCloseTo(expectedWindow, 1); } it('does not affect seek range if unset', async () => { // 15 seconds is three segment durations. await testWindowOverride(15); }); it('overrides default seek range if set', async () => { config.availabilityWindowOverride = 240; parser.configure(config); await testWindowOverride(240); }); }); it('sets discontinuity sequence numbers', async () => { const ref1 = makeReference( 'test:/main.mp4', 0, 2, /* syncTime= */ null, /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null, /* timestampOffset= */ 0); ref1.discontinuitySequence = 30; const ref2 = makeReference( 'test:/main2.mp4', 2, 4, /* syncTime= */ null, /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null, /* timestampOffset= */ 0); ref2.discontinuitySequence = 31; const manifest = await testInitialManifest( master, mediaWithDiscontinuity, [ref1, ref2]); await testUpdate( manifest, mediaWithUpdatedDiscontinuitySegment, [ref2]); }); // Test for https://github.com/shaka-project/shaka-player/issues/4223 it('parses streams with partial and preload hinted segments', async () => { playerInterface.isLowLatencyMode = () => true; const mediaWithPartialSegments = [ '#EXTM3U\n', '#EXT-X-TARGETDURATION:5\n', '#EXT-X-PART-INF:PART-TARGET=1.5\n', '#EXT-X-MAP:URI="init.mp4"\n', '#EXT-X-MEDIA-SEQUENCE:0\n', // ref includes partialRef, partialRef2 // partialRef '#EXT-X-PART:DURATION=2,URI="partial.mp4",INDEPENDENT=YES\n', // partialRef2 '#EXT-X-PART:DURATION=2,URI="partial2.mp4",INDEPENDENT=YES\n', '#EXTINF:4,\n', 'main.mp4\n', // ref2 includes partialRef3, preloadRef // partialRef3 '#EXT-X-PART:DURATION=2,URI="partial.mp4",INDEPENDENT=YES\n', // preloadRef '#EXT-X-PRELOAD-HINT:TYPE=PART,URI="partial.mp4"\n', ].join(''); const partialRef = makeReference( 'test:/partial.mp4', 0, 2, /* syncTime= */ null, /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null); const partialRef2 = makeReference( 'test:/partial2.mp4', 2, 4, /* syncTime= */ null, /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null); const ref = makeReference( 'test:/main.mp4', 0, 4, /* syncTime= */ null, /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null, /* timestampOffset= */ 0, [partialRef, partialRef2]); const partialRef3 = makeReference( 'test:/partial.mp4', 4, 6, /* syncTime= */ null, /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null); const preloadRef = makeReference( 'test:/partial.mp4', 6, 7.5, /* syncTime= */ null, /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null); preloadRef.markAsPreload(); preloadRef.markAsNonIndependent(); // ref2 is not fully published yet, so it doesn't have a segment uri. const ref2 = makeReference( '', 4, 7.5, /* syncTime= */ null, /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null, /* timestampOffset= */ 0, [partialRef3, preloadRef]); await testInitialManifest(master, mediaWithPartialSegments, [ref, ref2]); }); it('parses streams with partial and preload hinted segments and BYTERANGE', async () => { // eslint-disable-line max-len playerInterface.isLowLatencyMode = () => true; const mediaWithPartialSegments = [ '#EXTM3U\n', '#EXT-X-TARGETDURATION:5\n', '#EXT-X-PART-INF:PART-TARGET=1.5\n', '#EXT-X-MAP:URI="init.mp4"\n', '#EXT-X-MEDIA-SEQUENCE:0\n', // ref includes partialRef, partialRef2 // partialRef '#EXT-X-PART:DURATION=2,URI="ref1.mp4",BYTERANGE=200@0,', 'INDEPENDENT=YES\n', // partialRef2 '#EXT-X-PART:DURATION=2,URI="ref1.mp4",BYTERANGE=230@200,', 'INDEPENDENT=YES\n', '#EXTINF:4,\n', 'ref1.mp4\n', // ref2 includes partialRef3, preloadRef // partialRef3 '#EXT-X-PART:DURATION=2,URI="ref2.mp4",BYTERANGE=210@0,', 'INDEPENDENT=YES\n', // preloadRef '#EXT-X-PRELOAD-HINT:TYPE=PART,URI="ref2.mp4",BYTERANGE-START=210,', 'BYTERANGE-LENGTH=210\n', ].join(''); // If ReadableStream is defined we can apply some optimizations if (window.ReadableStream) { const ref = makeReference( 'test:/ref1.mp4', 0, 4, /* syncTime= */ null, /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null, /* timestampOffset= */ 0); ref.markAsByterangeOptimization(); // ref2 is not fully published yet, so it doesn't have a segment uri. const ref2 = makeReference( 'test:/ref2.mp4', 4, 7.5, /* syncTime= */ null, /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null, /* timestampOffset= */ 0); ref2.markAsByterangeOptimization(); ref2.markAsPreload(); await testInitialManifest(master, mediaWithPartialSegments, [ref, ref2]); } else { const partialRef = makeReference( 'test:/ref1.mp4', 0, 2, /* syncTime= */ null, /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ 199); const partialRef2 = makeReference( 'test:/ref1.mp4', 2, 4, /* syncTime= */ null, /* baseUri= */ '', /* startByte= */ 200, /* endByte= */ 429); const ref = makeReference( 'test:/ref1.mp4', 0, 4, /* syncTime= */ null, /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ 429, /* timestampOffset= */ 0, [partialRef, partialRef2]); const partialRef3 = makeReference( 'test:/ref2.mp4', 4, 6, /* syncTime= */ null, /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ 209); const preloadRef = makeReference( 'test:/ref2.mp4', 6, 7.5, /* syncTime= */ null, /* baseUri= */ '', /* startByte= */ 210, /* endByte= */ 419); preloadRef.markAsPreload(); preloadRef.markAsNonIndependent(); // ref2 is not fully published yet, so it doesn't have a segment uri. const ref2 = makeReference( '', 4, 7.5, /* syncTime= */ null, /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ 419, /* timestampOffset= */ 0, [partialRef3, preloadRef]); await testInitialManifest(master, mediaWithPartialSegments, [ref, ref2]); } }); // Test for https://github.com/shaka-project/shaka-player/issues/4223 it('ignores preload hinted segments without target duration', async () => { playerInterface.isLowLatencyMode = () => true; // Missing PART-TARGET, so preload hints are skipped. const mediaWithPartialSegments = [ '#EXTM3U\n', '#EXT-X-TARGETDURATION:5\n', '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n', '#EXT-X-MEDIA-SEQUENCE:0\n', '#EXTINF:4,\n', // ref1 'main.mp4\n', // ref2 includes partialRef, but not preloadRef // partialRef '#EXT-X-PART:DURATION=2,URI="partial.mp4",INDEPENDENT=YES\n', // preloadRef '#EXT-X-PRELOAD-HINT:TYPE=PART,URI="partial.mp4"\n', ].join(''); const ref = makeReference( 'test:/main.mp4', 0, 4, /* syncTime= */ null, /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null, /* timestampOffset= */ 0, []); const partialRef = makeReference( 'test:/partial.mp4', 4, 6, /* syncTime= */ null, /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null); // ref2 is not fully published yet, so it doesn't have a segment uri. const ref2 = makeReference( '', 4, 6, /* syncTime= */ null, /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null, /* timestampOffset= */ 0, [partialRef]); await testInitialManifest(master, mediaWithPartialSegments, [ref, ref2]); }); // Test for https://github.com/shaka-project/shaka-player/issues/4185 it('does not fail on preload hints with LL mode off', async () => { // LL mode must be off for this test! playerInterface.isLowLatencyMode = () => false; const mediaWithPartialSegments = [ '#EXTM3U\n', '#EXT-X-TARGETDURATION:5\n', '#EXT-X-PART-INF:PART-TARGET=1.5\n', '#EXTINF:4,\n', 'main.mp4\n', '#EXT-X-PART:DURATION=2,URI="partial.mp4",BYTERANGE=210@0\n', '#EXT-X-PRELOAD-HINT:TYPE=PART,URI="partial.mp4",BYTERANGE-START=210\n', ].join(''); // If this throws, the test fails. Otherwise, it passes. await testInitialManifest(master, mediaWithPartialSegments); }); describe('update', () => { it('adds new segments when they appear', async () => { const ref1 = makeReference( 'test:/main.mp4', 0, 2, /* syncTime= */ null); const ref2 = makeReference( 'test:/main2.mp4', 2, 4, /* syncTime= */ null); const manifest = await testInitialManifest(master, media, [ref1]); await testUpdate(manifest, mediaWithAdditionalSegment, [ref1, ref2]); }); it('evicts removed segments', async () => { const ref1 = makeReference( 'test:/main.mp4', 0, 2, /* syncTime= */ null); const ref2 = makeReference( 'test:/main2.mp4', 2, 4, /* syncTime= */ null); const manifest = await testInitialManifest( master, mediaWithAdditionalSegment, [ref1, ref2]); await testUpdate(manifest, mediaWithRemovedSegment, [ref2]); }); it('has correct references if switching after update', async () => { const ref1 = makeReference( 'test:/main.mp4', 0, 2, /* syncTime= */ null); const ref2 = makeReference( 'test:/main2.mp4', 2, 4, /* syncTime= */ null); const ref4 = makeReference( 'test:/main4.mp4', 2, 4, /* syncTime= */ null); const secondVariant = [ '#EXT-X-STREAM-INF:BANDWIDTH=300,CODECS="avc1",', 'RESOLUTION=1200x940,FRAME-RATE=60\n', 'video2', ].join(''); const masterWithTwoVariants = master + secondVariant; configureNetEngineForInitialManifest(masterWithTwoVariants, mediaWithAdditionalSegment, mediaWithAdditionalSegment2); const manifest = await parser.start('test:/master', playerInterface); await manifest.variants[0].video.createSegmentIndex(); ManifestParser.verifySegmentIndex( manifest.variants[0].video, [ref1, ref2]); expect(manifest.variants[1].video.segmentIndex).toBeNull(); // Update. fakeNetEngine .setResponseText('test:/video', mediaWithRemovedSegment) .setResponseText('test:/video2', mediaWithRemovedSegment2); await delayForUpdatePeriod(); // Switch. await manifest.variants[0].video.closeSegmentIndex(); await manifest.variants[1].video.createSegmentIndex(); // Check for variants to be as expected. expect(manifest.variants[0].video.segmentIndex).toBeNull(); ManifestParser.verifySegmentIndex( manifest.variants[1].video, [ref4]); }); it('handles switching during update', async () => { const ref1 = makeReference( 'test:/main.mp4', 0, 2, /* syncTime= */ null); const ref2 = makeReference( 'test:/main2.mp4', 2, 4, /* syncTime= */ null); const ref4 = makeReference( 'test:/main4.mp4', 2, 4, /* syncTime= */ null); const secondVariant = [ '#EXT-X-STREAM-INF:BANDWIDTH=300,CODECS="avc1",', 'RESOLUTION=1200x940,FRAME-RATE=60\n', 'video2', ].join(''); const masterWithTwoVariants = master + secondVariant; configureNetEngineForInitialManifest(masterWithTwoVariants, mediaWithAdditionalSegment, mediaWithAdditionalSegment2); const manifest = await parser.start('test:/master', playerInterface); await manifest.variants[0].video.createSegmentIndex(); ManifestParser.verifySegmentIndex( manifest.variants[0].video, [ref1, ref2]); expect(manifest.variants[1].video.segmentIndex).toBe(null); // Update. fakeNetEngine .setResponseText('test:/video', mediaWithRemovedSegment) .setResponseText('test:/video2', mediaWithRemovedSegment2); const updatePromise = parser.update(); // Verify that the update is not yet complete. expect(manifest.variants[0].video.segmentIndex).not.toBe(null); ManifestParser.verifySegmentIndex( manifest.variants[0].video, [ref1, ref2]); // Mid-update, switch. await manifest.variants[0].video.closeSegmentIndex(); await manifest.variants[1].video.createSegmentIndex(); // Finish the update. await updatePromise; // Check for variants to be as expected. expect(manifest.variants[0].video.segmentIndex).toBe(null); ManifestParser.verifySegmentIndex( manifest.variants[1].video, [ref4]); }); it('handles updates with redirects', async () => { const oldRef1 = makeReference( 'test:/main.mp4', 0, 2, /* syncTime= */ null); const newRef1 = makeReference( 'test:/redirected/main.mp4', 0, 2, /* syncTime= */ null); const newRef2 = makeReference( 'test:/redirected/main2.mp4', 2, 4, /* syncTime= */ null); let playlistFetchCount = 0; fakeNetEngine.setResponseFilter((type, response) => { // Simulate a redirect on the updated playlist by changing the // response URI on the second playlist fetch. if (response.uri == 'test:/video') { playlistFetchCount++; if (playlistFetchCount == 2) { response.uri = 'test:/redirected/video'; } } }); const manifest = await testInitialManifest(master, media, [oldRef1]); await testUpdate( manifest, mediaWithAdditionalSegment, [newRef1, newRef2]); }); it('parses start time from mp4 segments', async () => { const ref = makeReference( 'test:/main.mp4', 0, 2, /* syncTime= */ null); // In live content, we do not set timestampOffset. ref.timestampOffset = 0; await testInitialManifest(master, media, [ref]); }); it('gets start time on update without segment request', async () => { const ref1 = makeReference( 'test:/main.mp4', 0, 2, /* syncTime= */ null); const ref2 = makeReference( 'test:/main2.mp4', 2, 4, /* syncTime= */ null); const manifest = await testInitialManifest( master, mediaWithAdditionalSegment, [ref1, ref2]); fakeNetEngine.request.calls.reset(); 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. expect(fakeNetEngine.request).toHaveBeenCalledTimes(1); const type = shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_PLAYLIST; fakeNetEngine.expectRequest( 'test:/video', shaka.net.NetworkingEngine.RequestType.MANIFEST, {type}); }); it('request playlist delta updates to skip segments', async () => { const mediaWithDeltaUpdates = [ '#EXTM3U\n', '#EXT-X-PLAYLIST-TYPE:LIVE\n', '#EXT-X-TARGETDURATION:5\n', '#EXT-X-MEDIA-SEQUENCE:0\n', '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n', '#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=60.0,\n', '#EXTINF:2,\n', 'main0.mp4\n', '#EXTINF:2,\n', 'main1.mp4\n', ].join(''); const mediaWithSkippedSegments1 = [ '#EXTM3U\n', '#EXT-X-TARGETDURATION:5\n', '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n', '#EXT-X-MEDIA-SEQUENCE:1\n', '#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=60.0,\n', '#EXT-X-SKIP:SKIPPED-SEGMENTS=1\n', '#EXTINF:2,\n', 'main2.mp4\n', ].join(''); const mediaWithSkippedSegments2 = [ '#EXTM3U\n', '#EXT-X-TARGETDURATION:5\n', '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n', '#EXT-X-MEDIA-SEQUENCE:2\n', '#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=60.0,\n', '#EXT-X-SKIP:SKIPPED-SEGMENTS=1\n', '#EXTINF:2,\n', 'main3.mp4\n', ].join(''); fakeNetEngine.setResponseText( 'test:/video?_HLS_msn=2&_HLS_skip=YES', mediaWithSkippedSegments1); fakeNetEngine.setResponseText( 'test:/video?_HLS_msn=3&_HLS_skip=YES', mediaWithSkippedSegments2); playerInterface.isLowLatencyMode = () => true; await testInitialManifest(master, mediaWithDeltaUpdates); fakeNetEngine.request.calls.reset(); await delayForUpdatePeriod(); fakeNetEngine.expectRequest( 'test:/video?_HLS_msn=2&_HLS_skip=YES', shaka.net.NetworkingEngine.RequestType.MANIFEST, {type: shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_PLAYLIST}); await delayForUpdatePeriod(); fakeNetEngine.expectRequest( 'test:/video?_HLS_msn=3&_HLS_skip=YES', shaka.net.NetworkingEngine.RequestType.MANIFEST, {type: shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_PLAYLIST}); }); it('skips older segments', async () => { const mediaWithSkippedSegments = [ '#EXTM3U\n', '#EXT-X-TARGETDURATION:5\n', '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n', '#EXT-X-MEDIA-SEQUENCE:0\n', '#EXT-X-SKIP:SKIPPED-SEGMENTS=1\n', '#EXTINF:2,\n', 'main2.mp4\n', '#EXTINF:2,\n', 'main3.mp4\n', ].join(''); playerInterface.isLowLatencyMode = () => true; const ref1 = makeReference( 'test:/main.mp4', 0, 2, /* syncTime= */ null); const ref2 = makeReference( '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( manifest, mediaWithSkippedSegments, [ref1, ref2, ref3]); }); it('skips older segments with discontinuity', async () => { const mediaWithDiscontinuity2 = [ '#EXTM3U\n', '#EXT-X-PLAYLIST-TYPE:LIVE\n', '#EXT-X-TARGETDURATION:5\n', '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n', '#EXT-X-DISCONTINUITY-SEQUENCE:30\n', '#EXTINF:2,\n', 'main.mp4\n', '#EXT-X-DISCONTINUITY\n', '#EXTINF:2,\n', 'main2.mp4\n', '#EXTINF:2,\n', 'main3.mp4\n', ].join(''); const mediaWithSkippedSegments2 = [ '#EXTM3U\n', '#EXT-X-TARGETDURATION:5\n', '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n', '#EXT-X-MEDIA-SEQUENCE:0\n', '#EXT-X-DISCONTINUITY-SEQUENCE:30\n', '#EXT-X-SKIP:SKIPPED-SEGMENTS=2\n', '#EXTINF:2,\n', 'main3.mp4\n', '#EXTINF:2,\n', 'main4.mp4\n', ].join(''); playerInterface.isLowLatencyMode = () => true; const ref1 = makeReference( 'test:/main.mp4', 0, 2, /* syncTime= */ null, /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null, /* timestampOffset= */ 0); // Expect the timestamp offset to be set for the segment after the // EXT-X-DISCONTINUITY tag. const ref2 = makeReference( 'test:/main2.mp4', 2, 4, /* syncTime= */ null, /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null, /* timestampOffset= */ 0); // Expect the timestamp offset to be set for the segment, with the // EXT-X-DISCONTINUITY tag skipped in the playlist. const ref3 = makeReference( 'test:/main3.mp4', 4, 6, /* syncTime= */ null, /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null, /* timestampOffset= */ 0); const ref4 = makeReference( 'test:/main4.mp4', 6, 8, /* syncTime= */ null, /* 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( manifest, mediaWithSkippedSegments2, [ref1, ref2, ref3, ref4]); }); it('updates encryption keys', async () => { const initialKey = 'abc123'; const media = [ '#EXTM3U\n', '#EXT-X-TARGETDURATION:6\n', '#EXT-X-PLAYLIST-TYPE:EVENT\n', '#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,', 'KEYID=0X' + initialKey + ',', 'KEYFORMAT="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed",', 'URI="data:text/plain;base64,', 'dGhpcyBpbml0IGRhdGEgY29udGFpbnMgaGlkZGVuIHNlY3JldHMhISE', '",\n', '#EXT-X-MAP:URI="init.mp4"\n', '#EXTINF:5,\n', '#EXT-X-BYTERANGE:121090@616\n', 'main.mp4', ].join(''); const updatedKey = 'xyz345'; const updatedMedia = [ '#EXTM3U\n', '#EXT-X-TARGETDURATION:6\n', '#EXT-X-PLAYLIST-TYPE:EVENT\n', '#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,', 'KEYID=0X' + updatedKey + ',', 'KEYFORMAT="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed",', 'URI="data:text/plain;base64,', 'dGhpcyBpbml0IGRhdGEgY29udGFpbnMgaGlkZGVuIHNlY3JldHMhISE', '",\n', '#EXT-X-MAP:URI="init.mp4"\n', '#EXTINF:5,\n', '#EXT-X-BYTERANGE:121090@616\n', 'main.mp4', ].join(''); const manifest = await testInitialManifest(master, media, null); await testUpdate(manifest, updatedMedia, null); const keys = Array.from(manifest.variants[0].video.keyIds); expect(keys[0]).toBe(updatedKey); }); }); // describe('update') describe('createSegmentIndex', () => { it('handles multiple concurrent calls', async () => { const secondVariant = [ '#EXT-X-STREAM-INF:BANDWIDTH=300,CODECS="avc1",', 'RESOLUTION=1200x940,FRAME-RATE=60\n', 'video2', ].join(''); const masterWithTwoVariants = master + secondVariant; configureNetEngineForInitialManifest(masterWithTwoVariants, mediaWithAdditionalSegment, mediaWithAdditionalSegment2); const manifest = await parser.start('test:/master', playerInterface); // No segment index yet. expect(manifest.variants[0].video.segmentIndex).toBe(null); // Make two calls to create the segment index. const created1 = manifest.variants[0].video.createSegmentIndex(); const created2 = manifest.variants[0].video.createSegmentIndex(); // Still no segment index yet. expect(manifest.variants[0].video.segmentIndex).toBe(null); // Without caring what order these Promises complete in, we should be // able to see that neither resolves without a full segment index being // ready. created1.then(() => { expect(manifest.variants[0].video.segmentIndex).not.toBe(null); }); created2.then(() => { expect(manifest.variants[0].video.segmentIndex).not.toBe(null); }); // Now wait for both Promises to resolve. await Promise.all([created1, created2]); }); it('handles switching during createSegmentIndex', async () => { const secondVariant = [ '#EXT-X-STREAM-INF:BANDWIDTH=300,CODECS="avc1",', 'RESOLUTION=1200x940,FRAME-RATE=60\n', 'video2', ].join(''); const masterWithTwoVariants = master + secondVariant; configureNetEngineForInitialManifest(masterWithTwoVariants, mediaWithAdditionalSegment, mediaWithAdditionalSegment2); const manifest = await parser.start('test:/master', playerInterface); // No segment index yet. expect(manifest.variants[0].video.segmentIndex).toBe(null); // Make a call to create the segment index. const created1 = manifest.variants[0].video.createSegmentIndex(); // Still no segment index yet. expect(manifest.variants[0].video.segmentIndex).toBe(null); // Mid-create, switch. await manifest.variants[0].video.closeSegmentIndex(); const created2 = manifest.variants[1].video.createSegmentIndex(); // Finish the original creation call. await created1; // The first segment index should never have been created, because the // close call should have cancelled the work in progress. expect(manifest.variants[0].video.segmentIndex).toBe(null); // Finish the second creation call. await created2; // The second segment index is complete, because the create call was // never interrupted. expect(manifest.variants[1].video.segmentIndex).not.toBe(null); }); }); // describe('createSegmentIndex') }); // describe('playlist type LIVE') /** * @param {string} uri A relative URI to http://example.com * @param {number} start * @param {number} end * @param {?number} syncTime * @param {string=} baseUri * @param {number=} startByte * @param {?number=} endByte * @param {number=} timestampOffset * @param {!Array.<!shaka.media.SegmentReference>=} partialReferences * @param {?string=} tilesLayout * @return {!shaka.media.SegmentReference} */ function makeReference(uri, start, end, syncTime, baseUri, startByte, endByte, timestampOffset, partialReferences, tilesLayout) { return ManifestParser.makeReference(uri, start, end, baseUri, startByte, endByte, timestampOffset, partialReferences, tilesLayout, syncTime); } }); // describe('HlsParser live')