src.nth.io/

summaryrefslogtreecommitdiff
AgeCommit message (Collapse)AuthorFilesLines
2026-07-01release: v1.3.0v1.3.0Luke Hoersten2-2/+2
Unify firmware and plugin versions for the production release. Highlights since v1.1.0/1.2.0: - event wake fixed (self-healing camera-listener re-attach after the reload/ add storage race); doorbell/motion/person confirmed on hardware - stop() fully tears down (stop+start == fresh load); stale childId pruning; live-stream events ignored (no queue/relaunch) - triggers default to person+doorbell; doorbell hidden for non-doorbell cams - cold-start (wake -> live video) ~6s -> ~0.7s: prebuffered substream by id + request >= GOP + burst-friendly ffmpeg flags; snapshot removed - firmware: Loading screen on new stream connection (no stale-frame flash) - logging trimmed for chatty cameras - README/protocol docs rewritten for the TCP-socket streaming model Set VIEWPORT_VERSION and scrypted/package.json to 1.3.0.
2026-07-01scrypted: stamp SCRIPT_VERSION = 0be8f18Luke Hoersten1-1/+1
2026-07-01scrypted: trim stream logging volumeLuke Hoersten1-18/+30
A chatty camera keeps a viewport streaming for long stretches, and the old logging emitted ~5 cold-start stamps + a 10s window line per ~10s per stream. Cut it down: - keep a single cold-start stamp (first ffmpeg frame); drop spawned / first stdout byte / first socket.write. - gate the 10s window line on noteworthy windows only (socket-not-ready drops, a buffer-cap flush this window, firmware shedding >=2fps, or painted <20fps); healthy 24fps windows print nothing — the per-wake lines already show liveness. - log "registered" only when the pushed config actually changes, not on every 5-minute reregister cycle. lastRegisterSig cleared on stop() for fresh-load parity. No behavior change beyond logging.
2026-06-30docs: reframe protocol sections for the TCP-socket streaming modelLuke Hoersten1-10/+12
The Wake/Sleep, race-handling, and idempotency sections still described the old model where Scrypted streamed via POST /frame and learned of sleep from a /frame 409. Streaming now runs over the TCP data socket (:81); the firmware /frame endpoint remains only for one-shot snapshots/debug. Reframe the recovery model accordingly: - race-free property: frames arriving while asleep are discarded by the decode task (stream socket) / rejected 409 (/frame) — neither re-wakes; sleep is learned from the device's state=sleep callback + Scrypted's safety timer - drop every "recovered by the next /frame returning 409" clause in favor of the safety timer + discard-while-asleep - idempotency table: add the stream-socket row (paints if awake, discarded if asleep, skip-oldest) - Loading screen: shown on wake AND each new stream connection until first paint - Philosophy: list the MJPEG stream server (:81) and /firmware Left the POST /frame endpoint's own API/semantics intact — it still exists.
2026-06-30docs: update main README streaming section to the shipped TCP-socket pathLuke Hoersten1-16/+17
The Scrypted Integration section still framed the snapshot POST /frame path as v1 and MJPEG streaming as a future POST /stream milestone, contradicting the already-current "What's next" perf section. Bring it in line: - v1 is the live ffmpeg MJPEG stream over the raw TCP data socket (:81) with prebuffer fast-start (~0.7s wake-to-video); POST /stream was never the shape - replace the /frame streaming code example with the TCP socket + framed-MJPEG sketch; brightness default 100 (was 80) - sleep is learned via the device's sleep callback, not a /frame 409 - Status table: streaming + end-to-end are done/verified, not "not started" - Idle: timer resets on any painted frame, not specifically a /frame POST Left intact: the POST /frame API + protocol/idempotency sections — that firmware endpoint still exists for one-shot snapshots/debug and its 409-when- asleep semantics are unchanged.
2026-06-30docs: bring scrypted/README.md in line with current streaming behaviorLuke Hoersten1-16/+22
The v1 README still described the old snapshot-poll path. Update to match the shipped script: live MJPEG over a TCP data socket (port 81, ~24fps) via ffmpeg, not ~1fps snapshots. Fixes: - intro + v1 limitations: MJPEG streaming is done, not a v2 "POST /stream" TODO - Settings: replace the removed "Frame push interval" with the real Display fields (JPEG quality, Max Scrypted-side buffer, Stream prebuffer); add the Actions group; correct brightness default (100, was 80) - Wake triggers default is person+doorbell (was "all three"), doorbell only for doorbell-capable cameras - drop the stale snapshot-source / snapshot-interval / POST /frame 409 mentions - host field: no mDNS auto-resolve (was contradictory) - smoke test: Loading -> live video, not "snapshots flowing"
2026-06-30docs: document required Medium-stream prebuffer for fast wakeLuke Hoersten1-0/+11
Wake-to-live is ~0.7s only when the streamed substream keeps a rebroadcast prebuffer. Add a "Fast wake — camera prebuffer (required)" section to scrypted/README.md: enable Prebuffer on the STREAM: MEDIUM tab (default only prebuffers High), keep duration >= the detected keyframe interval (~5s), leave the viewport's Stream prebuffer (ms) at 6000, and how to verify via the log.
2026-06-30firmware: clear panel to Loading screen on new stream connectionLuke Hoersten1-0/+15
The panel used to hold its last framebuffer during the connect→first-frame gap, flashing a stale old frame before video appeared — more visible now that the Scrypted side no longer sends a bridging snapshot. The wake path already paints the Loading screen, but state_machine_set(AWAKE) is a no-op when the device is already awake (e.g. a stream restart), so the clear didn't happen there. Tie the clear to the stream connection instead: recv-task paints local_screens_show_loading() once per new conn_id, guarded by the decoder lock so the RGB565 present can't race a trailing decode-task paint, and gated on AWAKE so it never draws on a sleeping panel.
2026-06-30scrypted: stamp SCRIPT_VERSION = c6610c1Luke Hoersten1-1/+1
2026-06-30scrypted: remove the pre-stream snapshot entirelyLuke Hoersten1-156/+12
The prebuffered stream now paints in ~0.7s, so the takePicture→transform→POST first-paint bridge was consistently slower than the video it was meant to cover, while adding camera load and a stale-overpaint risk. Drop it: remove pushSnapshot and its call, the shouldPaint gate, and the firstStreamFrameSeen plumbing. On the slow paths (no/cold prebuffer) the panel holds its prior frame until the stream's first frame lands. buildVf stays (used by the live path).
2026-06-30scrypted: stamp SCRIPT_VERSION = 39aeeffLuke Hoersten1-1/+1
2026-06-30scrypted: gate snapshot POST on the live stream not yet paintingLuke Hoersten1-5/+23
Now that the prebuffered stream paints in ~0.7s, the parallel snapshot usually loses the race and, if slow (a Unifi takePicture cache-miss can be several seconds), would land after live video and overpaint it with a staler still. pushSnapshot takes a shouldPaint predicate; startStream passes () => !firstStreamFrameSeen (flipped by the ffmpeg first-frame handler), so a snapshot that finishes after the stream is already painting is dropped before the POST. Preserves the snapshot as a gap-filler for the slow paths (live-edge fallback / cold prebuffer) where the stream hasn't painted yet.
2026-06-30scrypted: stamp SCRIPT_VERSION = 860f539Luke Hoersten1-1/+1
2026-06-30scrypted: strip cold-start/event diagnosticsLuke Hoersten1-110/+7
Remove the temporary instrumentation used to diagnose the event-wake and cold-start work: the evtscan system-wide listener, the attach/trace/streamopt/ src/inputArgs logs, the StartStop entry logs, and ffmpeg -loglevel info/-nostats (back to error). Keep the lightweight one-shot cold-start stamps (spawned → first byte → first frame → first socket.write) and usingPrebuffer in the config line — cheap regression signals — plus all the actual fixes and the prebuffered- stream selection logic.
2026-06-30scrypted: stamp SCRIPT_VERSION = 24d6e90Luke Hoersten1-1/+1
2026-06-30scrypted: fix event wake, teardown, triggers; cut stream cold-start ~6s→~0.7sLuke Hoersten1-47/+390
Event/lifecycle fixes: - attachListener now self-heals: it's idempotent (tracks the bound camera per viewport) and re-runs on registerViewport success, so the camera subscription survives the reload/add storage-attach race that previously left a bound viewport with no listener until a manual re-save. - stop() always drains the global cleaner array (never gates on this.running) and also cancels bindingDebounce timers + clears the stream idle timeout on abort, so StartStop.stop()+start() == a fresh load with no orphaned ffmpeg, sockets, listeners, or intervals. - ignore all camera events while a stream is live or starting (guard moved to the top of handleCameraEvent) — the wake window is anchored to the first event; later events don't queue, relaunch, or extend it. - prune stale childIds whose storage container is gone (deleted device). Triggers: - default to person + doorbell (motion opt-in; doorbell cameras are chatty); doorbell only offered for doorbell-capable cameras. Reconcile stored triggers + re-render settings live when the camera binding changes. Cold-start (wake -> first live frame): ~6s -> ~0.7s. - Root cause: Unifi GOP is ~5s and the rebroadcast RTSP serves live-edge, so ffmpeg waited for the next keyframe. - Fix: select the prebuffered substream by id (smallest that covers the panel, not the 5MP High), request prebuffer >= GOP, and drop -avioflags direct / -fflags nobuffer on the prebuffer path so ffmpeg gulps the buffered-keyframe burst instead of trickling it. Live-edge fallback keeps the low-latency flags. Steady-state g2g unchanged (~50-100ms). Includes temporary diagnostics (evtscan, attach/trace/streamopt/src logs, ffmpeg -loglevel info) to be stripped in the follow-up commit.
2026-06-21scrypted: stamp SCRIPT_VERSION = 1bd5e9fLuke Hoersten1-1/+1
2026-06-21scrypted: subscribe log includes the interface listLuke Hoersten1-1/+1
2026-06-21scrypted: stamp SCRIPT_VERSION = 3a7ddbdLuke Hoersten1-1/+1
2026-06-21scrypted: fix listen() — one call per interface (was passing array, filter ↵Luke Hoersten1-51/+17
broken) ScryptedDevice.listen() takes a single ScryptedInterface | string | EventListenerOptions, not an array. We were passing [BinarySensor, MotionSensor, ObjectDetector] which stringifies to a comma-joined garbage filter, leaking unrelated events (Settings, ...) through and apparently dropping BinarySensor entirely — bell rings never surfaced. Also drops the child-device traversal: confirmed via unifi-protect/src/main.ts that the bell BinarySensor lives on the camera device itself for doorbells.
2026-06-21scrypted: stamp SCRIPT_VERSION = d2b75bdLuke Hoersten1-1/+1
2026-06-21scrypted: diagnostic — enumerate every BinarySensor deviceLuke Hoersten1-10/+10
2026-06-21scrypted: stamp SCRIPT_VERSION = c901984Luke Hoersten1-1/+1
2026-06-21scrypted: log every onBindingChanged re-attach with host/cameraIdLuke Hoersten1-0/+2
2026-06-21scrypted: stamp SCRIPT_VERSION = 5d7549bLuke Hoersten1-1/+1
2026-06-21scrypted: widen doorbell traversal — include BinarySensor siblings + ↵Luke Hoersten1-1/+26
diagnostic
2026-06-21scrypted: stamp SCRIPT_VERSION = f6d8e75Luke Hoersten1-1/+1
2026-06-21scrypted: override parent device type to Bridge (was hardcoded Unknown by ↵Luke Hoersten1-0/+31
Scripts plugin) The 'Unknown' text the user saw above the Status and Controls panel is the device TYPE, not the lifecycle state. Scripts plugin (plugins/core/src/script.ts:65) hardcodes type=ScryptedDeviceType.Unknown when it registers any script device, regardless of what interfaces the loaded class actually implements. Override it from inside the script: call deviceManager.onDeviceDiscovered ourselves with type=Bridge, which semantically fits our DeviceProvider that bridges multiple child viewport devices. The override runs after Scripts plugin's postRunScript-driven discovery so the later call wins. Pass the full interface set explicitly (auto-detection found from method names: Settings/DeviceProvider/DeviceCreator/HttpRequestHandler/ StartStop, plus Scripts base Scriptable+Program) — partial lists drop interfaces. The override is wrapped in try/catch — if the device record's provider mapping changes in a future Scrypted version, we degrade to the old Unknown label rather than the script failing to boot.
2026-06-21scrypted: stamp SCRIPT_VERSION = ba412e5Luke Hoersten1-1/+1
2026-06-21scrypted: drop OnOff — empirically StartStop is what the Scripts UI binds toLuke Hoersten1-46/+16
In v101fb3e we shipped StartStop + OnOff side-by-side to see which the 'Status and Controls' panel actually wired to. The v44c7a63 diagnostic session confirmed: clicking STOP fires stop() (StartStop), not turnOff() (OnOff). User's console showed: lifecycle: start() called (running=false) ... lifecycle: stop() called (running=true) stop: tearing down 1 resources So StartStop alone is the right interface. OnOff just produced a duplicate 'Status and Controls' panel for the same lifecycle. Drop it. Also dropping the verbose lifecycle: ... logging — we know the binding now. The cleanup mechanism is the load-bearing observable ('stop: tearing down N resources' from drainShutdownCleaners). The status text still rendering 'Unknown' (rather than Running/Stopped) is a separate cosmetic concern — possibly the duplicate panels were confusing the UI; with a single StartStop panel left, the displayed status may now reflect this.running correctly. To be verified.
2026-06-21scrypted: stamp SCRIPT_VERSION = 44c7a63Luke Hoersten1-1/+1
2026-06-21scrypted: log end-of-method state for lifecycle calls + bootstrap exceptionLuke Hoersten1-3/+10
start() previously logged only at entry, so a silent throw between 'Scrypted Viewport up' and 'this.running = true' would leave us guessing. Add explicit end-of-method logs with the post-write state, plus a try/catch around bootstrap that surfaces the exception message before re-throwing. This will tell us whether start() reaches its set this.running=true on script load (i.e. whether the prototype state-proxy write fires) or whether bootstrap is silently failing somewhere mid-way.
2026-06-21scrypted: stamp SCRIPT_VERSION = 101fb3eLuke Hoersten1-1/+1
2026-06-21scrypted: add OnOff alongside StartStop + lifecycle logging to identify ↵Luke Hoersten1-17/+41
panel binding The 'Status and Controls' panel in @scrypted/core 0.3.147 was rendering Unknown despite the previous commit adding StartStop. Reading the SDK (sdk/src/index.ts:195 + plugins/core/src/script.ts:47) confirmed how it should work: - ScryptedDeviceBase installs Object.defineProperty getters/setters for every interface property at runtime, so this.running = true propagates through _lazyLoadDeviceState → getDeviceState proxy → system state. - Scripts plugin's mergeHandler auto-detects interfaces by mapping method names: start/stop → StartStop, turnOn/turnOff → OnOff, putSetting/getSettings → Settings, etc. In theory the previous commit was sufficient. To pin down whether the panel is calling something different (older Scrypted UIs lean toward OnOff), this commit: - Implements OnOff alongside StartStop. Both pairs are bound to the same drain/bootstrap logic; whichever the panel calls, the user's intent succeeds. - Initialises this.running = false and this.on = false synchronously in the constructor so the device state record carries a defined value at registration time, instead of Scrypted defaulting to Unknown on undefined. - Logs 'lifecycle: X() called (...)' on every method entry. After the user reloads + clicks STOP / START, the console will reveal exactly which method Scrypted is invoking — or whether neither is. If one of the methods fires, the other is dead code we can drop. If neither fires on the panel's STOP/START button, the panel is a Scripts-runtime control unrelated to device interfaces and we need to look for a different lifecycle hook.
2026-06-21scrypted: stamp SCRIPT_VERSION = 39b0479Luke Hoersten1-1/+1
2026-06-21scrypted: log attachListener early-return when no camera assignedLuke Hoersten1-2/+5
Silent early-return at 'if (!v.cameraId) return;' makes a brand-new viewport with no camera selected look identical (in the console) to one that subscribed successfully — there's no positive or negative signal until you try to fire a camera event. After observing a fresh viewport produce zero output on a doorbell press, switching the early-return to a warning that says 'no camera assigned — open Settings and pick a camera; subscription skipped' so the missing configuration becomes self-evident.
2026-06-21scrypted: stamp SCRIPT_VERSION = 000ab3aLuke Hoersten1-1/+1
2026-06-21scrypted: implement StartStop for proper lifecycle UI + clean tear-downLuke Hoersten1-5/+50
The Scripts 'Status and Controls' panel previously rendered Unknown because the Provider exposed no lifecycle interface. STOP / START in that panel didn't do anything useful — Scrypted's only option was to unload the script entirely, which left the user's re-paste flow relying on the constructor-time globalThis cleanup hack (and even that didn't cover in-flight streams until 1ce49d3). This commit makes the panel real: class ScryptedViewportProvider ... implements ..., StartStop - async start() : drain shutdown cleaners, bootstrap, running=true - async stop() : drain shutdown cleaners, clear viewport+listener+ stream maps, running=false Both methods are idempotent (no-op when already in target state). The constructor calls start() automatically so script load still bootstraps without user action. The private start() method that did the actual provisioning is renamed to bootstrap() to free up the public name for the StartStop contract. registerShutdownCleaners is renamed to drainShutdownCleaners and now takes a 'reason' label so the log line distinguishes 'start: tearing down N resources' (previous load) from 'stop: tearing down N resources' (user-driven). Effect on the workflow the user described: 1. Click STOP in the Scripts UI → stop() drains every resource (streams, listeners, intervals) the provider currently holds. 2. Re-paste the new script. 3. New constructor runs → start() drains anything left behind → bootstrap re-discovers children + re-attaches listeners → ready. No more accumulating ghosts of prior loads.
2026-06-21scrypted: stamp SCRIPT_VERSION = 1ce49d3Luke Hoersten1-1/+1
2026-06-21scrypted: unified shutdown-cleaner array covers streams + listeners + timersLuke Hoersten1-32/+85
Scrypted's Scripts sandbox doesn't release a previous load's resources before constructing a new Provider instance, so anything long-lived survives a re-paste. Previously we hand-rolled cleanup via two separate globalThis hooks: __viewportListenerCleaners (camera event listeners) and __viewportRegisterInterval (the 5-minute re-register timer). Anything else — in-flight ffmpeg children, TCP sockets to the firmware, the per-stream stats interval — leaked across reloads. Visible symptom: re-pasting the script during an active stream left the old stream's ffmpeg + socket running against a dead Provider instance, accumulating across reloads until the host was restarted. Unify all cleanup into one globalThis array, __viewportShutdownCleaners, that every long-lived resource pushes onto. The next start() snapshots and drains it before doing anything else. Resources covered: - 5-minute re-register interval (clearInterval) - Every camera + child-device event listener (reg.removeListener) - Every in-flight stream's AbortController (abort.abort, which the existing abort listeners chain into socket.destroy() + ffmpeg SIGTERM + clearInterval on the stats logger) Each cleanup is removed from the array when the resource naturally ends (stream timeout / stopStream), so the list stays compact across many stream cycles rather than growing forever. Snapshot-before-walk avoids the iteration-skip bug where a stream abort fires its abort listener, which splices the closure out of the same array we're iterating. Snapshot the array, clear the original, then walk the snapshot. Doesn't address the 'Unknown' status panel in Scrypted Scripts UI — that's a separate cosmetic issue (we don't implement a lifecycle interface that reports running state).
2026-06-21scrypted: stamp SCRIPT_VERSION = 356e637Luke Hoersten1-1/+1
2026-06-21scrypted: retry once on Settings-page /state /config fetch failureLuke Hoersten1-4/+23
The Settings 'Status (live)' section reaches the device via two sequential GETs. A transient socket-level failure (httpd worker pool briefly saturated by the active stream connection, mid-reboot window, network jitter) used to leave the page showing 'device: offline / unreachable (fetch failed)' even though the device was fine — a refresh would clear it. One automatic 250 ms-backoff retry on either GET removes the sporadic false positive. The 3 s per-request timeout stays the same, so the worst-case page latency on a genuinely offline device is ~6.5 s (3 + 0.25 + 3) instead of 3 s — acceptable for a deliberate Settings open.
2026-06-21scrypted: stamp SCRIPT_VERSION = 2ca1307Luke Hoersten1-1/+1
2026-06-21scrypted: subscribe to camera child devices (fix Unifi doorbell wake)Luke Hoersten1-17/+39
Unifi doorbell cameras expose the bell button as a child device of the camera (separate nativeId, its own BinarySensor interface), not as a property of the camera itself. The previous code did cam.listen() on the parent camera only, so motion + person events arrived (those fire on the camera itself) but bell-press events never reached handleCameraEvent. HomeKit kept working because Scrypted's HomeKit bridge auto-syncs all child devices; our plugin was silently the only consumer missing the event. Confirmed by the trace log in 3b0ab73 staying silent across a bell press while motion events still printed. Fix: in attachListener, walk systemManager.getDeviceIds() and pick any device with providerId === cam.id as an additional listen target. Subscribe on all of them with the same iface list — listen() no-ops on unsupported ifaces, so we don't have to introspect each child. Tracking changed from Map<nativeId, EventListenerRegister> to Map<nativeId, EventListenerRegister[]> so detach/script-reload cleanup removes every listener, not just the camera's.
2026-06-20scrypted: stamp SCRIPT_VERSION = 3b0ab73Luke Hoersten1-1/+1
2026-06-20README: stream-server task split is the critical change that closed the ↵Luke Hoersten1-18/+19
painted<sent gap Document the architecture pivot. The per-frame budget table now reflects the long-lived TCP stream socket on port 81 (replaced the HTTP /frame pattern) and the new recv/decode/paint timings: recv 14-18ms on the wire (pure body), dec ~5ms, paint <35us, with decode-task idle ~30ms per frame waiting on recv. The critical change wasn't a TCP tuning knob — it was splitting recv off its own FreeRTOS task with a 3-buffer PSRAM ring and a 1-deep latest-frame slot to the decode/paint task (d1c8d45). The single-task loop blocked the socket for 6ms during decode+paint, which against the IDF-default 5760-byte window forced a stop-go cycle that capped painted at ~17fps. Raising the window to 65535 made it worse (g2g grew to 17s) because lwIP RX couldn't drain the bursts and there's no way to skip-oldest on the kernel queue. The task split eliminated the coupling: recv-task drains continuously regardless of decode timing, the kernel buffer stays near-empty, and painted now matches sent at the source rate. Backlog entries for body shrink and OTA marked done.
2026-06-20firmware: split stream recv into its own task with 3-buffer ping-pongLuke Hoersten5-246/+416
handle_client previously ran recv → decode → paint serially on one FreeRTOS task. The kernel TCP buffer filled during decode+paint (~6ms), and against the IDF-default 5760-byte window the sender naturally stop-go-rate-limited to ~consumption. Raising the window to 65535 (previous experiment) regressed g2g from ~100ms to 17s growing unbounded — the sender pumped 45+ segments per round into a kernel buffer the app couldn't drain in time, and there was no way to skip-oldest on the kernel queue. This commit decouples recv from decode+paint: recv-task: owns the socket. Reads header + body into one of three preallocated PSRAM body buffers. On body complete, swaps the just-filled buffer into a 1-deep pending slot and picks a free buffer for the next recv. If the slot already held a frame (decode is slow), drops oldest in place — mirror of the Scrypted-side skip-oldest from e5acf93. decode-task: waits on a binary semaphore. On signal, claims pending, then decodes + paints without holding any shared lock. Frees its prior buffer implicitly by overwriting s_decode_idx on the next claim. 3 PSRAM body buffers (~3MB of 28MB free) ensure the invariant {recv_idx, pending_idx, decode_idx} are pairwise distinct without ever blocking recv. jpeg_decoder.c grew an alloc_input_buffer helper + jpeg_decoder_decode now takes an explicit input pointer so the stream and http_api snapshot paths don't share scratch. New stats: - recv_dropped_oldest: per-window count of pending-slot overwrites - decode_idle_min/avg/max_us: time decode-task spent waiting on signal Measurement at IDF-default 5760 window, Unifi medium substream: before split: recv_avg=32ms recv_max~44ms fps=22-26 (recv blocked during 6ms decode+paint; chunk_max capped at 5760) after split: recv_avg=17ms recv_max=18-37ms fps=21-29 steady, decode_idle_avg=27-40ms (decode mostly waiting), drop_oldest=0, painted at source rate The bottleneck moved from 'decode+paint serializes recv' to the wire's own send rate. Bigger windows are now safe (recv-task drains continuously, can't bury us), but won't add fps until source rate goes up — that's a separate conversation.
2026-06-20firmware: keep TCP recv window at IDF defaults (5760) — bigger window ↵Luke Hoersten1-11/+11
regressed g2g Step-2 experiment from the iterative tuning plan: bumped CONFIG_LWIP_TCP_WND_DEFAULT 5760 → 65535 (and matching SND_BUF + RECVMBOX 6 → 16) under the hypothesis that the 4×MSS window throttled receive throughput. Instrumentation showed the window WAS the per-call throttle (recv_chunk_max was exactly 5760 before, grew to 33-60KB after). But end-to-end got dramatically worse: - g2g exploded from ~100ms steady to 17 SECONDS growing unbounded (1964 → 6010 → 10632 → 14725 → 17889ms over consecutive windows). - painted dropped from ~24fps to 15-20fps. - recv_avg rose from 32ms steady to 50-90ms with frequent 230-500ms outliers (the retransmit / RTO signature). - chunk_min fell to 61 bytes, chunk_avg dropped from 3415 to ~2200 — fragmentation as lwIP RX struggled with the bigger bursts. Root cause: the single recv→decode→paint task serializes; the kernel buffer fills against ANY window during the ~6ms decode+paint, but with 65535 bytes available the sender pumps ~45 segments before stopping vs ~4 before. That overruns the lwIP RX path (RECVMBOX=16 mailboxes, default pbuf pool) → drops → retransmits → multi-frame stalls. And Scrypted has no way to skip-oldest on the kernel queue, so frames just pile up on the wire / in the ESP32 kernel buffer → g2g grows linearly with time. Lesson: window size isn't the right knob until the receiver can drain at line rate independent of decode+paint. Next iteration: split recv into its own FreeRTOS task with a 1-deep latest-frame slot to the decode/paint task — only then revisit window.
2026-06-20scrypted: trace log on every camera event (diagnose missing doorbell wake)Luke Hoersten1-0/+7
handleCameraEvent now logs iface/typeof/data/details for every event the camera fires on BinarySensor/MotionSensor/ObjectDetector. Helps diagnose why a Unifi doorbell press isn't waking the viewport while the same event reaches HomeKit. Temporary — remove once root cause is confirmed.
2026-06-20firmware: recv-throughput instrumentation (FIONREAD pre-body, recv() ↵Luke Hoersten3-6/+104
call/chunk stats, SO_RCVBUF probe) Per-frame samples aggregated over the existing 30-frame window: - queued_at_body_start (FIONREAD just before body recv loop): how much of the frame the kernel already absorbed during the previous decode+paint. Close to jpeg_len → wire delivered the full frame while we were busy (we're decode/paint-bound). Much smaller → wire is throttled (window or buffer too small to absorb a frame in our paint window). - recv_calls: number of recv() syscalls the body read needed per frame. High → small chunks → window-throttled sender. - recv_chunk min/avg/max: bytes returned per recv() return in the window. Avg = window body bytes / total syscalls. - SO_RCVBUF: one-shot getsockopt at accept, logged and stashed in stats. Confirms whether sdkconfig values reached the build — TCP_WND_DEFAULT discrepancies are otherwise invisible. All surfaced in the windowed log and in /state JSON alongside the existing recv/dec/paint/idle stats. No behavior change yet.