diff --git a/AGENTS.md b/AGENTS.md index 8794464..eeb556e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,7 @@ # Agent notes - Read `SPEC.md` before changing behaviour. Create `IMPLEMENTATION_PLAN.md` with the first code: pins, spike results (SPEC §19), source layout, every deviation from the spec (numbered), and the platform guideline G1–G14 table (Item | Requirement | Status | Evidence | Change), as in mxl-multiviewer. -- The platform guideline (`mmz-srf/mxl-poc-platform` `docs/requests/leeo86-v1-readiness.md`, G1–G14) and its follow-ups win over this repo's habits. Record any conflict in SPEC §20. +- The platform guideline (the private platform repository, `docs/requests/leeo86-v1-readiness.md`, G1–G14) and its follow-ups win over this repo's habits. Record any conflict in SPEC §20. - C++20, CMake ≥ 3.24, Ninja. No GStreamer in the media path. `-Wall -Wextra`; a clean build is the lint signal. - Pins, each in exactly two places (`docker/Dockerfile` build args and `.github/workflows/ci.yaml`, with a "keep in sync" comment): - CEF `144.0.21+g4f5b28c+chromium-144.0.7559.248` (Linux x64 minimal distribution, hash-checked); diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index d05e323..037bc45 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -77,7 +77,7 @@ The image builds (2.2 GB uncompressed) and starts; `/readyz` turns 200 when the | `tests/integration/nmos_test.py` (IS-05 v1.2) | video and audio sender disabled and enabled: active document, `/api/v1/nmos` and `sender_enabled` agree; video flow read with `mxl-bs-grain-reader`: no new grain in 2 s while disabled, 102 in 2 s after enabling. Audio disabled, `docker restart`: still disabled after the restart, video enabled. Fixed on the way: `sender_enabled` was missing until the first IS-05 change (the document holds only changed states); every sender of the configuration is now reported | | Renderer probe | One GPU start on the lab reported `render_degraded` (renderer `none`: no WebGL context) and kept it until the restart; the next start had ANGLE on the A16 again. A software answer is now asked again twice, 5 s apart. Checked with `BROWSER_CHROMIUM_FLAGS_APPEND=disable-webgl`: two `render_probe_retry`, then `render_degraded` 11 s after the load; a normal start still reports `gpu` on the first probe | | Image size | 2.22 GB → 907 MB: `libcef.so` of the minimal CEF distribution carries its debug symbols (1.5 GB); `strip --strip-unneeded` in the CEF stage leaves 233 MB. The fonts SPEC §15 names stay (the apt layer is 442 MB). Afterwards in GPU mode on the lab: interaction and IME, §4.5 pages, template/crash/hang/DevTools, click → picture (max 148 ms), IS-05 tests all pass | -| AMWA (`tests/nmos/amwa.sh` in CI, first run) | IS-05-02 without failures. IS-05-01 test_11_01 and BCP-007-03-01 test_17: a Sender's active `mxl_flow_id` stayed `auto`; fixed (resolved to the sender's flow, checked on the lab: staged `auto`, active the real ids). IS-04-01: the node never registered with the tool's DNS-SD-advertised mock registry: nmos-cpp took the interface's DNS domain (`cloudapp.net` in CI, `media.int` in the lab) instead of `local.`, and Avahi failed every browse and advertisement with -65537 (not the uid: root fails the same). With `NMOS_DNS_SD=true` the domain is now `local.` (lab: 0 errors). Second run also found test_11_01 again: `make_connection_mxl_sender` leaves `auto` in `/active` from the start; the active parameters are now the real ids from creation | +| AMWA (`tests/nmos/amwa.sh` in CI, first run) | IS-05-02 without failures. IS-05-01 test_11_01 and BCP-007-03-01 test_17: a Sender's active `mxl_flow_id` stayed `auto`; fixed (resolved to the sender's flow, checked on the lab: staged `auto`, active the real ids). IS-04-01: the node never registered with the tool's DNS-SD-advertised mock registry: nmos-cpp took the interface's DNS domain (`cloudapp.net` in CI, an internal domain in the lab) instead of `local.`, and Avahi failed every browse and advertisement with -65537 (not the uid: root fails the same). With `NMOS_DNS_SD=true` the domain is now `local.` (lab: 0 errors). Second run also found test_11_01 again: `make_connection_mxl_sender` leaves `auto` in `/active` from the start; the active parameters are now the real ids from creation | | Pod-like run (read-only root, all capabilities dropped, uid 1000, only `/Volumes/mxl/` mounted from the host tmpfs) | ready, grains without misses, exit 143 on SIGTERM, domain emptied. Before: refused with 78, because the tmpfs check looked at the MXL root only; it now accepts a tmpfs at the domain directory (SPEC §13) | | CPU and memory metrics | `process_cpu_seconds_total`, `process_resident_memory_bytes`, `mxl_browser_source_cef_processes_cpu_seconds_total` and `_resident_bytes` (SPEC §12 named them, they were missing). Lab, static page: app 0.18 cores and 411 MB, CEF processes 721 MB | | Compose demo (`docker/docker-compose.demo.yaml`, lab, without Prometheus because the lab runs one) | registry, browser source, monitor and MediaMTX up; the route service plays the lower third and routes the monitor: channel 1 video `running`, both nodes registered, Grafana loads the dashboard (12 panels). Fixed on the way: `rhastie/nmos-cpp:latest-amd64` no longer exists (now the platform's registry image), loopback `NMOS_HOST_ADDRESS` is refused, the monitor (uid 1000) could not write a root-owned volume | diff --git a/README.md b/README.md index a29ba72..93a3e82 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,11 @@ Renders a web page offscreen with the Chromium Embedded Framework (CEF) and writes it as MXL flows: video (v210, optionally with a key as v210a or a separate key flow) and audio (float32, 48 kHz). One BeginFrame per MXL grain index keeps the page frame-locked to house time (TAI). Senders register with NMOS (IS-04/IS-05, BCP-007-03) and are routed like every other media function of the MXL PoC platform. An operator sees a preview and can interact with the page from the web UI, like the "Interact" window of the OBS Browser Source; HTML graphics templates are driven CasparCG-style (`play`, `stop`, `next`, `update`), and EBU OGraf v1 graphics play on up to 8 layers with a graphics library, a controller tab, an `ograf` widget and the OGraf Server API. -**Status: 1.2.0.** [`SPEC.md`](SPEC.md) is the contract, [`IMPLEMENTATION_PLAN.md`](IMPLEMENTATION_PLAN.md) records pins, spike results and deviations, [`docs/api.md`](docs/api.md) is the wire format of the HTTP and WebSocket API. +Part of [mxl-stack](https://github.com/LeeO86/mxl-stack), a collection of open-source MXL media functions with Docker Compose and Kubernetes examples. + +![mxl-browser-source: a lower third rendered over a transparent background](docs/images/web-ui.png) + +**Status: 1.4.0.** [`SPEC.md`](SPEC.md) is the contract, [`IMPLEMENTATION_PLAN.md`](IMPLEMENTATION_PLAN.md) records pins, spike results and deviations, [`docs/api.md`](docs/api.md) is the wire format of the HTTP and WebSocket API. ## Run it @@ -11,7 +15,7 @@ The own MXL domain must be on a tmpfs: the MXL root, or a tmpfs mounted at the d ```sh docker run -d --name browser --network host --shm-size 1g --ulimit rtprio=10 -u 1000:1000 \ -v /Volumes/mxl:/Volumes/mxl -v $PWD/config:/config \ - -e NMOS_REGISTRY_ADDRESS=10.0.0.10 -e NMOS_REGISTRY_PORT=8010 \ + -e NMOS_REGISTRY_ADDRESS=192.0.2.10 -e NMOS_REGISTRY_PORT=8010 \ -e MXL_OUTPUT_DOMAIN_DIR=/Volumes/mxl/browser-1 \ ghcr.io/leeo86/mxl-browser-source: ``` diff --git a/SPEC.md b/SPEC.md index 7fa1564..19d92f7 100644 --- a/SPEC.md +++ b/SPEC.md @@ -2,12 +2,12 @@ Status: v0.9 (implemented in this repository; spike results folded in, deviations in IMPLEMENTATION_PLAN.md) Repository: `LeeO86/mxl-browser-source` -Sibling projects this spec aligns with: `LeeO86/mxl-multiviewer`, `LeeO86/mxl-test-player`, `LeeO86/mxl-webrtc-monitor`, `LeeO86/mxl-decklink`, and the platform meta repo `mmz-srf/mxl-poc-platform` +Sibling projects this spec aligns with: `LeeO86/mxl-multiviewer`, `LeeO86/mxl-test-player`, `LeeO86/mxl-webrtc-monitor`, `LeeO86/mxl-decklink`, and the (private) platform repository Functional inspiration: the OBS Browser Source and its "Interact" window (`obsproject/obs-browser`), the CasparCG HTML producer and its template API (`CasparCG/server`, `src/modules/html`) The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are used as in RFC 2119. -The platform guideline G1–G14 (`mmz-srf/mxl-poc-platform`, `docs/requests/leeo86-v1-readiness.md`, "contract v1.0.0") and the follow-ups after the v1 releases (`docs/requests/leeo86-v1-followups.md`) apply in full. Where this document and the guideline disagree, the guideline wins; §20 lists every such conflict. Statements marked **(spike)** are not proven for the pinned CEF version and are settled by the spikes in §19 before the code depends on them. +The platform guideline G1–G14 (the private platform repository, `docs/requests/leeo86-v1-readiness.md`, "contract v1.0.0") and the follow-ups after the v1 releases (`docs/requests/leeo86-v1-followups.md`) apply in full. Where this document and the guideline disagree, the guideline wins; §20 lists every such conflict. Statements marked **(spike)** are not proven for the pinned CEF version and are settled by the spikes in §19 before the code depends on them. --- @@ -567,7 +567,7 @@ The file is one JSON document, written atomically by the UI and the API (`tmp` + }, "presets": [ {"name": "Bars", "url": "https://templates.local/bars.html", "background": "#000000"}, - {"name": "Scoreboard", "url": "https://graphics.media.int/scoreboard/", "css": "", "js": ""} + {"name": "Scoreboard", "url": "https://graphics.example.com/scoreboard/", "css": "", "js": ""} ], "senders": {"video": true, "key": true, "audio": true} } diff --git a/docs/images/web-ui.png b/docs/images/web-ui.png new file mode 100644 index 0000000..b230a42 Binary files /dev/null and b/docs/images/web-ui.png differ diff --git a/src/nmos/node.cpp b/src/nmos/node.cpp index 838670e..c9c6757 100644 --- a/src/nmos/node.cpp +++ b/src/nmos/node.cpp @@ -292,7 +292,7 @@ namespace mbs::nmos if (s.dnsSd) { // Multicast DNS-SD through Avahi works in "local." only. Unset, nmos-cpp takes the - // interface's DNS domain (media.int in the lab), and Avahi fails every browse and + // interface's DNS domain (an internal domain in the lab), and Avahi fails every browse and // advertisement with -65537. config[U("domain")] = web::json::value::string(U("local.")); } diff --git a/tests/unit/test_urlpolicy.cpp b/tests/unit/test_urlpolicy.cpp index 9ff67d5..4122f89 100644 --- a/tests/unit/test_urlpolicy.cpp +++ b/tests/unit/test_urlpolicy.cpp @@ -17,10 +17,10 @@ namespace TEST_CASE("URLs parse into scheme, host and port") { - auto u = mbs::ops::parseUrl("https://User@Graphics.Media.INT:8443/a?b#c"); + auto u = mbs::ops::parseUrl("https://User@Graphics.Example.COM:8443/a?b#c"); REQUIRE(u); CHECK(u->scheme == "https"); - CHECK(u->host == "graphics.media.int"); + CHECK(u->host == "graphics.example.com"); CHECK(u->port == 8443); u = mbs::ops::parseUrl("http://[fe80::1]/x"); REQUIRE(u); @@ -32,8 +32,8 @@ TEST_CASE("URLs parse into scheme, host and port") TEST_CASE("globs and CIDRs") { - CHECK(mbs::ops::globMatch("*.media.int", "gfx.media.int")); - CHECK_FALSE(mbs::ops::globMatch("*.media.int", "media.int")); + CHECK(mbs::ops::globMatch("*.example.com", "gfx.example.com")); + CHECK_FALSE(mbs::ops::globMatch("*.example.com", "example.com")); CHECK(mbs::ops::globMatch("score*board", "SCOREBOARD")); CHECK(mbs::ops::cidrMatch("10.42.0.0/16", "10.42.7.9")); CHECK_FALSE(mbs::ops::cidrMatch("10.42.0.0/16", "10.43.0.1")); @@ -88,11 +88,11 @@ TEST_CASE("deny adds to the built-in list; allow restricts to known hosts") UrlPolicy const deny(s); CHECK(reason(deny, "http://10.43.0.10/") == "denied"); CHECK(reason(deny, "https://api.internal.example/") == "denied"); - CHECK(reason(deny, "https://graphics.media.int/") == "ok"); + CHECK(reason(deny, "https://graphics.example.com/") == "ok"); - s.allow = {"*.media.int", "192.168.10.0/24"}; + s.allow = {"*.example.com", "192.168.10.0/24"}; UrlPolicy const allow(s); - CHECK(reason(allow, "https://graphics.media.int/lower-third/") == "ok"); + CHECK(reason(allow, "https://graphics.example.com/lower-third/") == "ok"); CHECK(reason(allow, "http://192.168.10.20/") == "ok"); CHECK(reason(allow, "https://example.org/") == "not_allowed"); CHECK(reason(allow, "https://templates.local/blank.html") == "ok"); @@ -118,31 +118,31 @@ TEST_CASE("OGraf mode with BROWSER_OGRAF_NETWORK=local: the library only, plus l TEST_CASE("a service worker's requests: refused while service workers are off, else the same policy") { UrlPolicySettings s; - s.allow = {"graphics.media.int"}; + s.allow = {"graphics.example.com"}; s.ownHost = "10.0.0.5"; s.ownPorts = {8160}; UrlPolicy const off(s); - CHECK(off.checkServiceWorker("https://graphics.media.int/data.json").reason == "service_worker"); + CHECK(off.checkServiceWorker("https://graphics.example.com/data.json").reason == "service_worker"); CHECK(off.checkServiceWorker("http://127.0.0.1:8160/api/v1/info").reason == "service_worker"); s.serviceWorkers = true; UrlPolicy const on(s); - CHECK(on.checkServiceWorker("https://graphics.media.int/data.json").allowed); + CHECK(on.checkServiceWorker("https://graphics.example.com/data.json").allowed); CHECK(on.checkServiceWorker("http://127.0.0.1:18471/probe").reason == "loopback"); CHECK(on.checkServiceWorker("http://169.254.169.254/latest/meta-data").reason == "link_local"); CHECK(on.checkServiceWorker("http://10.0.0.5:8160/api/v1/info").reason == "own_port"); CHECK(on.checkServiceWorker("http://172.17.0.1/").reason == "not_allowed"); CHECK(on.checkServiceWorker("data:text/plain,x").allowed); // a sub-resource, not a navigation s.localOnly = true; - CHECK(UrlPolicy(s).checkServiceWorker("https://graphics.media.int/data.json").reason == "ograf_local"); + CHECK(UrlPolicy(s).checkServiceWorker("https://graphics.example.com/data.json").reason == "ograf_local"); // The page's own requests do not depend on the setting. - CHECK(off.check("https://graphics.media.int/data.json", false).allowed); + CHECK(off.check("https://graphics.example.com/data.json", false).allowed); } TEST_CASE("same origin: scheme, host and port") { using mbs::ops::sameOrigin; CHECK(sameOrigin("http://10.0.0.5:18471/", "http://10.0.0.5:18471/nav-probe?x")); - CHECK(sameOrigin("https://Graphics.media.int/a", "https://graphics.media.int:443/b")); + CHECK(sameOrigin("https://Graphics.example.com/a", "https://graphics.example.com:443/b")); CHECK_FALSE(sameOrigin("http://10.0.0.5:18471/", "https://templates.local/blank.html")); CHECK_FALSE(sameOrigin("http://10.0.0.5:18471/", "http://10.0.0.5:18472/")); CHECK_FALSE(sameOrigin("http://a.example/", "https://a.example/"));