fix(plex): keep play state honest across a seek that restarts the session
Seeking to a spot outside the current remux window restarts the hls.js session, which detaches/re-attaches the media and fires 'emptied' — that silently flips the element to paused WITHOUT a 'pause' event. Because the play flag only tracked 'play'/'pause', the button kept showing 'playing' while the video had actually stopped, and on a slow (proxied) load the post-manifest play() could be rejected, leaving it stuck paused until a click on the video. Root cause confirmed by instrumenting the media events during a seek: emptied(paused=true) with no accompanying pause event. Fix: resync the play flag from video.paused on the settling events (emptied/canplay/seeked) so the button can never contradict reality; preserve the pre-seek play/pause intent through the session restart (loadSession takes resumePlay); and retry play once on 'canplay' so a rejected early play() still resumes. Verified on :8080 — backward seek (session restart), forward seek (native) and manual pause all keep the icon in sync (0 desync samples) and resume correctly.
This commit is contained in:
parent
2659991d84
commit
488e809f2b
1 changed files with 27 additions and 5 deletions
|
|
@ -271,7 +271,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
|||
|
||||
// --- media setup: (re)load a session for `id` at a start offset -------------------------------
|
||||
const loadSession = useCallback(
|
||||
async (startAt: number) => {
|
||||
async (startAt: number, resumePlay = true) => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
setReady(false);
|
||||
|
|
@ -301,6 +301,16 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
|||
const trueStart = sess.mode === "hls" && sess.start_s > 0 ? sess.start_s - HLS_SUB_LAG : sess.start_s;
|
||||
sessionStartRef.current = trueStart;
|
||||
setSessionK(trueStart);
|
||||
// Belt-and-suspenders resume: the play() fired on MANIFEST_PARSED / loadedmetadata can be
|
||||
// rejected when the media isn't ready yet (slow first segments over a proxy), which would leave
|
||||
// the video paused after a seek even though the user was watching. Retry once the element can
|
||||
// actually play. Idempotent — only acts if still paused.
|
||||
if (resumePlay) {
|
||||
const onCanPlay = () => {
|
||||
if (aliveRef.current && videoRef.current?.paused) videoRef.current.play().catch(() => {});
|
||||
};
|
||||
video.addEventListener("canplay", onCanPlay, { once: true });
|
||||
}
|
||||
if (hlsRef.current) {
|
||||
hlsRef.current.destroy();
|
||||
hlsRef.current = null;
|
||||
|
|
@ -313,7 +323,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
|||
// Subtitles are native <track>s (not muxed into the HLS), so nothing subtitle-related here.
|
||||
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||
setReady(true);
|
||||
if (aliveRef.current) video.play().catch(() => {});
|
||||
if (aliveRef.current && resumePlay) video.play().catch(() => {});
|
||||
});
|
||||
// Restore the selected audio track ONCE hls.js has parsed the renditions — doing it on
|
||||
// MANIFEST_PARSED was too early (the track list wasn't populated, so the set was dropped and
|
||||
|
|
@ -342,7 +352,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
|||
const onMeta = () => {
|
||||
if (sess.mode === "direct" && startAt > 0) video.currentTime = startAt;
|
||||
setReady(true);
|
||||
if (aliveRef.current) video.play().catch(() => {});
|
||||
if (aliveRef.current && resumePlay) video.play().catch(() => {});
|
||||
video.removeEventListener("loadedmetadata", onMeta);
|
||||
};
|
||||
video.addEventListener("loadedmetadata", onMeta);
|
||||
|
|
@ -417,13 +427,24 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
|||
setPlaying(false);
|
||||
patchPrefs({ wasPlaying: false });
|
||||
};
|
||||
// A seek that restarts the hls.js session detaches/re-attaches the media, firing 'emptied' —
|
||||
// which silently flips paused=true WITHOUT a 'pause' event, so the play/pause pair alone let the
|
||||
// button lie (⏸ shown while actually paused). Resync the flag from reality on the settling events
|
||||
// so the icon can never contradict the video. (Does not touch the wasPlaying pref.)
|
||||
const syncPlaying = () => setPlaying(!video.paused);
|
||||
video.addEventListener("timeupdate", onTime);
|
||||
video.addEventListener("play", onPlay);
|
||||
video.addEventListener("pause", onPause);
|
||||
video.addEventListener("emptied", syncPlaying);
|
||||
video.addEventListener("canplay", syncPlaying);
|
||||
video.addEventListener("seeked", syncPlaying);
|
||||
return () => {
|
||||
video.removeEventListener("timeupdate", onTime);
|
||||
video.removeEventListener("play", onPlay);
|
||||
video.removeEventListener("pause", onPause);
|
||||
video.removeEventListener("emptied", syncPlaying);
|
||||
video.removeEventListener("canplay", syncPlaying);
|
||||
video.removeEventListener("seeked", syncPlaying);
|
||||
};
|
||||
}, [id, patchPrefs]);
|
||||
|
||||
|
|
@ -470,14 +491,15 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
|||
(target: number) => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
const wasPlaying = !video.paused; // preserve play/pause intent across the seek
|
||||
const dur = durationRef.current || target;
|
||||
const T = Math.max(0, Math.min(target, dur - 1));
|
||||
absRef.current = T; // reflect the target immediately so a save/beacon right after a seek is accurate
|
||||
const rel = T - sessionStartRef.current;
|
||||
if (rel >= 0 && rel <= (video.seekable.length ? video.seekable.end(0) : 0) + 0.5) {
|
||||
video.currentTime = rel;
|
||||
video.currentTime = rel; // native seek within the loaded region keeps playing on its own
|
||||
} else {
|
||||
loadSession(T);
|
||||
loadSession(T, wasPlaying); // session restart: only auto-resume if we were playing
|
||||
}
|
||||
},
|
||||
[loadSession],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue