-
Notifications
You must be signed in to change notification settings - Fork 3
projectMM becomes an NDI source #80
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,125 @@ | ||||||||||||||
| # Plan — NDI output: projectMM as a video source | ||||||||||||||
|
|
||||||||||||||
| ## Context | ||||||||||||||
|
|
||||||||||||||
| Panel-card users on Discord (2026-08-24) asked for projectMM's rendered output to feed *their* | ||||||||||||||
| tools. One runs OBS → Spout → his own card driver and asked whether projectMM could be a Spout | ||||||||||||||
| source; another observed that OBS, Resolume and TouchDesigner all speak NDI, so projectMM could be | ||||||||||||||
| an NDI source and reach a Spout pipeline through one hop. | ||||||||||||||
|
|
||||||||||||||
| Input is not the gap: `NetworkReceiveEffect` already binds Art-Net, E1.31/sACN and DDP at once. | ||||||||||||||
| What is missing is the other direction — projectMM's pixels reaching a production visuals rig. | ||||||||||||||
|
|
||||||||||||||
| Decision recorded in [backlog-light § Integration with other LED and visuals tools](../../backlog/backlog-light.md): | ||||||||||||||
| **NDI first.** One implementation covers Windows, macOS, Linux and ARM, it discovers by name, and | ||||||||||||||
| it crosses machines. Spout (Windows) and Syphon (macOS) are lower latency and bit-exact but are | ||||||||||||||
| same-machine only, are two platform implementations, and leave Linux and the Pi with nothing. At | ||||||||||||||
| LED-wall pixel counts (a 256x256 wall is 65K pixels) the latency difference sits far below one | ||||||||||||||
| frame of the render loop, so coverage decides, not latency. | ||||||||||||||
|
|
||||||||||||||
| **Scope: output only.** NDI is bidirectional and an `NdiReceiveEffect` is a real second feature, | ||||||||||||||
| but it is not this branch. | ||||||||||||||
|
|
||||||||||||||
| ## The licensing constraint, and what it dictates | ||||||||||||||
|
|
||||||||||||||
| projectMM is GPL-3.0. The NDI runtime is proprietary and its licence requires a redistributor's own | ||||||||||||||
| EULA to carry NDI's terms forward, which GPL-3 forbids. **So projectMM must not redistribute it.** | ||||||||||||||
|
|
||||||||||||||
| This is not a blocker; it is a design constraint projectMM has already met once. **Npcap** is the | ||||||||||||||
| precedent: proprietary, required for raw L2 on Windows, and | ||||||||||||||
| [platform_desktop.cpp:814](../../../src/platform/desktop/platform_desktop.cpp) resolves `wpcap.dll` | ||||||||||||||
| with `LoadLibrary` rather than linking it, declaring the five functions with pcap's own signatures | ||||||||||||||
| rather than including `pcap.h`. The user installs Npcap; the panel-card tutorial says so; the binary | ||||||||||||||
| builds and runs identically without it and reports raw send unavailable. | ||||||||||||||
|
|
||||||||||||||
| NDI follows exactly that arrangement: | ||||||||||||||
|
|
||||||||||||||
| - **The user installs the NDI runtime.** We bundle nothing and ship no SDK. | ||||||||||||||
| - **Resolve at run time** (`LoadLibrary` on Windows, `dlopen` elsewhere), never link. | ||||||||||||||
| - **Declare the needed functions with the SDK's own signatures**, never include its headers — so the | ||||||||||||||
| SDK is not a build requirement for CI or for any contributor. | ||||||||||||||
| - **Degrade visibly** when it is absent (ADR 0002, allocate-and-degrade): a status line, not a | ||||||||||||||
| failure. | ||||||||||||||
|
|
||||||||||||||
| ## Design | ||||||||||||||
|
|
||||||||||||||
| ### The platform seam | ||||||||||||||
|
|
||||||||||||||
| NDI is a host capability, so it lives behind `platform::` like every other one, and the driver never | ||||||||||||||
| sees a `dlopen`. Following `hasNamedNetInterfaces`, each platform declares a `constexpr bool hasNdi` | ||||||||||||||
| in its own `platform_config.h`: **true on desktop, false on ESP32** (no runtime to load, and the | ||||||||||||||
| encode cost does not belong on a microcontroller). | ||||||||||||||
|
|
||||||||||||||
| The seam is deliberately tiny — four functions, mirroring the pcap surface: | ||||||||||||||
|
|
||||||||||||||
| ``` | ||||||||||||||
| bool ndiAvailable(); // runtime present and loaded | ||||||||||||||
| bool ndiSenderOpen(const char* name); // create a named source | ||||||||||||||
| void ndiSenderClose(); | ||||||||||||||
| bool ndiSendFrame(const uint8_t* rgb, uint16_t w, uint16_t h, uint8_t fps); | ||||||||||||||
| ``` | ||||||||||||||
|
Comment on lines
+55
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Specify the fenced-code language. Line 55 starts an unlabeled fenced code block. Add Proposed fix-```
+```cpp
bool ndiAvailable();📝 Committable suggestion
Suggested change
🧰 Tools🪛 markdownlint-cli2 (0.23.2)[warning] 55-55: Fenced code blocks should have a language specified (MD040, fenced-code-language) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||||||||||||||
|
|
||||||||||||||
| `ndiSendFrame` takes a tightly-packed RGB buffer and the geometry; the platform layer converts to | ||||||||||||||
| NDI's frame struct. A stub in the no-NDI build returns false from everything, which is what keeps | ||||||||||||||
| `mm_tests` and every ESP32 target compiling untouched. | ||||||||||||||
|
|
||||||||||||||
| ### The driver | ||||||||||||||
|
|
||||||||||||||
| `NdiDriver : DriverBase`, registered like the others in `main.cpp`, gated on `hasNdi` so it is | ||||||||||||||
| offered only where it can run. It follows `PreviewDriver` closely, which is the existing driver that | ||||||||||||||
| also turns the rendered buffer into a frame for a remote consumer: | ||||||||||||||
|
|
||||||||||||||
| - **Controls**: `sourceName` (what appears in OBS's source list, defaulting to the device name), | ||||||||||||||
| `fps` (a ceiling, as in PreviewDriver), plus the inherited correction controls. | ||||||||||||||
| - **`tick()`**: rate-limit to `fps`, read the source buffer, hand it to `platform::ndiSendFrame`. | ||||||||||||||
| `MM_NONBLOCKING`, and no allocation in the tick — the RGB staging buffer is sized in `prepare()`. | ||||||||||||||
| - **`prepare()`**: size the staging buffer to the layer, open the sender, set the status. Re-runs on | ||||||||||||||
| a geometry change, exactly as `affectsPrepare` governs elsewhere. | ||||||||||||||
| - **Status**, which is the whole user-facing diagnostic: | ||||||||||||||
| - no runtime → `NDI runtime not installed` | ||||||||||||||
| - open failed → the reason | ||||||||||||||
| - running → `sending <w>x<h> at <fps> fps as '<name>'` | ||||||||||||||
|
|
||||||||||||||
| ### What this deliberately does not do | ||||||||||||||
|
|
||||||||||||||
| - **No audio.** NDI carries it; projectMM has no video-audio pairing to send. | ||||||||||||||
| - **No NDI HX / compression choice.** Ship the default; add a control only if a user needs it. | ||||||||||||||
| - **No receive.** Its own feature. | ||||||||||||||
|
|
||||||||||||||
| ## Steps | ||||||||||||||
|
|
||||||||||||||
| 1. **The platform seam.** Declare `hasNdi` in both `platform_config.h` files and the four functions | ||||||||||||||
| in `platform.h`. Implement the runtime load in `platform_desktop.cpp` beside the Npcap block, | ||||||||||||||
| reusing its shape. ESP32 needs no implementation (the flag is false). | ||||||||||||||
| *Tests:* the stub path — `ndiAvailable()` false with no runtime, and every call safe. | ||||||||||||||
| 2. **`NdiDriver`.** The driver above, registered in `main.cpp` behind the `hasNdi` gate. | ||||||||||||||
| *Tests:* controls round-trip; a prepare with no runtime reports the status and does not crash; | ||||||||||||||
| the frame conversion is pinned against a known buffer. | ||||||||||||||
| 3. **Docs.** A driver card in `docs/moonmodules/light/drivers.md`, and a short section in the | ||||||||||||||
| panel-cards tutorial's sibling — where to install the runtime per OS, exactly as §6.1 does for | ||||||||||||||
| Npcap. | ||||||||||||||
| 4. **Bench.** Install the NDI runtime and OBS with the DistroAV plugin; confirm projectMM appears as | ||||||||||||||
| a source by name and that the wall's image arrives. **This is the gate: an output path is not | ||||||||||||||
| verified until a receiver shows the frames.** | ||||||||||||||
|
|
||||||||||||||
| ## Risks | ||||||||||||||
|
|
||||||||||||||
| 1. **Nothing is verifiable without a receiver.** Steps 1 and 2 can be written and unit-tested blind, | ||||||||||||||
| but "it works" requires step 4. The plan is ordered so the untestable claim comes last. | ||||||||||||||
| 2. **The SDK's function signatures must be right without including its headers.** Getting one wrong | ||||||||||||||
| is a silent crash rather than a compile error — the same hazard the Npcap block carries, and the | ||||||||||||||
| reason its comment names the five functions explicitly. Take them from the SDK's public docs and | ||||||||||||||
| record where each came from. | ||||||||||||||
| 3. **Frame format.** NDI wants a specific FourCC and stride; a mismatched stride shows as a skewed | ||||||||||||||
| image rather than an error. Pin the conversion with a unit test. | ||||||||||||||
| 4. **CPU cost of the encode**, which runs on the desktop render thread. Measure before claiming a | ||||||||||||||
| frame rate; the `fps` ceiling is the mitigation. | ||||||||||||||
|
|
||||||||||||||
| ## Verification | ||||||||||||||
|
|
||||||||||||||
| - `cmake --build build` (zero warnings) and `ctest` on a machine with **no** NDI runtime, proving the | ||||||||||||||
| degrade path is the default one CI sees. | ||||||||||||||
| - The same, on a machine **with** the runtime. | ||||||||||||||
| - OBS (DistroAV) on the same machine, then on a second machine, confirming the cross-machine claim | ||||||||||||||
| that chose NDI over Spout in the first place. | ||||||||||||||
| - The product owner's eyes on the OBS preview. | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -152,6 +152,33 @@ Origin: projectMM, on [MoonLight](https://github.com/ewowi/MoonLight/blob/main/s | |
|
|
||
| Detail: [technical](moxygen/PreviewDriver.md) | ||
|
|
||
| <a id="ndi"></a> | ||
|
|
||
| ### NDI 🖥️ · video out | ||
|
|
||
| Publishes the layer as an **NDI video source**, so OBS, Resolume, TouchDesigner, MadMapper or any other NDI receiver can pick projectMM up by name — on this machine or another one on the network. Where the Preview driver draws the lights for a person, this hands the same frame to a production tool as video. | ||
|
|
||
| The grid's `physicalWidth` × `physicalHeight` becomes the frame; each light is one pixel, with the driver's own output correction applied so a receiver sees what the wall sees. | ||
|
|
||
| **Desktop only.** The NDI runtime is a desktop library with no microcontroller build, so the driver is offered on macOS, Windows and Linux and not on an ESP32. | ||
|
|
||
| **You install the runtime; projectMM never ships it.** projectMM is GPL-3.0 and the NDI runtime is proprietary, so it is loaded on demand and never bundled — the same arrangement as Npcap for the [Panel Card](#panelcard) driver. Without it the driver simply reports `NDI runtime not installed`; nothing else changes. | ||
|
|
||
| - **macOS** — install [NDI Tools](https://ndi.video/tools/) (free). It ships the runtime inside its app bundles rather than system-wide, which projectMM knows to look for. A Resolume install also carries one. | ||
| - **Windows** — the [NDI Tools](https://ndi.video/tools/) or SDK installer puts `Processing.NDI.Lib.x64.dll` on the PATH. | ||
| - **Linux** — install the NDI SDK; projectMM looks for `libndi.so.5`, `libndi.so.6` and `libndi.so`. | ||
|
|
||
| To watch the output you need a receiver: **NDI Video Monitor** (part of NDI Tools) is the simplest, and OBS gains an "NDI Source" via the [DistroAV](https://github.com/DistroAV/DistroAV) plugin. | ||
|
|
||
| - `sourceName` — the name a receiver lists. Blank uses the device's own name. | ||
| - `fps` — frame-rate ceiling (default 30, 1–120). NDI paces the receiver from this. | ||
|
Comment on lines
+173
to
+174
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Describe the driver-owned frame-rate limit.
As per path instructions, "Docs land with the code, not at merge time: the module's spec and catalog card describe what actually shipped." 🤖 Prompt for AI AgentsSource: Path instructions |
||
|
|
||
| Status tells you where you are: `NDI runtime not installed` (install it), `could not create the NDI source` (the runtime is there but refused), or `sending <w>x<h> at <n> fps` when it is live. | ||
|
|
||
| Origin: projectMM, against NewTek/Vizrt's documented NDI C API | ||
|
|
||
| Detail: [technical](moxygen/NdiDriver.md) | ||
|
|
||
| ## LED driver — details | ||
|
|
||
| **Which driver?** | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Update the workaround status.
.github/workflows/release.yml, Lines 702-729, already definesrestage-pages-for-tagand automatically dispatches a main-context Pages deployment for eachv*tag. The “Until then” text and required manual command are stale. Mark the automatic follow-on as implemented and leave only any unresolved branch-protection alternative.As per path instructions, documentation must describe what actually shipped.
🧰 Tools
🪛 LanguageTool
[locale-violation] ~240-~240: In American English, ‘afterward’ is the preferred variant. ‘Afterwards’ is more commonly used in British English and other dialects.
Context: ...workflow run release.yml -f tag=vX.Y.Z` afterwards, which is exactly the kind of remember-...
(AFTERWARDS_US)
[style] ~240-~240: Consider an alternative for the overused word “exactly”.
Context: ...yml -f tag=vX.Y.Z` afterwards, which is exactly the kind of remember-to-do-it step a re...
(EXACTLY_PRECISELY)
🤖 Prompt for AI Agents
Source: Path instructions