fix(player): a Restart of the paused film plays the new session; a key on faded bands only shows them
The same-film restart read the paused state, restarted the session without play, and resumed the outgoing stream beside it. Space or Enter on a control the bands had faded from pressed it unseen; now it only brings them back, and Space plays or pauses.
This commit is contained in:
@@ -263,6 +263,7 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
|
||||
// Auto-hiding control bar (reveal on mouse move / any key; hide after 3s while playing).
|
||||
const {
|
||||
uiVisible,
|
||||
controlsShown,
|
||||
wake,
|
||||
holdOnHover,
|
||||
holdOnFocus,
|
||||
@@ -277,6 +278,7 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
|
||||
// one (E6) — not the focus a click leaves on a button, which would pin the bar until the next click;
|
||||
// `:focus-visible` is the browser's own "this focus came from the keyboard". Both bands wear these.
|
||||
const holdWhileHovered = {
|
||||
"data-player-band": "", // what a key on a faded-out control is told apart by (onKey, below)
|
||||
onPointerEnter: () => holdOnHover(true),
|
||||
onPointerLeave: () => holdOnHover(false),
|
||||
onFocus: (e: React.FocusEvent<HTMLElement>) => holdOnFocus(e.target.matches(":focus-visible")),
|
||||
@@ -792,10 +794,12 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
|
||||
|
||||
// --- seeking (native within the loaded region, else restart the session) ---------------------
|
||||
const seekTo = useCallback(
|
||||
(target: number) => {
|
||||
(target: number, play?: boolean) => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
const wasPlaying = !video.paused; // preserve play/pause intent across the seek
|
||||
// Play/pause intent across the seek: the caller's when it has one (a Restart plays), else
|
||||
// whatever the video was doing.
|
||||
const wasPlaying = play ?? !video.paused;
|
||||
const dur = durationRef.current || target;
|
||||
const T = Math.max(0, Math.min(target, dur - 1));
|
||||
// Reflect the target immediately so a save/beacon right after a seek is accurate — in the STATE
|
||||
@@ -813,6 +817,7 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
|
||||
rel <= (video.seekable.length ? video.seekable.end(0) : 0) + 0.5
|
||||
) {
|
||||
video.currentTime = rel; // native seek within the loaded region keeps playing on its own
|
||||
if (play) video.play().catch(() => {});
|
||||
} else {
|
||||
loadSession(T, wasPlaying); // session restart: only auto-resume if we were playing
|
||||
}
|
||||
@@ -849,9 +854,11 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
|
||||
if (otherId && otherId === id) {
|
||||
// The item already playing (the info overlay's collection strip lists the film itself):
|
||||
// nothing to switch to. The switch below would pause it and stop trusting its clock, and
|
||||
// `setId` to the same id loads nothing — a frozen 0:00 (E28 review round 2, P1).
|
||||
if (fromZero) seekTo(0);
|
||||
videoRef.current?.play().catch(() => {});
|
||||
// `setId` to the same id loads nothing — a frozen 0:00 (E28 review round 2, P1). The play
|
||||
// intent goes INTO the seek: a paused film whose session starts past 0 restarts the session,
|
||||
// and a play() beside that resumed the outgoing stream while the new one stayed paused.
|
||||
if (fromZero) seekTo(0, true);
|
||||
else videoRef.current?.play().catch(() => {});
|
||||
return;
|
||||
}
|
||||
if (otherId) {
|
||||
@@ -1134,12 +1141,26 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
|
||||
go(id);
|
||||
};
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
// A Space or Enter on a control the bands have faded out from under (a key user's focus stays
|
||||
// put when they hide) must not press it unseen — Back leaving the player, Next skipping the
|
||||
// episode. That key only brings the bands back; Space also does what a viewer who sees no
|
||||
// button means by it, and pauses or plays. Read before `wake` below shows them.
|
||||
const blind =
|
||||
!controlsShown() &&
|
||||
isActivationTarget(e) &&
|
||||
!!(e.target as Element | null)?.closest?.("[data-player-band]") &&
|
||||
(e.key === " " || e.key === "Enter");
|
||||
// Keyboard use also reveals the controls — but typing in a player field is not "using the
|
||||
// remote", and `dispatch` still lets Escape through from inside a field (the only key that
|
||||
// opts in), which is why this is a guard around `wake` rather than an early return.
|
||||
// A slider is not a text field: its arrows are the remote too (E28 review E6).
|
||||
if (!isTypingTarget(e) || (e.target as Element | null)?.matches?.('input[type="range"]'))
|
||||
wake();
|
||||
if (blind) {
|
||||
e.preventDefault();
|
||||
if (e.key === " ") togglePlay();
|
||||
return;
|
||||
}
|
||||
// The shared dialog Tab trap. This player paints an opaque full-viewport layer over the app,
|
||||
// so Tab leaving it lands on a control nobody can see, where the ring is invisible and the
|
||||
// next Enter fires something blind. Every other overlay in the tree traps (ui/Modal,
|
||||
@@ -1256,6 +1277,7 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
|
||||
nextId,
|
||||
onClose,
|
||||
wake,
|
||||
controlsShown,
|
||||
handleBack,
|
||||
skipProgressRef,
|
||||
cancelAutoSkipRef,
|
||||
|
||||
Reference in New Issue
Block a user