Files
punktfunk/include
enricobuehler 44692a0dc1 feat(host,client): the host tells the client when it stopped the stream itself
A Windows exclusive-topology eviction makes the host rebuild its capture
ring and encoder in place. It takes a few hundred milliseconds and is
entirely host-local: no packet is lost, the link never changes. But the
client's bitrate controller decides on 750 ms report windows, and a window
straddling that rebuild sees almost no stream. The 0.29 field log: 401 ms
of rebuild produced a window reporting actual_kbps=390 against a 20 000
target with loss_ppm=0, which the controller read as congestion — x0.7 and
slow start retired, three minutes at ~15 Mbps on a link that never dropped
a packet.

The client already knows how to throw a window away. `discard_abr_window`
feeds the controller nothing, sends no LossReport (so a bogus window cannot
spike the host's adaptive FEC) and closes the standing-latency detector as
not-loss-free. It had exactly one cause: the tail of the client's own speed
test. This adds the second, and it is the one party that actually knows —
the host.

`PipelineGap` (0x0A, host->client, 9 bytes) carries the rebuild's measured
span. A DURATION, never an instant: host and client clocks are not in the
same domain — 14.7 s apart in that same log — so an instant would need
skew-correcting before it meant anything. The client anchors the gap to its
own receive time and gap_ms is evidence for the log rather than an input to
arithmetic. The span is read from the transition trace's own total, so the
number the client logs is the total_ms on the host's trace line: one number,
checkable from either end of a field report.

Announced on both in-place rebuild paths, including the mode switch. The
mode switch is NOT already covered by the client's `on_mode_switch` reset:
that clears the learned caps, the three latency baselines and the proven
mark, which does mute OWD/decode/encode for a few windows — but it touches
neither `current_kbps`, `probing` nor `bad_windows`, and every signal that
needs no baseline (an unrecoverable frame, a flush, heavy loss over a
near-empty denominator, a keyframe-ask storm) still scores the straddling
window, where one severe verdict costs the same x0.7 plus slow start.

Deliberate limitation: only the window in flight is discarded. A rebuild
long enough to straddle a window boundary damaged the previous window too,
and that one is already decided. Retracting it would mean holding every
window back by a window in case a gap follows — trading a rare
over-reaction for a permanent one.

Additive: no ABI bump, no wire break. 0x0A extends the contiguous
0x01-0x09 rate-control block its only consumer already lives in, and is not
in the 0x30 clock block precisely because no clock domain is involved. A
client that predates it hits its "unknown control message" arm and keeps
today's behaviour; a host that predates it never sends one.

Tests: wire round trip including cross-decode against the three
identically-shaped rate-control messages either side of it (the type byte
is the only thing keeping a gap from re-decoding as a SetBitrate), and an
end-to-end pump test driving a real ControlTask over a real QUIC control
stream into a real DataPump, asserting the straddling window produces no
request and the next one reports normally. 464 core tests green, fmt and
clippy clean.

Not verified here: the Windows-gated eviction call site compiles on no
platform available locally (aws-lc-sys needs windows.h to cross-compile).
Type-checked by reading; owed a Windows runner build.

Known gaps, stated rather than papered over: a mode-switch rebuild that
FAILS keeps streaming the old mode and leaves its stall unannounced, and
three other in-place rebuild sites are still silent — the Gaming/Desktop
session switch, the ABR re-target's fallback open_video rebuild (~0.6 s,
and self-inflicted: the controller causing the stall its next window reads
as congestion), and reset_stalled_encoder.
2026-08-17 01:36:49 +02:00
..