grandi 0.1.0 → 1.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/README.md CHANGED
@@ -1,4 +1,7 @@
1
+ [![npm version](https://badgen.net/npm/v/grandi)](https://www.npmjs.com/package/grandi) [![CI](https://github.com/tux-tn/grandi/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/tux-tn/grandi/actions/workflows/ci.yml)
2
+
1
3
  # Grandi
4
+
2
5
  Grandi is a TypeScript-first native Node.js binding for NewTek NDI™ 6. It exposes the full NDI 6 SDK surface area through a Promise-based API, providing strongly typed helpers for sending and receiving professional video, audio, metadata, and tally data from Node.js applications. For more information on NDI™ , see <https://www.ndi.tv/>.
3
6
 
4
7
  ## Why this fork?
@@ -30,16 +33,16 @@ NDI™ was conceived as a grand vision for IP media transport. The earliest bind
30
33
  - Pull requests tackling these gaps (or other NDI 6.x+ features) are very welcome.
31
34
 
32
35
  ## Supported platforms
33
- Grandi currently ships prebuilt binaries for the same platforms supported by the original project. Additional platforms can be compiled from source if the NDI SDK supports them.
36
+ Grandi publishes per-architecture packages (`@grandi/<os>-<arch>`) that bundle the native addon and the matching NDI SDK runtime. Additional platforms can be compiled from source if the NDI SDK supports them.
34
37
 
35
38
  | Operating system | Architectures | Status | Notes |
36
39
  | --- | --- | --- | --- |
37
- | Windows | x86, x64 | ✅ Prebuilt | Visual Studio 2013 C runtime only needed when building locally; prebuilt binaries include the required DLLs. |
38
- | macOS | Universal (x64 + arm64) | ✅ Prebuilt | Ships with the official universal NDI™ driver, so Intel and Apple Silicon hosts run natively. |
39
- | Linux | x86, x64, armv7l, arm64 | ✅ Prebuilt | Built against the glibc-based NDI™ SDK; requires `libavahi-common.so.3`, `libavahi-client.so.3`, and the `avahi-daemon` service. |
40
+ | Windows | x86, x64 | ✅ Published as `@grandi/win32-ia32` and `@grandi/win32-x64`. Visual Studio 2013 C runtime only needed when building locally. |
41
+ | macOS | Universal (x64 + arm64) | ✅ Published as `@grandi/darwin-universal`, built against the official universal NDI™ runtime. |
42
+ | Linux | x86, x64, armv7l, arm64 | ✅ Published as `@grandi/linux-x64`, `@grandi/linux-arm64`, `@grandi/linux-armv7l`. Built against the glibc-based NDI™ SDK; requires `libavahi-common.so.3`, `libavahi-client.so.3`, and the `avahi-daemon` service. |
40
43
 
41
44
  ## Installation
42
- Install [Node.js](http://nodejs.org/) for your platform (tested against the current Long Term Support release). Then add the dependency to your project:
45
+ Install [Node.js](http://nodejs.org/) for your platform (tested against the current Long Term Support release). The main package automatically pulls the platform-specific optional dependency `@grandi/<os>-<arch>` when it exists, and falls back to building from source otherwise. Add the dependency to your project:
43
46
 
44
47
  ```bash
45
48
  npm install grandi
@@ -49,18 +52,18 @@ pnpm add grandi
49
52
  yarn add grandi
50
53
  ```
51
54
 
52
- On Windows, you only need the Visual Studio 2013 C run-times when building from source (e.g., running `npm install` without matching prebuilds). You can grab them from <https://www.microsoft.com/en-us/download/details.aspx?id=40784>, but end users consuming the published prebuilt binaries do **not** need to install them.
55
+ On Windows, you only need the Visual Studio 2013 C run-times when building from source (e.g., running `npm install` on an unsupported platform). You can grab them from <https://www.microsoft.com/en-us/download/details.aspx?id=40784>; consumers using the published `@grandi/win32-*` packages do **not** need to install them manually.
53
56
 
54
57
  Grandi is designed to be `require`d or `import`ed from your own applications:
55
58
 
56
- ```typescript
59
+ ```ts
57
60
  import grandi from "grandi";
58
61
  // or
59
62
  const grandi = require("grandi");
60
63
  ```
61
64
 
62
65
  ## Build & distribution
63
- The project uses [prebuildify](https://github.com/prebuild/prebuildify) to prebuild the Node.js addon and bundle the NDI SDK libraries for the supported targets shown above. Those prebuilds are resolved at runtime via [node-gyp-build](https://github.com/prebuild/node-gyp-build), so consumers do not need a compiler toolchain in most cases. When no prebuild matches the running platform, `node-gyp-build` falls back to compiling locally, letting advanced users add additional architectures.
66
+ Each platform package under the `@grandi/` scope bundles the native addon (`grandi.node`) and the corresponding NDI SDK runtime for that OS/arch. The root `grandi` package loads the matching optional dependency first and only compiles locally when no published package is available. Maintainers regenerate those packages from the official NDI SDK by running `node scripts/preinstall.mjs` (to download and stage the SDK) followed by a version bump (e.g., `node scripts/bump-version.mjs <version>`).
64
67
 
65
68
  ## Using Grandi
66
69
  This module allows a Node.js program to find, receive, and send NDI™ video, audio, metadata, and tally streams over IP networks. All calls are asynchronous and use JavaScript promises with all of the underlying work of NDI running on separate threads from the event loop. The following sections recap the most common workflows; for complete runnable demos, see `examples/simple-receiver.mjs` and `examples/simple-sender.mjs`.
@@ -68,7 +71,7 @@ This module allows a Node.js program to find, receive, and send NDI™ video, au
68
71
  ### Finding streams
69
72
  `grandi.find` resolves to a finder handle that can block for network updates and expose the discovered sources:
70
73
 
71
- ```javascript
74
+ ```ts
72
75
  import { setTimeout as sleep } from "node:timers/promises";
73
76
  import grandi from "grandi";
74
77
 
@@ -90,7 +93,7 @@ async function pickSource() {
90
93
 
91
94
  `find` accepts a single options object. Common filters:
92
95
 
93
- ```javascript
96
+ ```ts
94
97
  await grandi.find({
95
98
  showLocalSources: true, // include sources on the same machine
96
99
  groups: "studio3", // comma-separated list of group names
@@ -101,20 +104,20 @@ await grandi.find({
101
104
  ### Receiving streams
102
105
  First, find a stream or construct a `Source` object:
103
106
 
104
- ```javascript
107
+ ```ts
105
108
  const grandi = require("grandi");
106
109
  const source = { name: "<source_name>", urlAddress: "<ip>:<port>" };
107
110
  ```
108
111
 
109
112
  Create a receiver:
110
113
 
111
- ```javascript
114
+ ```ts
112
115
  const receiver = await grandi.receive({ source });
113
116
  ```
114
117
 
115
118
  Example receiver object:
116
119
 
117
- ```javascript
120
+ ```ts
118
121
  {
119
122
  embedded: [External],
120
123
  video: [Function],
@@ -130,7 +133,7 @@ Example receiver object:
130
133
 
131
134
  Configure receivers via options:
132
135
 
133
- ```javascript
136
+ ```ts
134
137
  const receiver = await grandi.receive({
135
138
  source,
136
139
  colorFormat: grandi.COLOR_FORMAT_UYVY_RGBA,
@@ -143,7 +146,7 @@ const receiver = await grandi.receive({
143
146
 
144
147
  #### Video
145
148
 
146
- ```javascript
149
+ ```ts
147
150
  const timeout = 5000;
148
151
  try {
149
152
  for (let i = 0; i < 10; i++) {
@@ -157,7 +160,7 @@ try {
157
160
 
158
161
  Example frame:
159
162
 
160
- ```javascript
163
+ ```ts
161
164
  {
162
165
  type: "video",
163
166
  xres: 1920,
@@ -175,7 +178,7 @@ Example frame:
175
178
 
176
179
  #### Audio
177
180
 
178
- ```javascript
181
+ ```ts
179
182
  const audioFrame = await receiver.audio(
180
183
  {
181
184
  audioFormat: grandi.AUDIO_FORMAT_INT_16_INTERLEAVED,
@@ -187,7 +190,7 @@ const audioFrame = await receiver.audio(
187
190
 
188
191
  Example result:
189
192
 
190
- ```javascript
193
+ ```ts
191
194
  {
192
195
  type: "audio",
193
196
  audioFormat: grandi.AUDIO_FORMAT_INT_16_INTERLEAVED,
@@ -204,7 +207,7 @@ Example result:
204
207
 
205
208
  #### Metadata
206
209
 
207
- ```javascript
210
+ ```ts
208
211
  const metadataFrame = await receiver.metadata();
209
212
  ```
210
213
 
@@ -212,7 +215,7 @@ Returns a `{ type: "metadata", data: string, ... }` frame, typically XML.
212
215
 
213
216
  #### Next available data
214
217
 
215
- ```javascript
218
+ ```ts
216
219
  const prefs = { audioFormat: grandi.AUDIO_FORMAT_FLOAT_32_SEPARATE };
217
220
  const payload = await receiver.data(prefs, 1000); // 1s timeout
218
221
  if (payload.type === "video") {
@@ -222,10 +225,31 @@ if (payload.type === "video") {
222
225
  }
223
226
  ```
224
227
 
228
+ ### Frame synchronization (pull-based playback)
229
+ If you want smoother playback clocked to your own loop (e.g., GPU vsync) instead of reacting to pushed frames, wrap an existing receiver in an NDI frame-synchronizer:
230
+
231
+ ```ts
232
+ const receiver = await grandi.receive({ source, colorFormat: grandi.ColorFormat.Fastest });
233
+ const fs = await grandi.framesync(receiver);
234
+
235
+ // Pull video; returns { type: "timeout" } until first frame arrives.
236
+ const video = await fs.video(grandi.FrameType.Progressive);
237
+
238
+ // Pull audio resampled to your requested cadence.
239
+ const audio = await fs.audio({ sampleRate: 48_000, noChannels: 2, noSamples: 1600 });
240
+
241
+ fs.destroy(); // destroy frame-sync first
242
+ receiver.destroy(); // then destroy the receiver
243
+ ```
244
+
245
+ Notes:
246
+ - `fs.video()` and `fs.audio()` always return immediately; they may duplicate/drop frames to match your call rate.
247
+ - Always destroy the frame-sync before destroying the receiver it wraps.
248
+
225
249
  ### Sending streams
226
250
  Sending is fully supported. Call `grandi.send` with a `SendOptions` object to create a `Sender` instance capable of video, audio, metadata, and tally interactions:
227
251
 
228
- ```javascript
252
+ ```ts
229
253
  const sender = await grandi.send({
230
254
  name: "grandi-demo",
231
255
  groups: "studio3",
@@ -281,32 +305,60 @@ Destroy senders and receivers explicitly when finished to release NDI resources.
281
305
  - `grandi.isSupportedCPU()` checks whether the host CPU can run the NDI SDK.
282
306
  - `grandi.initialize()` / `grandi.destroy()` control the lifetime of the underlying library in advanced scenarios.
283
307
 
308
+ ## Benchmark
309
+ A loopback benchmark script measures end-to-end send/receive throughput and latency on your machine. It spins up a sender and receiver locally, so a working NDI setup with discovery is required.
310
+
311
+ - Build first if needed (`npm run build`), then run `npm run bench -- [options]`. The script defaults to 1080p30 with audio and colored output.
312
+
313
+ | Option | Default | Description |
314
+ | --- | --- | --- |
315
+ | `--mode realtime|throughput` | `realtime` | `realtime` keeps to the target FPS with clocking enabled; `throughput` sends as fast as possible. |
316
+ | `--duration <sec>` | `5` | Run length in seconds. |
317
+ | `--fps <num>` | `30` | Target frames per second. |
318
+ | `--width <px>` | `1920` | Video width in pixels. |
319
+ | `--height <px>` | `1080` | Video height in pixels. |
320
+ | `--no-audio` | audio on | Disable sending/receiving audio. |
321
+ | `--framesync` | off | Pull frames via the NDI frame-sync API for smoother capture. |
322
+ | `--gc-every <ms>` | `500` | Force `global.gc()` at this interval when `--expose-gc` is enabled. |
323
+ | `--no-color` | color on | Disable ANSI color output. |
324
+
325
+ `npm run bench` adds `--expose-gc` to keep buffer reuse stable during long runs.
326
+
327
+ Example (10s, 1080p30, realtime):
328
+
329
+ ```bash
330
+ npm run bench -- --duration 10 --mode realtime --fps 30 --framesync
331
+ ```
332
+
333
+ Example (throughput stress, 720p, video only):
334
+
335
+ ```bash
336
+ npm run bench -- --mode throughput --width 1280 --height 720 --no-audio --no-color
337
+ ```
338
+
284
339
  ## Contributing
285
340
  Ready to hack on Grandi? Here’s the typical workflow.
286
341
 
287
342
  1. **Install prerequisites**
288
343
  - Node.js ≥ 20.19.5 and a working C/C++ toolchain for your platform.
289
344
  - Git LFS is *not* required; the NDI SDK is fetched dynamically.
290
- 2. **Download the NDI SDK + install deps**
291
- - Run `npm install` (or `pnpm install`/`yarn install`). The `scripts/preinstall.mjs` hook downloads and unpacks the official NDI SDK into `ndi/`, then `node-gyp-build` compiles the native addon for your host platform. Re-run `npm install` after deleting `ndi/` if you need to refresh the SDK.
292
- - To force-download all prebuild assets for release testing, run `npm run prebuild:download`.
293
- - Set `NDI_FORCE=1` if you need to run the downloader in an unpacked tarball (normally it only runs inside the git repo).
345
+ 2. **Download the NDI SDK (maintainers)**
346
+ - Run `node scripts/build-addon.mjs` to download and unpack the official NDI SDK into `ndi/` and populate the per-arch package folders under `packages/` with the right runtime files.
347
+ - Consumers don’t need this step; they install the appropriate `@grandi/<os>-<arch>` package from npm automatically.
294
348
  3. **TypeScript build**
295
349
  - `npm run build` compiles `src/` via `tsdown`, emitting ESM/CJS bundles and declaration files in `dist/`.
296
- - `npm run prebuild` packages native binaries for distribution (requires the SDK assets fetched earlier).
297
350
  4. **Native addon rebuild**
298
351
  - If you change C/C++ files under `lib/`, recompile with `npx node-gyp-build` (or simply re-run `npm install`). This uses the same loader that consumers invoke at runtime.
299
352
  5. **Testing**
300
353
  - `npm test` runs the full Vitest suite (unit + integration stubs).
301
354
  - `npm run test:unit` focuses on pure JS/TS tests.
302
- - `RUN_NDI_TESTS=1 npm run test:integration` exercises the native bindings against a real NDI environment; ensure you have available senders/receivers before running.
355
+ - `npm run test:integration` exercises the native bindings against a real NDI environment;
303
356
  - `npm run test:coverage` provides coverage data via `@vitest/coverage-v8`.
304
357
  6. **Linting & formatting**
305
358
  - `npm run lint` / `npm run format` cover the TypeScript/JavaScript sources via Biome.
306
359
  - `npm run format:cpp` formats the native sources with `clang-format`.
307
360
  7. **Manual verification**
308
361
  - `node examples/simple-sender.mjs` and `node examples/simple-receiver.mjs` are quick smoke tests for the send/receive API.
309
- - Use `NDI_FORCE=1 npm install` if you need to reassemble the NDI SDK artifacts even when they are already present.
310
362
 
311
363
  Before opening a pull request, make sure the linter, formatter, and tests all pass, and include context for any platform-specific considerations (e.g., SDK versions, OS dependencies).
312
364
 
@@ -314,70 +366,91 @@ Before opening a pull request, make sure the linter, formatter, and tests all pa
314
366
  This section documents every exported method and type surfaced by the module. Refer to `src/index.ts` and `src/types.ts` for authoritative definitions.
315
367
 
316
368
  ### Module exports
317
- - `find(options?: FindOptions): Promise<Finder>` — discover available NDI sources, optionally filtering by groups, local visibility, or explicit IPs. Resolves to a finder handle whose `sources()` method returns the latest snapshot.
318
- - `receive(options: ReceiveOptions): Promise<Receiver>` create a receiver bound to a specific `Source`. Customize color format, bandwidth caps, interlaced support, and a friendly name.
319
- - `send(options: SendOptions): Promise<Sender>` create a sender that can push video, audio, metadata, and tally updates into the NDI network.
320
- - `routing(params: { name?: string; groups?: string }): Promise<Routing>` build an NDI router that can switch downstream destinations to new sources via `routing.change`.
321
- - `initialize(): boolean` / `destroy(): boolean` manually initialize or tear down the shared NDI state. Automatically handled when using `send`/`receive`, but exposed for completeness.
322
- - `version(): string` report the bundled NDI SDK version.
323
- - `isSupportedCPU(): boolean` guard call that reports whether the host CPU meets the NDI SDK requirements.
324
- - Enum exports: `ColorFormat`, `AudioFormat`, `Bandwidth`, `FrameType`, `FourCC`.
325
- - Default export: the `grandi` object containing the methods above plus constant aliases such as `COLOR_FORMAT_FASTEST`, `BANDWIDTH_HIGHEST`, etc.
369
+
370
+ | Export | Returns | Purpose |
371
+ | --- | --- | --- |
372
+ | `find(options?: FindOptions)` | `Promise<Finder>` | Discover available NDI sources, optionally filtering by groups, local visibility, or explicit IPs. Resolves to a finder whose `sources()` method exposes the latest snapshot. |
373
+ | `receive(options: ReceiveOptions)` | `Promise<Receiver>` | Create a receiver bound to a specific `Source` with optional color format, bandwidth, interlaced, and naming tweaks. |
374
+ | `send(options: SendOptions)` | `Promise<Sender>` | Create a sender that can push video, audio, metadata, and tally updates into the NDI network. |
375
+ | `routing(params: { name?: string; groups?: string })` | `Promise<Routing>` | Build an NDI router that can switch downstream destinations to new sources via `routing.change`. |
376
+ | `initialize()` / `destroy()` | `boolean` | Manually initialize or tear down the shared NDI state; normally handled automatically by `send`/`receive`. |
377
+ | `version()` | `string` | Report the bundled NDI SDK version. |
378
+ | `isSupportedCPU()` | `boolean` | Guard call to confirm the host CPU meets the NDI SDK requirements. |
379
+ | `ColorFormat`, `AudioFormat`, `Bandwidth`, `FrameType`, `FourCC` | enums | Enumerations re-exported for convenience (also mirrored as constants on the default export). |
380
+ | `default` | `typeof grandi` | The `grandi` object containing the methods above plus constant aliases such as `COLOR_FORMAT_FASTEST`, `BANDWIDTH_HIGHEST`, etc. |
326
381
 
327
382
  ### Receiver interface
328
383
  `Receiver` instances expose:
329
384
 
330
- - `video(timeoutMs?: number): Promise<ReceivedVideoFrame>` obtain the next video frame.
331
- - `audio(optionsOrTimeout?: AudioReceiveOptions | number, timeoutMs?: number): Promise<ReceivedAudioFrame>` — fetch audio, optionally overriding format or reference level per call.
332
- - `metadata(timeoutMs?: number): Promise<ReceivedMetadataFrame>` resolve metadata frames (strings, typically XML).
333
- - `data(optionsOrTimeout?: AudioReceiveOptions | number, timeoutMs?: number): Promise<ReceiverDataFrame>` return whichever payload (video, audio, metadata, source change, status change) arrives first.
334
- - `tally(state: ReceiverTallyState): boolean` push tally states (program/preview) upstream to the sender.
335
- - `destroy(): boolean` release the underlying NDI receiver.
336
- - Properties: `embedded`, `source`, `colorFormat`, `bandwidth`, `allowVideoFields`, `name`.
385
+ | Call | Returns | Purpose |
386
+ | --- | --- | --- |
387
+ | `video(timeoutMs?: number)` | `Promise<ReceivedVideoFrame>` | Resolve with the next available video frame. |
388
+ | `audio(optionsOrTimeout?: AudioReceiveOptions \| number, timeoutMs?: number)` | `Promise<ReceivedAudioFrame>` | Fetch audio, overriding format/reference level per call if desired. |
389
+ | `metadata(timeoutMs?: number)` | `Promise<ReceivedMetadataFrame>` | Receive metadata frames (XML strings). |
390
+ | `data(optionsOrTimeout?: AudioReceiveOptions \| number, timeoutMs?: number)` | `Promise<ReceiverDataFrame>` | Return whichever payload (video/audio/metadata/source change/status change) arrives first. |
391
+ | `tally(state: ReceiverTallyState)` | `boolean` | Push tally states (program/preview) upstream to the sender. |
392
+ | `destroy()` | `boolean` | Release the underlying NDI receiver resources. |
393
+
394
+ Properties: `embedded`, `source`, `colorFormat`, `bandwidth`, `allowVideoFields`, `name`.
337
395
 
338
396
  ### Sender interface
339
397
  `Sender` instances surface:
340
398
 
341
- - `video(frame: VideoFrame): Promise<void>` send a video frame.
342
- - `audio(frame: AudioFrame): Promise<void>` send an audio frame.
343
- - `metadata(data: string): boolean` send metadata strings (XML).
344
- - `tally(): SenderTally` retrieve current tally state (program/preview booleans plus `changed` flag).
345
- - `connections(): number` how many actively connected receivers see this sender.
346
- - `sourcename(): string` fully qualified source name visible on the network.
347
- - `destroy(): boolean` dispose the sender and release network resources.
348
- - Properties: `embedded`, `name`, `groups`, `clockVideo`, `clockAudio`.
399
+ | Call | Returns | Purpose |
400
+ | --- | --- | --- |
401
+ | `video(frame: VideoFrame)` | `Promise<void>` | Send a video frame into the NDI network. |
402
+ | `audio(frame: AudioFrame)` | `Promise<void>` | Send an audio frame. |
403
+ | `metadata(data: string)` | `boolean` | Push metadata strings (XML). |
404
+ | `tally()` | `SenderTally` | Retrieve current tally state (program/preview booleans plus `changed`). |
405
+ | `connections()` | `number` | Report how many receivers are actively connected. |
406
+ | `sourcename()` | `string` | Fully qualified source name visible on the network. |
407
+ | `destroy()` | `boolean` | Dispose the sender and release network resources. |
408
+
409
+ Properties: `embedded`, `name`, `groups`, `clockVideo`, `clockAudio`.
349
410
 
350
411
  ### Routing interface
351
- - `change(source: Source): boolean` — switch destinations to a new source.
352
- - `clear(): boolean` disconnect the current source.
353
- - `connections(): number` monitor downstream subscriptions.
354
- - `sourcename(): string` current routed source name.
355
- - `destroy(): boolean` cleanly shut down.
412
+
413
+ | Call | Returns | Purpose |
414
+ | --- | --- | --- |
415
+ | `change(source: Source)` | `boolean` | Switch downstream destinations to a new source. |
416
+ | `clear()` | `boolean` | Disconnect the current source. |
417
+ | `connections()` | `number` | Monitor downstream subscriptions. |
418
+ | `sourcename()` | `string` | Current routed source name. |
419
+ | `destroy()` | `boolean` | Cleanly shut down the router. |
356
420
 
357
421
  ### Finder interface
358
- - `sources(): Source[]` — latest snapshot of discovered sources.
359
- - `wait(timeoutMs?: number): boolean` block for network updates up to the given timeout.
360
- - `destroy(): boolean` dispose the finder and associated background threads.
422
+
423
+ | Call | Returns | Purpose |
424
+ | --- | --- | --- |
425
+ | `sources()` | `Source[]` | Latest snapshot of discovered sources. |
426
+ | `wait(timeoutMs?: number)` | `boolean` | Block for network updates up to the given timeout. |
427
+ | `destroy()` | `boolean` | Dispose the finder and background threads. |
361
428
 
362
429
  ### Options and helper types
363
- - `Source` — `{ name: string; urlAddress?: string }`.
364
- - `FindOptions` `{ showLocalSources?: boolean; groups?: string; extraIPs?: string; }`.
365
- - `ReceiveOptions` `{ source: Source; colorFormat?: ColorFormat; bandwidth?: Bandwidth; allowVideoFields?: boolean; name?: string; }`.
366
- - `SendOptions` `{ name: string; groups?: string; clockVideo?: boolean; clockAudio?: boolean; }`.
367
- - `AudioReceiveOptions` `{ audioFormat?: AudioFormat; referenceLevel?: number; }` used when calling `receiver.audio` or `receiver.data`.
368
- - `ReceiverTallyState` `{ onProgram?: boolean; onPreview?: boolean; }`, used by `receiver.tally`.
369
- - `SenderTally` `{ changed: boolean; on_program: boolean; on_preview: boolean; }`, returned by `sender.tally`.
370
- - `PtpTimestamp` `[seconds, nanoseconds]` tuple aligning with the NDI SDK timestamp APIs.
371
- - `Timecode` `bigint | number | PtpTimestamp`, matching how timecodes are expressed throughout the SDK.
430
+
431
+ | Type | Shape | Notes |
432
+ | --- | --- | --- |
433
+ | `Source` | `{ name: string; urlAddress?: string }` | Identifies an NDI endpoint. |
434
+ | `FindOptions` | `{ showLocalSources?: boolean; groups?: string; extraIPs?: string; }` | Filters applied when discovering sources. |
435
+ | `ReceiveOptions` | `{ source: Source; colorFormat?: ColorFormat; bandwidth?: Bandwidth; allowVideoFields?: boolean; name?: string; }` | Required to create receivers with optional overrides. |
436
+ | `SendOptions` | `{ name: string; groups?: string; clockVideo?: boolean; clockAudio?: boolean; }` | Configure sender identity and sync behavior. |
437
+ | `AudioReceiveOptions` | `{ audioFormat?: AudioFormat; referenceLevel?: number; }` | Used when calling `receiver.audio` or `receiver.data`. |
438
+ | `ReceiverTallyState` | `{ onProgram?: boolean; onPreview?: boolean; }` | Provided to `receiver.tally` to reflect monitoring state. |
439
+ | `SenderTally` | `{ changed: boolean; on_program: boolean; on_preview: boolean; }` | Returned by `sender.tally`. |
440
+ | `PtpTimestamp` | `[seconds, nanoseconds]` | Tuple aligning with the NDI SDK timestamp APIs. |
441
+ | `Timecode` | `bigint \| number \| PtpTimestamp` | Matches how timecodes are expressed throughout the SDK. |
372
442
 
373
443
  ### Frame payload types
374
- - `VideoFrame` — describes outbound frames: resolution, frame rate (`frameRateN`, `frameRateD`), aspect ratio, `fourCC`, `frameFormatType`, `lineStrideBytes`, `data`, optional `timecode`, `timestamp`, `metadata`.
375
- - `ReceivedVideoFrame` extends `VideoFrame` with guaranteed `type: "video"` plus non-optional `timecode`/`timestamp`.
376
- - `AudioFrame` outbound audio payload (sample rate, number of channels/samples, stride, `data`, `fourCC`, optional timing/metadata).
377
- - `ReceivedAudioFrame` inbound audio payload including `audioFormat`, `referenceLevel`, `timecode`, `timestamp`.
378
- - `ReceivedMetadataFrame` metadata payloads (`type: "metadata"`, `length`, `timecode`, `timestamp`, `data` string).
379
- - `ReceiverDataFrame` discriminated union of `ReceivedVideoFrame | ReceivedAudioFrame | ReceivedMetadataFrame | SourceChangeEvent | StatusChangeEvent`.
380
- - `SourceChangeEvent` / `StatusChangeEvent` internal notifications delivered via `receiver.data` when the remote sender changes or becomes unavailable.
444
+
445
+ | Type | Description |
446
+ | --- | --- |
447
+ | `VideoFrame` | Outbound frame with resolution, frame rate (`frameRateN`/`frameRateD`), aspect ratio, `fourCC`, `frameFormatType`, `lineStrideBytes`, `data`, optional `timecode`, `timestamp`, `metadata`. |
448
+ | `ReceivedVideoFrame` | `VideoFrame` plus `type: "video"` and non-optional `timecode`/`timestamp`. |
449
+ | `AudioFrame` | Outbound audio payload including sample rate, channels/samples, stride, `data`, `fourCC`, optional timing/metadata. |
450
+ | `ReceivedAudioFrame` | Inbound audio payload with `audioFormat`, `referenceLevel`, `timecode`, `timestamp`. |
451
+ | `ReceivedMetadataFrame` | Metadata payload (`type: "metadata"`, `length`, `timecode`, `timestamp`, `data` string). |
452
+ | `ReceiverDataFrame` | Discriminated union of `ReceivedVideoFrame \| ReceivedAudioFrame \| ReceivedMetadataFrame \| SourceChangeEvent \| StatusChangeEvent`. |
453
+ | `SourceChangeEvent` / `StatusChangeEvent` | Internal notifications delivered via `receiver.data` when the remote sender changes or becomes unavailable. |
381
454
 
382
455
  ### Enum reference
383
456
  - `ColorFormat` — enumerates `BGRX_BGRA`, `UYVY_BGRA`, `RGBX_RGBA`, `UYVY_RGBA`, `Fastest`, `Best`, and `BGRX_BGRA_FLIPPED`. The `grandi.COLOR_FORMAT_*` constants map to these values.
@@ -396,7 +469,7 @@ Apart from the exceptions below, this software is released under the Apache 2.0
396
469
  The software uses libraries provided under a royalty-free license from NewTek, Inc. (see the [NDI SDK License](https://ndi.link/ndisdk_license) for full terms):
397
470
 
398
471
  - The `ndi/include` includes files are licensed separately by NewTek under the MIT license.
399
- - The DLL and library files are provided for installation convenience and are covered by the NewTek license in the `prebuild/` folder.
472
+ - The DLL and library files are provided for installation convenience and are covered by the NewTek license in the scoped packages under `packages/` (published as `@grandi/*`).
400
473
 
401
474
  ## Trademarks
402
475
  NDI™ is a trademark of NewTek, Inc.
package/package.json CHANGED
@@ -1,84 +1,83 @@
1
1
  {
2
- "name": "grandi",
3
- "version": "0.1.0",
4
- "description": "Node.JS native bindings to the Newtek NDI SDK.",
5
- "homepage": "https://github.com/tux-tn/grandi#readme",
6
- "keywords": [
7
- "Newtek",
8
- "NDI",
9
- "network",
10
- "device",
11
- "interface"
12
- ],
13
- "author": "Sarhan Aissi <npm@tux.tn>",
14
- "license": "Apache-2.0",
15
- "files": [
16
- "binding.gyp",
17
- "dist",
18
- "lib",
19
- "src",
20
- "scripts",
21
- "LICENSE",
22
- "NOTICE",
23
- "README.md",
24
- "prebuilds"
25
- ],
26
- "main": "./dist/index.mjs",
27
- "types": "./dist/index.d.mts",
28
- "exports": {
29
- ".": "./dist/index.mjs",
30
- "./package.json": "./package.json"
31
- },
32
- "gypfile": true,
33
- "repository": {
34
- "type": "git",
35
- "url": "https://github.com/tux-tn/grandi.git"
36
- },
37
- "bugs": {
38
- "url": "https://github.com/tux-tn/grandi/issues"
39
- },
40
- "dependencies": {
41
- "cross-spawn": "7.0.6",
42
- "cross-zip": "4.0.1",
43
- "execa": "9.6.0",
44
- "got": "14.6.3",
45
- "node-gyp-build": "^4.8.4",
46
- "shelljs": "0.10.0",
47
- "tmp": "0.2.5"
48
- },
49
- "devDependencies": {
50
- "@biomejs/biome": "2.3.4",
51
- "@types/bindings": "^1.5.5",
52
- "@types/cross-zip": "^4.0.2",
53
- "@types/node": "^24.10.0",
54
- "@types/shelljs": "^0.8.17",
55
- "@types/tmp": "^0.2.6",
56
- "@vitest/coverage-v8": "^4.0.8",
57
- "cross-env": "^10.1.0",
58
- "prebuildify": "^6.0.1",
59
- "shx": "0.4.0",
60
- "standard-version": "^9.5.0",
61
- "tsdown": "^0.16.1",
62
- "typescript": "^5.9.3",
63
- "vitest": "^4.0.8"
64
- },
65
- "scripts": {
66
- "preinstall": "node scripts/preinstall.mjs",
67
- "install": "node-gyp-build",
68
- "prebuild:download": "cross-env NDI_FORCE=1 node scripts/preinstall.mjs",
69
- "build": "tsdown",
70
- "prebuild": "prebuildify --napi --strip --target=node@20.19.5",
71
- "test": "vitest run",
72
- "test:watch": "vitest",
73
- "test:unit": "vitest run test/unit",
74
- "test:integration": "cross-env RUN_NDI_TESTS=1 vitest run test/integration",
75
- "test:coverage": "vitest run --coverage",
76
- "format": "biome format",
77
- "format:cpp": "clang-format -i lib/*.cc lib/*.h",
78
- "lint": "biome lint",
79
- "clean": "shx rm -rf ndi build"
80
- },
81
- "engines": {
82
- "node": ">=20.19.5"
83
- }
2
+ "name": "grandi",
3
+ "version": "1.1.0",
4
+ "description": "Node.JS native bindings to the Newtek NDI SDK.",
5
+ "homepage": "https://github.com/tux-tn/grandi#readme",
6
+ "keywords": [
7
+ "Newtek",
8
+ "NDI",
9
+ "network",
10
+ "device",
11
+ "interface"
12
+ ],
13
+ "author": "Sarhan Aissi <npm@tux.tn>",
14
+ "license": "Apache-2.0",
15
+ "files": [
16
+ "dist",
17
+ "LICENSE",
18
+ "NOTICE",
19
+ "README.md"
20
+ ],
21
+ "main": "./dist/index.mjs",
22
+ "types": "./dist/index.d.mts",
23
+ "exports": {
24
+ ".": "./dist/index.mjs",
25
+ "./package.json": "./package.json"
26
+ },
27
+ "gypfile": true,
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/tux-tn/grandi.git"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/tux-tn/grandi/issues"
34
+ },
35
+ "optionalDependencies": {
36
+ "@grandi/linux-x64": "^1.0.0",
37
+ "@grandi/linux-arm64": "^1.0.0",
38
+ "@grandi/linux-armv7l": "^1.0.0",
39
+ "@grandi/win32-x64": "^1.0.0",
40
+ "@grandi/win32-ia32": "^1.0.0",
41
+ "@grandi/darwin-universal": "1.0.0"
42
+ },
43
+ "dependencies": {
44
+ "cross-spawn": "7.0.6",
45
+ "cross-zip": "4.0.1",
46
+ "execa": "9.6.1",
47
+ "got": "14.6.5",
48
+ "node-gyp-build": "^4.8.4",
49
+ "shelljs": "0.10.0",
50
+ "tmp": "0.2.5"
51
+ },
52
+ "devDependencies": {
53
+ "@biomejs/biome": "2.3.9",
54
+ "@types/bindings": "^1.5.5",
55
+ "@types/cross-zip": "^4.0.2",
56
+ "@types/node": "^25.0.2",
57
+ "@types/shelljs": "^0.10.0",
58
+ "@types/tmp": "^0.2.6",
59
+ "@vitest/coverage-v8": "^4.0.15",
60
+ "cross-env": "^10.1.0",
61
+ "shx": "0.4.0",
62
+ "standard-version": "^9.5.0",
63
+ "tsdown": "^0.18.0",
64
+ "typescript": "^5.9.3",
65
+ "vitest": "^4.0.15"
66
+ },
67
+ "scripts": {
68
+ "build": "tsdown",
69
+ "test": "vitest run",
70
+ "test:watch": "vitest",
71
+ "test:unit": "vitest run test/unit",
72
+ "test:integration": "vitest run test/integration",
73
+ "bench": "node --expose-gc scripts/benchmark.mjs",
74
+ "test:coverage": "vitest run --coverage",
75
+ "format": "biome format",
76
+ "format:cpp": "clang-format -i lib/*.cc lib/*.h",
77
+ "lint": "biome lint",
78
+ "clean": "shx rm -rf ndi build"
79
+ },
80
+ "engines": {
81
+ "node": ">=20.19.5"
82
+ }
84
83
  }