Skip to content

Commit 101fb90

Browse files
authored
fix: calculate EXT-X-START TIME-OFFSET from actual playlist end for live streams (#1599)
For live streams with negative TIME-OFFSET in EXT-X-START, the offset was incorrectly calculated from seekableEnd, which already has liveEdgeDelay subtracted. This caused playback to start further behind the live edge than intended. Per HLS spec, negative TIME-OFFSET should be relative to the end of the last Media Segment. This fix adds liveEdgeDelay back to seekableEnd before applying the offset, making VHS behavior consistent with native HLS implementations (e.g., Safari). Example: With liveEdgeDelay=18 and TIME-OFFSET=-18: - Before: startPoint = seekableEnd - 18 (36s behind actual live edge) - After: startPoint = seekableEnd (18s behind, matching Safari)
1 parent bd810ea commit 101fb90

File tree

3 files changed

+48
-1
lines changed

3 files changed

+48
-1
lines changed

src/playlist-controller.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1221,7 +1221,15 @@ export class PlaylistController extends videojs.EventTarget {
12211221
const offset = media.start.timeOffset;
12221222

12231223
if (offset < 0) {
1224-
startPoint = Math.max(seekableEnd + offset, seekable.start(0));
1224+
// Per HLS spec, negative TIME-OFFSET is from the end of the last Media Segment.
1225+
// For live streams, seekableEnd already has liveEdgeDelay subtracted,
1226+
// so we need to add it back to get the actual playlist end.
1227+
// Clamp to seekableEnd to avoid seeking into the unsafe live edge zone.
1228+
const main = this.mainPlaylistLoader_.main;
1229+
const liveEdgeDelay = Vhs.Playlist.liveEdgeDelay(main, media);
1230+
const actualPlaylistEnd = seekableEnd + liveEdgeDelay;
1231+
1232+
startPoint = Math.max(Math.min(actualPlaylistEnd + offset, seekableEnd), seekable.start(0));
12251233
} else {
12261234
startPoint = Math.min(seekableEnd, offset);
12271235
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#EXTM3U
2+
#EXT-X-START:TIME-OFFSET=-30,PRECISE=YES
3+
#EXT-X-TARGETDURATION:10
4+
#EXTINF:10,
5+
media-00001.ts
6+
#EXTINF:10,
7+
media-00002.ts
8+
#EXTINF:10,
9+
media-00003.ts
10+
#EXTINF:10,
11+
media-00004.ts
12+
#EXTINF:10,
13+
media-00005.ts
14+
#EXTINF:10,
15+
media-00006.ts

test/videojs-http-streaming.test.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,30 @@ QUnit.test('seeks to negative offset point', function(assert) {
485485
assert.strictEqual(currentTime, 35, 'seeked to negative offset');
486486
});
487487

488+
QUnit.test('seeks to negative offset on live stream from actual playlist end', function(assert) {
489+
let currentTime = 0;
490+
491+
this.player.autoplay(true);
492+
this.player.on('seeking', () => {
493+
currentTime = this.player.currentTime();
494+
});
495+
this.player.src({
496+
src: 'startLiveNegative.m3u8',
497+
type: 'application/vnd.apple.mpegurl'
498+
});
499+
500+
this.clock.tick(1);
501+
502+
openMediaSource(this.player, this.clock);
503+
this.player.tech_.trigger('play');
504+
this.standardXHRResponse(this.requests.shift());
505+
this.clock.tick(1);
506+
507+
// 6 segments * 10s = 60s total, liveEdgeDelay = 30, TIME-OFFSET = -30
508+
// Per HLS spec, offset is from playlist end (60s), so startPoint = 60 - 30 = 30
509+
assert.strictEqual(currentTime, 30, 'seeked to negative offset from actual playlist end on live stream');
510+
});
511+
488512
QUnit.test(
489513
'duration is set when the source opens after the playlist is loaded',
490514
function(assert) {

0 commit comments

Comments
 (0)