From e4a546ce29a3a29dc814b7d115a1b9c206385559 Mon Sep 17 00:00:00 2001 From: Luke Hoersten Date: Sat, 20 Jun 2026 11:38:14 -0500 Subject: phase 4: glass-to-glass via 16-byte stream header + /state stream stats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- main/http_api.c | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) (limited to 'main/http_api.c') diff --git a/main/http_api.c b/main/http_api.c index cb6a8b3..85992b1 100644 --- a/main/http_api.c +++ b/main/http_api.c @@ -16,6 +16,7 @@ #include "mdns_service.h" #include "net_eth.h" #include "nvs_config.h" +#include "stream_server.h" #include "state_machine.h" #include "viewport_state.h" @@ -67,6 +68,35 @@ static esp_err_t state_get_handler(httpd_req_t *req) viewport_state_unlock(); + // Most recent closed window of live-stream stats. Populated every + // 30 painted stream frames; zero before the first window rolls. + // last_paint_event_us_low is the low 32 bits of the Scrypted + // host monotonic µs at camera-event arrival, passed verbatim + // through the stream header. Script computes glass-to-glass as + // (now_us_low - last_paint_event_us_low) with wrap. + stream_server_stats_t stream; + stream_server_snapshot_stats(&stream); + cJSON *s = cJSON_CreateObject(); + cJSON_AddNumberToObject(s, "frames", (double)stream.frames); + cJSON_AddNumberToObject(s, "bytes", (double)stream.bytes); + cJSON_AddNumberToObject(s, "window_us", (double)stream.window_us); + cJSON_AddNumberToObject(s, "window_end_us", (double)stream.window_end_us); + cJSON_AddNumberToObject(s, "recv_min_us", (double)stream.recv_min_us); + cJSON_AddNumberToObject(s, "recv_avg_us", (double)stream.recv_avg_us); + cJSON_AddNumberToObject(s, "recv_max_us", (double)stream.recv_max_us); + cJSON_AddNumberToObject(s, "dec_min_us", (double)stream.dec_min_us); + cJSON_AddNumberToObject(s, "dec_avg_us", (double)stream.dec_avg_us); + cJSON_AddNumberToObject(s, "dec_max_us", (double)stream.dec_max_us); + cJSON_AddNumberToObject(s, "paint_min_us", (double)stream.pnt_min_us); + cJSON_AddNumberToObject(s, "paint_avg_us", (double)stream.pnt_avg_us); + cJSON_AddNumberToObject(s, "paint_max_us", (double)stream.pnt_max_us); + cJSON_AddNumberToObject(s, "idle_min_us", (double)stream.idle_min_us); + cJSON_AddNumberToObject(s, "idle_avg_us", (double)stream.idle_avg_us); + cJSON_AddNumberToObject(s, "idle_max_us", (double)stream.idle_max_us); + cJSON_AddNumberToObject(s, "last_paint_event_us_low", + (double)stream.last_paint_event_us_low); + cJSON_AddItemToObject(root, "stream", s); + char *body = cJSON_PrintUnformatted(root); cJSON_Delete(root); if (!body) return ESP_ERR_NO_MEM; -- cgit v1.2.3