@camstack/addon-pipeline 1.1.31 → 1.1.33

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.
Files changed (69) hide show
  1. package/dist/audio-analyzer/index.js +2 -2
  2. package/dist/audio-analyzer/index.mjs +2 -2
  3. package/dist/audio-codec-ffmpeg/index.js +118 -3
  4. package/dist/audio-codec-ffmpeg/index.mjs +118 -3
  5. package/dist/decoder-ffmpeg/index.js +74 -9
  6. package/dist/decoder-ffmpeg/index.mjs +74 -9
  7. package/dist/decoder-nodeav/index.js +218 -16
  8. package/dist/decoder-nodeav/index.mjs +218 -16
  9. package/dist/detection-pipeline/index.js +34 -3
  10. package/dist/detection-pipeline/index.mjs +34 -3
  11. package/dist/{dist-Cwc0TUQr.js → dist-BrIE85BG.js} +38 -1
  12. package/dist/{dist-DjuGmyG9.mjs → dist-CfSORkv2.mjs} +38 -1
  13. package/dist/{ffmpeg-args-C5GPp8Cw.mjs → ffmpeg-args-CpeRWwL4.mjs} +7 -4
  14. package/dist/{ffmpeg-args-D6h1edXK.js → ffmpeg-args-Dl9mLcOC.js} +7 -4
  15. package/dist/{frame-handle-plane-Bkxz-TTD.mjs → frame-handle-plane-C05rq9Zs.mjs} +108 -17
  16. package/dist/{frame-handle-plane-DQNCTrpC.js → frame-handle-plane-zMju9vmk.js} +108 -17
  17. package/dist/{frame-ring-sink-8LLV-cvH.js → frame-ring-sink-B-DxHelU.js} +175 -22
  18. package/dist/{frame-ring-sink-ClEWjiRU.mjs → frame-ring-sink-B_WjDX0x.mjs} +164 -23
  19. package/dist/motion-wasm/index.js +1 -1
  20. package/dist/motion-wasm/index.mjs +1 -1
  21. package/dist/pipeline-runner/index.js +86 -16
  22. package/dist/pipeline-runner/index.mjs +85 -17
  23. package/dist/recorder/index.js +1 -1
  24. package/dist/recorder/index.mjs +1 -1
  25. package/dist/stream-broker/_stub.js +1 -1
  26. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D567z31g.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DPFAbcUo.mjs} +3 -3
  27. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DtoYGpTp.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BsrrdOBU.mjs} +1 -1
  28. package/dist/stream-broker/{hostInit-B_b3PIZB.mjs → hostInit-C8slrxw1.mjs} +3 -3
  29. package/dist/stream-broker/index.js +165 -27
  30. package/dist/stream-broker/index.mjs +165 -27
  31. package/dist/stream-broker/remoteEntry.js +1 -1
  32. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-x-DZOuQL.js → MaskShapeCanvas-DI4BY7W2-WUVfFzT-.js} +1 -1
  33. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-DTeuDGCU.js → MotionZonesSettings-NcxxQN8r-CF7FXXLs.js} +1 -1
  34. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-CirtxO3e.js → PrivacyMaskSettings-APgPLF7p-qQCpVob3.js} +1 -1
  35. package/embed-dist/assets/{index-CpUy8OIE.js → index-BNhisVUZ.js} +4 -4
  36. package/embed-dist/index.html +1 -1
  37. package/package.json +1 -1
  38. package/python/__pycache__/inference_pool.cpython-313.pyc +0 -0
  39. package/python/__pycache__/inference_pool.cpython-314.pyc +0 -0
  40. package/python/__pycache__/test_inference_pool_backpressure.cpython-314.pyc +0 -0
  41. package/python/__pycache__/test_inference_pool_coreml_cache.cpython-314.pyc +0 -0
  42. package/python/__pycache__/test_inference_pool_device_selection.cpython-314.pyc +0 -0
  43. package/python/__pycache__/yamnet_audio.cpython-314.pyc +0 -0
  44. package/python/postprocessors/__pycache__/__init__.cpython-312.pyc +0 -0
  45. package/python/postprocessors/__pycache__/__init__.cpython-313.pyc +0 -0
  46. package/python/postprocessors/__pycache__/_safety.cpython-313.pyc +0 -0
  47. package/python/postprocessors/__pycache__/arcface.cpython-312.pyc +0 -0
  48. package/python/postprocessors/__pycache__/arcface.cpython-313.pyc +0 -0
  49. package/python/postprocessors/__pycache__/clip.cpython-313.pyc +0 -0
  50. package/python/postprocessors/__pycache__/ctc.cpython-312.pyc +0 -0
  51. package/python/postprocessors/__pycache__/ctc.cpython-313.pyc +0 -0
  52. package/python/postprocessors/__pycache__/saliency.cpython-312.pyc +0 -0
  53. package/python/postprocessors/__pycache__/saliency.cpython-313.pyc +0 -0
  54. package/python/postprocessors/__pycache__/scrfd.cpython-312.pyc +0 -0
  55. package/python/postprocessors/__pycache__/scrfd.cpython-313.pyc +0 -0
  56. package/python/postprocessors/__pycache__/softmax.cpython-312.pyc +0 -0
  57. package/python/postprocessors/__pycache__/softmax.cpython-313.pyc +0 -0
  58. package/python/postprocessors/__pycache__/test_clip.cpython-313-pytest-9.1.1.pyc +0 -0
  59. package/python/postprocessors/__pycache__/test_saliency.cpython-313-pytest-9.1.1.pyc +0 -0
  60. package/python/postprocessors/__pycache__/yamnet.cpython-312.pyc +0 -0
  61. package/python/postprocessors/__pycache__/yamnet.cpython-313.pyc +0 -0
  62. package/python/postprocessors/__pycache__/yolo.cpython-312.pyc +0 -0
  63. package/python/postprocessors/__pycache__/yolo.cpython-313.pyc +0 -0
  64. package/python/postprocessors/__pycache__/yolo_seg.cpython-312.pyc +0 -0
  65. package/python/postprocessors/__pycache__/yolo_seg.cpython-313.pyc +0 -0
  66. package/dist/constants-CAqYVigN.js +0 -6615
  67. package/dist/constants-oYgW36Ty.mjs +0 -3587
  68. package/dist/dist-D3Ytt-_Y.js +0 -41111
  69. package/dist/dist-DluJuxV5.mjs +0 -37976
@@ -2,9 +2,9 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-Cwc0TUQr.js");
6
- const require_frame_ring_sink = require("../frame-ring-sink-8LLV-cvH.js");
7
- const require_ffmpeg_args = require("../ffmpeg-args-D6h1edXK.js");
5
+ const require_dist = require("../dist-BrIE85BG.js");
6
+ const require_frame_ring_sink = require("../frame-ring-sink-B-DxHelU.js");
7
+ const require_ffmpeg_args = require("../ffmpeg-args-Dl9mLcOC.js");
8
8
  let _camstack_shm_ring = require("@camstack/shm-ring");
9
9
  let node_crypto = require("node:crypto");
10
10
  //#region src/decoder-nodeav/scaler-geometry.ts
@@ -22,6 +22,13 @@ function resolveScalerAction(current, incoming) {
22
22
  }
23
23
  //#endregion
24
24
  //#region src/decoder-nodeav/nodeav-decoder-session.ts
25
+ /**
26
+ * Pull-mode re-dial backoff. On a pull input error/EOF the loop waits this long
27
+ * before re-opening the URL — mirrors the ffmpeg pull decoder's 3s re-dial so a
28
+ * transient RTSP restream blip (source suspended on idle, keyframe wait) does
29
+ * not hot-loop `Demuxer.open`.
30
+ */
31
+ var PULL_REDIAL_MS = 3e3;
25
32
  /** Map our canonical backend name to the node-av `AV_HWDEVICE_TYPE_*` constant. */
26
33
  function backendToHwDeviceConst(backend, consts) {
27
34
  switch (backend) {
@@ -58,11 +65,11 @@ var _nav = null;
58
65
  var _consts = null;
59
66
  var _sharp = null;
60
67
  async function getNodeAv() {
61
- if (!_nav) _nav = await Promise.resolve().then(() => require("../dist-D3Ytt-_Y.js"));
68
+ if (!_nav) _nav = await import("node-av");
62
69
  return _nav;
63
70
  }
64
71
  async function getConstants() {
65
- if (!_consts) _consts = await Promise.resolve().then(() => require("../constants-CAqYVigN.js"));
72
+ if (!_consts) _consts = await import("node-av/constants");
66
73
  return _consts;
67
74
  }
68
75
  async function getSharp() {
@@ -196,6 +203,20 @@ var NodeAvDecoderSession = class NodeAvDecoderSession {
196
203
  activeHwAccel = "none";
197
204
  hwDevice = null;
198
205
  swTransferFrame = null;
206
+ /** True once `openStream` has armed the pull loop; drives `isPullMode`. */
207
+ pullActive = false;
208
+ /** One-shot guard so a stray push into a pull session logs once, not per-packet. */
209
+ pullStrayPushLogged = false;
210
+ /** The URL the pull loop (re)dials — updatable while active. */
211
+ pullUrl = null;
212
+ /** Pending re-dial timer + its resolver, so `destroy` can wake it instantly. */
213
+ pullRedialTimer = null;
214
+ pullRedialResolve = null;
215
+ /** High-level demuxer/decoder for the CURRENT dial — recreated per re-dial. */
216
+ pullDemuxer = null;
217
+ pullDecoder = null;
218
+ /** HW context — resolved once, reused across re-dials, freed in `destroy`. */
219
+ pullHwContext = null;
199
220
  constructor(config, logger = noopLogger, options) {
200
221
  this.config = { ...config };
201
222
  const sessionTags = {};
@@ -465,8 +486,171 @@ var NodeAvDecoderSession = class NodeAvDecoderSession {
465
486
  this.scalerInitializing = false;
466
487
  }
467
488
  }
489
+ /**
490
+ * Start PULL mode: libav dials `url` and demuxes it in-process, decoding
491
+ * every frame through the same emit path as push mode. Idempotent while
492
+ * active (updates the re-dial target). Throws if the session already took a
493
+ * push packet — a session is push-mode or pull-mode for its whole life.
494
+ */
495
+ openStream(url) {
496
+ if (this.destroyed) return Promise.resolve();
497
+ if (this.inputPackets > 0 || this.initialized || this.initializing) throw new Error("node-av decoder: openStream on a push-fed session — a session is push-mode or pull-mode, never both");
498
+ if (this.pullActive) {
499
+ this.pullUrl = url;
500
+ return Promise.resolve();
501
+ }
502
+ this.pullActive = true;
503
+ this.pullUrl = url;
504
+ this.logger.info("node-av decoder: pull-mode openStream — dialing input URL", { meta: { url } });
505
+ this.runPullLoop().catch((err) => {
506
+ this.logger.error("node-av decoder: pull loop crashed", { meta: { error: require_dist.errMsg(err) } });
507
+ });
508
+ return Promise.resolve();
509
+ }
510
+ /**
511
+ * The pull-mode supervisor loop: resolve HW once, then repeatedly dial +
512
+ * decode the URL, re-dialing after {@link PULL_REDIAL_MS} on any exit/error
513
+ * until the session is destroyed. Each iteration rebuilds a fresh
514
+ * demuxer/decoder (the previous one is disposed in `closePullInput`).
515
+ */
516
+ async runPullLoop() {
517
+ const nav = await getNodeAv();
518
+ const C = await getConstants();
519
+ this.EAGAIN = C.AVERROR_EAGAIN;
520
+ this.PIX_FMT_GRAY8 = C.AV_PIX_FMT_GRAY8;
521
+ this.PIX_FMT_RGB24 = C.AV_PIX_FMT_RGB24;
522
+ this.SWS_FAST_BILINEAR = C.SWS_FAST_BILINEAR;
523
+ nav.Log.setLevel(C.AV_LOG_FATAL);
524
+ if (this.outputMode === "jpeg" && !this.sharpFn) this.sharpFn = await getSharp();
525
+ await this.ensurePullHwContext(nav, C);
526
+ while (!this.destroyed && this.pullActive) {
527
+ const url = this.pullUrl;
528
+ if (!url) break;
529
+ try {
530
+ await this.pullDialAndDecode(nav, C, url);
531
+ } catch (err) {
532
+ if (this.destroyed) break;
533
+ this.logger.warn("node-av decoder: pull-mode dial ended — re-dial scheduled", { meta: {
534
+ error: require_dist.errMsg(err),
535
+ redialInMs: PULL_REDIAL_MS
536
+ } });
537
+ } finally {
538
+ this.closePullInput();
539
+ }
540
+ if (this.destroyed || !this.pullActive) break;
541
+ await this.pullSleep(PULL_REDIAL_MS);
542
+ }
543
+ }
544
+ /**
545
+ * One dial: open the demuxer, pick the video stream, create a HW/SW decoder
546
+ * that downloads + normalises frames to YUV420P (so the shared scaler in
547
+ * `emitDecodedFrame` reads plain software planes regardless of hwaccel), and
548
+ * pump `demuxer.packets(video) → decoder.frames → emitDecodedFrame` until the
549
+ * stream ends or the session is torn down.
550
+ */
551
+ async pullDialAndDecode(nav, C, url) {
552
+ const demuxer = await nav.Demuxer.open(url, { options: { rtsp_transport: "tcp" } });
553
+ if (this.destroyed || !this.pullActive) {
554
+ demuxer[Symbol.dispose]?.();
555
+ return;
556
+ }
557
+ this.pullDemuxer = demuxer;
558
+ const videoStream = demuxer.video();
559
+ if (!videoStream) throw new Error("node-av decoder: pull input has no video stream");
560
+ const decoder = await nav.Decoder.create(videoStream, {
561
+ ...this.pullHwContext ? { hardware: this.pullHwContext } : {},
562
+ rescale: { pixelFormat: C.AV_PIX_FMT_YUV420P }
563
+ });
564
+ if (this.destroyed || !this.pullActive) {
565
+ decoder[Symbol.dispose]?.();
566
+ return;
567
+ }
568
+ this.pullDecoder = decoder;
569
+ this.logger.info("node-av pull decoder initialized", { meta: {
570
+ codec: this.config.codec,
571
+ output: this.outputMode,
572
+ hwAccel: this.activeHwAccel,
573
+ streamIndex: videoStream.index
574
+ } });
575
+ for await (const frame of decoder.frames(demuxer.packets(videoStream.index))) {
576
+ if (this.destroyed || !this.pullActive) break;
577
+ if (!frame) continue;
578
+ this.inputPackets++;
579
+ try {
580
+ this.emitDecodedFrame(frame);
581
+ } finally {
582
+ frame.free();
583
+ }
584
+ }
585
+ }
586
+ /**
587
+ * Resolve and build the HW context for pull mode ONCE. Explicit backend →
588
+ * single try; `'auto'` → the kernel resolver's ordered list; no resolver →
589
+ * software. The first `HardwareContext.create` that succeeds wins; all
590
+ * failures fall through to software decode (`hardware: null`).
591
+ */
592
+ async ensurePullHwContext(nav, C) {
593
+ if (this.hwaccelPref === "none") {
594
+ this.activeHwAccel = "none";
595
+ return;
596
+ }
597
+ const explicit = this.hwaccelPref === "auto" ? null : this.hwaccelPref;
598
+ const resolution = this.hwaccelResolver ? await this.hwaccelResolver.resolve(explicit) : explicit ? {
599
+ preferred: [explicit],
600
+ rationale: "explicit (no resolver)"
601
+ } : {
602
+ preferred: [],
603
+ rationale: "auto + no resolver → software"
604
+ };
605
+ for (const backend of resolution.preferred) {
606
+ const deviceType = backendToHwDeviceConst(backend, C);
607
+ if (!deviceType) continue;
608
+ const hw = nav.HardwareContext.create(deviceType);
609
+ if (!hw) {
610
+ this.logger.warn("node-av: pull hwaccel context create failed — trying next", { meta: { backend } });
611
+ continue;
612
+ }
613
+ this.pullHwContext = hw;
614
+ this.activeHwAccel = backend;
615
+ return;
616
+ }
617
+ this.logger.warn("node-av: no pull hwaccel backend initialised — using software", { meta: {
618
+ attempted: resolution.preferred.join(","),
619
+ rationale: resolution.rationale
620
+ } });
621
+ this.activeHwAccel = "none";
622
+ }
623
+ /** Dispose the CURRENT dial's demuxer + decoder (HW context is reused). */
624
+ closePullInput() {
625
+ if (this.pullDecoder) {
626
+ this.pullDecoder[Symbol.dispose]?.();
627
+ this.pullDecoder = null;
628
+ }
629
+ if (this.pullDemuxer) {
630
+ this.pullDemuxer[Symbol.dispose]?.();
631
+ this.pullDemuxer = null;
632
+ }
633
+ }
634
+ /** Interruptible sleep for the re-dial backoff — `destroy` resolves it early. */
635
+ pullSleep(ms) {
636
+ return new Promise((resolve) => {
637
+ this.pullRedialResolve = resolve;
638
+ this.pullRedialTimer = setTimeout(() => {
639
+ this.pullRedialTimer = null;
640
+ this.pullRedialResolve = null;
641
+ resolve();
642
+ }, ms);
643
+ });
644
+ }
468
645
  pushPacket(packet) {
469
646
  if (this.destroyed) return;
647
+ if (this.pullActive) {
648
+ if (!this.pullStrayPushLogged) {
649
+ this.pullStrayPushLogged = true;
650
+ this.logger.debug("node-av decoder: pushPacket on a pull-mode session — ignored");
651
+ }
652
+ return;
653
+ }
470
654
  this.inputPackets++;
471
655
  if (!this.initialized && !this.initializing && packet.keyframe) {
472
656
  this.initDecoder().then(() => {
@@ -847,6 +1031,21 @@ var NodeAvDecoderSession = class NodeAvDecoderSession {
847
1031
  this.destroyed = true;
848
1032
  this.frameCallbacks.clear();
849
1033
  this.handleCallbacks.clear();
1034
+ this.pullActive = false;
1035
+ if (this.pullRedialTimer) {
1036
+ clearTimeout(this.pullRedialTimer);
1037
+ this.pullRedialTimer = null;
1038
+ }
1039
+ if (this.pullRedialResolve) {
1040
+ const resolve = this.pullRedialResolve;
1041
+ this.pullRedialResolve = null;
1042
+ resolve();
1043
+ }
1044
+ this.closePullInput();
1045
+ if (this.pullHwContext) {
1046
+ if (!this.pullHwContext.isDisposed) this.pullHwContext.dispose();
1047
+ this.pullHwContext = null;
1048
+ }
850
1049
  this.frameRingSink?.destroy();
851
1050
  this.frameRingSink = null;
852
1051
  this.dstFrame?.[Symbol.dispose]?.();
@@ -876,7 +1075,7 @@ var NodeAvDecoderSession = class NodeAvDecoderSession {
876
1075
  };
877
1076
  }
878
1077
  get isPullMode() {
879
- return false;
1078
+ return this.pullActive;
880
1079
  }
881
1080
  };
882
1081
  //#endregion
@@ -955,14 +1154,16 @@ var DecoderNodeAvAddon = class extends require_dist.BaseAddon {
955
1154
  }];
956
1155
  }
957
1156
  /**
958
- * Resolve the effective hwaccel backend for a new session. Reads
959
- * this addon's own `hwaccel` setting first. `'auto'` defers to the
960
- * session's local resolver (`ctx.kernel.hwaccel`) which probes the
961
- * host and picks. No more orchestrator round-trip decoder addon
962
- * is self-sufficient for this setting as of phase 2d.
1157
+ * Resolve the effective hwaccel backend for a new session. Reads the SHARED
1158
+ * per-node override from the `decoder-ffmpeg` OWNER the single value the
1159
+ * Pipeline UI edits — exactly as `resolveDecoderBackend` reads the owner's
1160
+ * `backend`. This makes a manual hwaccel override take effect even when
1161
+ * node-av is the active backend (its OWN `hwaccel@node` is never surfaced by
1162
+ * the UI). `'auto'` defers to the session's local resolver
1163
+ * (`ctx.kernel.hwaccel`). Fails safe to `'auto'`.
963
1164
  */
964
1165
  resolveHwAccelPref() {
965
- return this.config.hwaccel;
1166
+ return require_frame_ring_sink.resolveDecoderHwAccel(this.ctx.api, this.resolveLocalNodeId(), this.ctx.logger);
966
1167
  }
967
1168
  /**
968
1169
  * Re-run the platform probe on this host and persist the detected
@@ -1003,7 +1204,7 @@ var DecoderNodeAvAddon = class extends require_dist.BaseAddon {
1003
1204
  return {
1004
1205
  id: "decoder-nodeav",
1005
1206
  name: "Decoder (node-av)",
1006
- isPullMode: false,
1207
+ isPullMode: true,
1007
1208
  priority: 10
1008
1209
  };
1009
1210
  }
@@ -1021,7 +1222,7 @@ var DecoderNodeAvAddon = class extends require_dist.BaseAddon {
1021
1222
  }
1022
1223
  async createSession(config) {
1023
1224
  const sessionId = (0, node_crypto.randomUUID)();
1024
- const hwaccel = this.resolveHwAccelPref();
1225
+ const hwaccel = await this.resolveHwAccelPref();
1025
1226
  const { frameSink } = config;
1026
1227
  const nodeId = this.resolveLocalNodeId();
1027
1228
  const session = new NodeAvDecoderSession(config, this.ctx.logger, {
@@ -1121,8 +1322,9 @@ var DecoderNodeAvAddon = class extends require_dist.BaseAddon {
1121
1322
  });
1122
1323
  }
1123
1324
  async openStream(input) {
1124
- if (!this.sessions.get(input.sessionId)) throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
1125
- input.url;
1325
+ const session = this.sessions.get(input.sessionId);
1326
+ if (!session) throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
1327
+ await session.openStream(input.url);
1126
1328
  }
1127
1329
  async pullFrames(input) {
1128
1330
  if (!this.sessions.has(input.sessionId)) throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
@@ -1,6 +1,6 @@
1
- import { B as errMsg, V as BaseAddon, d as HWACCEL_OPTIONS, s as DEFAULT_DECODER_HWACCEL_CONFIG, x as decoderCapability } from "../dist-DjuGmyG9.mjs";
2
- import { a as resolveDecoderBackend, i as NotifyingRingBuffer, n as RING_BUDGET_MB, r as makeSegmentName, t as DecoderFrameRingSink } from "../frame-ring-sink-ClEWjiRU.mjs";
3
- import { f as rankDecodeHwAccels } from "../ffmpeg-args-C5GPp8Cw.mjs";
1
+ import { B as errMsg, V as BaseAddon, d as HWACCEL_OPTIONS, s as DEFAULT_DECODER_HWACCEL_CONFIG, x as decoderCapability } from "../dist-CfSORkv2.mjs";
2
+ import { a as resolveDecoderBackend, i as NotifyingRingBuffer, n as RING_BUDGET_MB, o as resolveDecoderHwAccel, r as makeSegmentName, t as DecoderFrameRingSink } from "../frame-ring-sink-B_WjDX0x.mjs";
3
+ import { f as rankDecodeHwAccels } from "../ffmpeg-args-CpeRWwL4.mjs";
4
4
  import { FrameRingReaderCache } from "@camstack/shm-ring";
5
5
  import { randomUUID } from "node:crypto";
6
6
  //#region src/decoder-nodeav/scaler-geometry.ts
@@ -18,6 +18,13 @@ function resolveScalerAction(current, incoming) {
18
18
  }
19
19
  //#endregion
20
20
  //#region src/decoder-nodeav/nodeav-decoder-session.ts
21
+ /**
22
+ * Pull-mode re-dial backoff. On a pull input error/EOF the loop waits this long
23
+ * before re-opening the URL — mirrors the ffmpeg pull decoder's 3s re-dial so a
24
+ * transient RTSP restream blip (source suspended on idle, keyframe wait) does
25
+ * not hot-loop `Demuxer.open`.
26
+ */
27
+ var PULL_REDIAL_MS = 3e3;
21
28
  /** Map our canonical backend name to the node-av `AV_HWDEVICE_TYPE_*` constant. */
22
29
  function backendToHwDeviceConst(backend, consts) {
23
30
  switch (backend) {
@@ -54,11 +61,11 @@ var _nav = null;
54
61
  var _consts = null;
55
62
  var _sharp = null;
56
63
  async function getNodeAv() {
57
- if (!_nav) _nav = await import("../dist-DluJuxV5.mjs");
64
+ if (!_nav) _nav = await import("node-av");
58
65
  return _nav;
59
66
  }
60
67
  async function getConstants() {
61
- if (!_consts) _consts = await import("../constants-oYgW36Ty.mjs");
68
+ if (!_consts) _consts = await import("node-av/constants");
62
69
  return _consts;
63
70
  }
64
71
  async function getSharp() {
@@ -192,6 +199,20 @@ var NodeAvDecoderSession = class NodeAvDecoderSession {
192
199
  activeHwAccel = "none";
193
200
  hwDevice = null;
194
201
  swTransferFrame = null;
202
+ /** True once `openStream` has armed the pull loop; drives `isPullMode`. */
203
+ pullActive = false;
204
+ /** One-shot guard so a stray push into a pull session logs once, not per-packet. */
205
+ pullStrayPushLogged = false;
206
+ /** The URL the pull loop (re)dials — updatable while active. */
207
+ pullUrl = null;
208
+ /** Pending re-dial timer + its resolver, so `destroy` can wake it instantly. */
209
+ pullRedialTimer = null;
210
+ pullRedialResolve = null;
211
+ /** High-level demuxer/decoder for the CURRENT dial — recreated per re-dial. */
212
+ pullDemuxer = null;
213
+ pullDecoder = null;
214
+ /** HW context — resolved once, reused across re-dials, freed in `destroy`. */
215
+ pullHwContext = null;
195
216
  constructor(config, logger = noopLogger, options) {
196
217
  this.config = { ...config };
197
218
  const sessionTags = {};
@@ -461,8 +482,171 @@ var NodeAvDecoderSession = class NodeAvDecoderSession {
461
482
  this.scalerInitializing = false;
462
483
  }
463
484
  }
485
+ /**
486
+ * Start PULL mode: libav dials `url` and demuxes it in-process, decoding
487
+ * every frame through the same emit path as push mode. Idempotent while
488
+ * active (updates the re-dial target). Throws if the session already took a
489
+ * push packet — a session is push-mode or pull-mode for its whole life.
490
+ */
491
+ openStream(url) {
492
+ if (this.destroyed) return Promise.resolve();
493
+ if (this.inputPackets > 0 || this.initialized || this.initializing) throw new Error("node-av decoder: openStream on a push-fed session — a session is push-mode or pull-mode, never both");
494
+ if (this.pullActive) {
495
+ this.pullUrl = url;
496
+ return Promise.resolve();
497
+ }
498
+ this.pullActive = true;
499
+ this.pullUrl = url;
500
+ this.logger.info("node-av decoder: pull-mode openStream — dialing input URL", { meta: { url } });
501
+ this.runPullLoop().catch((err) => {
502
+ this.logger.error("node-av decoder: pull loop crashed", { meta: { error: errMsg(err) } });
503
+ });
504
+ return Promise.resolve();
505
+ }
506
+ /**
507
+ * The pull-mode supervisor loop: resolve HW once, then repeatedly dial +
508
+ * decode the URL, re-dialing after {@link PULL_REDIAL_MS} on any exit/error
509
+ * until the session is destroyed. Each iteration rebuilds a fresh
510
+ * demuxer/decoder (the previous one is disposed in `closePullInput`).
511
+ */
512
+ async runPullLoop() {
513
+ const nav = await getNodeAv();
514
+ const C = await getConstants();
515
+ this.EAGAIN = C.AVERROR_EAGAIN;
516
+ this.PIX_FMT_GRAY8 = C.AV_PIX_FMT_GRAY8;
517
+ this.PIX_FMT_RGB24 = C.AV_PIX_FMT_RGB24;
518
+ this.SWS_FAST_BILINEAR = C.SWS_FAST_BILINEAR;
519
+ nav.Log.setLevel(C.AV_LOG_FATAL);
520
+ if (this.outputMode === "jpeg" && !this.sharpFn) this.sharpFn = await getSharp();
521
+ await this.ensurePullHwContext(nav, C);
522
+ while (!this.destroyed && this.pullActive) {
523
+ const url = this.pullUrl;
524
+ if (!url) break;
525
+ try {
526
+ await this.pullDialAndDecode(nav, C, url);
527
+ } catch (err) {
528
+ if (this.destroyed) break;
529
+ this.logger.warn("node-av decoder: pull-mode dial ended — re-dial scheduled", { meta: {
530
+ error: errMsg(err),
531
+ redialInMs: PULL_REDIAL_MS
532
+ } });
533
+ } finally {
534
+ this.closePullInput();
535
+ }
536
+ if (this.destroyed || !this.pullActive) break;
537
+ await this.pullSleep(PULL_REDIAL_MS);
538
+ }
539
+ }
540
+ /**
541
+ * One dial: open the demuxer, pick the video stream, create a HW/SW decoder
542
+ * that downloads + normalises frames to YUV420P (so the shared scaler in
543
+ * `emitDecodedFrame` reads plain software planes regardless of hwaccel), and
544
+ * pump `demuxer.packets(video) → decoder.frames → emitDecodedFrame` until the
545
+ * stream ends or the session is torn down.
546
+ */
547
+ async pullDialAndDecode(nav, C, url) {
548
+ const demuxer = await nav.Demuxer.open(url, { options: { rtsp_transport: "tcp" } });
549
+ if (this.destroyed || !this.pullActive) {
550
+ demuxer[Symbol.dispose]?.();
551
+ return;
552
+ }
553
+ this.pullDemuxer = demuxer;
554
+ const videoStream = demuxer.video();
555
+ if (!videoStream) throw new Error("node-av decoder: pull input has no video stream");
556
+ const decoder = await nav.Decoder.create(videoStream, {
557
+ ...this.pullHwContext ? { hardware: this.pullHwContext } : {},
558
+ rescale: { pixelFormat: C.AV_PIX_FMT_YUV420P }
559
+ });
560
+ if (this.destroyed || !this.pullActive) {
561
+ decoder[Symbol.dispose]?.();
562
+ return;
563
+ }
564
+ this.pullDecoder = decoder;
565
+ this.logger.info("node-av pull decoder initialized", { meta: {
566
+ codec: this.config.codec,
567
+ output: this.outputMode,
568
+ hwAccel: this.activeHwAccel,
569
+ streamIndex: videoStream.index
570
+ } });
571
+ for await (const frame of decoder.frames(demuxer.packets(videoStream.index))) {
572
+ if (this.destroyed || !this.pullActive) break;
573
+ if (!frame) continue;
574
+ this.inputPackets++;
575
+ try {
576
+ this.emitDecodedFrame(frame);
577
+ } finally {
578
+ frame.free();
579
+ }
580
+ }
581
+ }
582
+ /**
583
+ * Resolve and build the HW context for pull mode ONCE. Explicit backend →
584
+ * single try; `'auto'` → the kernel resolver's ordered list; no resolver →
585
+ * software. The first `HardwareContext.create` that succeeds wins; all
586
+ * failures fall through to software decode (`hardware: null`).
587
+ */
588
+ async ensurePullHwContext(nav, C) {
589
+ if (this.hwaccelPref === "none") {
590
+ this.activeHwAccel = "none";
591
+ return;
592
+ }
593
+ const explicit = this.hwaccelPref === "auto" ? null : this.hwaccelPref;
594
+ const resolution = this.hwaccelResolver ? await this.hwaccelResolver.resolve(explicit) : explicit ? {
595
+ preferred: [explicit],
596
+ rationale: "explicit (no resolver)"
597
+ } : {
598
+ preferred: [],
599
+ rationale: "auto + no resolver → software"
600
+ };
601
+ for (const backend of resolution.preferred) {
602
+ const deviceType = backendToHwDeviceConst(backend, C);
603
+ if (!deviceType) continue;
604
+ const hw = nav.HardwareContext.create(deviceType);
605
+ if (!hw) {
606
+ this.logger.warn("node-av: pull hwaccel context create failed — trying next", { meta: { backend } });
607
+ continue;
608
+ }
609
+ this.pullHwContext = hw;
610
+ this.activeHwAccel = backend;
611
+ return;
612
+ }
613
+ this.logger.warn("node-av: no pull hwaccel backend initialised — using software", { meta: {
614
+ attempted: resolution.preferred.join(","),
615
+ rationale: resolution.rationale
616
+ } });
617
+ this.activeHwAccel = "none";
618
+ }
619
+ /** Dispose the CURRENT dial's demuxer + decoder (HW context is reused). */
620
+ closePullInput() {
621
+ if (this.pullDecoder) {
622
+ this.pullDecoder[Symbol.dispose]?.();
623
+ this.pullDecoder = null;
624
+ }
625
+ if (this.pullDemuxer) {
626
+ this.pullDemuxer[Symbol.dispose]?.();
627
+ this.pullDemuxer = null;
628
+ }
629
+ }
630
+ /** Interruptible sleep for the re-dial backoff — `destroy` resolves it early. */
631
+ pullSleep(ms) {
632
+ return new Promise((resolve) => {
633
+ this.pullRedialResolve = resolve;
634
+ this.pullRedialTimer = setTimeout(() => {
635
+ this.pullRedialTimer = null;
636
+ this.pullRedialResolve = null;
637
+ resolve();
638
+ }, ms);
639
+ });
640
+ }
464
641
  pushPacket(packet) {
465
642
  if (this.destroyed) return;
643
+ if (this.pullActive) {
644
+ if (!this.pullStrayPushLogged) {
645
+ this.pullStrayPushLogged = true;
646
+ this.logger.debug("node-av decoder: pushPacket on a pull-mode session — ignored");
647
+ }
648
+ return;
649
+ }
466
650
  this.inputPackets++;
467
651
  if (!this.initialized && !this.initializing && packet.keyframe) {
468
652
  this.initDecoder().then(() => {
@@ -843,6 +1027,21 @@ var NodeAvDecoderSession = class NodeAvDecoderSession {
843
1027
  this.destroyed = true;
844
1028
  this.frameCallbacks.clear();
845
1029
  this.handleCallbacks.clear();
1030
+ this.pullActive = false;
1031
+ if (this.pullRedialTimer) {
1032
+ clearTimeout(this.pullRedialTimer);
1033
+ this.pullRedialTimer = null;
1034
+ }
1035
+ if (this.pullRedialResolve) {
1036
+ const resolve = this.pullRedialResolve;
1037
+ this.pullRedialResolve = null;
1038
+ resolve();
1039
+ }
1040
+ this.closePullInput();
1041
+ if (this.pullHwContext) {
1042
+ if (!this.pullHwContext.isDisposed) this.pullHwContext.dispose();
1043
+ this.pullHwContext = null;
1044
+ }
846
1045
  this.frameRingSink?.destroy();
847
1046
  this.frameRingSink = null;
848
1047
  this.dstFrame?.[Symbol.dispose]?.();
@@ -872,7 +1071,7 @@ var NodeAvDecoderSession = class NodeAvDecoderSession {
872
1071
  };
873
1072
  }
874
1073
  get isPullMode() {
875
- return false;
1074
+ return this.pullActive;
876
1075
  }
877
1076
  };
878
1077
  //#endregion
@@ -951,14 +1150,16 @@ var DecoderNodeAvAddon = class extends BaseAddon {
951
1150
  }];
952
1151
  }
953
1152
  /**
954
- * Resolve the effective hwaccel backend for a new session. Reads
955
- * this addon's own `hwaccel` setting first. `'auto'` defers to the
956
- * session's local resolver (`ctx.kernel.hwaccel`) which probes the
957
- * host and picks. No more orchestrator round-trip decoder addon
958
- * is self-sufficient for this setting as of phase 2d.
1153
+ * Resolve the effective hwaccel backend for a new session. Reads the SHARED
1154
+ * per-node override from the `decoder-ffmpeg` OWNER the single value the
1155
+ * Pipeline UI edits — exactly as `resolveDecoderBackend` reads the owner's
1156
+ * `backend`. This makes a manual hwaccel override take effect even when
1157
+ * node-av is the active backend (its OWN `hwaccel@node` is never surfaced by
1158
+ * the UI). `'auto'` defers to the session's local resolver
1159
+ * (`ctx.kernel.hwaccel`). Fails safe to `'auto'`.
959
1160
  */
960
1161
  resolveHwAccelPref() {
961
- return this.config.hwaccel;
1162
+ return resolveDecoderHwAccel(this.ctx.api, this.resolveLocalNodeId(), this.ctx.logger);
962
1163
  }
963
1164
  /**
964
1165
  * Re-run the platform probe on this host and persist the detected
@@ -999,7 +1200,7 @@ var DecoderNodeAvAddon = class extends BaseAddon {
999
1200
  return {
1000
1201
  id: "decoder-nodeav",
1001
1202
  name: "Decoder (node-av)",
1002
- isPullMode: false,
1203
+ isPullMode: true,
1003
1204
  priority: 10
1004
1205
  };
1005
1206
  }
@@ -1017,7 +1218,7 @@ var DecoderNodeAvAddon = class extends BaseAddon {
1017
1218
  }
1018
1219
  async createSession(config) {
1019
1220
  const sessionId = randomUUID();
1020
- const hwaccel = this.resolveHwAccelPref();
1221
+ const hwaccel = await this.resolveHwAccelPref();
1021
1222
  const { frameSink } = config;
1022
1223
  const nodeId = this.resolveLocalNodeId();
1023
1224
  const session = new NodeAvDecoderSession(config, this.ctx.logger, {
@@ -1117,8 +1318,9 @@ var DecoderNodeAvAddon = class extends BaseAddon {
1117
1318
  });
1118
1319
  }
1119
1320
  async openStream(input) {
1120
- if (!this.sessions.get(input.sessionId)) throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
1121
- input.url;
1321
+ const session = this.sessions.get(input.sessionId);
1322
+ if (!session) throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
1323
+ await session.openStream(input.url);
1122
1324
  }
1123
1325
  async pullFrames(input) {
1124
1326
  if (!this.sessions.has(input.sessionId)) throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-DxM2DSns.js");
6
- const require_dist = require("../dist-Cwc0TUQr.js");
6
+ const require_dist = require("../dist-BrIE85BG.js");
7
7
  const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
8
8
  let _camstack_shm_ring = require("@camstack/shm-ring");
9
9
  let node_fs = require("node:fs");
@@ -185,6 +185,35 @@ function pickBestRuntime(env, bestBackendHint) {
185
185
  device: defaultDeviceFor(chosen)
186
186
  };
187
187
  }
188
+ /**
189
+ * The ordered device candidates a runtime will try for an `'auto'`/`'all'`
190
+ * device pick — mirrors what the python inference pool actually resolves
191
+ * (openvino `_resolve_ov_devices`: NPU>GPU>CPU; coreml compute-units `all`:
192
+ * ANE>GPU>CPU). Surfaced in `probedBestEngine` so the UI shows the real
193
+ * accelerator order (`openvino/auto [NPU, GPU, CPU]`) instead of a bare `auto`
194
+ * tag. Returns `[]` for runtimes with a single fixed device (onnx → CPU) — the
195
+ * caller then keeps the plain `backend/device` label.
196
+ */
197
+ function engineDeviceSequence(runtimeId, env) {
198
+ const hw = env.hardware;
199
+ switch (runtimeId) {
200
+ case "openvino": {
201
+ const seq = [];
202
+ if (hw?.npu?.type === "intel-npu") seq.push("NPU");
203
+ if (hw?.gpu?.type === "intel") seq.push("GPU");
204
+ seq.push("CPU");
205
+ return seq;
206
+ }
207
+ case "coreml": {
208
+ const seq = [];
209
+ if (hw?.npu?.type === "apple-ane") seq.push("ANE");
210
+ if (hw?.gpu != null) seq.push("GPU");
211
+ seq.push("CPU");
212
+ return seq;
213
+ }
214
+ default: return [];
215
+ }
216
+ }
188
217
  //#endregion
189
218
  //#region src/detection-pipeline/engine-store-keys.ts
190
219
  /**
@@ -6423,14 +6452,16 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6423
6452
  try {
6424
6453
  hardware = await this.addonCtx?.kernel?.inferenceEngine?.resolveHardware() ?? null;
6425
6454
  } catch {}
6426
- const pick = pickBestRuntime(runtimeEnvFromProcess(hardware ? toProbedHardware(hardware) : null), null);
6455
+ const env = runtimeEnvFromProcess(hardware ? toProbedHardware(hardware) : null);
6456
+ const pick = pickBestRuntime(env, null);
6427
6457
  const best = {
6428
6458
  runtime: "python",
6429
6459
  backend: pick.runtimeId,
6430
6460
  format: modelFormatFor(pick.runtimeId),
6431
6461
  device: pick.device
6432
6462
  };
6433
- const probedLabel = `${best.backend}/${best.device ?? "default"}`;
6463
+ const deviceSeq = engineDeviceSequence(pick.runtimeId, env);
6464
+ const probedLabel = deviceSeq.length > 1 ? `${best.backend}/${best.device ?? "default"} [${deviceSeq.join(", ")}]` : `${best.backend}/${best.device ?? "default"}`;
6434
6465
  const rpNode = this.localProbeNodeId();
6435
6466
  if (pick.runtimeId !== "onnx" || hardware !== null) {
6436
6467
  await this.writeStore({ [nodeEngineKey("probedBestEngine", rpNode)]: probedLabel });