From b9a2df3e6c7b1184c29e96b7e01c14acd3f7ea56 Mon Sep 17 00:00:00 2001 From: Luke Hoersten Date: Mon, 15 Jun 2026 08:31:00 -0500 Subject: scrypted: 5s HTTP timeout + don't relaunch active stream on every event MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- scrypted/scrypted-viewport.ts | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) (limited to 'scrypted') 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(); + 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) { -- cgit v1.2.3