History: fix snapshot timestamp and scrub-release seek behavior

(cherry picked from commit 7b6e26fdbbb878ec7443b29a335bff1990d0b9f5)
This commit is contained in:
nrlcode 2026-03-24 12:41:15 -07:00
parent b668e802d9
commit 24b27dfa4a
3 changed files with 64 additions and 5 deletions

View File

@ -51,6 +51,7 @@ type HlsVideoPlayerProps = {
onTimeUpdate?: (time: number) => void;
onPlaying?: () => void;
onSeekToTime?: (timestamp: number, play?: boolean) => void;
onGetAbsoluteTimestamp?: (playerSeconds: number) => number | undefined;
setFullResolution?: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
onUploadFrame?: (playTime: number) => Promise<AxiosResponse> | undefined;
toggleFullscreen?: () => void;
@ -76,6 +77,7 @@ export default function HlsVideoPlayer({
onTimeUpdate,
onPlaying,
onSeekToTime,
onGetAbsoluteTimestamp,
setFullResolution,
onUploadFrame,
toggleFullscreen,
@ -282,7 +284,7 @@ export default function HlsVideoPlayer({
const getVideoTime = useCallback(() => {
const currentTime = videoRef.current?.currentTime;
if (!currentTime) {
if (currentTime == undefined) {
return undefined;
}
@ -323,7 +325,7 @@ export default function HlsVideoPlayer({
onSeek={(diff) => {
const currentTime = videoRef.current?.currentTime;
if (!videoRef.current || !currentTime) {
if (!videoRef.current || currentTime == undefined) {
return;
}
@ -339,7 +341,7 @@ export default function HlsVideoPlayer({
onUploadFrame={async () => {
const frameTime = getVideoTime();
if (frameTime && onUploadFrame) {
if (frameTime != undefined && onUploadFrame) {
const resp = await onUploadFrame(frameTime);
if (resp && resp.status == 200) {
@ -353,6 +355,35 @@ export default function HlsVideoPlayer({
}
}
}}
onSnapshot={async () => {
const frameTime = getVideoTime();
const snapshotTimestamp =
frameTime != undefined
? (onGetAbsoluteTimestamp?.(frameTime) ?? frameTime)
: undefined;
const result = await grabVideoSnapshot(videoRef.current);
if (result.success) {
downloadSnapshot(
result.data.dataUrl,
generateSnapshotFilename(
camera ?? "recording",
snapshotTimestamp,
),
);
toast.success(
t("snapshot.downloadStarted", { ns: "views/live" }),
{
position: "top-center",
},
);
} else {
toast.error(t("snapshot.captureFailed", { ns: "views/live" }), {
position: "top-center",
});
}
}}
snapshotTitle={t("snapshot.takeSnapshot", { ns: "views/live" })}
fullscreen={fullscreen}
toggleFullscreen={toggleFullscreen}
containerRef={containerRef}
@ -484,7 +515,7 @@ export default function HlsVideoPlayer({
const frameTime = getVideoTime();
if (frameTime) {
if (frameTime != undefined) {
onTimeUpdate(frameTime);
}
}}

View File

@ -406,6 +406,9 @@ export default function DynamicVideoPlayer({
onSeekToTime(timestamp, play);
}
}}
onGetAbsoluteTimestamp={(playerSeconds) =>
controller ? controller.getProgress(playerSeconds) : undefined
}
onPlaying={() => {
if (isScrubbing) {
playerRef.current?.pause();

View File

@ -257,6 +257,8 @@ export function RecordingView({
const [scrubbing, setScrubbing] = useState(false);
const [currentTime, setCurrentTime] = useState<number>(startTime);
const [playerTime, setPlayerTime] = useState(startTime);
const wasScrubbingRef = useRef(false);
const pendingScrubSeekTimeRef = useRef<number | null>(null);
const updateSelectedSegment = useCallback(
(currentTime: number, updateStartTime: boolean) => {
@ -347,6 +349,17 @@ export function RecordingView({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentTime, scrubbing]);
useEffect(() => {
// force an explicit seek when the user releases timeline scrubbing,
// and guard against old player time updates overwriting the new target.
if (wasScrubbingRef.current && !scrubbing) {
pendingScrubSeekTimeRef.current = currentTime;
mainControllerRef.current?.seekToTimestamp(currentTime, true);
}
wasScrubbingRef.current = scrubbing;
}, [scrubbing, currentTime]);
const [fullResolution, setFullResolution] = useState<VideoResolutionType>({
width: 0,
height: 0,
@ -820,7 +833,19 @@ export function RecordingView({
fullscreen={fullscreen}
onTimestampUpdate={(timestamp) => {
setPlayerTime(timestamp);
const pendingScrubSeekTime =
pendingScrubSeekTimeRef.current;
if (pendingScrubSeekTime != null) {
// keep the scrubbed target time anchored until playback catches up.
if (Math.abs(timestamp - pendingScrubSeekTime) <= 1) {
pendingScrubSeekTimeRef.current = null;
setCurrentTime(timestamp);
}
} else {
setCurrentTime(timestamp);
}
Object.values(previewRefs.current ?? {}).forEach((prev) =>
prev.scrubToTimestamp(Math.floor(timestamp)),
);