mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-01-28 23:14:56 +03:00
Miscellaneous Fixes (0.17 beta) (#21558)
Some checks are pending
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
Some checks are pending
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
* 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 <nickmowen213@gmail.com>
This commit is contained in:
parent
99d48ecbc3
commit
74d14cb8ca
@ -48,7 +48,7 @@ onnxruntime == 1.22.*
|
|||||||
transformers == 4.45.*
|
transformers == 4.45.*
|
||||||
# Generative AI
|
# Generative AI
|
||||||
google-generativeai == 0.8.*
|
google-generativeai == 0.8.*
|
||||||
ollama == 0.5.*
|
ollama == 0.6.*
|
||||||
openai == 1.65.*
|
openai == 1.65.*
|
||||||
# push notifications
|
# push notifications
|
||||||
py-vapid == 1.9.*
|
py-vapid == 1.9.*
|
||||||
|
|||||||
@ -31,7 +31,10 @@ Each installation and even camera can have different parameters for what is cons
|
|||||||
<details>
|
<details>
|
||||||
<summary>Default Activity Context Prompt</summary>
|
<summary>Default Activity Context Prompt</summary>
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
|
review:
|
||||||
|
genai:
|
||||||
|
activity_context_prompt: |
|
||||||
### Normal Activity Indicators (Level 0)
|
### Normal Activity Indicators (Level 0)
|
||||||
- Known/verified people in any zone at any time
|
- Known/verified people in any zone at any time
|
||||||
- People with pets in residential areas
|
- People with pets in residential areas
|
||||||
|
|||||||
@ -80,12 +80,15 @@ function MSEPlayer({
|
|||||||
const videoRef = useRef<HTMLVideoElement>(null);
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
const wsRef = useRef<WebSocket | null>(null);
|
const wsRef = useRef<WebSocket | null>(null);
|
||||||
const reconnectTIDRef = useRef<number | null>(null);
|
const reconnectTIDRef = useRef<number | null>(null);
|
||||||
|
const intentionalDisconnectRef = useRef<boolean>(false);
|
||||||
const ondataRef = useRef<((data: ArrayBufferLike) => void) | null>(null);
|
const ondataRef = useRef<((data: ArrayBufferLike) => void) | null>(null);
|
||||||
const onmessageRef = useRef<{
|
const onmessageRef = useRef<{
|
||||||
[key: string]: (msg: { value: string; type: string }) => void;
|
[key: string]: (msg: { value: string; type: string }) => void;
|
||||||
}>({});
|
}>({});
|
||||||
const msRef = useRef<MediaSource | null>(null);
|
const msRef = useRef<MediaSource | null>(null);
|
||||||
const mseCodecRef = useRef<string | null>(null);
|
const mseCodecRef = useRef<string | null>(null);
|
||||||
|
const mseTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const mseResponseReceivedRef = useRef<boolean>(false);
|
||||||
|
|
||||||
const wsURL = useMemo(() => {
|
const wsURL = useMemo(() => {
|
||||||
return `${baseUrl.replace(/^http/, "ws")}live/mse/api/ws?src=${camera}`;
|
return `${baseUrl.replace(/^http/, "ws")}live/mse/api/ws?src=${camera}`;
|
||||||
@ -152,8 +155,11 @@ function MSEPlayer({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onConnect = useCallback(() => {
|
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);
|
setWsState(WebSocket.CONNECTING);
|
||||||
|
|
||||||
setConnectTS(Date.now());
|
setConnectTS(Date.now());
|
||||||
@ -172,13 +178,50 @@ function MSEPlayer({
|
|||||||
setBufferTimeout(undefined);
|
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);
|
setIsPlaying(false);
|
||||||
|
|
||||||
if (wsRef.current) {
|
if (wsRef.current) {
|
||||||
setWsState(WebSocket.CLOSED);
|
const ws = wsRef.current;
|
||||||
wsRef.current.close();
|
|
||||||
wsRef.current = null;
|
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]);
|
}, [bufferTimeout]);
|
||||||
|
|
||||||
const handlePause = useCallback(() => {
|
const handlePause = useCallback(() => {
|
||||||
@ -188,7 +231,14 @@ function MSEPlayer({
|
|||||||
}
|
}
|
||||||
}, [isPlaying, playbackEnabled]);
|
}, [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);
|
setWsState(WebSocket.OPEN);
|
||||||
|
|
||||||
wsRef.current?.addEventListener("message", (ev) => {
|
wsRef.current?.addEventListener("message", (ev) => {
|
||||||
@ -205,10 +255,27 @@ function MSEPlayer({
|
|||||||
ondataRef.current = null;
|
ondataRef.current = null;
|
||||||
onmessageRef.current = {};
|
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();
|
||||||
};
|
// onMse is defined below and stable
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const reconnect = (timeout?: number) => {
|
const reconnect = (timeout?: number) => {
|
||||||
|
// Don't reconnect if intentional disconnect was flagged
|
||||||
|
if (intentionalDisconnectRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setWsState(WebSocket.CONNECTING);
|
setWsState(WebSocket.CONNECTING);
|
||||||
wsRef.current = null;
|
wsRef.current = null;
|
||||||
|
|
||||||
@ -221,28 +288,79 @@ function MSEPlayer({
|
|||||||
}, delay);
|
}, 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;
|
if (wsState === WebSocket.CLOSED) return;
|
||||||
reconnect();
|
reconnect();
|
||||||
};
|
// reconnect is defined below and stable
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [wsState]);
|
||||||
|
|
||||||
const sendWithTimeout = (value: object, timeout: number) => {
|
const sendWithTimeout = (value: object, timeout: number) => {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((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(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
|
// Only reject if we haven't received a response yet
|
||||||
|
if (!mseResponseReceivedRef.current) {
|
||||||
|
mseTimeoutRef.current = null;
|
||||||
reject(new Error("Timeout waiting for response"));
|
reject(new Error("Timeout waiting for response"));
|
||||||
|
}
|
||||||
}, timeout);
|
}, timeout);
|
||||||
|
|
||||||
send(value);
|
mseTimeoutRef.current = timeoutId;
|
||||||
|
|
||||||
// Override the onmessageRef handler for mse type to resolve the promise on response
|
// Override the onmessageRef handler for mse type to resolve the promise on response
|
||||||
const originalHandler = onmessageRef.current["mse"];
|
const originalHandler = onmessageRef.current["mse"];
|
||||||
onmessageRef.current["mse"] = (msg) => {
|
onmessageRef.current["mse"] = (msg) => {
|
||||||
if (msg.type === "mse") {
|
if (msg.type === "mse") {
|
||||||
clearTimeout(timeoutId);
|
// Mark that we've received the response
|
||||||
if (originalHandler) originalHandler(msg);
|
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();
|
resolve();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
send(value);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -292,14 +410,16 @@ function MSEPlayer({
|
|||||||
},
|
},
|
||||||
(fallbackTimeout ?? 3) * 1000,
|
(fallbackTimeout ?? 3) * 1000,
|
||||||
).catch(() => {
|
).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) {
|
if (wsRef.current) {
|
||||||
onDisconnect();
|
onDisconnect();
|
||||||
}
|
|
||||||
if (isIOS || isSafari) {
|
if (isIOS || isSafari) {
|
||||||
handleError("mse-decode", "Safari cannot open MediaSource.");
|
handleError("mse-decode", "Safari cannot open MediaSource.");
|
||||||
} else {
|
} else {
|
||||||
handleError("startup", "Error opening MediaSource.");
|
handleError("startup", "Error opening MediaSource.");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{ once: true },
|
{ once: true },
|
||||||
@ -532,13 +652,6 @@ function MSEPlayer({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// iOS 17.1+ uses ManagedMediaSource
|
|
||||||
const MediaSourceConstructor =
|
|
||||||
"ManagedMediaSource" in window ? window.ManagedMediaSource : MediaSource;
|
|
||||||
|
|
||||||
// @ts-expect-error for typing
|
|
||||||
msRef.current = new MediaSourceConstructor();
|
|
||||||
|
|
||||||
onConnect();
|
onConnect();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user