Files
streamyfin/components/video-player/controls/hooks/useVideoTime.ts
Lance Chant 0fea901133 fix(player): resume replay, TV surface recovery, and finite time guards
ExoPlayer:
   - Pass the resume position into setMediaSource() instead of prepare()-ing
     from 0 and then seekTo()-ing, which replayed the first few seconds before
     jumping to the resume point.
   - Drop the redundant initial seek that mirrors the resume position (fired by
     the JS direct-player layer once tracks are ready). ExoPlayer re-buffers on
     every seek — even a no-op to the current position — so it stuttered startup.

   MPV (Android TV):
   - Recover the video pipeline when returning from the screensaver / app
     background while paused. TV uses zero-copy hwdec=mediacodec, which binds
     MediaCodec directly to the display surface; when the screensaver invalidates
     that surface the decoder is left bound to dead buffers and mpv disables the
     video track. Register TV-only activity-lifecycle callbacks and reload at the
     cached position on resume to recreate the decoder against the live surface
     (Android counterpart to iOS's performDecoderReset()).

   Video time controls:
   - Initialize remainingTime to 0 instead of Infinity and guard non-finite
     values, so the first paint shows "0:00" (and "—" for the end time) rather
     than "Infinityh NaNm NaNs" and an Invalid Date before the first progress
     update

Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com>
2026-07-14 10:55:10 +02:00

74 lines
2.1 KiB
TypeScript

import { useCallback, useRef, useState } from "react";
import {
runOnJS,
type SharedValue,
useAnimatedReaction,
} from "react-native-reanimated";
interface UseVideoTimeProps {
progress: SharedValue<number>;
max: SharedValue<number>;
isSeeking: SharedValue<boolean>;
}
/**
* Hook to manage video time display.
* MPV player uses milliseconds for time values.
*/
export function useVideoTime({ progress, max, isSeeking }: UseVideoTimeProps) {
const [currentTime, setCurrentTime] = useState(0);
// Start at 0 (not Infinity) so the controls' first paint — before the first
// progress/max update — shows "0:00" instead of formatting Infinity into
// "Infinityh NaNm NaNs" and an Invalid Date for "ends at".
const [remainingTime, setRemainingTime] = useState(0);
const lastCurrentTimeRef = useRef(0);
const lastRemainingTimeRef = useRef(0);
const updateTimes = useCallback(
(currentProgress: number, maxValue: number) => {
// MPV uses milliseconds
const current = currentProgress;
const remaining = maxValue - currentProgress;
// Only update state if the displayed time actually changed (avoid sub-second updates)
const currentSeconds = Math.floor(current / 1000);
const remainingSeconds = Math.floor(remaining / 1000);
const lastCurrentSeconds = Math.floor(lastCurrentTimeRef.current / 1000);
const lastRemainingSeconds = Math.floor(
lastRemainingTimeRef.current / 1000,
);
if (
currentSeconds !== lastCurrentSeconds ||
remainingSeconds !== lastRemainingSeconds
) {
setCurrentTime(current);
setRemainingTime(remaining);
lastCurrentTimeRef.current = current;
lastRemainingTimeRef.current = remaining;
}
},
[],
);
useAnimatedReaction(
() => ({
progress: progress.value,
max: max.value,
isSeeking: isSeeking.value,
}),
(result) => {
if (!result.isSeeking) {
runOnJS(updateTimes)(result.progress, result.max);
}
},
[updateTimes],
);
return {
currentTime,
remainingTime,
};
}