Skip to content

Commit 74d14cb

Browse files
hawkeye217NickM-27
andauthored
Miscellaneous Fixes (0.17 beta) (#21558)
* mse player improvements - fix WebSocket race condition by registering message handlers before sending and avoid closing CONNECTING sockets to eliminate "Socket is not connected" errors. - attempt to resolve Safari MSE timeout and handler issues by wrapping temporary handlers in try/catch and stabilizing the permanent mse handler so SourceBuffer setup completes reliably. - add intentional disconnect tracking to prevent unwanted reconnects during navigation/StrictMode cycles * Update Ollama * additional MSE tweaks * Turn activity context prompt into a yaml example --------- Co-authored-by: Nicolas Mowen <[email protected]>
1 parent 99d48ec commit 74d14cb

File tree

3 files changed

+174
-58
lines changed

3 files changed

+174
-58
lines changed

docker/main/requirements-wheels.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ onnxruntime == 1.22.*
4848
transformers == 4.45.*
4949
# Generative AI
5050
google-generativeai == 0.8.*
51-
ollama == 0.5.*
51+
ollama == 0.6.*
5252
openai == 1.65.*
5353
# push notifications
5454
py-vapid == 1.9.*

docs/docs/configuration/genai/review_summaries.md

Lines changed: 37 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -31,40 +31,43 @@ Each installation and even camera can have different parameters for what is cons
3131
<details>
3232
<summary>Default Activity Context Prompt</summary>
3333

34-
```
35-
### Normal Activity Indicators (Level 0)
36-
- Known/verified people in any zone at any time
37-
- People with pets in residential areas
38-
- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving
39-
- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime
40-
- Activity confined to public areas only (sidewalks, streets) without entering property at any time
41-
42-
### Suspicious Activity Indicators (Level 1)
43-
- **Testing or attempting to open doors/windows/handles on vehicles or buildings** — ALWAYS Level 1 regardless of time or duration
44-
- **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
45-
- Taking items that don't belong to them (packages, objects from porches/driveways)
46-
- Climbing or jumping fences/barriers to access property
47-
- Attempting to conceal actions or items from view
48-
- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence
49-
50-
### Critical Threat Indicators (Level 2)
51-
- Holding break-in tools (crowbars, pry bars, bolt cutters)
52-
- Weapons visible (guns, knives, bats used aggressively)
53-
- Forced entry in progress
54-
- Physical aggression or violence
55-
- Active property damage or theft in progress
56-
57-
### Assessment Guidance
58-
Evaluate in this order:
59-
60-
1. **If person is verified/known** → Level 0 regardless of time or activity
61-
2. **If person is unidentified:**
62-
- Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) → Level 1
63-
- Check actions: If testing doors/handles, taking items, climbing → Level 1
64-
- Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service worker) → Level 0
65-
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)
66-
67-
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.
34+
```yaml
35+
review:
36+
genai:
37+
activity_context_prompt: |
38+
### Normal Activity Indicators (Level 0)
39+
- Known/verified people in any zone at any time
40+
- People with pets in residential areas
41+
- Deliveries or services during daytime/evening (6 AM - 10 PM): carrying packages to doors/porches, placing items, leaving
42+
- Services/maintenance workers with visible tools, uniforms, or service vehicles during daytime
43+
- Activity confined to public areas only (sidewalks, streets) without entering property at any time
44+
45+
### Suspicious Activity Indicators (Level 1)
46+
- **Testing or attempting to open doors/windows/handles on vehicles or buildings** — ALWAYS Level 1 regardless of time or duration
47+
- **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
48+
- Taking items that don't belong to them (packages, objects from porches/driveways)
49+
- Climbing or jumping fences/barriers to access property
50+
- Attempting to conceal actions or items from view
51+
- Prolonged loitering: remaining in same area without visible purpose throughout most of the sequence
52+
53+
### Critical Threat Indicators (Level 2)
54+
- Holding break-in tools (crowbars, pry bars, bolt cutters)
55+
- Weapons visible (guns, knives, bats used aggressively)
56+
- Forced entry in progress
57+
- Physical aggression or violence
58+
- Active property damage or theft in progress
59+
60+
### Assessment Guidance
61+
Evaluate in this order:
62+
63+
1. **If person is verified/known** → Level 0 regardless of time or activity
64+
2. **If person is unidentified:**
65+
- Check time: If late night/early morning (11 PM - 5 AM) AND in private areas (driveways, near vehicles/buildings) → Level 1
66+
- Check actions: If testing doors/handles, taking items, climbing → Level 1
67+
- Otherwise, if daytime/evening (6 AM - 10 PM) with clear legitimate purpose (delivery, service worker) → Level 0
68+
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)
69+
70+
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.
6871
```
6972
7073
</details>

web/src/components/player/MsePlayer.tsx

Lines changed: 136 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,15 @@ function MSEPlayer({
8080
const videoRef = useRef<HTMLVideoElement>(null);
8181
const wsRef = useRef<WebSocket | null>(null);
8282
const reconnectTIDRef = useRef<number | null>(null);
83+
const intentionalDisconnectRef = useRef<boolean>(false);
8384
const ondataRef = useRef<((data: ArrayBufferLike) => void) | null>(null);
8485
const onmessageRef = useRef<{
8586
[key: string]: (msg: { value: string; type: string }) => void;
8687
}>({});
8788
const msRef = useRef<MediaSource | null>(null);
8889
const mseCodecRef = useRef<string | null>(null);
90+
const mseTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
91+
const mseResponseReceivedRef = useRef<boolean>(false);
8992

9093
const wsURL = useMemo(() => {
9194
return `${baseUrl.replace(/^http/, "ws")}live/mse/api/ws?src=${camera}`;
@@ -152,8 +155,11 @@ function MSEPlayer({
152155
}, []);
153156

154157
const onConnect = useCallback(() => {
155-
if (!videoRef.current?.isConnected || !wsURL || wsRef.current) return false;
158+
if (!videoRef.current?.isConnected || !wsURL || wsRef.current) {
159+
return false;
160+
}
156161

162+
intentionalDisconnectRef.current = false;
157163
setWsState(WebSocket.CONNECTING);
158164

159165
setConnectTS(Date.now());
@@ -172,13 +178,50 @@ function MSEPlayer({
172178
setBufferTimeout(undefined);
173179
}
174180

181+
// Clear any pending MSE timeout
182+
if (mseTimeoutRef.current !== null) {
183+
clearTimeout(mseTimeoutRef.current);
184+
mseTimeoutRef.current = null;
185+
}
186+
187+
// Clear any pending reconnect attempts
188+
if (reconnectTIDRef.current !== null) {
189+
clearTimeout(reconnectTIDRef.current);
190+
reconnectTIDRef.current = null;
191+
}
192+
175193
setIsPlaying(false);
176194

177195
if (wsRef.current) {
178-
setWsState(WebSocket.CLOSED);
179-
wsRef.current.close();
196+
const ws = wsRef.current;
180197
wsRef.current = null;
198+
const currentReadyState = ws.readyState;
199+
200+
intentionalDisconnectRef.current = true;
201+
setWsState(WebSocket.CLOSED);
202+
203+
// Remove event listeners to prevent them firing during close
204+
try {
205+
ws.removeEventListener("open", onOpen);
206+
ws.removeEventListener("close", onClose);
207+
} catch {
208+
// Ignore errors removing listeners
209+
}
210+
211+
// Only call close() if the socket is OPEN or CLOSING
212+
// For CONNECTING or CLOSED sockets, just let it die
213+
if (
214+
currentReadyState === WebSocket.OPEN ||
215+
currentReadyState === WebSocket.CLOSING
216+
) {
217+
try {
218+
ws.close();
219+
} catch {
220+
// Ignore close errors
221+
}
222+
}
181223
}
224+
// eslint-disable-next-line react-hooks/exhaustive-deps
182225
}, [bufferTimeout]);
183226

184227
const handlePause = useCallback(() => {
@@ -188,7 +231,14 @@ function MSEPlayer({
188231
}
189232
}, [isPlaying, playbackEnabled]);
190233

191-
const onOpen = () => {
234+
const onOpen = useCallback(() => {
235+
// If we were marked for intentional disconnect while connecting, close immediately
236+
if (intentionalDisconnectRef.current) {
237+
wsRef.current?.close();
238+
wsRef.current = null;
239+
return;
240+
}
241+
192242
setWsState(WebSocket.OPEN);
193243

194244
wsRef.current?.addEventListener("message", (ev) => {
@@ -205,10 +255,27 @@ function MSEPlayer({
205255
ondataRef.current = null;
206256
onmessageRef.current = {};
207257

258+
// Reset the MSE response flag for this new connection
259+
mseResponseReceivedRef.current = false;
260+
261+
// Create a fresh MediaSource for this connection to avoid stale sourceopen events
262+
// from previous connections interfering with this one
263+
const MediaSourceConstructor =
264+
"ManagedMediaSource" in window ? window.ManagedMediaSource : MediaSource;
265+
// @ts-expect-error for typing
266+
msRef.current = new MediaSourceConstructor();
267+
208268
onMse();
209-
};
269+
// onMse is defined below and stable
270+
// eslint-disable-next-line react-hooks/exhaustive-deps
271+
}, []);
210272

211273
const reconnect = (timeout?: number) => {
274+
// Don't reconnect if intentional disconnect was flagged
275+
if (intentionalDisconnectRef.current) {
276+
return;
277+
}
278+
212279
setWsState(WebSocket.CONNECTING);
213280
wsRef.current = null;
214281

@@ -221,28 +288,79 @@ function MSEPlayer({
221288
}, delay);
222289
};
223290

224-
const onClose = () => {
291+
const onClose = useCallback(() => {
292+
// Don't reconnect if this was an intentional disconnect
293+
if (intentionalDisconnectRef.current) {
294+
// Reset the flag so future connects are allowed
295+
intentionalDisconnectRef.current = false;
296+
return;
297+
}
298+
225299
if (wsState === WebSocket.CLOSED) return;
226300
reconnect();
227-
};
301+
// reconnect is defined below and stable
302+
// eslint-disable-next-line react-hooks/exhaustive-deps
303+
}, [wsState]);
228304

229305
const sendWithTimeout = (value: object, timeout: number) => {
230306
return new Promise<void>((resolve, reject) => {
307+
// Don't start timeout if WS isn't connected - this can happen when
308+
// sourceopen fires from a previous connection after we've already disconnected
309+
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
310+
// Reject so caller knows this didn't work
311+
reject(new Error("WebSocket not connected"));
312+
return;
313+
}
314+
315+
// If we've already received an MSE response for this connection, don't start another timeout
316+
if (mseResponseReceivedRef.current) {
317+
resolve();
318+
return;
319+
}
320+
321+
// Clear any existing MSE timeout from a previous attempt
322+
if (mseTimeoutRef.current !== null) {
323+
clearTimeout(mseTimeoutRef.current);
324+
mseTimeoutRef.current = null;
325+
}
326+
231327
const timeoutId = setTimeout(() => {
232-
reject(new Error("Timeout waiting for response"));
328+
// Only reject if we haven't received a response yet
329+
if (!mseResponseReceivedRef.current) {
330+
mseTimeoutRef.current = null;
331+
reject(new Error("Timeout waiting for response"));
332+
}
233333
}, timeout);
234334

235-
send(value);
335+
mseTimeoutRef.current = timeoutId;
236336

237337
// Override the onmessageRef handler for mse type to resolve the promise on response
238338
const originalHandler = onmessageRef.current["mse"];
239339
onmessageRef.current["mse"] = (msg) => {
240340
if (msg.type === "mse") {
241-
clearTimeout(timeoutId);
242-
if (originalHandler) originalHandler(msg);
341+
// Mark that we've received the response
342+
mseResponseReceivedRef.current = true;
343+
344+
// Clear the timeout (use ref to clear the current one, not closure)
345+
if (mseTimeoutRef.current !== null) {
346+
clearTimeout(mseTimeoutRef.current);
347+
mseTimeoutRef.current = null;
348+
}
349+
350+
// Call original handler in try-catch so errors don't prevent promise resolution
351+
if (originalHandler) {
352+
try {
353+
originalHandler(msg);
354+
} catch (e) {
355+
// Don't reject - we got the response, just let the error bubble
356+
}
357+
}
358+
243359
resolve();
244360
}
245361
};
362+
363+
send(value);
246364
});
247365
};
248366

@@ -292,13 +410,15 @@ function MSEPlayer({
292410
},
293411
(fallbackTimeout ?? 3) * 1000,
294412
).catch(() => {
413+
// Only report errors if we actually had a connection that failed
414+
// If WS wasn't connected, this is a stale sourceopen event from a previous connection
295415
if (wsRef.current) {
296416
onDisconnect();
297-
}
298-
if (isIOS || isSafari) {
299-
handleError("mse-decode", "Safari cannot open MediaSource.");
300-
} else {
301-
handleError("startup", "Error opening MediaSource.");
417+
if (isIOS || isSafari) {
418+
handleError("mse-decode", "Safari cannot open MediaSource.");
419+
} else {
420+
handleError("startup", "Error opening MediaSource.");
421+
}
302422
}
303423
});
304424
},
@@ -532,13 +652,6 @@ function MSEPlayer({
532652
return;
533653
}
534654

535-
// iOS 17.1+ uses ManagedMediaSource
536-
const MediaSourceConstructor =
537-
"ManagedMediaSource" in window ? window.ManagedMediaSource : MediaSource;
538-
539-
// @ts-expect-error for typing
540-
msRef.current = new MediaSourceConstructor();
541-
542655
onConnect();
543656

544657
return () => {

0 commit comments

Comments
 (0)