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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
2 changes: 1 addition & 1 deletion IMPLEMENTATION_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<instance>` 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 |
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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:<version>
```
Expand Down
6 changes: 3 additions & 3 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down Expand Up @@ -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}
}
Expand Down
Binary file added docs/images/web-ui.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/nmos/node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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."));
}
Expand Down
26 changes: 13 additions & 13 deletions tests/unit/test_urlpolicy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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"));
Expand Down Expand Up @@ -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");
Expand All @@ -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/"));
Expand Down
Loading