diff options
| author | Luke Hoersten <[email protected]> | 2026-06-15 08:31:00 -0500 |
|---|---|---|
| committer | Luke Hoersten <[email protected]> | 2026-06-15 08:31:00 -0500 |
| commit | b9a2df3e6c7b1184c29e96b7e01c14acd3f7ea56 (patch) | |
| tree | 64857ce6820648774431f3b2a1aab9d9e6a3ad6a | |
| parent | 827ea716db0ee8f58deb5af9ea67ca69cab58e99 (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.ts | 27 |
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) { |
