From 9e563823aceef3d1f1a05327ae969abd36e7b707 Mon Sep 17 00:00:00 2001 From: nrlcode Date: Thu, 26 Mar 2026 09:32:55 -0700 Subject: [PATCH] Snapshot: guard against repeated download clicks --- web/src/components/player/HlsVideoPlayer.tsx | 59 +++++++++++++------- web/src/components/player/VideoControls.tsx | 11 +++- 2 files changed, 49 insertions(+), 21 deletions(-) diff --git a/web/src/components/player/HlsVideoPlayer.tsx b/web/src/components/player/HlsVideoPlayer.tsx index cbf177b99..0462e7b7d 100644 --- a/web/src/components/player/HlsVideoPlayer.tsx +++ b/web/src/components/player/HlsVideoPlayer.tsx @@ -231,6 +231,7 @@ export default function HlsVideoPlayer({ const [mobileCtrlTimeout, setMobileCtrlTimeout] = useState(); const [controls, setControls] = useState(isMobile); const [controlsOpen, setControlsOpen] = useState(false); + const [isSnapshotLoading, setIsSnapshotLoading] = useState(false); const [zoomScale, setZoomScale] = useState(1.0); const [videoDimensions, setVideoDimensions] = useState<{ width: number; @@ -277,27 +278,44 @@ export default function HlsVideoPlayer({ }, [videoRef, inpointOffset]); const handleSnapshot = useCallback(async () => { - const frameTime = getVideoTime(); - const result = await grabVideoSnapshot(videoRef.current); - - if (result.success) { - downloadSnapshot( - result.data.dataUrl, - generateSnapshotFilename( - camera ?? "recording", - currentTime ?? frameTime, - config?.ui?.timezone, - ), - ); - toast.success(t("snapshot.downloadStarted", { ns: "views/live" }), { - position: "top-center", - }); - } else { - toast.error(t("snapshot.captureFailed", { ns: "views/live" }), { - position: "top-center", - }); + if (isSnapshotLoading) { + return; } - }, [camera, config?.ui?.timezone, currentTime, getVideoTime, t, videoRef]); + + setIsSnapshotLoading(true); + try { + const frameTime = getVideoTime(); + const result = await grabVideoSnapshot(videoRef.current); + + if (result.success) { + downloadSnapshot( + result.data.dataUrl, + generateSnapshotFilename( + camera ?? "recording", + currentTime ?? frameTime, + config?.ui?.timezone, + ), + ); + toast.success(t("snapshot.downloadStarted", { ns: "views/live" }), { + position: "top-center", + }); + } else { + toast.error(t("snapshot.captureFailed", { ns: "views/live" }), { + position: "top-center", + }); + } + } finally { + setIsSnapshotLoading(false); + } + }, [ + camera, + config?.ui?.timezone, + currentTime, + getVideoTime, + isSnapshotLoading, + t, + videoRef, + ]); const onSnapshot = camera ? handleSnapshot : undefined; return ( @@ -365,6 +383,7 @@ export default function HlsVideoPlayer({ } }} onSnapshot={onSnapshot} + snapshotLoading={isSnapshotLoading} snapshotTitle={t("snapshot.takeSnapshot", { ns: "views/live" })} fullscreen={fullscreen} toggleFullscreen={toggleFullscreen} diff --git a/web/src/components/player/VideoControls.tsx b/web/src/components/player/VideoControls.tsx index 2995f9fe3..4e60c6376 100644 --- a/web/src/components/player/VideoControls.tsx +++ b/web/src/components/player/VideoControls.tsx @@ -75,6 +75,7 @@ type VideoControlsProps = { onSetPlaybackRate: (rate: number) => void; onUploadFrame?: () => void; onSnapshot?: () => void; + snapshotLoading?: boolean; snapshotTitle?: string; toggleFullscreen?: () => void; containerRef?: React.MutableRefObject; @@ -98,6 +99,7 @@ export default function VideoControls({ onSetPlaybackRate, onUploadFrame, onSnapshot, + snapshotLoading = false, snapshotTitle, toggleFullscreen, containerRef, @@ -302,10 +304,17 @@ export default function VideoControls({ {features.snapshot && onSnapshot && ( ) => { e.stopPropagation(); + if (snapshotLoading) { + return; + } onSnapshot(); }} />