@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 +16 -0
- package/LICENSE.md +80 -0
- package/README.md +104 -2
- package/THIRD-PARTY-NOTICES.md +106 -0
- package/lib/addon.js +43 -0
- package/lib/admin.js +72 -0
- package/lib/frame.js +58 -0
- package/lib/host-client.js +272 -0
- package/lib/host.js +219 -0
- package/lib/ndi-config.js +47 -0
- package/lib/receivers.js +131 -0
- package/lib/vendor/PROVENANCE.md +12 -0
- package/lib/vendor/jpeg-encoder.js +814 -0
- package/lib/vendor/jpeg-js.LICENSE +25 -0
- package/nodes/ndi-live.html +98 -0
- package/nodes/ndi-live.js +236 -0
- package/nodes/ndi-thumbnail.html +126 -0
- package/nodes/ndi-thumbnail.js +97 -0
- package/package.json +50 -4
- package/prebuilds/darwin-universal/ndi_preview.node +0 -0
- package/prebuilds/win32-x64/ndi_preview.node +0 -0
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
|
-
#
|
|
1
|
+
# Node-RED NDI Preview
|
|
2
2
|
|
|
3
|
-
|
|
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 };
|