diff options
| author | Luke Hoersten <[email protected]> | 2026-06-20 11:38:14 -0500 |
|---|---|---|
| committer | Luke Hoersten <[email protected]> | 2026-06-20 11:38:14 -0500 |
| commit | e4a546ce29a3a29dc814b7d115a1b9c206385559 (patch) | |
| tree | 88c22070093349c81d953c2bc18f628146db972d /main/stream_server.h | |
| parent | d5eef650e7640e5622c384e6783fe4ff201e172e (diff) | |
phase 4: glass-to-glass via 16-byte stream header + /state stream stats
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.
Diffstat (limited to 'main/stream_server.h')
| -rw-r--r-- | main/stream_server.h | 45 |
1 files changed, 43 insertions, 2 deletions
diff --git a/main/stream_server.h b/main/stream_server.h index ef8a334..1d13457 100644 --- a/main/stream_server.h +++ b/main/stream_server.h @@ -1,6 +1,7 @@ #pragma once #include <stdint.h> +#include <stdbool.h> #include "esp_err.h" // Raw-TCP frame ingestion server. Replaces the per-frame HTTP /frame @@ -9,10 +10,50 @@ // setup, HTTP parsing, and the 200ms Nagle/delayed-ACK deadlocks // that intermittently spiked the HTTP path to ~250ms wall. // -// Wire protocol (one connection, repeating; all integers big-endian): -// [4 bytes: jpeg_len ][4 bytes: seq][jpeg_len bytes: JPEG body] +// Wire protocol — one connection, repeating; all integers big-endian. +// Two formats are accepted on every connection: +// +// v1 (16-byte header, current): +// [u32 magic = 0x56505254 "VPRT"] +// [u32 jpeg_len] +// [u32 seq] +// [u32 event_us_low] low 32 bits of Scrypted-host monotonic µs +// at camera-event arrival; firmware passes +// this through verbatim to /state so the +// script can compute glass-to-glass. +// [jpeg_len bytes: JPEG body] +// +// v0 (8-byte header, legacy): +// [u32 jpeg_len] +// [u32 seq] +// [jpeg_len bytes: JPEG body] +// +// We sniff the first 4 bytes: if they spell "VPRT" the connection is +// v1, otherwise we interpret bytes 0-3 as jpeg_len (v0) and continue. +// The v0 path will be removed once all Scrypted scripts in the field +// have rolled forward. // // One client at a time. New client = previous client's seq counter // is reset (so each stream session starts fresh, and stale frames // from a previous reconnect can't paint over current ones). esp_err_t stream_server_start(uint16_t port); + +// Snapshot of the most recent 30-frame window plus the latest +// painted frame's pass-through event timestamp. Locking is internal +// to stream_server — the caller just reads. All durations in +// microseconds. Fields are zero before the first window rolls. +typedef struct { + uint64_t frames; // count of painted frames in the window + uint64_t bytes; // total body bytes received in the window + uint64_t window_us; // wall-clock span of the window + uint64_t window_end_us; // esp_timer_get_time() at window roll + uint32_t recv_min_us, recv_avg_us, recv_max_us; + uint32_t dec_min_us, dec_avg_us, dec_max_us; + uint32_t pnt_min_us, pnt_avg_us, pnt_max_us; + uint32_t idle_min_us, idle_avg_us, idle_max_us; + uint32_t last_paint_event_us_low; // last v1 frame's event_us_low, + // 0 if none seen yet on this + // boot or last frame was v0 +} stream_server_stats_t; + +void stream_server_snapshot_stats(stream_server_stats_t *out); |
