Snapshot: guard against repeated download clicks

This commit is contained in:
nrlcode 2026-03-26 09:32:55 -07:00
parent 84c18365cc
commit 9e563823ac
2 changed files with 49 additions and 21 deletions

View File

@ -231,6 +231,7 @@ export default function HlsVideoPlayer({
const [mobileCtrlTimeout, setMobileCtrlTimeout] = useState<NodeJS.Timeout>(); const [mobileCtrlTimeout, setMobileCtrlTimeout] = useState<NodeJS.Timeout>();
const [controls, setControls] = useState(isMobile); const [controls, setControls] = useState(isMobile);
const [controlsOpen, setControlsOpen] = useState(false); const [controlsOpen, setControlsOpen] = useState(false);
const [isSnapshotLoading, setIsSnapshotLoading] = useState(false);
const [zoomScale, setZoomScale] = useState(1.0); const [zoomScale, setZoomScale] = useState(1.0);
const [videoDimensions, setVideoDimensions] = useState<{ const [videoDimensions, setVideoDimensions] = useState<{
width: number; width: number;
@ -277,6 +278,12 @@ export default function HlsVideoPlayer({
}, [videoRef, inpointOffset]); }, [videoRef, inpointOffset]);
const handleSnapshot = useCallback(async () => { const handleSnapshot = useCallback(async () => {
if (isSnapshotLoading) {
return;
}
setIsSnapshotLoading(true);
try {
const frameTime = getVideoTime(); const frameTime = getVideoTime();
const result = await grabVideoSnapshot(videoRef.current); const result = await grabVideoSnapshot(videoRef.current);
@ -297,7 +304,18 @@ export default function HlsVideoPlayer({
position: "top-center", position: "top-center",
}); });
} }
}, [camera, config?.ui?.timezone, currentTime, getVideoTime, t, videoRef]); } finally {
setIsSnapshotLoading(false);
}
}, [
camera,
config?.ui?.timezone,
currentTime,
getVideoTime,
isSnapshotLoading,
t,
videoRef,
]);
const onSnapshot = camera ? handleSnapshot : undefined; const onSnapshot = camera ? handleSnapshot : undefined;
return ( return (
@ -365,6 +383,7 @@ export default function HlsVideoPlayer({
} }
}} }}
onSnapshot={onSnapshot} onSnapshot={onSnapshot}
snapshotLoading={isSnapshotLoading}
snapshotTitle={t("snapshot.takeSnapshot", { ns: "views/live" })} snapshotTitle={t("snapshot.takeSnapshot", { ns: "views/live" })}
fullscreen={fullscreen} fullscreen={fullscreen}
toggleFullscreen={toggleFullscreen} toggleFullscreen={toggleFullscreen}

View File

@ -75,6 +75,7 @@ type VideoControlsProps = {
onSetPlaybackRate: (rate: number) => void; onSetPlaybackRate: (rate: number) => void;
onUploadFrame?: () => void; onUploadFrame?: () => void;
onSnapshot?: () => void; onSnapshot?: () => void;
snapshotLoading?: boolean;
snapshotTitle?: string; snapshotTitle?: string;
toggleFullscreen?: () => void; toggleFullscreen?: () => void;
containerRef?: React.MutableRefObject<HTMLDivElement | null>; containerRef?: React.MutableRefObject<HTMLDivElement | null>;
@ -98,6 +99,7 @@ export default function VideoControls({
onSetPlaybackRate, onSetPlaybackRate,
onUploadFrame, onUploadFrame,
onSnapshot, onSnapshot,
snapshotLoading = false,
snapshotTitle, snapshotTitle,
toggleFullscreen, toggleFullscreen,
containerRef, containerRef,
@ -302,10 +304,17 @@ export default function VideoControls({
{features.snapshot && onSnapshot && ( {features.snapshot && onSnapshot && (
<TbCameraDown <TbCameraDown
aria-label={snapshotTitle} aria-label={snapshotTitle}
className="size-5 cursor-pointer" aria-disabled={snapshotLoading}
className={cn(
"size-5",
snapshotLoading ? "cursor-not-allowed opacity-50" : "cursor-pointer",
)}
title={snapshotTitle} title={snapshotTitle}
onClick={(e: React.MouseEvent<SVGElement>) => { onClick={(e: React.MouseEvent<SVGElement>) => {
e.stopPropagation(); e.stopPropagation();
if (snapshotLoading) {
return;
}
onSnapshot(); onSnapshot();
}} }}
/> />