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 -------------------------------
|
// --- media setup: (re)load a session for `id` at a start offset -------------------------------
|
||||||
const loadSession = useCallback(
|
const loadSession = useCallback(
|
||||||
async (startAt: number) => {
|
async (startAt: number, resumePlay = true) => {
|
||||||
const video = videoRef.current;
|
const video = videoRef.current;
|
||||||
if (!video) return;
|
if (!video) return;
|
||||||
setReady(false);
|
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;
|
const trueStart = sess.mode === "hls" && sess.start_s > 0 ? sess.start_s - HLS_SUB_LAG : sess.start_s;
|
||||||
sessionStartRef.current = trueStart;
|
sessionStartRef.current = trueStart;
|
||||||
setSessionK(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) {
|
if (hlsRef.current) {
|
||||||
hlsRef.current.destroy();
|
hlsRef.current.destroy();
|
||||||
hlsRef.current = null;
|
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.
|
// Subtitles are native <track>s (not muxed into the HLS), so nothing subtitle-related here.
|
||||||
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||||
setReady(true);
|
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
|
// 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
|
// 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 = () => {
|
const onMeta = () => {
|
||||||
if (sess.mode === "direct" && startAt > 0) video.currentTime = startAt;
|
if (sess.mode === "direct" && startAt > 0) video.currentTime = startAt;
|
||||||
setReady(true);
|
setReady(true);
|
||||||
if (aliveRef.current) video.play().catch(() => {});
|
if (aliveRef.current && resumePlay) video.play().catch(() => {});
|
||||||
video.removeEventListener("loadedmetadata", onMeta);
|
video.removeEventListener("loadedmetadata", onMeta);
|
||||||
};
|
};
|
||||||
video.addEventListener("loadedmetadata", onMeta);
|
video.addEventListener("loadedmetadata", onMeta);
|
||||||
|
|
@ -417,13 +427,24 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||||
setPlaying(false);
|
setPlaying(false);
|
||||||
patchPrefs({ wasPlaying: 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("timeupdate", onTime);
|
||||||
video.addEventListener("play", onPlay);
|
video.addEventListener("play", onPlay);
|
||||||
video.addEventListener("pause", onPause);
|
video.addEventListener("pause", onPause);
|
||||||
|
video.addEventListener("emptied", syncPlaying);
|
||||||
|
video.addEventListener("canplay", syncPlaying);
|
||||||
|
video.addEventListener("seeked", syncPlaying);
|
||||||
return () => {
|
return () => {
|
||||||
video.removeEventListener("timeupdate", onTime);
|
video.removeEventListener("timeupdate", onTime);
|
||||||
video.removeEventListener("play", onPlay);
|
video.removeEventListener("play", onPlay);
|
||||||
video.removeEventListener("pause", onPause);
|
video.removeEventListener("pause", onPause);
|
||||||
|
video.removeEventListener("emptied", syncPlaying);
|
||||||
|
video.removeEventListener("canplay", syncPlaying);
|
||||||
|
video.removeEventListener("seeked", syncPlaying);
|
||||||
};
|
};
|
||||||
}, [id, patchPrefs]);
|
}, [id, patchPrefs]);
|
||||||
|
|
||||||
|
|
@ -470,14 +491,15 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
|
||||||
(target: number) => {
|
(target: number) => {
|
||||||
const video = videoRef.current;
|
const video = videoRef.current;
|
||||||
if (!video) return;
|
if (!video) return;
|
||||||
|
const wasPlaying = !video.paused; // preserve play/pause intent across the seek
|
||||||
const dur = durationRef.current || target;
|
const dur = durationRef.current || target;
|
||||||
const T = Math.max(0, Math.min(target, dur - 1));
|
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
|
absRef.current = T; // reflect the target immediately so a save/beacon right after a seek is accurate
|
||||||
const rel = T - sessionStartRef.current;
|
const rel = T - sessionStartRef.current;
|
||||||
if (rel >= 0 && rel <= (video.seekable.length ? video.seekable.end(0) : 0) + 0.5) {
|
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 {
|
} else {
|
||||||
loadSession(T);
|
loadSession(T, wasPlaying); // session restart: only auto-resume if we were playing
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[loadSession],
|
[loadSession],
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue