From 73f0ca663bf59c990dab691754014dd291936f3c Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Fri, 14 Nov 2025 21:25:01 -0600 Subject: [PATCH] ensure on demand recording stops when browser closes --- web/src/views/live/LiveCameraView.tsx | 34 +++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/web/src/views/live/LiveCameraView.tsx b/web/src/views/live/LiveCameraView.tsx index 9e639ab7b..65257326f 100644 --- a/web/src/views/live/LiveCameraView.tsx +++ b/web/src/views/live/LiveCameraView.tsx @@ -850,6 +850,29 @@ function FrigateCameraFeatures({ } }, [activeToastId, t]); + const endEventViaBeacon = useCallback(() => { + if (!recordingEventIdRef.current) return; + + const url = `${window.location.origin}/api/events/${recordingEventIdRef.current}/end`; + const payload = JSON.stringify({ + end_time: Math.ceil(Date.now() / 1000), + }); + + // this needs to be a synchronous XMLHttpRequest to guarantee the PUT + // reaches the server before the browser kills the page + const xhr = new XMLHttpRequest(); + try { + xhr.open("PUT", url, false); + xhr.setRequestHeader("Content-Type", "application/json"); + xhr.setRequestHeader("X-CSRF-TOKEN", "1"); + xhr.setRequestHeader("X-CACHE-BYPASS", "1"); + xhr.withCredentials = true; + xhr.send(payload); + } catch (e) { + // Silently ignore errors during unload + } + }, []); + const handleEventButtonClick = useCallback(() => { if (isRecording) { endEvent(); @@ -887,8 +910,19 @@ function FrigateCameraFeatures({ }, [camera.name, isRestreamed, preferredLiveMode, t]); useEffect(() => { + // Handle page unload/close (browser close, tab close, refresh, navigation to external site) + const handleBeforeUnload = () => { + if (recordingEventIdRef.current) { + endEventViaBeacon(); + } + }; + + window.addEventListener("beforeunload", handleBeforeUnload); + // ensure manual event is stopped when component unmounts return () => { + window.removeEventListener("beforeunload", handleBeforeUnload); + if (recordingEventIdRef.current) { endEvent(); }