From bca62521bf7f8ce08b66bd31681d07a724e4dbfc Mon Sep 17 00:00:00 2001 From: Julian Domingo Date: Fri, 5 Apr 2024 12:23:21 -0700 Subject: [PATCH] fix(HLS): Only offset segment ref times when needed w/ EXT-X-MEDIA-SEQUENCE (#6378) Fixes #6377 When choosing to synchronize HLS streams using `EXT-X-MEDIA-SEQUENCE` instead of `EXT-X-PROGRAM-DATE-TIME` during LIVE playlist variant switches, Shaka unnecessarily drops 'old' segments and offsets the segment references of the new playlist so that the earliest reference represents media time `0`: https://github.com/shaka-project/shaka-player/blob/ea740ba2468f3b035d463ea9933aa7eeccf5c748/lib/hls/hls_parser.js#L610-L613 This is problematic, as the `StreamingEngine`'s media time used to download new segments is based off the latest segment references: https://github.com/shaka-project/shaka-player/blob/ea740ba2468f3b035d463ea9933aa7eeccf5c748/lib/media/streaming_engine.js#L1248-L1250 https://github.com/shaka-project/shaka-player/blob/ea740ba2468f3b035d463ea9933aa7eeccf5c748/lib/media/streaming_engine.js#L1385 For example: ``` Playlist download #1 EXT-X-MEDIA-SEQUENCE Media Time 0 0 1 6 2 12 3 18 Playlist download #2 (what happens now) EXT-X-MEDIA-SEQUENCE Media Time 6 0 7 6 8 12 9 18 Playlist download #2 (desired behavior) EXT-X-MEDIA-SEQUENCE Media Time 6 36 7 42 8 48 9 54 ``` Without this fix, and given the above example, if Shaka tries to request the segment at `time=36`, it will fail because the media state only has segment references up to `time=18`. Until the manifests, 'catch up', the player freezes; this can be especially problematic when a large amount of time accumulates before a variant switch occurs. This has been confirmed by Pluto TV to fix their freezing issues. --- lib/hls/hls_parser.js | 24 +++++++++--------- test/hls/hls_live_unit.js | 52 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 11 deletions(-) diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 65545861ba..146f0fa6f0 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -568,7 +568,7 @@ shaka.hls.HlsParser = class { if (goog.DEBUG) { const firstSequenceStartTime = mediaSequenceToStartTime.values().next().value; - goog.asserts.assert( + shaka.log.warning( firstSequenceStartTime == segment0.startTime, 'Sequence number map is not ordered as expected!'); } @@ -605,16 +605,18 @@ shaka.hls.HlsParser = class { // Drop any earlier references. const numSegmentsToDrop = this.minSequenceNumber_ - streamInfo.firstSequenceNumber; - segmentIndex.dropFirstReferences(numSegmentsToDrop); - - // Now adjust timestamps back to begin at 0. - const segmentN = segmentIndex.earliestReference(); - if (segmentN) { - const streamOffset = -segmentN.startTime; - // Modify all SegmentReferences equally. - streamInfo.stream.segmentIndex.offset(streamOffset); - // Update other parts of streamInfo the same way. - this.offsetStreamInfo_(streamInfo, streamOffset); + if (numSegmentsToDrop > 0) { + segmentIndex.dropFirstReferences(numSegmentsToDrop); + + // Now adjust timestamps back to begin at 0. + const segmentN = segmentIndex.earliestReference(); + if (segmentN) { + const streamOffset = -segmentN.startTime; + // Modify all SegmentReferences equally. + streamInfo.stream.segmentIndex.offset(streamOffset); + // Update other parts of streamInfo the same way. + this.offsetStreamInfo_(streamInfo, streamOffset); + } } } } diff --git a/test/hls/hls_live_unit.js b/test/hls/hls_live_unit.js index 799dcd6d8a..3329dc1e98 100644 --- a/test/hls/hls_live_unit.js +++ b/test/hls/hls_live_unit.js @@ -921,6 +921,58 @@ describe('HlsParser live', () => { manifest.variants[1].video, [ref4]); }); + describe('when ignoreManifestProgramDateTime is set', () => { + const config = shaka.util.PlayerConfiguration.createDefault().manifest; + config.hls.ignoreManifestProgramDateTime = true; + + it('does not reset segment times when switching', async () => { + parser.configure(config); + + const ref1 = makeReference( + 'test:/main.mp4', 0, 2, /* syncTime= */ null); + const ref2 = makeReference( + 'test:/main2.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(); + + // In the initial playlist, we know the earliest start time is 0, at + // EXT-X-MEDIA-SEQUENCE of 0. + expect( + manifest.variants[0].video.segmentIndex.earliestReference() + .getStartTime()) + .toBe(0); + + // Update. + fakeNetEngine + .setResponseText('test:/video', mediaWithRemovedSegment) + .setResponseText('test:/video2', mediaWithRemovedSegment2); + await delayForUpdatePeriod(); + + // Switch. The new variant starts at EXT-X-MEDIA-SEQUENCE of 1. + await manifest.variants[0].video.closeSegmentIndex(); + await manifest.variants[1].video.createSegmentIndex(); + + // The earliest start time of the new segmentIndex should therefore be + // 2. + expect(manifest.variants[0].video.segmentIndex).toBeNull(); + const segIdx = manifest.variants[1].video.segmentIndex; + expect(segIdx.earliestReference().getStartTime()).toBe(2); + }); + }); + it('handles switching during update', async () => { const ref1 = makeReference( 'test:/main.mp4', 0, 2, /* syncTime= */ null);