diff --git a/frontend/src/components/PlexPlayer.tsx b/frontend/src/components/PlexPlayer.tsx index 4fc7663b..61124c86 100644 --- a/frontend/src/components/PlexPlayer.tsx +++ b/frontend/src/components/PlexPlayer.tsx @@ -261,7 +261,15 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) { hlsRef.current = null; } if (sess.mode === "hls" && Hls.isSupported()) { - const hls = new Hls({ maxBufferLength: 30, enableWorker: true }); + // startPosition: 0 — our HLS is a growing EVENT playlist with no ENDLIST (see _HLS_TAIL in + // stream.py), which hls.js classifies as LIVE. Its default (startPosition: -1) then starts + // playback at the live edge and hugs it, so it never builds a forward buffer (rebuffer + // spinners), and on a resume/seek session the segments' absolute PTS (-copyts, offset by the + // server's -ss) don't line up with the live-edge timeline → 0 fragments load → permanent + // stall. But the seek-restart model means fragment 0 IS the requested content position + // (`?t=` positioned the server ffmpeg there), so we always want to start VOD-style at 0 and + // buffer forward up to maxBufferLength. Applies to remux + transcode alike (both use copyts). + const hls = new Hls({ maxBufferLength: 30, enableWorker: true, startPosition: 0 }); hlsRef.current = hls; hls.loadSource(`${sess.url}?t=${Math.floor(startAt)}`); hls.attachMedia(video);