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 [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}

View File

@ -75,6 +75,7 @@ type VideoControlsProps = {
onSetPlaybackRate: (rate: number) => void;
onUploadFrame?: () => void;
onSnapshot?: () => void;
snapshotLoading?: boolean;
snapshotTitle?: string;
toggleFullscreen?: () => void;
containerRef?: React.MutableRefObject<HTMLDivElement | null>;
@ -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 && (
<TbCameraDown
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}
onClick={(e: React.MouseEvent<SVGElement>) => {
e.stopPropagation();
if (snapshotLoading) {
return;
}
onSnapshot();
}}
/>