diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 3c03b6cedc..9e0df8da0b 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -1893,9 +1893,20 @@ shaka.hls.HlsParser = class { const pPreviousReference = i == 0 ? previousReference : partialSegmentRefs[partialSegmentRefs.length - 1]; const pStartTime = (i == 0) ? startTime : pPreviousReference.endTime; - const pDuration = Number(item.getAttributeValue('DURATION')); - // A preload hinted partial segment doesn't have duration information, - // so its startTime and endTime are the same. + + // If DURATION is missing from this partial segment, use the target + // partial duration from the top of the playlist, which is a required + // attribute for content with partial segments. + const pDuration = Number(item.getAttributeValue('DURATION')) || + this.partialTargetDuration_; + + // If for some reason we have neither an explicit duration, nor a target + // partial duration, we should SKIP this partial segment to avoid + // duplicating content in the presentation timeline. + if (!pDuration) { + continue; + } + const pEndTime = pStartTime + pDuration; let pStartByte = 0; diff --git a/test/hls/hls_live_unit.js b/test/hls/hls_live_unit.js index e5994a1d0f..655a0317c8 100644 --- a/test/hls/hls_live_unit.js +++ b/test/hls/hls_live_unit.js @@ -539,11 +539,13 @@ describe('HlsParser live', () => { ManifestParser.verifySegmentIndex(video, [ref1, 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",BYTERANGE="616@0"\n', '#EXT-X-MEDIA-SEQUENCE:0\n', // ref includes partialRef, partialRef2 @@ -576,26 +578,73 @@ describe('HlsParser live', () => { 'test:/partial2.mp4', 2, 4, /* syncTime= */ null, /* baseUri= */ '', /* startByte= */ 200, /* endByte= */ 429); + const ref = makeReference( + 'test:/main.mp4', 0, 4, /* syncTime= */ null, + /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ 429, + /* timestampOffset= */ 0, [partialRef, partialRef2]); + const partialRef3 = makeReference( 'test:/partial.mp4', 4, 6, /* syncTime= */ null, /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ 209); - // A preload hinted partial segment doesn't have duration information, - // so its startTime and endTime are the same. const preloadRef = makeReference( - 'test:/partial.mp4', 6, 6, /* syncTime= */ null, + 'test:/partial.mp4', 6, 7.5, /* syncTime= */ null, /* baseUri= */ '', /* startByte= */ 210, /* endByte= */ null); + // 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]); + + const manifest = await parser.start('test:/master', playerInterface); + const video = manifest.variants[0].video; + await video.createSegmentIndex(); + ManifestParser.verifySegmentIndex(video, [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",BYTERANGE=210@0\n', + // preloadRef + '#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= */ 429, - /* timestampOffset= */ 0, [partialRef, partialRef2]); + /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null, + /* timestampOffset= */ 0, []); + + const partialRef = makeReference( + 'test:/partial.mp4', 4, 6, /* syncTime= */ null, + /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ 209); // 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, [partialRef3, preloadRef]); + /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ 209, + /* timestampOffset= */ 0, [partialRef]); const manifest = await parser.start('test:/master', playerInterface); const video = manifest.variants[0].video; @@ -611,6 +660,7 @@ describe('HlsParser live', () => { 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',