From b97c2502b74277f9ea9dace97e8188201d5570fb Mon Sep 17 00:00:00 2001 From: Luke Hoersten Date: Fri, 19 Jun 2026 19:59:14 -0500 Subject: streaming pivot: raw TCP data plane, drop per-frame HTTP entirely MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- main/stream_server.h | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 main/stream_server.h (limited to 'main/stream_server.h') diff --git a/main/stream_server.h b/main/stream_server.h new file mode 100644 index 0000000..ef8a334 --- /dev/null +++ b/main/stream_server.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include "esp_err.h" + +// Raw-TCP frame ingestion server. Replaces the per-frame HTTP /frame +// POST loop with a single long-lived TCP connection that streams +// length+seq prefixed JPEGs back-to-back. Eliminates per-frame TCP +// 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] +// +// 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); -- cgit v1.2.3