1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
|
#pragma once
#include <stdint.h>
#include <stdbool.h>
#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.
// 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; // body bytes of painted frames 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;
// Recv-throughput diagnostics. Per-frame samples aggregated over the
// window. queued_at_body_start = FIONREAD just before the body recv
// loop runs; if it's close to jpeg_len the wire delivered the whole
// frame while we were decoding the previous one (we're not the bottleneck).
// recv_calls = number of recv() syscalls the body read needed (high →
// many small chunks → window-throttled sender). recv_chunk = body
// bytes returned per single recv() call within the window.
uint32_t queued_min, queued_avg, queued_max; // bytes
uint32_t recv_calls_min, recv_calls_avg, recv_calls_max; // syscalls per frame body
uint32_t recv_chunk_min, recv_chunk_avg, recv_chunk_max; // bytes per recv() return
uint32_t so_rcvbuf; // SO_RCVBUF observed at accept (0 = unknown)
// Receiver-side skip-oldest. Counts frames where recv-task finished
// a body but the previous frame was still sitting in the pending
// slot (decode-task hadn't taken it yet). The older pending frame
// gets overwritten by the just-received fresher one; the counter
// captures how often the recv loop outran decode in this window.
uint32_t recv_dropped_oldest;
uint32_t decode_idle_min_us, decode_idle_avg_us, decode_idle_max_us;
// Time decode-task spent waiting on the slot signal between frames.
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
// TCP-window decomposition. Together with recv/dec/paint these account
// for the whole frame interval:
// interval ≈ hdr_gap (sender idle) + recv (wire) + pend_age (handoff
// queue wait) + dec + paint.
// wire = instantaneous throughput while the body drained (jpeg_len /
// recv_us). Ceiling ≈ TCP_WND / RTT — the definitive window-throttle
// metric: scales with CONFIG_LWIP_TCP_WND iff the window is the limiter.
uint32_t wire_min_kbps, wire_avg_kbps, wire_max_kbps;
// Blocked-with-nothing-to-read time between previous body completion
// and next header completion. Large → sender-paced (we are NOT the
// bottleneck); ~0 → back-to-back arrivals, receive path is the limiter.
uint32_t hdr_gap_min_us, hdr_gap_avg_us, hdr_gap_max_us;
// publish → decode-claim latency of painted frames. Healthy: bounded
// well under a frame interval. Growing window-over-window = latency
// backlog building (the failure mode that killed the last WND raise).
uint32_t pend_age_min_us, pend_age_avg_us, pend_age_max_us;
} stream_server_stats_t;
void stream_server_snapshot_stats(stream_server_stats_t *out);
|