Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e7da4e3
restamp: rate-derive the monotonic grain step (fix 60fps err runaway)…
guycochran Oct 7, 2026
54d2e8a
restamp: replace step re-sync with a proportional slewing servo (the …
guycochran Oct 7, 2026
fe317f3
IN-005 ingress registry + Timing-Model soak verdict (Tier 2 complete)
guycochran Oct 7, 2026
3050d72
data essence: ANC / CEA-608 captions as a video/smpte291 MXL data flo…
guycochran Oct 7, 2026
871c066
nmos: IS-05 connection shim (routable) + Timed-Data readiness note (T…
guycochran Oct 7, 2026
489b115
restamp: drive a local wall-clock grain grid + drop ahead-of-realtime…
guycochran Oct 7, 2026
7c362b0
restamp: fix KeyError('last_mapped') + add probe-driving regression t…
guycochran Oct 7, 2026
13fcc0c
restamp: err diag = grid residual, not stale source-vs-offset gap (HW…
guycochran Oct 7, 2026
83c83ab
docs: ADR-001 — NMOS owns connection control, we own the media engine
guycochran Oct 7, 2026
888417f
nmos: IS-05 v1.2 + MXL flow identity in transport_params (ADR-001 con…
guycochran Oct 7, 2026
2d49d7d
nmos: portable --facility default (127.0.0.1:3100), resolving PR #38'…
guycochran Oct 8, 2026
4b49227
tests: drop now-stale nmos_node.py allowlist entry (follows the facil…
guycochran Oct 8, 2026
f89d3e6
review: fix self-review findings — dead relock metric, soak false ver…
guycochran Oct 8, 2026
9afee9a
restamp: re-anchor the grid on a gross discontinuity (fix reconnect s…
guycochran Oct 8, 2026
1754153
restamp: monotonic floor on the audio re-anchor (fix audio -5 restart…
guycochran Oct 8, 2026
16339a3
fan-out: leaky tee branches so one slow leg can't SRT-error the other…
guycochran Oct 8, 2026
5a55bd8
site: $400-box milestone + EFA (Nitro v3→v4) correction in changelog;…
guycochran Oct 9, 2026
9e2d8d2
docs(review): drop the '(Riedel)' employer tag + private-correction f…
guycochran Oct 9, 2026
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
26 changes: 24 additions & 2 deletions docker/guest-ingest.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@
# docker build -f docker/guest-ingest.Dockerfile -t mxl-guest-ingest:local .
# Run (quickstart wires this): one container per guest slot, SRT in → MXL flow.

# Base pinned by digest for reproducibility (docs/VERSIONS.md). Override to track
# upstream: docker build --build-arg BASE=ghcr.io/cbcrc/test-generator:latest ...
# Base pinned by digest for reproducibility (docs/VERSIONS.md). This digest bakes
# in MXL SDK v1.1.0 (released 2026-09-09; Flow + Fabric API) — the gst-mxl-rs
# contract contribution_core._restamp relies on (mxlsink maps pts->grain index via
# one shared clock offset D and has NO backward-index guard, so the writer requires
# monotonic PTS from us). Verify with the commands in docs/VERSIONS.md before a
# cold-clone proof; a base refresh could silently change the SDK version.
# Override to track upstream: docker build --build-arg BASE=ghcr.io/cbcrc/test-generator:latest ...
ARG BASE=ghcr.io/cbcrc/test-generator@sha256:09cad0981475095ab948ca51511d4fbdc0521e2a23632d50abaf14fc3847cd92
FROM ${BASE}

Expand All @@ -29,6 +34,23 @@ RUN apt-get update -qq \
&& apt-get install -y -qq --no-install-recommends gstreamer1.0-libav \
&& rm -rf /var/lib/apt/lists/*

# OPT-IN: closed-caption / ANC (ST-2038) data-flow support for CaptionFileAdapter
# (Tier 3.1, docs/ROADMAP-FROM-SPECS-2026-10.md). Needs `ccconverter`
# (gstreamer1.0-plugins-bad) + the `rsclosedcaption` plugin (cctost2038anc /
# tttocea608) from gst-plugins-rs >= 0.14, which the stock cbcrc base lacks. OFF by
# default so the HW-proven image is byte-identical; enable for a caption build with:
# docker build --build-arg WITH_CAPTIONS=1 -f docker/guest-ingest.Dockerfile ...
# NOTE: rsclosedcaption is not in Ubuntu apt; the upstream devcontainer builds it from
# source (rust/gst-mxl-rs/README.md). This arg installs plugins-bad and documents the
# remaining gst-plugins-rs source build — it is NOT yet HW-verified end to end.
ARG WITH_CAPTIONS=0
RUN if [ "$WITH_CAPTIONS" = "1" ]; then \
apt-get update -qq \
&& apt-get install -y -qq --no-install-recommends gstreamer1.0-plugins-bad \
&& rm -rf /var/lib/apt/lists/* ; \
echo "WITH_CAPTIONS=1: plugins-bad (ccconverter) installed; rsclosedcaption (gst-plugins-rs >=0.14) must still be built from source — see rust/gst-mxl-rs/README.md" ; \
fi

# the contribution seam (core + adapters + entrypoints, flat so imports resolve from /opt/seam)
# guest_audio.py = the A/V guest's AUDIO leg (v0.3); facility.py lets both legs resolve
# flow UUIDs from config/facility.json (copied below) with the baked-in fallback.
Expand Down
99 changes: 99 additions & 0 deletions docs/ADR-001-nmos-control-plane.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<!-- SPDX-FileCopyrightText: 2026 Contributors to the Media eXchange Layer project. -->
<!-- SPDX-License-Identifier: Apache-2.0 -->

# ADR-001: NMOS owns connection control; we own the media engine

- **Status:** Accepted
- **Date:** 2026-10-08
- **Decision owners:** mxl-cloud-production-demo maintainers
- **Supersedes / relates to:** `docs/ARCHITECTURE-CORE-AND-SKINS.md`,
`docs/ROADMAP-FROM-SPECS-2026-10.md`, `docs/FINDINGS.md` §6 (reader-wedge)

## Context

Two signals from people who define this space prompted a deliberate architectural
decision rather than a drift:

1. **cbcrc/mxl-hands-on#34** (our report: "readers wedge silently when a source flow is
recreated; status APIs still say running"). The cbcrc maintainer closed it as
wontfix-for-the-test-apps: *"The right solution for this will be to embrace NMOS when it
is going to be available."* He pointed at `garethsb/mxl-nmos-c-example`.
2. **`garethsb/mxl-nmos-c-example`** (Gareth Sylvester-Bradley, NVIDIA/AMWA) — a producer
Node + consumer Node where the **consumer opens MXL flows only after an IS-05 activation**,
carrying `{mxl_flow_id, mxl_domain_id}` in the IS-05 `transport_params`, with disable/
re-enable. It is a *connection* demo: it contains no switcher — no selector, keyer, mixer,
restamp, or contribution ingress.

The tempting but wrong framing is "build the switcher with NMOS **instead of** what we're
doing." That treats a control protocol and a media application as alternatives. They are
different layers. NvNmos + Gareth's example give you zero switching; our engine is not a
connection router. The question is not either/or — it is *which layer owns what*.

## Decision

**Build and own the media engine. Delegate connection control to NMOS. Do not reinvent the
data plane or the control protocol.**

Layer ownership:

| Layer | What it is | Owner |
|---|---|---|
| Data plane | grains in shared memory (`mxlsink`/`mxlsrc`) | **MXL SDK** — consume, never reinvent |
| **Media engine** | select / cut / key / mix / restamp / conform; program & preview | **This project** — the differentiated, unowned work |
| **Contribution ingress** | SRT/RTSP guests → MXL, restamped onto the local grain clock (IN-005) | **This project** |
| **Connection control** | discover flows; route Sender→Receiver; re-open on recreation | **NMOS** (IS-04 discovery, IS-05 connection) |
| Timed metadata | ANC / captions / SCTE as event flows | **MXL v1.2** (dmf-mxl#327) — consume when it lands |

Corollaries:
- **The media engine is the product.** No one else in the ecosystem is building the open
software switcher; the connection demos and the SDK stop at the boundaries of our layer.
This is our positioning and we invest here.
- **Connection control converges to NMOS.** Our REST cut API and the `/api/mxl/repair`
cascade are the *operational* form of what IS-05 standardizes. We promote NMOS from a
side "shim" to the **primary** routing/connection interface over time.
- **The reader-wedge (#34) proper fix is NMOS re-activation, not stat-polling.** A reader
re-opens a recreated flow through an IS-05 (re)activation — the pattern Gareth's consumer
demonstrates — rather than holding a stale handle. We adopt this once a controller is in
the loop; until then the repair cascade remains the pragmatic patch.

## What we explicitly will NOT do

- **Not** reimplement the MXL shared-memory data plane.
- **Not** hand-roll a bespoke connection/routing protocol when IS-04/IS-05 exist. (This is
exactly what #34's closure warned against; we take the hint.)
- **Not** throw away the media engine to "adopt NMOS." NMOS has no media engine to adopt.
- **Not** hard-depend on NvNmos / `nvnmosd`. Gareth's repo and `gst-nmos-rs` are C-API
*references*; our node stays a lightweight BCP-007-03 implementation. We copy the **wire
format** (standard IS-05), not the daemon.

## Convergence steps (how we get from here to the decision)

Current state: `tools/nmos_node.py` does IS-04 discovery + an IS-05 **`connection/v1.2`**
shim whose receiver `/staged` activation drives a real selector cut (PR #37).

Convergence toward the reference (steps 1–4 ✅ done on PR #37, step 5 pending):
1. ✅ **Carry flow identity in `transport_params`**, not just IS-04 tags:
`transport_params: [{ "mxl_flow_id": "<uuid>", "mxl_domain_id": "<uuid|null>" }]`.
2. ✅ **Connection API bumped to v1.2** (the version `connect.sh` in the reference speaks);
device control = `sr-ctrl/v1.2`.
3. ✅ **Sender/Receiver disable + re-enable** (IS-05 `master_enable` false→true; disable
activates without forcing a program cut).
4. ✅ **Domain UUID advertised** as `mxl_domain_id` (resolved from `domain_def.json`'s `id`,
then env, then the legacy string).
5. ⬜ **Reader re-open via re-activation** (the #34 fix) — wire `flow_stabilizer` / the repair
path to re-drive IS-05 activation on flow recreation, once a controller drives activations.

None of these touch the media engine; they are all in the control-plane layer, which is the
point of this ADR.

## Consequences

- **Positive:** we invest effort where we are differentiated (media engine, contribution
seam) and standards-compliant where the ecosystem already converged (NMOS control). We
align with the maintainers who define the space instead of diverging from them.
- **Positive:** the reader-wedge gets an architecturally-correct fix path, not just a patch.
- **Cost:** maturing the NMOS node is real work (the 5 steps above), and full reader-reopen
needs a controller in the loop — deferred until that exists.
- **Risk accepted:** until the convergence steps land, our connection control is non-standard
(REST + repair cascade). That is a known, bounded gap with a written plan to close it — not
a drift.
24 changes: 24 additions & 0 deletions docs/CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,34 @@ Already env-driven before this change (unchanged): `MXL_GUEST_HOST`, `MXL_GUEST_
|---|---|---|
| `MXL_SELFHEAL` | `scripts/quickstart.sh` — start `tools/mxl-selfheal.sh --watch` (selector + relay drift recovery) | **unset / off**. Set `MXL_SELFHEAL=1` to enable. |
| `MXL_SELFHEAL_INTERVAL` | `tools/mxl-selfheal.sh` — seconds between `--watch` passes | `15` |
| `MXL_INGRESS_DIR` | `tools/contribution_core.py` — directory for the IN-005 ingress registry (one `<flow8>.json` per ingest: source caps/provenance + the restamp's locked offset, running err, grain step, hard-relock count). Read by `tools/ingress-soak.sh`. | `/tmp/mxl-ingress`. Set **empty** to disable (no file written). |

Self-heal is **opt-in**. Desired-state reporting (`docs/DESIRED-STATE-v0.md`) stays
report-only and is separate from this watcher.

## IN-005 ingress registry + soak verdict

Each contribution ingest writes an **IN-005 ingress record** (`$MXL_INGRESS_DIR/<flow8>.json`)
as the restamp locks and runs — the signal's negotiated source caps (provenance) and the
timing adjustments applied (locked offset, running wall-clock `err_ms`, grain step,
hard-relock count). AMWA IN-005 ("External Signal Ingress for DMF") asks that these be
*traceable*; this is that trace. Inspect it live:

```sh
cat /tmp/mxl-ingress/9e111e00.json
```

`tools/ingress-soak.sh` turns those records into an objective PASS/FAIL in the **Timing
Model's own terms** ("latency low and mostly constant over hours"): over a sampling window
it asserts frames keep advancing (essence moving, not DRIFT), `|err_ms|` stays bounded
(`ERR_BAND_MS`), `err_ms` doesn't trend away (`ERR_DRIFT_MS`), and `hard_relocks` does not
climb (no re-lock churn — the Oct-7 60fps-restamp symptom). This is the acceptance test for
the 60fps restamp HW re-verify and any guest soak:

```sh
DURATION=300 INTERVAL=10 tools/ingress-soak.sh guest1 # 5-min soak, PASS/FAIL + exit code
```

## Security-related switches (see [SECURITY.md](../SECURITY.md))

| Variable | Used by | Default |
Expand Down
Loading
Loading