diff --git a/docker/main/requirements-wheels.txt b/docker/main/requirements-wheels.txt index 1a1043b19..30e32ba6a 100644 --- a/docker/main/requirements-wheels.txt +++ b/docker/main/requirements-wheels.txt @@ -48,7 +48,7 @@ onnxruntime == 1.22.* transformers == 4.45.* # Generative AI google-generativeai == 0.8.* -ollama == 0.5.* +ollama == 0.6.* openai == 1.65.* # push notifications py-vapid == 1.9.* diff --git a/docs/docs/configuration/genai/review_summaries.md b/docs/docs/configuration/genai/review_summaries.md index 04a5b5e94..99ee48d0f 100644 --- a/docs/docs/configuration/genai/review_summaries.md +++ b/docs/docs/configuration/genai/review_summaries.md @@ -31,40 +31,43 @@ Each installation and even camera can have different parameters for what is cons
Default Activity Context Prompt -``` -### Normal Activity Indicators (Level 0) -- Known/verified people in any zone at any time -- People with pets in residential areas -- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving -- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime -- Activity confined to public areas only (sidewalks, streets) without entering property at any time +```yaml +review: + genai: + activity_context_prompt: | + ### Normal Activity Indicators (Level 0) + - Known/verified people in any zone at any time + - People with pets in residential areas + - Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving + - Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime + - Activity confined to public areas only (sidewalks, streets) without entering property at any time -### Suspicious Activity Indicators (Level 1) -- **Testing or attempting to open doors/windows/handles on vehicles or buildings** — ALWAYS Level 1 regardless of time or duration -- **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** — ALWAYS Level 1 regardless of activity or duration -- Taking items that don't belong to them (packages, objects from porches/driveways) -- Climbing or jumping fences/barriers to access property -- Attempting to conceal actions or items from view -- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence + ### Suspicious Activity Indicators (Level 1) + - **Testing or attempting to open doors/windows/handles on vehicles or buildings** — ALWAYS Level 1 regardless of time or duration + - **Unidentified person in private areas (driveways, near vehicles/buildings) during late night/early morning (11 PM - 5 AM)** — ALWAYS Level 1 regardless of activity or duration + - Taking items that don't belong to them (packages, objects from porches/driveways) + - Climbing or jumping fences/barriers to access property + - Attempting to conceal actions or items from view + - Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence -### Critical Threat Indicators (Level 2) -- Holding break-in tools (crowbars, pry bars, bolt cutters) -- Weapons visible (guns, knives, bats used aggressively) -- Forced entry in progress -- Physical aggression or violence -- Active property damage or theft in progress + ### Critical Threat Indicators (Level 2) + - Holding break-in tools (crowbars, pry bars, bolt cutters) + - Weapons visible (guns, knives, bats used aggressively) + - Forced entry in progress + - Physical aggression or violence + - Active property damage or theft in progress -### Assessment Guidance -Evaluate in this order: + ### Assessment Guidance + Evaluate in this order: -1. **If person is verified/known** → Level 0 regardless of time or activity -2. **If person is unidentified:** - - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) → Level 1 - - Check actions: If testing doors/handles, taking items, climbing → Level 1 - - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service worker) → Level 0 -3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1) + 1. **If person is verified/known** → Level 0 regardless of time or activity + 2. **If person is unidentified:** + - Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) → Level 1 + - Check actions: If testing doors/handles, taking items, climbing → Level 1 + - Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service worker) → Level 0 + 3. **Escalate to Level 2 if:** Weapons, break-in tools, forced entry in progress, violence, or active property damage visible (escalates from Level 0 or 1) -The mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is. + The mere presence of an unidentified person in private areas during late night hours is inherently suspicious and warrants human review, regardless of what activity they appear to be doing or how brief the sequence is. ```
diff --git a/web/src/components/player/MsePlayer.tsx b/web/src/components/player/MsePlayer.tsx index bab00a2f8..1a2b1b6cb 100644 --- a/web/src/components/player/MsePlayer.tsx +++ b/web/src/components/player/MsePlayer.tsx @@ -80,12 +80,15 @@ function MSEPlayer({ const videoRef = useRef(null); const wsRef = useRef(null); const reconnectTIDRef = useRef(null); + const intentionalDisconnectRef = useRef(false); const ondataRef = useRef<((data: ArrayBufferLike) => void) | null>(null); const onmessageRef = useRef<{ [key: string]: (msg: { value: string; type: string }) => void; }>({}); const msRef = useRef(null); const mseCodecRef = useRef(null); + const mseTimeoutRef = useRef | null>(null); + const mseResponseReceivedRef = useRef(false); const wsURL = useMemo(() => { return `${baseUrl.replace(/^http/, "ws")}live/mse/api/ws?src=${camera}`; @@ -152,8 +155,11 @@ function MSEPlayer({ }, []); const onConnect = useCallback(() => { - if (!videoRef.current?.isConnected || !wsURL || wsRef.current) return false; + if (!videoRef.current?.isConnected || !wsURL || wsRef.current) { + return false; + } + intentionalDisconnectRef.current = false; setWsState(WebSocket.CONNECTING); setConnectTS(Date.now()); @@ -172,13 +178,50 @@ function MSEPlayer({ setBufferTimeout(undefined); } + // Clear any pending MSE timeout + if (mseTimeoutRef.current !== null) { + clearTimeout(mseTimeoutRef.current); + mseTimeoutRef.current = null; + } + + // Clear any pending reconnect attempts + if (reconnectTIDRef.current !== null) { + clearTimeout(reconnectTIDRef.current); + reconnectTIDRef.current = null; + } + setIsPlaying(false); if (wsRef.current) { - setWsState(WebSocket.CLOSED); - wsRef.current.close(); + const ws = wsRef.current; wsRef.current = null; + const currentReadyState = ws.readyState; + + intentionalDisconnectRef.current = true; + setWsState(WebSocket.CLOSED); + + // Remove event listeners to prevent them firing during close + try { + ws.removeEventListener("open", onOpen); + ws.removeEventListener("close", onClose); + } catch { + // Ignore errors removing listeners + } + + // Only call close() if the socket is OPEN or CLOSING + // For CONNECTING or CLOSED sockets, just let it die + if ( + currentReadyState === WebSocket.OPEN || + currentReadyState === WebSocket.CLOSING + ) { + try { + ws.close(); + } catch { + // Ignore close errors + } + } } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [bufferTimeout]); const handlePause = useCallback(() => { @@ -188,7 +231,14 @@ function MSEPlayer({ } }, [isPlaying, playbackEnabled]); - const onOpen = () => { + const onOpen = useCallback(() => { + // If we were marked for intentional disconnect while connecting, close immediately + if (intentionalDisconnectRef.current) { + wsRef.current?.close(); + wsRef.current = null; + return; + } + setWsState(WebSocket.OPEN); wsRef.current?.addEventListener("message", (ev) => { @@ -205,10 +255,27 @@ function MSEPlayer({ ondataRef.current = null; onmessageRef.current = {}; + // Reset the MSE response flag for this new connection + mseResponseReceivedRef.current = false; + + // Create a fresh MediaSource for this connection to avoid stale sourceopen events + // from previous connections interfering with this one + const MediaSourceConstructor = + "ManagedMediaSource" in window ? window.ManagedMediaSource : MediaSource; + // @ts-expect-error for typing + msRef.current = new MediaSourceConstructor(); + onMse(); - }; + // onMse is defined below and stable + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); const reconnect = (timeout?: number) => { + // Don't reconnect if intentional disconnect was flagged + if (intentionalDisconnectRef.current) { + return; + } + setWsState(WebSocket.CONNECTING); wsRef.current = null; @@ -221,28 +288,79 @@ function MSEPlayer({ }, delay); }; - const onClose = () => { + const onClose = useCallback(() => { + // Don't reconnect if this was an intentional disconnect + if (intentionalDisconnectRef.current) { + // Reset the flag so future connects are allowed + intentionalDisconnectRef.current = false; + return; + } + if (wsState === WebSocket.CLOSED) return; reconnect(); - }; + // reconnect is defined below and stable + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [wsState]); const sendWithTimeout = (value: object, timeout: number) => { return new Promise((resolve, reject) => { + // Don't start timeout if WS isn't connected - this can happen when + // sourceopen fires from a previous connection after we've already disconnected + if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) { + // Reject so caller knows this didn't work + reject(new Error("WebSocket not connected")); + return; + } + + // If we've already received an MSE response for this connection, don't start another timeout + if (mseResponseReceivedRef.current) { + resolve(); + return; + } + + // Clear any existing MSE timeout from a previous attempt + if (mseTimeoutRef.current !== null) { + clearTimeout(mseTimeoutRef.current); + mseTimeoutRef.current = null; + } + const timeoutId = setTimeout(() => { - reject(new Error("Timeout waiting for response")); + // Only reject if we haven't received a response yet + if (!mseResponseReceivedRef.current) { + mseTimeoutRef.current = null; + reject(new Error("Timeout waiting for response")); + } }, timeout); - send(value); + mseTimeoutRef.current = timeoutId; // Override the onmessageRef handler for mse type to resolve the promise on response const originalHandler = onmessageRef.current["mse"]; onmessageRef.current["mse"] = (msg) => { if (msg.type === "mse") { - clearTimeout(timeoutId); - if (originalHandler) originalHandler(msg); + // Mark that we've received the response + mseResponseReceivedRef.current = true; + + // Clear the timeout (use ref to clear the current one, not closure) + if (mseTimeoutRef.current !== null) { + clearTimeout(mseTimeoutRef.current); + mseTimeoutRef.current = null; + } + + // Call original handler in try-catch so errors don't prevent promise resolution + if (originalHandler) { + try { + originalHandler(msg); + } catch (e) { + // Don't reject - we got the response, just let the error bubble + } + } + resolve(); } }; + + send(value); }); }; @@ -292,13 +410,15 @@ function MSEPlayer({ }, (fallbackTimeout ?? 3) * 1000, ).catch(() => { + // Only report errors if we actually had a connection that failed + // If WS wasn't connected, this is a stale sourceopen event from a previous connection if (wsRef.current) { onDisconnect(); - } - if (isIOS || isSafari) { - handleError("mse-decode", "Safari cannot open MediaSource."); - } else { - handleError("startup", "Error opening MediaSource."); + if (isIOS || isSafari) { + handleError("mse-decode", "Safari cannot open MediaSource."); + } else { + handleError("startup", "Error opening MediaSource."); + } } }); }, @@ -532,13 +652,6 @@ function MSEPlayer({ return; } - // iOS 17.1+ uses ManagedMediaSource - const MediaSourceConstructor = - "ManagedMediaSource" in window ? window.ManagedMediaSource : MediaSource; - - // @ts-expect-error for typing - msRef.current = new MediaSourceConstructor(); - onConnect(); return () => {