src.nth.io/

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke Hoersten <[email protected]>2026-06-15 08:31:00 -0500
committerLuke Hoersten <[email protected]>2026-06-15 08:31:00 -0500
commitb9a2df3e6c7b1184c29e96b7e01c14acd3f7ea56 (patch)
tree64857ce6820648774431f3b2a1aab9d9e6a3ad6a
parent827ea716db0ee8f58deb5af9ea67ca69cab58e99 (diff)
scrypted: 5s HTTP timeout + don't relaunch active stream on every event
Two changes to handle a burst of motion events without piling up startStream calls and tripping the 1s /state-POST timeout: - HTTP_TIMEOUT_MS 1000 → 5000. The firmware's single httpd task processes one TCP connection at a time; under heavy /frame streaming a /state {wake} can queue behind 1–3 in-flight /frames before landing. 1s was too tight. - handleCameraEvent now ignores repeat triggers if a stream is already live for that viewport (this.streams.has(name)) or already starting (streamStarting set). Sustained motion fires MotionSensor every ~500ms; we used to launch a fresh startStream each time, racing with the previous one's ffmpeg spawn + state POST.
-rw-r--r--scrypted/scrypted-viewport.ts27
1 files changed, 25 insertions, 2 deletions
diff --git a/scrypted/scrypted-viewport.ts b/scrypted/scrypted-viewport.ts
index a782745..68386e2 100644
--- a/scrypted/scrypted-viewport.ts
+++ b/scrypted/scrypted-viewport.ts
@@ -79,7 +79,13 @@ type SettingValue = any;
// Settings page so it can be tweaked without editing the script.
const DEFAULT_FRAME_INTERVAL_MS = 500; // ~2 fps — snapshot-based; cameras typically can't sustain faster
const REREGISTER_INTERVAL_MS = 5 * 60_000;
-const HTTP_TIMEOUT_MS = 1_000;
+// 5s gives /state + /config posts enough headroom to slip in between
+// /frame POSTs when the device is mid-stream. The firmware's single
+// httpd task processes one connection at a time; under heavy /frame
+// load a /state {wake} can queue behind 1–3 in-flight /frames before
+// landing. 1s was tripping when a burst of camera events triggered
+// back-to-back startStream calls.
+const HTTP_TIMEOUT_MS = 5_000;
const DEFAULT_IDLE_TIMEOUT_MS = 60_000;
const DEFAULT_BRIGHTNESS = 80;
@@ -493,6 +499,15 @@ class ScryptedViewportProvider extends ScryptedDeviceBase
// Camera event → stream
// ------------------------------------------------------------------------
+ // Per-viewport "stream is actively starting" guard. handleCameraEvent
+ // can fire multiple times in the same second (MotionSensor often
+ // re-asserts every ~500 ms while motion is sustained); without this,
+ // each event launches its own startStream which races with the
+ // previous one and saturates the firmware's httpd. If a stream is
+ // already live we just leave it running — the per-stream timeout
+ // anchored to the event still fires correctly.
+ private streamStarting = new Set<string>();
+
private handleCameraEvent(v: Viewport, details: any, data: any) {
const iface = details.eventInterface;
const allowed = v.triggers;
@@ -505,7 +520,15 @@ class ScryptedViewportProvider extends ScryptedDeviceBase
}
if (!trigger) return;
this.console.log(`event ${iface} -> "${v.name}": wake`);
- this.startStream(v).catch(e => this.console.error("startStream failed", e));
+ // If a stream is already in flight for this viewport, the event
+ // is just reinforcement — the existing ffmpeg child is already
+ // pushing frames. We do NOT relaunch (would race with previous).
+ if (this.streams.has(v.name)) return;
+ if (this.streamStarting.has(v.nativeId!)) return;
+ this.streamStarting.add(v.nativeId!);
+ this.startStream(v)
+ .catch(e => this.console.error("startStream failed", e))
+ .finally(() => this.streamStarting.delete(v.nativeId!));
}
private async startStream(v: Viewport) {