src.nth.io/

summaryrefslogtreecommitdiff
path: root/main/stream_server.h
AgeCommit message (Collapse)AuthorFilesLines
2026-06-20firmware: recv-throughput instrumentation (FIONREAD pre-body, recv() ↵Luke Hoersten1-0/+11
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.
2026-06-20phase 4: glass-to-glass via 16-byte stream header + /state stream statsLuke Hoersten1-2/+43
Wire format change: stream frames now carry a 4-byte "VPRT" magic + 4-byte jpeg_len + 4-byte seq + 4-byte event_us_low. Total 16 bytes (was 8). The firmware sniffs the first 4 bytes per frame: if they spell VPRT it reads the remaining 12 bytes of v1 header; otherwise it interprets bytes 0-3 as jpeg_len for the old v0 8-byte format and reads 4 more for seq. Lets a v1 firmware accept a v0 (legacy) Scrypted script during the rollout window. v0 will be removed once all field deployments roll forward. event_us_low is the low 32 bits of the Scrypted host's monotonic µs at camera-event arrival. The firmware does NOT interpret it (the clocks aren't sync'd); it just stamps it on every painted frame and exposes the most recent value via /state. The script polls /state every 5s during an active stream, reads last_paint_event_us_low, and computes glass-to-glass = (now_us_low - last_paint_event_us_low) with 32-bit wrap. 30s sanity ceiling on the wrap to discard event timestamps from before the stream started. Also expose the firmware's just-closed 30-frame window stats via /state under the "stream" key — frames, bytes, window_us, plus min/avg/max for recv/dec/paint/idle. Lets external tools (a curl loop, the Scrypted plugin, etc) poll the firmware's view without parsing serial logs. Firmware: - stream_server.h: 16-byte v1 wire spec, stream_server_stats_t struct, stream_server_snapshot_stats(out) getter. - stream_server.c: magic-detect header read path, last_event_us_low capture into per-connection state, portMUX-protected window-stats snapshot at every 30-frame roll. - http_api.c: GET /state JSON gains a "stream" sub-object with the full snapshot. - viewport_state.h: VIEWPORT_VERSION 1.0.0 → 1.1.0 (new /state shape). Scrypted: - startStream captures eventUsLow = (tEvent * 1000) >>> 0. - TCP demux loop writes the 16-byte v1 header with the VPRT magic. - New fwPoller setInterval (5s) fetches /state, parses .stream, computes g2g, emits one summary line per poll cycle.
2026-06-19streaming pivot: raw TCP data plane, drop per-frame HTTP entirelyLuke Hoersten1-0/+18
Replaces the per-frame HTTP POST loop with a single long-lived TCP connection on port 81. The HTTP control plane (/state, /config, /frame for snapshot) stays unchanged. Wire protocol (big-endian, repeating until connection close): [4 bytes jpeg_len][4 bytes seq][jpeg_len bytes JPEG] Why --- The HTTP path hit a measured floor of ~37ms p50 with intermittent ~230ms p95 spikes that survived every Nagle/keep-alive fix attempt. Each frame paid: TCP setup (or pool churn), HTTP parsing, body recv, decode, paint, response write, response ACK. Streaming removes everything except recv + decode + paint. Firmware (main/stream_server.[ch], new) --------------------------------------- - TCP listen on configurable port (81) in its own FreeRTOS task, one client at a time (matches the one-stream-per-device model). - Per accepted socket: TCP_NODELAY on, then loop reading 8-byte header → jpeg body → through the existing jpeg_decoder + display paths. Same stale-seq guard as the HTTP /frame handler (reset per connection so each session starts at seq 1). - Frames received while asleep are still drained (to stay framed) but not painted. The HTTP control-plane POST /state {wake} resumes painting on the next frame. - Every 30 painted frames a structured serial log shows per-stage timing + sustained MB/s — replaces the cross-side Server-Timing header (no HTTP response to attach it to anymore). Scrypted side ------------- - net.createConnection({ host, port: 81, noDelay: true }) opened once per stream session. On disconnect/error/close we auto- reconnect after 500ms. - ffmpeg stdout demux writes [header][body] directly to the socket. sock.write() returning false sets a backpressured flag that drops incoming ffmpeg frames until 'drain' fires — natural TCP backpressure handles "firmware can't keep up" without us modeling it manually. - Stripped the entire fetch-based timing infrastructure (pushStreamFrame, fetchSamples, parseServerTiming, depth histogram, Server-Timing parser). Replaced with one stream-shaped log every 10s: fps + MB/s + socket.write p50/p95/max + drop count + backpressured flag. - pushSnapshot (one-shot first-paint) still uses the HTTP /frame endpoint — small and infrequent, not worth reworking. - postJSON still uses node:http for /state and /config. State machine status flags -------------------------- Extended boot-time flags array from 6 → 7 slots so the stream server's bring-up shows up alongside ETH/MDNS/HTTP. Layout is now E M H S D J T (was E M H D J T).