@jamezjb/node-red-ndi-preview 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (13 November 2026)
4
+
5
+ First version.
6
+
7
+ - `ndi-thumbnail`: a JPEG still of an NDI® source every few seconds or on each message, with frame statistics.
8
+ The message has the same shape as an http request node's reply.
9
+ - `ndi-live`: a live Motion-JPEG preview at `<httpNodeRoot>/ndi-live/<node id>.mjpg` (and one still at `.jpg`).
10
+ It receives only while someone is watching, encodes each frame once for all viewers, and limits the number of
11
+ viewers.
12
+ - All NDI work runs in a separate helper process, which is restarted if it crashes or hangs, so Node-RED keeps
13
+ running. Nodes that show the same source share one connection to it. Tally is never set.
14
+ - Uses the NDI Runtime the user installs (NDI 5 or 6); contains no NDI code. Without the runtime the nodes say
15
+ "NDI Runtime not installed (see ndi.video)" and do nothing else.
16
+ - macOS (universal) and Windows x64. No npm dependencies.
package/LICENSE.md ADDED
@@ -0,0 +1,80 @@
1
+ Copyright (c) 2026 James Breedon
2
+
3
+ Licensed under the PolyForm Noncommercial License 1.0.0.
4
+ For commercial licensing, contact james@jamesbreedon.com.
5
+
6
+ ---
7
+
8
+ # PolyForm Noncommercial License 1.0.0
9
+
10
+ <https://polyformproject.org/licenses/noncommercial/1.0.0>
11
+
12
+ ## Acceptance
13
+
14
+ In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
15
+
16
+ ## Copyright License
17
+
18
+ The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
19
+
20
+ ## Distribution License
21
+
22
+ The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
23
+
24
+ ## Notices
25
+
26
+ You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
27
+
28
+ > Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
29
+
30
+ ## Changes and New Works License
31
+
32
+ The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
33
+
34
+ ## Patent License
35
+
36
+ The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
37
+
38
+ ## Noncommercial Purposes
39
+
40
+ Any noncommercial purpose is a permitted purpose.
41
+
42
+ ## Personal Uses
43
+
44
+ Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose.
45
+
46
+ ## Noncommercial Organizations
47
+
48
+ Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding.
49
+
50
+ ## Fair Use
51
+
52
+ You may have "fair use" rights for the software under the law. These terms do not limit them.
53
+
54
+ ## No Other Rights
55
+
56
+ These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
57
+
58
+ ## Patent Defense
59
+
60
+ If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
61
+
62
+ ## Violations
63
+
64
+ The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
65
+
66
+ ## No Liability
67
+
68
+ ***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
69
+
70
+ ## Definitions
71
+
72
+ The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
73
+
74
+ **You** refers to the individual or entity agreeing to these terms.
75
+
76
+ **Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
77
+
78
+ **Your licenses** are all the licenses granted to you for the software under these terms.
79
+
80
+ **Use** means anything you do with the software requiring one of your licenses.
package/README.md CHANGED
@@ -1,3 +1,105 @@
1
- # Temporary Holding Version
1
+ # Node-RED NDI Preview
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Node-RED nodes that show NDI® sources:
4
+
5
+ - **`ndi-thumbnail`**: a JPEG still of a source every few seconds (or on each message), with frame statistics.
6
+ - **`ndi-live`**: a silent, low-latency live preview of a source that any browser can show, for checking a picture.
7
+
8
+ NDI® is a registered trademark of Vizrt NDI AB. This project is not affiliated with or endorsed by Vizrt NDI AB;
9
+ "NDI" in its name says only what the nodes work with. More about NDI: <https://ndi.video/>.
10
+
11
+ ## Install
12
+
13
+ In Node-RED: **Manage palette**, **Install**, search for `@jamezjb/node-red-ndi-preview`. Or, in your Node-RED user
14
+ folder (usually `~/.node-red`):
15
+
16
+ npm install @jamezjb/node-red-ndi-preview
17
+
18
+ Then restart Node-RED.
19
+
20
+ ## What you need
21
+
22
+ - **The NDI Runtime, installed by you, on the computer that runs Node-RED.** This package contains no NDI code.
23
+ Download the free NDI 6 Runtime, or NDI Tools (which includes it), from <https://ndi.video/tools/>, install it,
24
+ then restart Node-RED. Without it, the nodes say "NDI Runtime not installed (see ndi.video)" and do nothing
25
+ else. NDI 5 and 6 runtimes work; NDI 6 is recommended.
26
+ - **macOS** (11 or later, Apple silicon or Intel) or **Windows x64** (10 or 11). Other systems are not supported:
27
+ the nodes load, and say so.
28
+ - Node-RED 3.1 or later, on Node.js 18 or later.
29
+
30
+ The package has no npm dependencies.
31
+
32
+ ## The nodes
33
+
34
+ ### ndi-thumbnail
35
+
36
+ Pick a source by name ("Find sources" lists what the Node-RED computer can see) or give its `ip:port`. Every *N*
37
+ seconds, or whenever a message arrives, it sends:
38
+
39
+ - `msg.payload`: the JPEG (a Buffer), `msg.statusCode` 200 and `msg.headers['content-type']` `image/jpeg`, the same
40
+ shape as an http request node's reply, so it can feed flows built for a camera's still-image URL;
41
+ - `msg.ndi`: `{source, width, height, fps, fourCC, bandwidth, lastFrame, runtime, thumbWidth, thumbHeight, bytes,
42
+ grabMs, connections, framesTotal, framesDropped, connectedForMs}`.
43
+
44
+ When no picture arrives it sends `payload: null` with `msg.ndi.error`, and warns once per outage. Send `"stop"` (or
45
+ `false`) to pause and disconnect, and `"start"` (or `true`) to resume. `msg.source` / `msg.address` override the
46
+ configured source for one message.
47
+
48
+ ### ndi-live
49
+
50
+ Serves the source as Motion-JPEG on Node-RED's HTTP node server (the same place as *http in* nodes):
51
+
52
+ - `<httpNodeRoot>/ndi-live/<node id>.mjpg`: the live picture; show it with `<img src="…">` in any browser or
53
+ dashboard template;
54
+ - `<httpNodeRoot>/ndi-live/<node id>.jpg`: one still.
55
+
56
+ It sends the path once when the flow starts and in reply to every message. Defaults: 10 frames a second (at most
57
+ 15), 640 pixels wide, at most 4 viewers. It is a preview, not a programme monitor: no sound, a fraction of a second
58
+ behind live.
59
+
60
+ The connection to the source opens when the first viewer starts watching and closes a few seconds after the last
61
+ one stops, so an unwatched preview uses no network and no processor time. Each frame is encoded once for all
62
+ viewers, and a viewer on a slow connection skips frames instead of falling behind. Send `"stop"` to end every
63
+ viewer and refuse new ones, `"start"` to allow them again.
64
+
65
+ **Who can watch:** the endpoints sit behind Node-RED's `httpNodeAuth` if your settings set it. Without it, anyone
66
+ who can reach Node-RED's HTTP endpoints can watch.
67
+
68
+ ### Both nodes
69
+
70
+ - **Network use.** *Proxy* (the default) asks the sender for its low-bandwidth stream. *Full* asks for the full
71
+ stream, which can be over 100 Mb/s per camera.
72
+ - **One connection per source.** Nodes that show the same source share one NDI connection to it.
73
+ - **Tally is never set**, so a preview never lights a camera's on-air lamp.
74
+ - **Safe for Node-RED.** All NDI work runs in a separate helper process. If the NDI Runtime crashes or hangs, the
75
+ helper is restarted (with a pause that grows each time), and Node-RED keeps running. After five crashes in a
76
+ minute it stops trying and the nodes say so, until Node-RED is restarted.
77
+ - **Where the runtime is found.** On Windows, the folder the NDI 6 Runtime installer records in
78
+ `NDI_RUNTIME_DIR_V6`; on macOS, `/usr/local/lib/libndi.dylib`. To use a runtime somewhere else, set
79
+ `NDI_PREVIEW_RUNTIME` to the full path of the library before starting Node-RED.
80
+ - **Windows firewall.** The helper asks the NDI Runtime to receive over TCP, which a firewall without an allow rule
81
+ for Node-RED does not block. Your own NDI settings (groups, discovery server, extra IPs) still apply. If you set
82
+ `NDI_CONFIG_DIR` yourself, your configuration is used unchanged.
83
+
84
+ ## Building from source
85
+
86
+ The add-on is a small C file (`native/ndi_preview.c`) using Node-API. It loads the NDI Runtime the user installed
87
+ at run time and links no NDI library. Building it needs the NDI SDK's header files: install the NDI SDK from
88
+ <https://ndi.video/> (it is not part of this repository).
89
+
90
+ - macOS: `npm run build` (needs the Xcode Command Line Tools; set `NDI_SDK_DIR` if the SDK is not in
91
+ `/Library/NDI SDK for Apple`).
92
+ - Windows x64: `native/build-win.ps1` (needs the Visual Studio C++ build tools with the Windows SDK, and the NDI SDK
93
+ for Windows).
94
+
95
+ Tests: `npm test` runs the unit tests with a synthetic stand-in for NDI, so it needs no NDI and no network.
96
+ `npm run check` also checks the add-on and the package contents and, on macOS, runs an end-to-end test against a
97
+ local test source. That test runs every NDI process in a macOS sandbox that only allows loopback traffic.
98
+
99
+ ## Licence
100
+
101
+ Copyright (c) 2026 James Breedon. Licensed under the [PolyForm Noncommercial License 1.0.0](LICENSE.md): free for
102
+ noncommercial use.
103
+
104
+ Third-party code and terms are listed in [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md), including the terms that
105
+ apply to the NDI software this package works with.
@@ -0,0 +1,106 @@
1
+ # Third-party notices
2
+
3
+ ## NDI®
4
+
5
+ NDI® is a registered trademark of Vizrt NDI AB. More about NDI: <https://ndi.video/>.
6
+
7
+ This package contains no NDI software. It works with the NDI Runtime that you download from <https://ndi.video/>
8
+ and install yourself, and its add-on was built using the NDI SDK's header files. The NDI Runtime and the NDI SDK are
9
+ licensed by Vizrt NDI AB under their own terms, which you accept when you install them. When you use this package
10
+ with NDI software, these terms also apply to you:
11
+
12
+ - You may not modify the NDI SDK, the NDI Runtime or any other NDI product, or any part of them.
13
+ - You may not reverse engineer, disassemble or recompile the NDI SDK, the NDI Runtime or any other NDI product, or
14
+ any protocol they use (whether sent or received over a network, or used inside a computer), and you may not try to.
15
+ - You may not circumvent any technical limitation in the NDI SDK, the NDI Runtime or any other NDI product.
16
+ - You may not remove, obscure or alter any proprietary notice or label in the NDI SDK, the NDI Runtime or any other
17
+ NDI product.
18
+ - Vizrt NDI AB and its licensors give no warranty of any kind for their software, and, as far as the law allows,
19
+ they are not liable for any damages (direct, indirect, incidental or consequential) arising from the use of this
20
+ package with their software.
21
+ - You must comply with all relevant export laws and regulations of the United States, so that NDI software is not
22
+ exported, directly or indirectly, in breach of United States law.
23
+ - The NDI SDK and NDI Runtime are copyright Vizrt NDI AB. All rights reserved.
24
+ - If you use this package to develop products of your own that use NDI, those products must comply with the NDI SDK
25
+ License Agreement, including its requirement of current and complete NDI compatibility, and their end-user licence
26
+ must include these terms.
27
+
28
+ The NDI SDK License Agreement: <https://ndi.link/ndisdk_license>.
29
+
30
+ Formats such as H.264, H.265 and AAC (used by some NDI|HX sources) may need licences of their own. The NDI Runtime
31
+ decodes them; making sure that use is licensed is your responsibility.
32
+
33
+ ## jpeg-js encoder
34
+
35
+ `lib/vendor/jpeg-encoder.js` is the JPEG encoder from jpeg-js 0.4.4 (<https://github.com/jpeg-js/jpeg-js>),
36
+ unchanged. Provenance: `lib/vendor/PROVENANCE.md`.
37
+
38
+ jpeg-js licence (`lib/vendor/jpeg-js.LICENSE`):
39
+
40
+ ```
41
+ Copyright (c) 2014, Eugene Ware
42
+ All rights reserved.
43
+
44
+ Redistribution and use in source and binary forms, with or without
45
+ modification, are permitted provided that the following conditions are met:
46
+
47
+ 1. Redistributions of source code must retain the above copyright
48
+ notice, this list of conditions and the following disclaimer.
49
+ 2. Redistributions in binary form must reproduce the above copyright
50
+ notice, this list of conditions and the following disclaimer in the
51
+ documentation and/or other materials provided with the distribution.
52
+ 3. Neither the name of Eugene Ware nor the names of its contributors
53
+ may be used to endorse or promote products derived from this software
54
+ without specific prior written permission.
55
+
56
+ THIS SOFTWARE IS PROVIDED BY EUGENE WARE ''AS IS'' AND ANY
57
+ EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
58
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
59
+ DISCLAIMED. IN NO EVENT SHALL EUGENE WARE BE LIABLE FOR ANY
60
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
61
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
62
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
63
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
64
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
65
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
66
+ ```
67
+
68
+ The encoder is itself a port of Adobe's ActionScript 3 JPEG encoder. Its licence, from the header of
69
+ `lib/vendor/jpeg-encoder.js`:
70
+
71
+ ```
72
+ Copyright (c) 2008, Adobe Systems Incorporated
73
+ All rights reserved.
74
+
75
+ Redistribution and use in source and binary forms, with or without
76
+ modification, are permitted provided that the following conditions are
77
+ met:
78
+
79
+ * Redistributions of source code must retain the above copyright notice,
80
+ this list of conditions and the following disclaimer.
81
+
82
+ * Redistributions in binary form must reproduce the above copyright
83
+ notice, this list of conditions and the following disclaimer in the
84
+ documentation and/or other materials provided with the distribution.
85
+
86
+ * Neither the name of Adobe Systems Incorporated nor the names of its
87
+ contributors may be used to endorse or promote products derived from
88
+ this software without specific prior written permission.
89
+
90
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
91
+ IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
92
+ THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
93
+ PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
94
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
95
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
96
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
97
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
98
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
99
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
100
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
101
+ ```
102
+
103
+ ## Node.js
104
+
105
+ The add-on is compiled against Node.js's Node-API header files (MIT licence, Copyright Node.js contributors). No
106
+ Node.js code is included in this package.
package/lib/addon.js ADDED
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ // Where the prebuilt add-on for this computer lives, and loading it. The add-on is only ever loaded inside the NDI
3
+ // host process (lib/host.js), never inside Node-RED itself.
4
+ const path = require("path");
5
+ const fs = require("fs");
6
+
7
+ const PREBUILDS = path.join(__dirname, "..", "prebuilds");
8
+
9
+ function platformDir(platform = process.platform, arch = process.arch) {
10
+ if (platform === "darwin" && (arch === "arm64" || arch === "x64")) return "darwin-universal";
11
+ if (platform === "win32" && arch === "x64") return "win32-x64";
12
+ return null;
13
+ }
14
+
15
+ // -> absolute path of the add-on for this computer, or null when there is none
16
+ function addonPath() {
17
+ const dir = platformDir();
18
+ if (!dir) return null;
19
+ const p = path.join(PREBUILDS, dir, "ndi_preview.node");
20
+ return fs.existsSync(p) ? p : null;
21
+ }
22
+
23
+ // -> null when this computer can run the nodes, else a sentence saying why not
24
+ function unsupportedReason() {
25
+ if (process.env.NDI_PREVIEW_TEST_ADDON) return null;
26
+ if (!platformDir()) {
27
+ return `NDI preview is not available on ${process.platform}-${process.arch}: it runs on macOS and on Windows x64`;
28
+ }
29
+ if (!addonPath()) return `the NDI preview add-on for ${platformDir()} is missing from this installation`;
30
+ return null;
31
+ }
32
+
33
+ // NDI_PREVIEW_TEST_ADDON is for this package's own tests: a JavaScript stand-in with the add-on's API that makes
34
+ // synthetic frames and uses no network.
35
+ function loadAddon() {
36
+ const fake = process.env.NDI_PREVIEW_TEST_ADDON;
37
+ if (fake) return require(path.resolve(fake));
38
+ const p = addonPath();
39
+ if (!p) throw new Error(unsupportedReason() || "the NDI preview add-on is missing");
40
+ return require(p);
41
+ }
42
+
43
+ module.exports = { addonPath, platformDir, unsupportedReason, loadAddon };
package/lib/admin.js ADDED
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ // What both node types share in Node-RED: the editor's admin endpoints and the status texts.
3
+ const { getClient } = require("./host-client");
4
+
5
+ const NOT_INSTALLED = { fill: "grey", shape: "ring", text: "NDI Runtime not installed (see ndi.video)" };
6
+ const CHECKING = { fill: "grey", shape: "ring", text: "checking for the NDI Runtime" };
7
+
8
+ function statusForRuntime(rt) {
9
+ if (rt.ok) return null;
10
+ if (rt.unsupported) return { fill: "grey", shape: "ring", text: "not available on this computer" };
11
+ if (rt.failed) return { fill: "red", shape: "ring", text: "NDI helper stopped; restart Node-RED" };
12
+ if (rt.host) return { fill: "red", shape: "ring", text: "NDI helper not answering; retrying" };
13
+ return NOT_INSTALLED;
14
+ }
15
+
16
+ let registered = false;
17
+ let logged = false;
18
+
19
+ // The runtime check a node does when it starts. The NDI host is started by the first node (or the editor), not
20
+ // when Node-RED loads the palette, so a Node-RED with no NDI nodes deployed runs no extra process.
21
+ // The result is logged once.
22
+ function checkRuntime(RED) {
23
+ return getClient(RED).runtime().then((rt) => {
24
+ if (!logged && !rt.host) {
25
+ logged = true;
26
+ if (rt.ok) RED.log.info(`[ndi-preview] NDI Runtime ${rt.version} (${rt.path})`);
27
+ else RED.log.warn(`[ndi-preview] ${rt.error}.${rt.help ? " " + rt.help + "." : ""} The NDI preview nodes stay idle.`);
28
+ }
29
+ return rt;
30
+ });
31
+ }
32
+
33
+ // The runtime check for one node: calls onResult with each answer. A problem with the NDI host itself (it did not
34
+ // answer, or crashed once) is not final: the check is repeated every 10 s until there is a real answer or the node
35
+ // closes. A missing runtime or a host that has given up is final.
36
+ function watchRuntime(RED, isClosed, onResult) {
37
+ const attempt = () => {
38
+ if (isClosed()) return;
39
+ checkRuntime(RED).then((rt) => {
40
+ if (isClosed()) return;
41
+ onResult(rt);
42
+ if (!rt.ok && rt.host && !rt.failed) { const t = setTimeout(attempt, 10000); t.unref?.(); }
43
+ });
44
+ };
45
+ attempt();
46
+ }
47
+
48
+ // Registered once, whichever node type loads first.
49
+ function registerAdmin(RED) {
50
+ if (registered) return;
51
+ registered = true;
52
+
53
+ // Is the NDI Runtime installed on the Node-RED computer? (the editor's banner)
54
+ RED.httpAdmin.get("/ndi-preview/runtime", RED.auth.needsPermission("ndi-preview.read"), async (req, res) => {
55
+ const rt = await getClient(RED).runtime({ refresh: req.query.refresh === "1" });
56
+ res.json({ ok: rt.ok, version: rt.version, error: rt.error, help: rt.help, unsupported: !!rt.unsupported });
57
+ });
58
+
59
+ // One short NDI discovery from the Node-RED computer (the editor's "Find sources" button).
60
+ RED.httpAdmin.get("/ndi-preview/sources", RED.auth.needsPermission("ndi-preview.read"), async (req, res) => {
61
+ try {
62
+ const client = getClient(RED);
63
+ const rt = await client.runtime();
64
+ if (!rt.ok) { res.status(503).json({ error: rt.error, help: rt.help }); return; }
65
+ res.json(await client.sources(2000));
66
+ } catch (e) {
67
+ res.status(500).json({ error: e.message });
68
+ }
69
+ });
70
+ }
71
+
72
+ module.exports = { registerAdmin, checkRuntime, watchRuntime, statusForRuntime, NOT_INSTALLED, CHECKING };
package/lib/frame.js ADDED
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ // Pure-JS frame helpers: any NDI video frame -> RGBA at thumbnail size -> JPEG Buffer.
3
+ // No native code here, so this file is unit-testable without the NDI runtime.
4
+ const encodeJpeg = require("./vendor/jpeg-encoder"); // vendored jpeg-js 0.4.4 encoder (lib/vendor/PROVENANCE.md)
5
+
6
+ const FOURCC = { UYVY: 1498831189, BGRA: 1095911234, BGRX: 1481787202, RGBA: 1094862674, RGBX: 1480738642 };
7
+ const fourccName = (v) => Object.keys(FOURCC).find((k) => FOURCC[k] === v) || String(v);
8
+
9
+ // Box-filter (area average) downscale of a packed 4-byte-per-pixel frame straight into RGBA.
10
+ // Handles RGBX/RGBA and BGRX/BGRA (channel swap) and UYVY (4:2:2 -> RGB, BT.709 limited range).
11
+ function toThumbRGBA(frame, outW) {
12
+ const { xres: W, yres: H, lineStrideBytes: stride, data, fourCC } = frame;
13
+ const w = Math.max(16, Math.min(outW | 0 || 320, W));
14
+ const h = Math.max(9, Math.round((H * w) / W));
15
+ const out = Buffer.alloc(w * h * 4);
16
+ const bgr = fourCC === FOURCC.BGRX || fourCC === FOURCC.BGRA;
17
+ const uyvy = fourCC === FOURCC.UYVY;
18
+ if (!uyvy && !bgr && fourCC !== FOURCC.RGBX && fourCC !== FOURCC.RGBA) {
19
+ throw new Error(`unsupported pixel format ${fourccName(fourCC)} (ask for colorFormat RGBX_RGBA)`);
20
+ }
21
+ const fx = W / w, fy = H / h;
22
+ // sample a capped grid inside each output cell (≤4×4) — cheap and still smooth for 1080p -> 320
23
+ const sx = Math.max(1, Math.min(4, Math.floor(fx))), sy = Math.max(1, Math.min(4, Math.floor(fy)));
24
+ for (let oy = 0; oy < h; oy++) {
25
+ for (let ox = 0; ox < w; ox++) {
26
+ let r = 0, g = 0, b = 0, n = 0;
27
+ for (let j = 0; j < sy; j++) {
28
+ const y = Math.min(H - 1, Math.floor(oy * fy + ((j + 0.5) * fy) / sy));
29
+ const row = y * stride;
30
+ for (let i = 0; i < sx; i++) {
31
+ const x = Math.min(W - 1, Math.floor(ox * fx + ((i + 0.5) * fx) / sx));
32
+ if (uyvy) {
33
+ const p = row + (x >> 1) * 4; // U Y0 V Y1
34
+ const Y = (x & 1 ? data[p + 3] : data[p + 1]) - 16, U = data[p] - 128, V = data[p + 2] - 128;
35
+ r += 1.164 * Y + 1.793 * V; g += 1.164 * Y - 0.213 * U - 0.533 * V; b += 1.164 * Y + 2.112 * U;
36
+ } else {
37
+ const p = row + x * 4;
38
+ if (bgr) { b += data[p]; g += data[p + 1]; r += data[p + 2]; }
39
+ else { r += data[p]; g += data[p + 1]; b += data[p + 2]; }
40
+ }
41
+ n++;
42
+ }
43
+ }
44
+ const o = (oy * w + ox) * 4;
45
+ out[o] = clamp(r / n); out[o + 1] = clamp(g / n); out[o + 2] = clamp(b / n); out[o + 3] = 255;
46
+ }
47
+ }
48
+ return { data: out, width: w, height: h };
49
+ }
50
+ const clamp = (v) => (v < 0 ? 0 : v > 255 ? 255 : v | 0);
51
+
52
+ function frameToJpeg(frame, outW, quality) {
53
+ const rgba = toThumbRGBA(frame, outW);
54
+ const q = Math.max(10, Math.min(95, quality | 0 || 70));
55
+ return { jpeg: encodeJpeg(rgba, q).data, width: rgba.width, height: rgba.height };
56
+ }
57
+
58
+ module.exports = { toThumbRGBA, frameToJpeg, fourccName, FOURCC };