@apocaliss92/nodelink-js 0.5.2 → 0.6.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/dist/index.d.cts CHANGED
@@ -5284,6 +5284,88 @@ interface CompositeStreamPipOptions {
5284
5284
  teleRtspUrl?: string;
5285
5285
  /** ffmpeg `-rtsp_transport` value (default: tcp). */
5286
5286
  rtspTransport?: 'tcp' | 'udp';
5287
+ /**
5288
+ * Absolute path to the `ffmpeg` binary the compositor should `spawn()`.
5289
+ *
5290
+ * When unset, falls back to the bare string `"ffmpeg"`, which relies on
5291
+ * the host's `PATH`. This works on standard Linux / macOS Node setups
5292
+ * but fails silently on environments where the embedding process
5293
+ * strips PATH or ships its own ffmpeg in a non-discoverable location:
5294
+ *
5295
+ * - Scrypted on Windows (the Electron host launches plugins with a
5296
+ * minimal PATH; the bundled ffmpeg lives at a static absolute path
5297
+ * only `sdk.mediaManager.getFFmpegPath()` knows about).
5298
+ * - Scrypted on macOS App Store / sandboxed installs.
5299
+ * - Distroless / minimal Docker base images where ffmpeg is mounted
5300
+ * at a fixed path but not in PATH.
5301
+ *
5302
+ * Consumers in any of those environments should resolve the path
5303
+ * themselves (e.g. via the Scrypted SDK) and pass it through here.
5304
+ */
5305
+ ffmpegPath?: string;
5306
+ /**
5307
+ * Output video encoder name (passed verbatim as `-c:v`). Default
5308
+ * `libx264` — pure software. On a host with hardware-accelerated
5309
+ * H.264 encoding, switching this is the single biggest perf win
5310
+ * available (5-10x less CPU for the same output bitrate). Supported
5311
+ * by ffmpeg in the wild:
5312
+ *
5313
+ * - Intel/AMD QuickSync: `h264_qsv`
5314
+ * - Intel/AMD VAAPI: `h264_vaapi`
5315
+ * - NVIDIA NVENC: `h264_nvenc`
5316
+ * - Apple VideoToolbox: `h264_videotoolbox`
5317
+ * - Raspberry Pi / ARM: `h264_v4l2m2m`
5318
+ *
5319
+ * Caveat: when not `libx264`, the libx264-specific knobs
5320
+ * (`encoderPreset`, `crf`, `x264-params`) are silently dropped because
5321
+ * each HW encoder has its own option vocabulary. Use
5322
+ * {@link extraOutputArgs} to pass encoder-specific options
5323
+ * (e.g. `["-q:v","23"]` for QSV, `["-preset","fast"]` for NVENC).
5324
+ */
5325
+ videoEncoder?: string;
5326
+ /**
5327
+ * libx264 preset (default `ultrafast`). Only consulted when
5328
+ * {@link videoEncoder} is left at its default (libx264). Valid:
5329
+ * `ultrafast | superfast | veryfast | faster | fast | medium | slow |
5330
+ * slower | veryslow | placebo`. Slower = better compression / more CPU.
5331
+ */
5332
+ encoderPreset?: string;
5333
+ /**
5334
+ * CRF (constant rate factor) for libx264. Default 23 (visually
5335
+ * lossless-ish). Range 0 (lossless, huge) — 51 (worst, tiny). Lower
5336
+ * by 2 to roughly halve the bitrate at a given quality. Only consulted
5337
+ * when {@link videoEncoder} is libx264.
5338
+ */
5339
+ crf?: number;
5340
+ /**
5341
+ * Output keyframe interval in seconds. Default 1 — important for
5342
+ * mid-stream join latency (a new HLS / RTSP client only starts
5343
+ * decoding from the next keyframe). Lower = faster join, larger
5344
+ * stream; higher = better compression, slower join.
5345
+ */
5346
+ gopSeconds?: number;
5347
+ /**
5348
+ * Free-form ffmpeg arguments inserted at the START of the argv (after
5349
+ * `-hide_banner -loglevel`). Use for hardware-accel decode hints that
5350
+ * must appear before any `-i`:
5351
+ *
5352
+ * extraGlobalArgs: ["-hwaccel","videotoolbox"]
5353
+ * extraGlobalArgs: ["-hwaccel","qsv","-qsv_device","/dev/dri/renderD128"]
5354
+ *
5355
+ * No validation — invalid args here WILL crash ffmpeg.
5356
+ */
5357
+ extraGlobalArgs?: readonly string[];
5358
+ /**
5359
+ * Free-form ffmpeg arguments inserted just BEFORE the `pipe:1` output.
5360
+ * Use for encoder-specific options when {@link videoEncoder} is not
5361
+ * `libx264`:
5362
+ *
5363
+ * extraOutputArgs: ["-q:v","23"] // qsv/vaapi
5364
+ * extraOutputArgs: ["-preset","fast","-rc","cbr"] // nvenc
5365
+ *
5366
+ * No validation — invalid args here WILL crash ffmpeg.
5367
+ */
5368
+ extraOutputArgs?: readonly string[];
5287
5369
  }
5288
5370
  type CompositeStreamOptions = {
5289
5371
  api: ReolinkBaichuanApi;
@@ -5326,6 +5408,24 @@ type CompositeStreamOptions = {
5326
5408
  teleRtspUrl?: string;
5327
5409
  /** ffmpeg `-rtsp_transport` value (default: tcp). */
5328
5410
  rtspTransport?: 'tcp' | 'udp';
5411
+ /**
5412
+ * Absolute path to the `ffmpeg` binary the compositor should `spawn()`.
5413
+ * Mirrors {@link CompositeStreamPipOptions.ffmpegPath} — see its docstring
5414
+ * for the rationale. Defaults to `"ffmpeg"` (PATH lookup) when unset.
5415
+ */
5416
+ ffmpegPath?: string;
5417
+ /** See {@link CompositeStreamPipOptions.videoEncoder}. */
5418
+ videoEncoder?: string;
5419
+ /** See {@link CompositeStreamPipOptions.encoderPreset}. */
5420
+ encoderPreset?: string;
5421
+ /** See {@link CompositeStreamPipOptions.crf}. */
5422
+ crf?: number;
5423
+ /** See {@link CompositeStreamPipOptions.gopSeconds}. */
5424
+ gopSeconds?: number;
5425
+ /** See {@link CompositeStreamPipOptions.extraGlobalArgs}. */
5426
+ extraGlobalArgs?: readonly string[];
5427
+ /** See {@link CompositeStreamPipOptions.extraOutputArgs}. */
5428
+ extraOutputArgs?: readonly string[];
5329
5429
  /**
5330
5430
  * How long to wait for each input stream to produce frames before starting ffmpeg.
5331
5431
  * Battery cameras can take several seconds to wake and begin streaming.
package/dist/index.d.ts CHANGED
@@ -4145,6 +4145,24 @@ export declare type CompositeStreamOptions = {
4145
4145
  teleRtspUrl?: string;
4146
4146
  /** ffmpeg `-rtsp_transport` value (default: tcp). */
4147
4147
  rtspTransport?: 'tcp' | 'udp';
4148
+ /**
4149
+ * Absolute path to the `ffmpeg` binary the compositor should `spawn()`.
4150
+ * Mirrors {@link CompositeStreamPipOptions.ffmpegPath} — see its docstring
4151
+ * for the rationale. Defaults to `"ffmpeg"` (PATH lookup) when unset.
4152
+ */
4153
+ ffmpegPath?: string;
4154
+ /** See {@link CompositeStreamPipOptions.videoEncoder}. */
4155
+ videoEncoder?: string;
4156
+ /** See {@link CompositeStreamPipOptions.encoderPreset}. */
4157
+ encoderPreset?: string;
4158
+ /** See {@link CompositeStreamPipOptions.crf}. */
4159
+ crf?: number;
4160
+ /** See {@link CompositeStreamPipOptions.gopSeconds}. */
4161
+ gopSeconds?: number;
4162
+ /** See {@link CompositeStreamPipOptions.extraGlobalArgs}. */
4163
+ extraGlobalArgs?: readonly string[];
4164
+ /** See {@link CompositeStreamPipOptions.extraOutputArgs}. */
4165
+ extraOutputArgs?: readonly string[];
4148
4166
  /**
4149
4167
  * How long to wait for each input stream to produce frames before starting ffmpeg.
4150
4168
  * Battery cameras can take several seconds to wake and begin streaming.
@@ -4192,6 +4210,88 @@ export declare interface CompositeStreamPipOptions {
4192
4210
  teleRtspUrl?: string;
4193
4211
  /** ffmpeg `-rtsp_transport` value (default: tcp). */
4194
4212
  rtspTransport?: 'tcp' | 'udp';
4213
+ /**
4214
+ * Absolute path to the `ffmpeg` binary the compositor should `spawn()`.
4215
+ *
4216
+ * When unset, falls back to the bare string `"ffmpeg"`, which relies on
4217
+ * the host's `PATH`. This works on standard Linux / macOS Node setups
4218
+ * but fails silently on environments where the embedding process
4219
+ * strips PATH or ships its own ffmpeg in a non-discoverable location:
4220
+ *
4221
+ * - Scrypted on Windows (the Electron host launches plugins with a
4222
+ * minimal PATH; the bundled ffmpeg lives at a static absolute path
4223
+ * only `sdk.mediaManager.getFFmpegPath()` knows about).
4224
+ * - Scrypted on macOS App Store / sandboxed installs.
4225
+ * - Distroless / minimal Docker base images where ffmpeg is mounted
4226
+ * at a fixed path but not in PATH.
4227
+ *
4228
+ * Consumers in any of those environments should resolve the path
4229
+ * themselves (e.g. via the Scrypted SDK) and pass it through here.
4230
+ */
4231
+ ffmpegPath?: string;
4232
+ /**
4233
+ * Output video encoder name (passed verbatim as `-c:v`). Default
4234
+ * `libx264` — pure software. On a host with hardware-accelerated
4235
+ * H.264 encoding, switching this is the single biggest perf win
4236
+ * available (5-10x less CPU for the same output bitrate). Supported
4237
+ * by ffmpeg in the wild:
4238
+ *
4239
+ * - Intel/AMD QuickSync: `h264_qsv`
4240
+ * - Intel/AMD VAAPI: `h264_vaapi`
4241
+ * - NVIDIA NVENC: `h264_nvenc`
4242
+ * - Apple VideoToolbox: `h264_videotoolbox`
4243
+ * - Raspberry Pi / ARM: `h264_v4l2m2m`
4244
+ *
4245
+ * Caveat: when not `libx264`, the libx264-specific knobs
4246
+ * (`encoderPreset`, `crf`, `x264-params`) are silently dropped because
4247
+ * each HW encoder has its own option vocabulary. Use
4248
+ * {@link extraOutputArgs} to pass encoder-specific options
4249
+ * (e.g. `["-q:v","23"]` for QSV, `["-preset","fast"]` for NVENC).
4250
+ */
4251
+ videoEncoder?: string;
4252
+ /**
4253
+ * libx264 preset (default `ultrafast`). Only consulted when
4254
+ * {@link videoEncoder} is left at its default (libx264). Valid:
4255
+ * `ultrafast | superfast | veryfast | faster | fast | medium | slow |
4256
+ * slower | veryslow | placebo`. Slower = better compression / more CPU.
4257
+ */
4258
+ encoderPreset?: string;
4259
+ /**
4260
+ * CRF (constant rate factor) for libx264. Default 23 (visually
4261
+ * lossless-ish). Range 0 (lossless, huge) — 51 (worst, tiny). Lower
4262
+ * by 2 to roughly halve the bitrate at a given quality. Only consulted
4263
+ * when {@link videoEncoder} is libx264.
4264
+ */
4265
+ crf?: number;
4266
+ /**
4267
+ * Output keyframe interval in seconds. Default 1 — important for
4268
+ * mid-stream join latency (a new HLS / RTSP client only starts
4269
+ * decoding from the next keyframe). Lower = faster join, larger
4270
+ * stream; higher = better compression, slower join.
4271
+ */
4272
+ gopSeconds?: number;
4273
+ /**
4274
+ * Free-form ffmpeg arguments inserted at the START of the argv (after
4275
+ * `-hide_banner -loglevel`). Use for hardware-accel decode hints that
4276
+ * must appear before any `-i`:
4277
+ *
4278
+ * extraGlobalArgs: ["-hwaccel","videotoolbox"]
4279
+ * extraGlobalArgs: ["-hwaccel","qsv","-qsv_device","/dev/dri/renderD128"]
4280
+ *
4281
+ * No validation — invalid args here WILL crash ffmpeg.
4282
+ */
4283
+ extraGlobalArgs?: readonly string[];
4284
+ /**
4285
+ * Free-form ffmpeg arguments inserted just BEFORE the `pipe:1` output.
4286
+ * Use for encoder-specific options when {@link videoEncoder} is not
4287
+ * `libx264`:
4288
+ *
4289
+ * extraOutputArgs: ["-q:v","23"] // qsv/vaapi
4290
+ * extraOutputArgs: ["-preset","fast","-rc","cbr"] // nvenc
4291
+ *
4292
+ * No validation — invalid args here WILL crash ffmpeg.
4293
+ */
4294
+ extraOutputArgs?: readonly string[];
4195
4295
  }
4196
4296
 
4197
4297
  /**
package/dist/index.js CHANGED
@@ -2517,10 +2517,18 @@ var CompositeStream = class extends EventEmitter {
2517
2517
  }
2518
2518
  }
2519
2519
  async startFfmpegCompositionFromRtspUrls(mainWidth, mainHeight, pipWidth, pipHeight, position, widerRtspUrl, teleRtspUrl, rtspTransport) {
2520
+ const videoEncoder = this.options.videoEncoder ?? "libx264";
2521
+ const isX264 = videoEncoder === "libx264";
2522
+ const encoderPreset = this.options.encoderPreset ?? "ultrafast";
2523
+ const crf = this.options.crf ?? 23;
2524
+ const gopSeconds = this.options.gopSeconds ?? 1;
2525
+ const assumedFps = 30;
2526
+ const gopFrames = Math.max(1, Math.round(gopSeconds * assumedFps));
2520
2527
  const ffmpegArgs = [
2521
2528
  "-hide_banner",
2522
2529
  "-loglevel",
2523
2530
  "error",
2531
+ ...this.options.extraGlobalArgs ?? [],
2524
2532
  "-fflags",
2525
2533
  "+genpts",
2526
2534
  // Input 0: wider
@@ -2541,27 +2549,33 @@ var CompositeStream = class extends EventEmitter {
2541
2549
  // Output: always H.264 Annex-B
2542
2550
  "-an",
2543
2551
  "-c:v",
2544
- "libx264",
2552
+ videoEncoder,
2545
2553
  "-g",
2546
- "30",
2554
+ String(gopFrames),
2547
2555
  "-keyint_min",
2548
- "30",
2556
+ String(gopFrames),
2549
2557
  "-sc_threshold",
2550
2558
  "0",
2551
- "-x264-params",
2552
- "aud=1:repeat-headers=1:keyint=30:min-keyint=30:scenecut=0",
2553
- "-preset",
2554
- "ultrafast",
2555
- "-tune",
2556
- "zerolatency",
2557
- "-crf",
2558
- "23",
2559
+ ...isX264 ? [
2560
+ "-x264-params",
2561
+ `aud=1:repeat-headers=1:keyint=${gopFrames}:min-keyint=${gopFrames}:scenecut=0`,
2562
+ "-preset",
2563
+ encoderPreset,
2564
+ "-tune",
2565
+ "zerolatency",
2566
+ "-crf",
2567
+ String(crf)
2568
+ ] : [],
2569
+ ...this.options.extraOutputArgs ?? [],
2559
2570
  "-f",
2560
2571
  "h264",
2561
2572
  "pipe:1"
2562
2573
  ];
2563
- this.logger.log?.(`[CompositeStream] Starting ffmpeg (rtsp inputs): ${ffmpegArgs.join(" ")}`);
2564
- this.ffmpegProcess = spawn3("ffmpeg", ffmpegArgs, {
2574
+ const ffmpegBin = this.options.ffmpegPath ?? "ffmpeg";
2575
+ this.logger.log?.(
2576
+ `[CompositeStream] Starting ffmpeg (rtsp inputs): bin=${ffmpegBin} args=${ffmpegArgs.join(" ")}`
2577
+ );
2578
+ this.ffmpegProcess = spawn3(ffmpegBin, ffmpegArgs, {
2565
2579
  stdio: ["ignore", "pipe", "pipe"]
2566
2580
  });
2567
2581
  this.ffmpegProcess.on("error", (error) => {
@@ -2628,10 +2642,18 @@ var CompositeStream = class extends EventEmitter {
2628
2642
  "-i",
2629
2643
  "pipe:3"
2630
2644
  ];
2645
+ const videoEncoder = this.options.videoEncoder ?? "libx264";
2646
+ const isX264 = videoEncoder === "libx264";
2647
+ const encoderPreset = this.options.encoderPreset ?? "ultrafast";
2648
+ const crf = this.options.crf ?? 23;
2649
+ const gopSeconds = this.options.gopSeconds ?? 1;
2650
+ const assumedFps = 30;
2651
+ const gopFrames = Math.max(1, Math.round(gopSeconds * assumedFps));
2631
2652
  const ffmpegArgs = [
2632
2653
  "-hide_banner",
2633
2654
  "-loglevel",
2634
2655
  "error",
2656
+ ...this.options.extraGlobalArgs ?? [],
2635
2657
  "-fflags",
2636
2658
  "+genpts",
2637
2659
  "-probesize",
@@ -2650,33 +2672,40 @@ var CompositeStream = class extends EventEmitter {
2650
2672
  "-map",
2651
2673
  "[out]",
2652
2674
  "-c:v",
2653
- "libx264",
2654
- // Re-encode for compatibility
2655
- // Make the stream easy to join mid-flight: frequent IDRs + in-band headers + AUD.
2656
- // Without this, a new client may wait many seconds for the next keyframe.
2675
+ videoEncoder,
2676
+ // Make the stream easy to join mid-flight: frequent IDRs + in-band
2677
+ // headers + AUD. Without this, a new client may wait many seconds
2678
+ // for the next keyframe.
2657
2679
  "-g",
2658
- "30",
2680
+ String(gopFrames),
2659
2681
  "-keyint_min",
2660
- "30",
2682
+ String(gopFrames),
2661
2683
  "-sc_threshold",
2662
2684
  "0",
2663
- "-x264-params",
2664
- "aud=1:repeat-headers=1:keyint=30:min-keyint=30:scenecut=0",
2665
- "-preset",
2666
- "ultrafast",
2667
- "-tune",
2668
- "zerolatency",
2669
- "-crf",
2670
- "23",
2685
+ // libx264-specific knobs. We deliberately skip these for HW encoders
2686
+ // — each one has its own option vocabulary (`-q:v`, `-rc`, etc.)
2687
+ // and the user is expected to express them via extraOutputArgs.
2688
+ ...isX264 ? [
2689
+ "-x264-params",
2690
+ `aud=1:repeat-headers=1:keyint=${gopFrames}:min-keyint=${gopFrames}:scenecut=0`,
2691
+ "-preset",
2692
+ encoderPreset,
2693
+ "-tune",
2694
+ "zerolatency",
2695
+ "-crf",
2696
+ String(crf)
2697
+ ] : [],
2698
+ ...this.options.extraOutputArgs ?? [],
2671
2699
  "-f",
2672
2700
  "h264",
2673
2701
  "pipe:1"
2674
2702
  // Output (stdout)
2675
2703
  ];
2704
+ const ffmpegBin = this.options.ffmpegPath ?? "ffmpeg";
2676
2705
  this.logger.log?.(
2677
- `[CompositeStream] Starting ffmpeg: ${ffmpegArgs.join(" ")}`
2706
+ `[CompositeStream] Starting ffmpeg: bin=${ffmpegBin} args=${ffmpegArgs.join(" ")}`
2678
2707
  );
2679
- this.ffmpegProcess = spawn3("ffmpeg", ffmpegArgs, {
2708
+ this.ffmpegProcess = spawn3(ffmpegBin, ffmpegArgs, {
2680
2709
  stdio: ["pipe", "pipe", "pipe", "pipe"]
2681
2710
  });
2682
2711
  this.ffmpegProcess.on("error", (error) => {
@@ -3384,6 +3413,20 @@ async function createRfc4571TcpServerInternal(options) {
3384
3413
  ...forceH264 !== void 0 ? { forceH264 } : defaultForceH264 ? { forceH264: true } : {},
3385
3414
  ...compositeOptions?.assumeH264Inputs !== void 0 ? { assumeH264Inputs: compositeOptions.assumeH264Inputs } : {},
3386
3415
  ...compositeOptions?.disableTranscode !== void 0 ? { disableTranscode: compositeOptions.disableTranscode } : {},
3416
+ // Propagate ffmpeg binary path — required when the embedder strips
3417
+ // PATH (Scrypted on Windows, Electron sandboxes, distroless Docker)
3418
+ // and the bundled ffmpeg is at a fixed absolute path only the
3419
+ // embedder knows.
3420
+ ...compositeOptions?.ffmpegPath ? { ffmpegPath: compositeOptions.ffmpegPath } : {},
3421
+ // Encoder tuning knobs — see CompositeStreamPipOptions for the
3422
+ // semantic contract on each one. Plumbed verbatim so the
3423
+ // CompositeStream layer can apply defaults.
3424
+ ...compositeOptions?.videoEncoder ? { videoEncoder: compositeOptions.videoEncoder } : {},
3425
+ ...compositeOptions?.encoderPreset ? { encoderPreset: compositeOptions.encoderPreset } : {},
3426
+ ...typeof compositeOptions?.crf === "number" ? { crf: compositeOptions.crf } : {},
3427
+ ...typeof compositeOptions?.gopSeconds === "number" ? { gopSeconds: compositeOptions.gopSeconds } : {},
3428
+ ...compositeOptions?.extraGlobalArgs ? { extraGlobalArgs: compositeOptions.extraGlobalArgs } : {},
3429
+ ...compositeOptions?.extraOutputArgs ? { extraOutputArgs: compositeOptions.extraOutputArgs } : {},
3387
3430
  logger
3388
3431
  });
3389
3432
  isCompositeStream = true;