Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,57 @@ tests/compat/run-matrix.sh --tier blocking # whole tier
- Both tiers (blocking and informational) gate PR CI; only the weekly upstream-drift
job (unpinned HEADs) is non-blocking.

### srt-sink extended metrics

`tests/compat/srt-sink/` is the mock SRT receiver used by the compat harness. Beyond the original 4 frozen keys (`bytes_received`, `disconnects`, `handshake_ms`, `error`), `srt-sink` now emits 6 additional keys in `result.json`:

| Key | Source | Description |
|-----|--------|-------------|
| `ts_packets` | `ts_continuity.h` | Total 188-byte TS packets received |
| `ts_sync_errors` | `ts_continuity.h` | Packets whose first byte is not `0x47` |
| `ts_cc_errors` | `ts_continuity.h` | Continuity-counter discontinuities (excl. null PID 0x1FFF and adaptation-field `discontinuity_indicator`) |
| `pkt_rcv_loss` | `srt_bstats` `pktRcvLossTotal` | SRT-level receive loss (cumulative, summed across reconnects) |
| `pkt_rcv_drop` | `srt_bstats` `pktRcvDropTotal` | SRT-level receive drop (too-late packets) |
| `pkt_retrans` | `srt_bstats` `pktRetransTotal` | SRT-level retransmissions |

The TS parser lives in `srt-sink/ts_continuity.h` (header-only, dependency-free). It reassembles 188-byte packets across `srt_recv` boundaries, tracks per-PID continuity counters, and excludes null PID `0x1FFF` and adaptation-only packets from CC checks. Unit tests: `srt-sink/ts_continuity_test.cpp` (registered as ctest `ts-continuity` in `srt-sink/CMakeLists.txt`).

**`--retransmitalgo 0|1`** — new `srt-sink` flag. Sets `SRTO_RETRANSMITALGO` on the listener (pre-bind, inherited by accepted sockets). `0` = always retransmit on NAK; `1` = selective retransmit (default SRT behavior).

**`--packetfilter <str>`** — new `srt-sink` flag. Sets `SRTO_PACKETFILTER` on the listener (pre-bind, inherited by accepted sockets). The accepted socket's negotiated filter is written to `result.json` as `"packetfilter"` (non-empty = FEC negotiated; `""` = responder cleared the filter).

**`--reorderfreeze 0|1`** — new `srt-sink` flag. Sets `SRTO_REORDERFREEZE` via the raw numeric opt id `(SRT_SOCKOPT)120` so it compiles against any libsrt version. Reports `reorderfreeze=on|off|unsupported` in the startup banner.

### Profile validation A/B matrix

`tests/compat/scenarios/profile-validation-matrix.sh` is the A/B orchestrator for the four non-FEC receive profiles. It runs paired alternating reps (baseline patched libsrt vs freeze profile) under netem reorder stress and gates on six quality clauses:

1. `disconnects == 0` (both arms)
2. `ts_sync_errors == 0` (profile arm)
3. `ts_cc_errors <= baseline median`
4. `median goodput >= 99% baseline`
5. `p95 pkt_rcv_drop <= baseline`
6. `wire_amp <= 1.10× baseline median` (wire bytes / bytes_received)

Registered in `matrix.yaml` as scenario `profile-validation-matrix` (tier: blocking, privileged: true). Run manually via `tests/compat/run-matrix.sh --tier blocking`. Results: all four non-FEC profiles (Balanced/Low-Latency/Resilient/Classic) PASS all six clauses; wire amplification ratios 1.054–1.078× (well under 1.10×). Evidence: `test-results/srt-receive-profiles/task-6-srt-receive-profiles.json`.

The `reorder-stress.sh` scenario is parameterized (BITRATE_KBPS, RX_LATENCY_MS, NAKREPORT, LOSSMAXTTL, REORDERFREEZE, PROFILE_LABEL, NETEM_SEED) and now emits TS-continuity + SRT counters + `goodput_bps` + `wire_amp` into `result.json`. Default run is byte-identical to the pre-matrix behavior (Rule E).

### FEC connect-matrix

`tests/compat/scenarios/fec-connect-matrix.sh` proves the one-sided FEC packet-filter negotiation behavior (direct SRT loopback; SRTLA is transparent UDP so negotiation is SRT-level). Four cases:

| Case | Caller filter | Listener filter | Result |
|------|--------------|-----------------|--------|
| (a) | FEC full config | `fec` (accept-form) | FEC negotiated — `packetfilter` non-empty |
| (b) | plain (no filter) | `fec` (accept-form) | PLAIN — responder clears filter, `packetfilter=""` |
| (c) | FEC full config | conflicting `fec,cols:20,rows:20` | HARD REJECT `SRT_REJ_FILTER` — `bytes_received=0` |
| (d) | FEC full config | no filter | ADOPTED — listener adopts caller config (informational) |

Cases (a)/(b)/(c) are gated; (d) is informational. Registered in `matrix.yaml` as scenario `fec-connect-matrix`. See `docs/COMPATIBILITY.md §6` for the full mechanism and empirical results.

**Key finding:** a listener with NO `packetfilter` does NOT reject a FEC caller — it adopts the caller's config (SRT `checkApplyFilterConfig` "good deal" else-branch). The genuine `SRT_REJ_FILTER` hard reject is a filter-config CONFLICT, not the absence of a filter. This is why L1 in `irl-srt-server` uses the accept-form `"fec"` and serves both FEC and non-FEC callers on the same port.

## COMPATIBILITY TESTING

The compat matrix (`tests/compat/matrix.yaml`) registers every tested sender/receiver
Expand Down
56 changes: 54 additions & 2 deletions docs/COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,59 @@ The CeraLive stack uses two patched libsrt forks (CERALIVE/srt for the device im

---

## 6. ENABLE_ALGO_COMPARISON Decision
## 6. SRT FEC Connect-Matrix (one-sided packet-filter negotiation)

FEC is an **SRT-level** feature: `SRTO_PACKETFILTER=fec` is negotiated in the SRT
handshake, end-to-end between the caller and the listener. SRTLA underneath is a
transparent UDP relay and does not touch it — so a FEC stream "rides" whichever
SRT listener it terminates on. The receive-profile design uses one **fec-accept**
listener (L1, `SRTO_PACKETFILTER=fec` — just the type) for device senders. This
matrix proves that one such listener serves both FEC and non-FEC senders, and
where the negotiation actually hard-fails.

The caller is the device (initiator); the listener is the cloud receiver
(responder). Filter strings: a full-config FEC sender is
`fec,layout:staircase,rows:10,cols:10,arq:onreq`; the fec-accept listener is just
`fec`.

| # | Caller (sender) | Listener config | Result | Negotiated `SRTO_PACKETFILTER` on accepted socket |
|---|---|---|---|---|
| a | FEC (full config) | `fec` (fec-accept) | **Connect — FEC negotiated** | non-empty (merged config) |
| b | plain (no filter) | `fec` (fec-accept) | **Connect — PLAIN** (responder clears it per-connection) | empty `""` |
| c | FEC (full config) | conflicting `fec,…` (incompatible dims) | **HARD REJECT — `SRT_REJ_FILTER`** | n/a (no accept, 0 bytes) |
| d | FEC (full config) | *no packetfilter* (empty) | **Connect — FEC adopted** by the listener ("good deal") | non-empty (caller's config) |

**Why case (b) means no separate FEC listener is needed.** A listener that set a
filter the caller never requested does not reject — the responder branch in
`srtcore/core.cpp` (`checkApplyFilterConfig` + the post-handshake check "agent has
configured packetfilter, but peer didn't request it") **clears** the filter for
that one connection and connects plain. So the single fec-accept L1 accepts a FEC
device (case a, full negotiation) **and** a non-FEC sender such as BELABOX
(case b, cleared per-connection). One listener, both senders — no second FEC port.

**Where the reject boundary actually is.** The genuine `SRT_REJ_FILTER` hard
reject (case c) is a filter-config **conflict**, *not* the mere absence of a
filter. A listener with **no** packetfilter does **not** reject a FEC caller — it
takes the caller's config as a "good deal" and runs FEC anyway (case d). This
corrects the earlier mental model that a "non-FEC listener" would reject a FEC
sender: on a packet-filter-capable libsrt (≥ 1.4.0; system libsrt 1.5.x) absence
is permissive (adopt), and only an irreconcilable config closes the connection.
The one-sided config rule is documented upstream in
[`srt/docs/features/packet-filtering-and-fec.md`](https://github.com/Haivision/srt/blob/master/docs/features/packet-filtering-and-fec.md)
("one party defines the full configuration while the other only defines the
matching packet filter type … if the options specified are in conflict, the
connection will be rejected").

This matrix is exercised end-to-end by the
[`fec-connect-matrix`](../tests/compat/scenarios/fec-connect-matrix.sh) harness
scenario, which drives a real FEC/plain SRT caller into `srt-sink --packetfilter`
and asserts the negotiated filter the sink reads off each accepted socket
(`"packetfilter"` in the result JSON). Cases (a)+(b)+(c) gate the scenario;
case (d) is recorded as an informational observation.

---

## 7. ENABLE_ALGO_COMPARISON Decision

`ENABLE_ALGO_COMPARISON` is defined in `src/receiver_config.h` with a default of `1`:

Expand All @@ -213,7 +265,7 @@ Changing this default requires a deliberate ADR, not a drive-by edit.

---

## 7. Maintenance Policy
## 8. Maintenance Policy

### Pin refresh
When a Tier 1 or Tier 2 implementation cuts a new release, update the pin in
Expand Down
66 changes: 66 additions & 0 deletions tests/compat/matrix.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,72 @@ scenarios:
precedent), end-to-end SRT `disconnects` is recorded but is NOT a pass
gate for the isolation phase. Unprivileged: netem_require exits 77 (SKIP).

- name: ts-continuity-loopback
script: tests/compat/scenarios/ts-continuity-loopback.sh
requires: srt-live-transmit
skip_exit: 3
gated_on_disconnects: false
proves: >-
Real-SRT loopback over 127.0.0.1 proving srt-sink's MPEG-TS continuity
metric is correct AND falsifiable: a clean transport stream sent raw
yields ts_sync_errors==0 and ts_cc_errors==0, while the SAME TS with one
payload-bearing packet excised yields ts_cc_errors>0. Raw byte passthrough
(srt-live-transmit, NOT ffmpeg — which re-muxes the container and would
heal the injected drop) preserves the continuity break end-to-end; the
send is paced to ~real time and null-padded to a whole SRT chunk so the
live socket neither drops nor zero-pads into spurious sync errors.
Unprivileged; SKIPs (exit 3) without srt-live-transmit. Integration
companion to the hermetic `ts-continuity` ctest (same proof, no network).

- name: fec-connect-matrix
script: tests/compat/scenarios/fec-connect-matrix.sh
requires: srt-live-transmit
skip_exit: 3
gated_on_disconnects: false
proves: >-
SRT one-sided packet-filter (FEC) connect negotiation against a single
fec-accept listener (srt-sink --packetfilter fec), the receiver profile
(L1) that device FEC senders ride. Reads the negotiated SRTO_PACKETFILTER
off the accepted socket from srt-sink's result JSON. (a) a full-config FEC
caller negotiates FEC: the accepted socket reports a non-empty merged
config and bytes flow. (b) a plain caller against the SAME listener
connects PLAIN: the responder clears the filter per-connection (accepted
SRTO_PACKETFILTER empty), bytes flow, NOT rejected — proving ONE fec-accept
listener serves both FEC and non-FEC senders, so no separate FEC port is
needed. (c) an irreconcilable listener filter config HARD-REJECTS with
SRT_REJ_FILTER (caller logs ERROR:FILTER, sink accepts nothing, 0 bytes).
Falsifiable: (a) and (b) share the identical listener yet must report
opposite filter results. Also records (informational, not gated) that a
no-filter listener ADOPTS a FEC caller's config (good-deal) rather than
rejecting — the reject boundary is a config CONFLICT, not the absence of a
filter. Unprivileged; SKIPs (exit 3) without srt-live-transmit.

- name: profile-validation-matrix
script: tests/compat/scenarios/profile-validation-matrix.sh
tier: blocking
privileged: true
notes: >-
A/B validation matrix: the 4 NON-FEC receive profiles vs the patched-libsrt
baseline under cross-link reorder stress, judged on srt-sink TS-continuity
(Task 5), NOT a bytes-only proxy. Drives scenarios/reorder-stress.sh paired/
alternating (shared per-rep netem seed) and swaps libsrt under srt-sink: the
profile arm = the reorderfreeze-1.5.5 build (opt-in SRTO_REORDERFREEZE via
srt-sink --reorderfreeze) plus the recipe flags; the baseline arm = the
patched CERALIVE/srt build (unconditional BELABOX merge, no flag). Profile
pairs (each freeze-arm vs its paired baseline at matched latency/bitrate/
seed): Balanced freeze+NAK @1500ms, Low-Latency freeze+NAK @250ms, Resilient
freeze+NAK @3500ms, Classic freeze+NAK-off @800ms. EQUAL gate per pair:
disconnects==0, ts_sync_errors==0, ts_cc_errors<=baseline, median goodput>=
99% baseline, p95 late-drop<=baseline, wire-amplification<=1.10x baseline.
Plus a regression-validation cell (Balanced NAK-on vs baseline @{500,1500,
3500}ms) and a LOSSMAXTTL sweep axis {30,200,1000} to pick the cap. Blocking
verdict = all 4 non-FEC profiles PASS AND the regression cell PASSes; the
sweep + a stock-decay control arm are informational/falsifiability. FEC
scope: the Low-Latency+FEC row is gated separately (fec-connect-matrix).
Builds the two libsrt via lib/build-libsrt-matrix.sh if absent; evidence
table -> test-results/srt-receive-profiles/task-6-srt-receive-profiles.json.
Unprivileged: netem.sh `require` exits 77 (SKIP).

excluded:
- name: moblink-rust
repo: https://github.com/datagutt/moblink-rust
Expand Down
Loading
Loading