diff --git a/src/playlist-controller.js b/src/playlist-controller.js index aa989d5eb..d4181509f 100644 --- a/src/playlist-controller.js +++ b/src/playlist-controller.js @@ -1221,7 +1221,15 @@ export class PlaylistController extends videojs.EventTarget { const offset = media.start.timeOffset; if (offset < 0) { - startPoint = Math.max(seekableEnd + offset, seekable.start(0)); + // Per HLS spec, negative TIME-OFFSET is from the end of the last Media Segment. + // For live streams, seekableEnd already has liveEdgeDelay subtracted, + // so we need to add it back to get the actual playlist end. + // Clamp to seekableEnd to avoid seeking into the unsafe live edge zone. + const main = this.mainPlaylistLoader_.main; + const liveEdgeDelay = Vhs.Playlist.liveEdgeDelay(main, media); + const actualPlaylistEnd = seekableEnd + liveEdgeDelay; + + startPoint = Math.max(Math.min(actualPlaylistEnd + offset, seekableEnd), seekable.start(0)); } else { startPoint = Math.min(seekableEnd, offset); } diff --git a/test/manifests/startLiveNegative.m3u8 b/test/manifests/startLiveNegative.m3u8 new file mode 100644 index 000000000..6b988fd49 --- /dev/null +++ b/test/manifests/startLiveNegative.m3u8 @@ -0,0 +1,15 @@ +#EXTM3U +#EXT-X-START:TIME-OFFSET=-30,PRECISE=YES +#EXT-X-TARGETDURATION:10 +#EXTINF:10, +media-00001.ts +#EXTINF:10, +media-00002.ts +#EXTINF:10, +media-00003.ts +#EXTINF:10, +media-00004.ts +#EXTINF:10, +media-00005.ts +#EXTINF:10, +media-00006.ts diff --git a/test/videojs-http-streaming.test.js b/test/videojs-http-streaming.test.js index 5c143ff4c..65d860b09 100644 --- a/test/videojs-http-streaming.test.js +++ b/test/videojs-http-streaming.test.js @@ -485,6 +485,30 @@ QUnit.test('seeks to negative offset point', function(assert) { assert.strictEqual(currentTime, 35, 'seeked to negative offset'); }); +QUnit.test('seeks to negative offset on live stream from actual playlist end', function(assert) { + let currentTime = 0; + + this.player.autoplay(true); + this.player.on('seeking', () => { + currentTime = this.player.currentTime(); + }); + this.player.src({ + src: 'startLiveNegative.m3u8', + type: 'application/vnd.apple.mpegurl' + }); + + this.clock.tick(1); + + openMediaSource(this.player, this.clock); + this.player.tech_.trigger('play'); + this.standardXHRResponse(this.requests.shift()); + this.clock.tick(1); + + // 6 segments * 10s = 60s total, liveEdgeDelay = 30, TIME-OFFSET = -30 + // Per HLS spec, offset is from playlist end (60s), so startPoint = 60 - 30 = 30 + assert.strictEqual(currentTime, 30, 'seeked to negative offset from actual playlist end on live stream'); +}); + QUnit.test( 'duration is set when the source opens after the playlist is loaded', function(assert) {