@camstack/addon-pipeline 1.1.32 → 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 +71 -6
  6. package/dist/decoder-ffmpeg/index.mjs +71 -6
  7. package/dist/decoder-nodeav/index.js +209 -9
  8. package/dist/decoder-nodeav/index.mjs +209 -9
  9. package/dist/detection-pipeline/index.js +1 -1
  10. package/dist/detection-pipeline/index.mjs +1 -1
  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-DvdhCFr3.js → frame-ring-sink-B-DxHelU.js} +74 -25
  18. package/dist/{frame-ring-sink-sY-8XUsH.mjs → frame-ring-sink-B_WjDX0x.mjs} +69 -26
  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-DvdhCFr3.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
@@ -1005,7 +1204,7 @@ var DecoderNodeAvAddon = class extends require_dist.BaseAddon {
1005
1204
  return {
1006
1205
  id: "decoder-nodeav",
1007
1206
  name: "Decoder (node-av)",
1008
- isPullMode: false,
1207
+ isPullMode: true,
1009
1208
  priority: 10
1010
1209
  };
1011
1210
  }
@@ -1123,8 +1322,9 @@ var DecoderNodeAvAddon = class extends require_dist.BaseAddon {
1123
1322
  });
1124
1323
  }
1125
1324
  async openStream(input) {
1126
- if (!this.sessions.get(input.sessionId)) throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
1127
- 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);
1128
1328
  }
1129
1329
  async pullFrames(input) {
1130
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, o as resolveDecoderHwAccel, r as makeSegmentName, t as DecoderFrameRingSink } from "../frame-ring-sink-sY-8XUsH.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
@@ -1001,7 +1200,7 @@ var DecoderNodeAvAddon = class extends BaseAddon {
1001
1200
  return {
1002
1201
  id: "decoder-nodeav",
1003
1202
  name: "Decoder (node-av)",
1004
- isPullMode: false,
1203
+ isPullMode: true,
1005
1204
  priority: 10
1006
1205
  };
1007
1206
  }
@@ -1119,8 +1318,9 @@ var DecoderNodeAvAddon = class extends BaseAddon {
1119
1318
  });
1120
1319
  }
1121
1320
  async openStream(input) {
1122
- if (!this.sessions.get(input.sessionId)) throw new Error(`decoder-nodeav: unknown sessionId ${input.sessionId}`);
1123
- 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);
1124
1324
  }
1125
1325
  async pullFrames(input) {
1126
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");
@@ -1,5 +1,5 @@
1
1
  import { t as __require } from "../chunk-BdkLduGY.mjs";
2
- import { $ as sleep, B as errMsg, E as hfModelUrl, F as runtimeDevices$1, G as EventCategory, K as createEvent, M as pipelineExecutorCapability, R as supportedRuntimes$1, S as defaultDeviceFor$1, T as evaluateZoneRules, V as BaseAddon, X as parseJsonUnknown, a as COCO_TO_MACRO, i as COCO_80_LABELS, m as YAMNET_TO_MACRO, q as hydrateSchema, r as AUDIO_MACRO_LABELS, t as APPLE_SA_TO_MACRO, w as detectionPipelineCapability } from "../dist-DjuGmyG9.mjs";
2
+ import { $ as sleep, B as errMsg, E as hfModelUrl, F as runtimeDevices$1, G as EventCategory, K as createEvent, M as pipelineExecutorCapability, R as supportedRuntimes$1, S as defaultDeviceFor$1, T as evaluateZoneRules, V as BaseAddon, X as parseJsonUnknown, a as COCO_TO_MACRO, i as COCO_80_LABELS, m as YAMNET_TO_MACRO, q as hydrateSchema, r as AUDIO_MACRO_LABELS, t as APPLE_SA_TO_MACRO, w as detectionPipelineCapability } from "../dist-CfSORkv2.mjs";
3
3
  import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
4
4
  import { a as isModelDownloaded, i as ensureModel, n as deleteModelFromDisk } from "../model-download-service-C-IHWnXx-BPy6aoAx.mjs";
5
5
  import { FrameRingReaderCache } from "@camstack/shm-ring";
@@ -16328,7 +16328,17 @@ var AgentAddonConfigSchema = object({
16328
16328
  });
16329
16329
  var AgentPipelineSettingsSchema = object({
16330
16330
  addonDefaults: record(string(), AgentAddonConfigSchema).readonly(),
16331
- maxCameras: number().int().nonnegative().nullable().default(null)
16331
+ maxCameras: number().int().nonnegative().nullable().default(null),
16332
+ /** Per-node detection weight (relative share for the quota balancer). */
16333
+ detectWeight: number().positive().optional(),
16334
+ /** Node is eligible to run the detection pipeline (decode + inference). */
16335
+ detect: boolean().optional(),
16336
+ /** Node is eligible to host decoder sessions. */
16337
+ decode: boolean().optional(),
16338
+ /** Node is eligible to run audio-analyzer sessions. */
16339
+ audio: boolean().optional(),
16340
+ /** Node is eligible to be the ingest / source-owner (serve the restream). */
16341
+ ingest: boolean().optional()
16332
16342
  });
16333
16343
  var CameraPipelineForAgentSchema = object({
16334
16344
  steps: array(PipelineStepInputSchema).readonly(),
@@ -16634,6 +16644,21 @@ method(object({
16634
16644
  }), object({ success: literal(true) }), {
16635
16645
  kind: "mutation",
16636
16646
  auth: "admin"
16647
+ }), method(object({
16648
+ agentNodeId: string(),
16649
+ detectWeight: number().positive().nullable()
16650
+ }), object({ success: literal(true) }), {
16651
+ kind: "mutation",
16652
+ auth: "admin"
16653
+ }), method(object({
16654
+ agentNodeId: string(),
16655
+ detect: boolean().nullable().optional(),
16656
+ decode: boolean().nullable().optional(),
16657
+ audio: boolean().nullable().optional(),
16658
+ ingest: boolean().nullable().optional()
16659
+ }), object({ success: literal(true) }), {
16660
+ kind: "mutation",
16661
+ auth: "admin"
16637
16662
  }), method(object({ deviceId: number() }), CameraPipelineSettingsSchema.nullable()), method(object({
16638
16663
  deviceId: number(),
16639
16664
  addonId: string(),
@@ -22621,6 +22646,18 @@ Object.freeze({
22621
22646
  addonId: null,
22622
22647
  access: "create"
22623
22648
  },
22649
+ "pipelineOrchestrator.setAgentCapabilities": {
22650
+ capName: "pipeline-orchestrator",
22651
+ capScope: "system",
22652
+ addonId: null,
22653
+ access: "create"
22654
+ },
22655
+ "pipelineOrchestrator.setAgentDetectWeight": {
22656
+ capName: "pipeline-orchestrator",
22657
+ capScope: "system",
22658
+ addonId: null,
22659
+ access: "create"
22660
+ },
22624
22661
  "pipelineOrchestrator.setAgentMaxCameras": {
22625
22662
  capName: "pipeline-orchestrator",
22626
22663
  capScope: "system",