diff --git a/web/src/components/player/MsePlayer.tsx b/web/src/components/player/MsePlayer.tsx index b8ab2da66..6a41a93e6 100644 --- a/web/src/components/player/MsePlayer.tsx +++ b/web/src/components/player/MsePlayer.tsx @@ -28,8 +28,6 @@ function MSEPlayer({ onPlaying, setFullResolution, }: MSEPlayerProps) { - let connectTS: number = 0; - const RECONNECT_TIMEOUT: number = 30000; const CODECS: string[] = [ @@ -46,6 +44,7 @@ function MSEPlayer({ const visibilityCheck: boolean = !pip; const [wsState, setWsState] = useState(WebSocket.CLOSED); + const [connectTS, setConnectTS] = useState(0); const videoRef = useRef(null); const wsRef = useRef(null); @@ -103,14 +102,14 @@ function MSEPlayer({ setWsState(WebSocket.CONNECTING); - // TODO may need to check this later - // eslint-disable-next-line - connectTS = Date.now(); + setConnectTS(Date.now()); wsRef.current = new WebSocket(wsURL); wsRef.current.binaryType = "arraybuffer"; - wsRef.current.addEventListener("open", () => onOpen()); - wsRef.current.addEventListener("close", () => onClose()); + wsRef.current.addEventListener("open", onOpen); + wsRef.current.addEventListener("close", onClose); + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps }, [wsURL]); const onDisconnect = useCallback(() => { @@ -121,7 +120,7 @@ function MSEPlayer({ } }, []); - const onOpen = useCallback(() => { + const onOpen = () => { setWsState(WebSocket.OPEN); wsRef.current?.addEventListener("message", (ev) => { @@ -139,23 +138,25 @@ function MSEPlayer({ onmessageRef.current = {}; onMse(); - // only run once - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const onClose = useCallback(() => { - if (wsState === WebSocket.CLOSED) return; + }; + const reconnect = (timeout?: number) => { setWsState(WebSocket.CONNECTING); wsRef.current = null; - const delay = Math.max(RECONNECT_TIMEOUT - (Date.now() - connectTS), 0); + const delay = + timeout ?? Math.max(RECONNECT_TIMEOUT - (Date.now() - connectTS), 0); reconnectTIDRef.current = window.setTimeout(() => { reconnectTIDRef.current = null; onConnect(); }, delay); - }, [wsState, connectTS, onConnect]); + }; + + const onClose = () => { + if (wsState === WebSocket.CLOSED) return; + reconnect(); + }; const onMse = () => { if ("ManagedMediaSource" in window) { @@ -305,7 +306,13 @@ function MSEPlayer({ onLoadedData={onPlaying} onLoadedMetadata={handleLoadedMetadata} muted={!audioEnabled} - onError={onClose} + onError={() => { + if (wsRef.current) { + wsRef.current.close(); + wsRef.current = null; + reconnect(5000); + } + }} /> ); } diff --git a/web/src/components/settings/MotionTuner.tsx b/web/src/components/settings/MotionTuner.tsx index c127f8373..ad149de54 100644 --- a/web/src/components/settings/MotionTuner.tsx +++ b/web/src/components/settings/MotionTuner.tsx @@ -42,7 +42,7 @@ export default function MotionTuner({ const [changedValue, setChangedValue] = useState(false); const [isLoading, setIsLoading] = useState(false); - const { addMessage, clearMessages } = useContext(StatusBarMessagesContext)!; + const { addMessage, removeMessage } = useContext(StatusBarMessagesContext)!; const { send: sendMotionThreshold } = useMotionThreshold(selectedCamera); const { send: sendMotionContourArea } = useMotionContourArea(selectedCamera); @@ -148,21 +148,23 @@ export default function MotionTuner({ const onCancel = useCallback(() => { setMotionSettings(origMotionSettings); setChangedValue(false); - clearMessages("motion_tuner"); - }, [origMotionSettings, clearMessages]); + removeMessage("motion_tuner", `motion_tuner_${selectedCamera}`); + }, [origMotionSettings, removeMessage, selectedCamera]); useEffect(() => { if (changedValue) { addMessage( "motion_tuner", - "Unsaved motion tuner changes", + `Unsaved motion tuner changes (${selectedCamera})`, undefined, - "motion_tuner", + `motion_tuner_${selectedCamera}`, ); } else { - clearMessages("motion_tuner"); + removeMessage("motion_tuner", `motion_tuner_${selectedCamera}`); } - }, [changedValue, addMessage, clearMessages]); + // we know that these deps are correct + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [changedValue, selectedCamera]); useEffect(() => { document.title = "Motion Tuner - Frigate"; diff --git a/web/src/components/settings/PolygonItem.tsx b/web/src/components/settings/PolygonItem.tsx index 2c5b92d37..836bc38f7 100644 --- a/web/src/components/settings/PolygonItem.tsx +++ b/web/src/components/settings/PolygonItem.tsx @@ -26,7 +26,7 @@ import { toRGBColorString, } from "@/utils/canvasUtil"; import { Polygon, PolygonType } from "@/types/canvas"; -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useContext, useMemo, useState } from "react"; import axios from "axios"; import { Toaster } from "@/components/ui/sonner"; import { toast } from "sonner"; @@ -34,6 +34,7 @@ import useSWR from "swr"; import { FrigateConfig } from "@/types/frigateConfig"; import { reviewQueries } from "@/utils/zoneEdutUtil"; import IconWrapper from "../ui/icon-wrapper"; +import { StatusBarMessagesContext } from "@/context/statusbar-provider"; type PolygonItemProps = { polygon: Polygon; @@ -57,6 +58,7 @@ export default function PolygonItem({ const { data: config, mutate: updateConfig } = useSWR("config"); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const { addMessage } = useContext(StatusBarMessagesContext)!; const [isLoading, setIsLoading] = useState(false); const cameraConfig = useMemo(() => { @@ -198,6 +200,12 @@ export default function PolygonItem({ const handleDelete = () => { setActivePolygonIndex(undefined); saveToConfig(polygon); + addMessage( + "masks_zones", + "Restart required (masks/zones changed)", + undefined, + "masks_zones", + ); }; return ( diff --git a/web/src/context/statusbar-provider.tsx b/web/src/context/statusbar-provider.tsx index 60954f7be..03c4752dc 100644 --- a/web/src/context/statusbar-provider.tsx +++ b/web/src/context/statusbar-provider.tsx @@ -29,7 +29,7 @@ type StatusBarMessagesContextValue = { color?: string, messageId?: string, link?: string, - ) => string; + ) => string | undefined; removeMessage: (key: string, messageId: string) => void; clearMessages: (key: string) => void; }; @@ -52,26 +52,51 @@ export function StatusBarMessagesProvider({ messageId?: string, link?: string, ) => { - const id = messageId || Date.now().toString(); - const msgColor = color || "text-danger"; - setMessagesState((prevMessages) => ({ - ...prevMessages, - [key]: [ - ...(prevMessages[key] || []), - { id, text: message, color: msgColor, link }, - ], - })); + if (!key || !message) return; + + const id = messageId ?? Date.now().toString(); + const msgColor = color ?? "text-danger"; + + setMessagesState((prevMessages) => { + const existingMessages = prevMessages[key] || []; + // Check if a message with the same ID already exists + const messageIndex = existingMessages.findIndex((msg) => msg.id === id); + + const newMessage = { id, text: message, color: msgColor, link }; + + // If the message exists, replace it, otherwise add the new message + let updatedMessages; + if (messageIndex > -1) { + updatedMessages = [ + ...existingMessages.slice(0, messageIndex), + newMessage, + ...existingMessages.slice(messageIndex + 1), + ]; + } else { + updatedMessages = [...existingMessages, newMessage]; + } + + return { + ...prevMessages, + [key]: updatedMessages, + }; + }); + return id; }, [], ); - const removeMessage = useCallback((key: string, messageId: string) => { - setMessagesState((prevMessages) => ({ - ...prevMessages, - [key]: prevMessages[key].filter((msg) => msg.id !== messageId), - })); - }, []); + const removeMessage = useCallback( + (key: string, messageId: string) => { + if (!messages || !key || !messages[key]) return; + setMessagesState((prevMessages) => ({ + ...prevMessages, + [key]: prevMessages[key].filter((msg) => msg.id !== messageId), + })); + }, + [messages], + ); const clearMessages = useCallback((key: string) => { setMessagesState((prevMessages) => {