From 5167717177edc256d06afef469b0a206ed0b88e4 Mon Sep 17 00:00:00 2001 From: Joey Parrish Date: Wed, 11 May 2022 11:11:31 -0700 Subject: [PATCH] fix(hls): Fix X-PRELOAD-HINT failure with LL mode off (#4212) When LL mode was off, the HLS parser would throw on X-PRELOAD-HINT segments. The logic for requiring EXTINF has been corrected so that this will not happen. Closes #4185 --- lib/hls/hls_parser.js | 34 +++++++++++++++++++++++----------- test/hls/hls_live_unit.js | 26 ++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 9caf029212..21daac7da2 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -1739,8 +1739,8 @@ shaka.hls.HlsParser = class { * @param {number} timestampOffset * @param {!Map.} variables * @param {string} absoluteMediaPlaylistUri - * @return {!shaka.media.SegmentReference} * @param {string} type + * @return {shaka.media.SegmentReference} * @private */ createSegmentReference_( @@ -1756,9 +1756,23 @@ shaka.hls.HlsParser = class { let startByte = 0; let endByte = null; + if (!extinfTag) { + if (hlsSegment.partialSegments.length == 0) { + // EXTINF tag must be available if the segment has no partial segments. + throw new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.MANIFEST, + shaka.util.Error.Code.HLS_REQUIRED_TAG_MISSING, 'EXTINF'); + } else if (!this.lowLatencyMode_) { + // Without EXTINF and without low-latency mode, partial segments get + // ignored. + return null; + } + } + // Create SegmentReferences for the partial segments. const partialSegmentRefs = []; - if (this.lowLatencyMode_ && hlsSegment.partialSegments.length) { + if (this.lowLatencyMode_) { for (let i = 0; i < hlsSegment.partialSegments.length; i++) { const item = hlsSegment.partialSegments[i]; const pPreviousReference = i == 0 ? @@ -1799,14 +1813,6 @@ shaka.hls.HlsParser = class { /* appendWindowEnd= */ Infinity); partialSegmentRefs.push(partial); } // for-loop of hlsSegment.partialSegments - } else { - // EXTINF tag must be available if the segment has no partial segments. - if (!extinfTag) { - throw new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.MANIFEST, - shaka.util.Error.Code.HLS_REQUIRED_TAG_MISSING, 'EXTINF'); - } } // If the segment has EXTINF tag, set the segment's end time, start byte @@ -1825,6 +1831,7 @@ shaka.hls.HlsParser = class { } else { endTime = partialSegmentRefs[partialSegmentRefs.length - 1].endTime; } + // If the segment has EXT-X-BYTERANGE tag, set the start byte and end byte // base on the byterange information. If segment has no EXT-X-BYTERANGE tag // and has partial segments, set the start byte and end byte base on the @@ -2054,7 +2061,9 @@ shaka.hls.HlsParser = class { playlist.absoluteUri, type); - references.push(reference); + if (reference) { + references.push(reference); + } } else if (!this.lowLatencyMode_) { // If a segment has no extinfTag, it must contain partial segments. shaka.log.alwaysWarn('Low-latency HLS live stream detected, but ' + @@ -2233,6 +2242,9 @@ shaka.hls.HlsParser = class { variables, /* absoluteMediaPlaylistUri= */ '', type); + goog.asserts.assert( + segmentRef != null, 'Segment reference should not be null!'); + // If we are updating the manifest, we can usually skip fetching the segment // by examining the references we already have. This won't be possible if // there was some kind of lag or delay updating the manifest on the server, diff --git a/test/hls/hls_live_unit.js b/test/hls/hls_live_unit.js index 571cab9b5e..582632d55a 100644 --- a/test/hls/hls_live_unit.js +++ b/test/hls/hls_live_unit.js @@ -699,6 +699,32 @@ describe('HlsParser live', () => { ManifestParser.verifySegmentIndex(video, [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-MAP:URI="init.mp4",BYTERANGE="616@0"\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(''); + + fakeNetEngine + .setResponseText('test:/master', master) + .setResponseText('test:/video', mediaWithPartialSegments) + .setResponseValue('test:/init.mp4', initSegmentData) + .setResponseValue('test:/main.mp4', segmentData) + .setResponseValue('test:/partial.mp4', segmentData); + + // If this throws, the test fails. Otherwise, it passes. + await parser.start('test:/master', playerInterface); + }); + describe('update', () => { it('adds new segments when they appear', async () => { const ref1 = ManifestParser.makeReference('test:/main.mp4', 2, 4);