@camstack/addon-pipeline 1.1.23 → 1.1.25

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 (41) 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 +1 -1
  4. package/dist/audio-codec-ffmpeg/index.mjs +1 -1
  5. package/dist/decoder-ffmpeg/index.js +84 -12
  6. package/dist/decoder-ffmpeg/index.mjs +84 -12
  7. package/dist/detection-pipeline/index.js +2 -2
  8. package/dist/detection-pipeline/index.mjs +2 -2
  9. package/dist/{dist-pd_-3C0T.mjs → dist-BecXbIzJ.mjs} +64 -2
  10. package/dist/{dist-CE3a05qT.js → dist-wzWBZ26C.js} +69 -1
  11. package/dist/frame-handle-plane-BAKiW6t4.js +647 -0
  12. package/dist/frame-handle-plane-DGg0Aevs.mjs +636 -0
  13. package/dist/hub-hostname-DAJXlOgV.js +54 -0
  14. package/dist/hub-hostname-cCknRYKj.mjs +49 -0
  15. package/dist/{model-download-service-C-IHWnXx-BnQ_awK4.js → model-download-service-C-IHWnXx-DxM2DSns.js} +1 -2
  16. package/dist/motion-wasm/index.js +1 -1
  17. package/dist/motion-wasm/index.mjs +1 -1
  18. package/dist/pipeline-runner/index.js +305 -10
  19. package/dist/pipeline-runner/index.mjs +305 -10
  20. package/dist/recorder/index.js +267 -65
  21. package/dist/recorder/index.mjs +267 -65
  22. package/dist/stream-broker/_stub.js +39 -39
  23. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BVFAEkIB.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CWPU9tbs.mjs} +2 -2
  24. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-wfWYFiTT.mjs +26 -0
  25. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-lpFwIH9e.mjs +26 -0
  26. package/dist/stream-broker/{hostInit-DHej3Ep9.mjs → hostInit-B7Fx3R_6.mjs} +2 -2
  27. package/dist/stream-broker/index.js +7 -611
  28. package/dist/stream-broker/index.mjs +5 -609
  29. package/dist/stream-broker/remoteEntry.js +1 -1
  30. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-BqXU75it.js → MaskShapeCanvas-DI4BY7W2-CPkQfKnu.js} +1 -1
  31. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-Ct2fpgCd.js → MotionZonesSettings-NcxxQN8r-DlLMQBag.js} +1 -1
  32. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-a9eFVPPl.js → PrivacyMaskSettings-APgPLF7p-kNcauIAN.js} +1 -1
  33. package/embed-dist/assets/index-DrJ0ee3f.css +2 -0
  34. package/embed-dist/assets/index-xpPLFfsT.js +80 -0
  35. package/embed-dist/index.html +2 -2
  36. package/package.json +1 -1
  37. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BFeBlYRQ.mjs +0 -26
  38. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BC1Jt6G8.mjs +0 -26
  39. package/embed-dist/assets/index-C7SSikPl.css +0 -2
  40. package/embed-dist/assets/index-DhdVxWXL.js +0 -80
  41. package/dist/{model-download-service-C-IHWnXx-3Mmeob3l.mjs → model-download-service-C-IHWnXx-BPy6aoAx.mjs} +1 -1
@@ -2,8 +2,8 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-BnQ_awK4.js");
6
- const require_dist = require("../dist-CE3a05qT.js");
5
+ const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-DxM2DSns.js");
6
+ const require_dist = require("../dist-wzWBZ26C.js");
7
7
  let node_fs = require("node:fs");
8
8
  node_fs = require_model_download_service_C_IHWnXx.__toESM(node_fs);
9
9
  let node_path = require("node:path");
@@ -1,6 +1,6 @@
1
1
  import { t as __require } from "../chunk-BdkLduGY.mjs";
2
- import { B as BaseAddon, E as mapAudioLabelToMacro, J as hydrateSchema, g as audioAnalyzerCapability, h as audioAnalysisCapability, l as HF_BASE_URL, n as AUDIO_BACKEND_CHOICES, o as DEFAULT_AUDIO_ANALYZER_CONFIG, z as errMsg } from "../dist-pd_-3C0T.mjs";
3
- import { r as downloadFile } from "../model-download-service-C-IHWnXx-3Mmeob3l.mjs";
2
+ import { B as errMsg, D as mapAudioLabelToMacro, V as BaseAddon, Y as hydrateSchema, _ as audioAnalyzerCapability, g as audioAnalysisCapability, n as AUDIO_BACKEND_CHOICES, o as DEFAULT_AUDIO_ANALYZER_CONFIG, u as HF_BASE_URL } from "../dist-BecXbIzJ.mjs";
3
+ import { r as downloadFile } from "../model-download-service-C-IHWnXx-BPy6aoAx.mjs";
4
4
  import * as fs from "node:fs";
5
5
  import * as path$1 from "node:path";
6
6
  //#region src/audio-analyzer/audio-pipeline.ts
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-CE3a05qT.js");
5
+ const require_dist = require("../dist-wzWBZ26C.js");
6
6
  let node_crypto = require("node:crypto");
7
7
  let node_child_process = require("node:child_process");
8
8
  //#region src/audio-codec-ffmpeg/ffmpeg-audio-process.ts
@@ -1,4 +1,4 @@
1
- import { B as BaseAddon, _ as audioCodecCapability, z as errMsg } from "../dist-pd_-3C0T.mjs";
1
+ import { B as errMsg, V as BaseAddon, v as audioCodecCapability } from "../dist-BecXbIzJ.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { spawn } from "node:child_process";
4
4
  //#region src/audio-codec-ffmpeg/ffmpeg-audio-process.ts
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-CE3a05qT.js");
5
+ const require_dist = require("../dist-wzWBZ26C.js");
6
6
  const require_frame_dropper = require("../frame-dropper-7RTo_YyG.js");
7
7
  let _camstack_shm_ring = require("@camstack/shm-ring");
8
8
  let node_crypto = require("node:crypto");
@@ -145,8 +145,10 @@ function buildVideoFilter(hwaccel, pixel, outWidth, outHeight, maxW, gpuScaleFil
145
145
  * stream line from stderr to learn the exact output W×H (the broker does not
146
146
  * pass source geometry for the shm path). `-nostats` suppresses the periodic
147
147
  * `frame= …` counter while keeping the one-shot Output block.
148
- * - The input is `pipe:0` (Annex-B packets written to stdin); the output is raw
149
- * video on `pipe:1`.
148
+ * - The input is `pipe:0` (Annex-B packets written to stdin) in push mode, or
149
+ * `options.inputUrl` (the owner's restream, dialed by ffmpeg itself —
150
+ * `-rtsp_transport tcp` for rtsp URLs) in pull mode; the output is raw video
151
+ * on `pipe:1` either way.
150
152
  */
151
153
  function buildFfmpegDecodeArgs(config, options) {
152
154
  const inputFormat = codecToInputFormat(config.codec);
@@ -162,6 +164,16 @@ function buildFfmpegDecodeArgs(config, options) {
162
164
  if (needsHwDownload(hwaccel)) hwaccelArgs.push("-hwaccel_device", options.renderDevice ?? "/dev/dri/renderD128");
163
165
  if (keepFramesOnGpu(hwaccel, gpuScaleFilter)) hwaccelArgs.push("-hwaccel_output_format", hwaccel);
164
166
  }
167
+ const inputArgs = options.inputUrl !== void 0 ? [
168
+ ...options.inputUrl.startsWith("rtsp") ? ["-rtsp_transport", "tcp"] : [],
169
+ "-i",
170
+ options.inputUrl
171
+ ] : [
172
+ "-f",
173
+ inputFormat,
174
+ "-i",
175
+ "pipe:0"
176
+ ];
165
177
  return [
166
178
  ...require_frame_dropper.logBannerArgs("info"),
167
179
  "-nostats",
@@ -174,10 +186,7 @@ function buildFfmpegDecodeArgs(config, options) {
174
186
  "-analyzeduration",
175
187
  "0",
176
188
  ...hwaccelArgs,
177
- "-f",
178
- inputFormat,
179
- "-i",
180
- "pipe:0",
189
+ ...inputArgs,
181
190
  "-vf",
182
191
  vf,
183
192
  "-f",
@@ -650,6 +659,16 @@ var noopLogger = {
650
659
  * hwaccel init failure (missing render node, unsupported codec on GPU, the
651
660
  * Intel `qsv` `code=171`) surfaces this early. */
652
661
  var HWACCEL_FALLBACK_WINDOW_MS = 4e3;
662
+ /**
663
+ * Delay before a PULL-MODE session re-dials its input URL after the child
664
+ * exits (RTSP EOF, restream teardown, network blip). Push mode never
665
+ * self-respawns on a plain exit — the broker's packet feed drives recovery —
666
+ * but a pull-mode child owns its own input, so the session must re-dial or a
667
+ * single EOF would silence it forever. The delay turns a hard-down source
668
+ * into a slow probe instead of a spawn storm; the consumer tearing the
669
+ * session down (`destroy`) cancels the pending re-dial.
670
+ */
671
+ var PULL_REDIAL_DELAY_MS = 3e3;
653
672
  var FfmpegDecoderSession = class {
654
673
  config;
655
674
  logger;
@@ -680,6 +699,15 @@ var FfmpegDecoderSession = class {
680
699
  process = null;
681
700
  spawnedAtMs = 0;
682
701
  /**
702
+ * PULL-MODE input URL (P2b). `null` = push mode (packets via stdin — the
703
+ * default). Set by {@link openStream}; every (re)spawn since reads this URL
704
+ * directly instead of `pipe:0`. A session is push-mode or pull-mode for its
705
+ * whole life — `openStream` on a packet-fed session throws.
706
+ */
707
+ pullUrl = null;
708
+ /** Pending pull-mode re-dial timer (armed on a pull child exit). */
709
+ pullRedialTimer = null;
710
+ /**
683
711
  * Splits ffmpeg's raw stdout byte stream into frames, filling each DIRECTLY
684
712
  * into its shm slot (zero-copy) — created once the output geometry is known.
685
713
  */
@@ -733,7 +761,25 @@ var FfmpegDecoderSession = class {
733
761
  return this.frameRingSink;
734
762
  }
735
763
  get isPullMode() {
736
- return false;
764
+ return this.pullUrl !== null;
765
+ }
766
+ /**
767
+ * Switch the session to PULL MODE: (re)spawn ffmpeg reading `url` directly
768
+ * (`-rtsp_transport tcp -i <url>` for rtsp) instead of stdin. The whole
769
+ * output path (filtergraph, rawvideo pipe, shm ring sink) is unchanged.
770
+ *
771
+ * - Calling it again re-dials with the FRESH url (a hub restream bounce
772
+ * mints a new token URL — never cache the old one).
773
+ * - Calling it on a session already fed via `pushPacket` throws: a session
774
+ * is push-mode or pull-mode, never both (mixing inputs would corrupt the
775
+ * decode).
776
+ */
777
+ async openStream(url) {
778
+ if (this.destroyed) return;
779
+ if (this.pullUrl === null && this.inputPackets > 0) throw new Error("ffmpeg decoder: openStream on a push-fed session — a session is push-mode or pull-mode, never both");
780
+ this.pullUrl = url;
781
+ this.logger.info("ffmpeg decoder: pull-mode openStream — dialing input URL", { meta: { url } });
782
+ this.spawnFfmpeg();
737
783
  }
738
784
  ensureFrameRingSink() {
739
785
  if (this.frameRingSink === null) {
@@ -751,6 +797,7 @@ var FfmpegDecoderSession = class {
751
797
  }
752
798
  spawnFfmpeg() {
753
799
  if (this.destroyed) return;
800
+ this.clearPullRedial();
754
801
  this.killFfmpeg();
755
802
  this.abortOpenSlot();
756
803
  this.splitter = null;
@@ -767,7 +814,8 @@ var FfmpegDecoderSession = class {
767
814
  }, {
768
815
  hwaccel: this.hwaccel,
769
816
  renderDevice: this.renderDevice,
770
- gpuScaleFilter: this.activeGpuScaleFilter
817
+ gpuScaleFilter: this.activeGpuScaleFilter,
818
+ ...this.pullUrl !== null ? { inputUrl: this.pullUrl } : {}
771
819
  });
772
820
  let child;
773
821
  try {
@@ -854,6 +902,27 @@ var FfmpegDecoderSession = class {
854
902
  signal,
855
903
  outputFrames: this.outputFrames
856
904
  } });
905
+ if (this.pullUrl !== null) {
906
+ this.clearPullRedial();
907
+ const timer = setTimeout(() => {
908
+ this.pullRedialTimer = null;
909
+ this.spawnFfmpeg();
910
+ }, PULL_REDIAL_DELAY_MS);
911
+ timer.unref?.();
912
+ this.pullRedialTimer = timer;
913
+ this.logger.info("ffmpeg decoder: pull-mode child exited — re-dial scheduled", { meta: {
914
+ code,
915
+ signal,
916
+ redialInMs: PULL_REDIAL_DELAY_MS
917
+ } });
918
+ }
919
+ }
920
+ /** Cancel a pending pull-mode re-dial, if armed. */
921
+ clearPullRedial() {
922
+ if (this.pullRedialTimer !== null) {
923
+ clearTimeout(this.pullRedialTimer);
924
+ this.pullRedialTimer = null;
925
+ }
857
926
  }
858
927
  handleStderr(data) {
859
928
  const text = data.toString();
@@ -958,6 +1027,7 @@ var FfmpegDecoderSession = class {
958
1027
  }
959
1028
  pushPacket(packet) {
960
1029
  if (this.destroyed) return;
1030
+ if (this.pullUrl !== null) return;
961
1031
  const stdin = this.process?.stdin;
962
1032
  if (!stdin) return;
963
1033
  this.inputPackets++;
@@ -990,6 +1060,7 @@ var FfmpegDecoderSession = class {
990
1060
  async destroy() {
991
1061
  if (this.destroyed) return;
992
1062
  this.destroyed = true;
1063
+ this.clearPullRedial();
993
1064
  this.killFfmpeg();
994
1065
  this.abortOpenSlot();
995
1066
  this.splitter = null;
@@ -1235,7 +1306,7 @@ var DecoderFfmpegAddon = class extends require_dist.BaseAddon {
1235
1306
  return {
1236
1307
  id: "decoder-ffmpeg",
1237
1308
  name: "Decoder (ffmpeg)",
1238
- isPullMode: false,
1309
+ isPullMode: true,
1239
1310
  priority: 20
1240
1311
  };
1241
1312
  }
@@ -1346,8 +1417,9 @@ var DecoderFfmpegAddon = class extends require_dist.BaseAddon {
1346
1417
  });
1347
1418
  }
1348
1419
  async openStream(input) {
1349
- if (!this.sessions.get(input.sessionId)) throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`);
1350
- input.url;
1420
+ const session = this.sessions.get(input.sessionId);
1421
+ if (!session) throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`);
1422
+ await session.openStream(input.url);
1351
1423
  }
1352
1424
  async pullFrames(input) {
1353
1425
  if (!this.sessions.has(input.sessionId)) throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`);
@@ -1,4 +1,4 @@
1
- import { B as BaseAddon, b as decoderCapability, f as RingBuffer, s as DEFAULT_DECODER_HWACCEL_CONFIG, u as HWACCEL_OPTIONS, z as errMsg } from "../dist-pd_-3C0T.mjs";
1
+ import { B as errMsg, V as BaseAddon, d as HWACCEL_OPTIONS, p as RingBuffer, s as DEFAULT_DECODER_HWACCEL_CONFIG, x as decoderCapability } from "../dist-BecXbIzJ.mjs";
2
2
  import { r as logBannerArgs, t as FrameDropper } from "../frame-dropper-CwkBTPGV.mjs";
3
3
  import { FrameRingReaderCache, FrameRingWriter, MIN_RING_SLOTS, computeSegmentSize, computeSlotByteLength, createSegment, deriveSlotCount } from "@camstack/shm-ring";
4
4
  import { randomUUID } from "node:crypto";
@@ -141,8 +141,10 @@ function buildVideoFilter(hwaccel, pixel, outWidth, outHeight, maxW, gpuScaleFil
141
141
  * stream line from stderr to learn the exact output W×H (the broker does not
142
142
  * pass source geometry for the shm path). `-nostats` suppresses the periodic
143
143
  * `frame= …` counter while keeping the one-shot Output block.
144
- * - The input is `pipe:0` (Annex-B packets written to stdin); the output is raw
145
- * video on `pipe:1`.
144
+ * - The input is `pipe:0` (Annex-B packets written to stdin) in push mode, or
145
+ * `options.inputUrl` (the owner's restream, dialed by ffmpeg itself —
146
+ * `-rtsp_transport tcp` for rtsp URLs) in pull mode; the output is raw video
147
+ * on `pipe:1` either way.
146
148
  */
147
149
  function buildFfmpegDecodeArgs(config, options) {
148
150
  const inputFormat = codecToInputFormat(config.codec);
@@ -158,6 +160,16 @@ function buildFfmpegDecodeArgs(config, options) {
158
160
  if (needsHwDownload(hwaccel)) hwaccelArgs.push("-hwaccel_device", options.renderDevice ?? "/dev/dri/renderD128");
159
161
  if (keepFramesOnGpu(hwaccel, gpuScaleFilter)) hwaccelArgs.push("-hwaccel_output_format", hwaccel);
160
162
  }
163
+ const inputArgs = options.inputUrl !== void 0 ? [
164
+ ...options.inputUrl.startsWith("rtsp") ? ["-rtsp_transport", "tcp"] : [],
165
+ "-i",
166
+ options.inputUrl
167
+ ] : [
168
+ "-f",
169
+ inputFormat,
170
+ "-i",
171
+ "pipe:0"
172
+ ];
161
173
  return [
162
174
  ...logBannerArgs("info"),
163
175
  "-nostats",
@@ -170,10 +182,7 @@ function buildFfmpegDecodeArgs(config, options) {
170
182
  "-analyzeduration",
171
183
  "0",
172
184
  ...hwaccelArgs,
173
- "-f",
174
- inputFormat,
175
- "-i",
176
- "pipe:0",
185
+ ...inputArgs,
177
186
  "-vf",
178
187
  vf,
179
188
  "-f",
@@ -646,6 +655,16 @@ var noopLogger = {
646
655
  * hwaccel init failure (missing render node, unsupported codec on GPU, the
647
656
  * Intel `qsv` `code=171`) surfaces this early. */
648
657
  var HWACCEL_FALLBACK_WINDOW_MS = 4e3;
658
+ /**
659
+ * Delay before a PULL-MODE session re-dials its input URL after the child
660
+ * exits (RTSP EOF, restream teardown, network blip). Push mode never
661
+ * self-respawns on a plain exit — the broker's packet feed drives recovery —
662
+ * but a pull-mode child owns its own input, so the session must re-dial or a
663
+ * single EOF would silence it forever. The delay turns a hard-down source
664
+ * into a slow probe instead of a spawn storm; the consumer tearing the
665
+ * session down (`destroy`) cancels the pending re-dial.
666
+ */
667
+ var PULL_REDIAL_DELAY_MS = 3e3;
649
668
  var FfmpegDecoderSession = class {
650
669
  config;
651
670
  logger;
@@ -676,6 +695,15 @@ var FfmpegDecoderSession = class {
676
695
  process = null;
677
696
  spawnedAtMs = 0;
678
697
  /**
698
+ * PULL-MODE input URL (P2b). `null` = push mode (packets via stdin — the
699
+ * default). Set by {@link openStream}; every (re)spawn since reads this URL
700
+ * directly instead of `pipe:0`. A session is push-mode or pull-mode for its
701
+ * whole life — `openStream` on a packet-fed session throws.
702
+ */
703
+ pullUrl = null;
704
+ /** Pending pull-mode re-dial timer (armed on a pull child exit). */
705
+ pullRedialTimer = null;
706
+ /**
679
707
  * Splits ffmpeg's raw stdout byte stream into frames, filling each DIRECTLY
680
708
  * into its shm slot (zero-copy) — created once the output geometry is known.
681
709
  */
@@ -729,7 +757,25 @@ var FfmpegDecoderSession = class {
729
757
  return this.frameRingSink;
730
758
  }
731
759
  get isPullMode() {
732
- return false;
760
+ return this.pullUrl !== null;
761
+ }
762
+ /**
763
+ * Switch the session to PULL MODE: (re)spawn ffmpeg reading `url` directly
764
+ * (`-rtsp_transport tcp -i <url>` for rtsp) instead of stdin. The whole
765
+ * output path (filtergraph, rawvideo pipe, shm ring sink) is unchanged.
766
+ *
767
+ * - Calling it again re-dials with the FRESH url (a hub restream bounce
768
+ * mints a new token URL — never cache the old one).
769
+ * - Calling it on a session already fed via `pushPacket` throws: a session
770
+ * is push-mode or pull-mode, never both (mixing inputs would corrupt the
771
+ * decode).
772
+ */
773
+ async openStream(url) {
774
+ if (this.destroyed) return;
775
+ if (this.pullUrl === null && this.inputPackets > 0) throw new Error("ffmpeg decoder: openStream on a push-fed session — a session is push-mode or pull-mode, never both");
776
+ this.pullUrl = url;
777
+ this.logger.info("ffmpeg decoder: pull-mode openStream — dialing input URL", { meta: { url } });
778
+ this.spawnFfmpeg();
733
779
  }
734
780
  ensureFrameRingSink() {
735
781
  if (this.frameRingSink === null) {
@@ -747,6 +793,7 @@ var FfmpegDecoderSession = class {
747
793
  }
748
794
  spawnFfmpeg() {
749
795
  if (this.destroyed) return;
796
+ this.clearPullRedial();
750
797
  this.killFfmpeg();
751
798
  this.abortOpenSlot();
752
799
  this.splitter = null;
@@ -763,7 +810,8 @@ var FfmpegDecoderSession = class {
763
810
  }, {
764
811
  hwaccel: this.hwaccel,
765
812
  renderDevice: this.renderDevice,
766
- gpuScaleFilter: this.activeGpuScaleFilter
813
+ gpuScaleFilter: this.activeGpuScaleFilter,
814
+ ...this.pullUrl !== null ? { inputUrl: this.pullUrl } : {}
767
815
  });
768
816
  let child;
769
817
  try {
@@ -850,6 +898,27 @@ var FfmpegDecoderSession = class {
850
898
  signal,
851
899
  outputFrames: this.outputFrames
852
900
  } });
901
+ if (this.pullUrl !== null) {
902
+ this.clearPullRedial();
903
+ const timer = setTimeout(() => {
904
+ this.pullRedialTimer = null;
905
+ this.spawnFfmpeg();
906
+ }, PULL_REDIAL_DELAY_MS);
907
+ timer.unref?.();
908
+ this.pullRedialTimer = timer;
909
+ this.logger.info("ffmpeg decoder: pull-mode child exited — re-dial scheduled", { meta: {
910
+ code,
911
+ signal,
912
+ redialInMs: PULL_REDIAL_DELAY_MS
913
+ } });
914
+ }
915
+ }
916
+ /** Cancel a pending pull-mode re-dial, if armed. */
917
+ clearPullRedial() {
918
+ if (this.pullRedialTimer !== null) {
919
+ clearTimeout(this.pullRedialTimer);
920
+ this.pullRedialTimer = null;
921
+ }
853
922
  }
854
923
  handleStderr(data) {
855
924
  const text = data.toString();
@@ -954,6 +1023,7 @@ var FfmpegDecoderSession = class {
954
1023
  }
955
1024
  pushPacket(packet) {
956
1025
  if (this.destroyed) return;
1026
+ if (this.pullUrl !== null) return;
957
1027
  const stdin = this.process?.stdin;
958
1028
  if (!stdin) return;
959
1029
  this.inputPackets++;
@@ -986,6 +1056,7 @@ var FfmpegDecoderSession = class {
986
1056
  async destroy() {
987
1057
  if (this.destroyed) return;
988
1058
  this.destroyed = true;
1059
+ this.clearPullRedial();
989
1060
  this.killFfmpeg();
990
1061
  this.abortOpenSlot();
991
1062
  this.splitter = null;
@@ -1231,7 +1302,7 @@ var DecoderFfmpegAddon = class extends BaseAddon {
1231
1302
  return {
1232
1303
  id: "decoder-ffmpeg",
1233
1304
  name: "Decoder (ffmpeg)",
1234
- isPullMode: false,
1305
+ isPullMode: true,
1235
1306
  priority: 20
1236
1307
  };
1237
1308
  }
@@ -1342,8 +1413,9 @@ var DecoderFfmpegAddon = class extends BaseAddon {
1342
1413
  });
1343
1414
  }
1344
1415
  async openStream(input) {
1345
- if (!this.sessions.get(input.sessionId)) throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`);
1346
- input.url;
1416
+ const session = this.sessions.get(input.sessionId);
1417
+ if (!session) throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`);
1418
+ await session.openStream(input.url);
1347
1419
  }
1348
1420
  async pullFrames(input) {
1349
1421
  if (!this.sessions.has(input.sessionId)) throw new Error(`decoder-ffmpeg: unknown sessionId ${input.sessionId}`);
@@ -2,8 +2,8 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-BnQ_awK4.js");
6
- const require_dist = require("../dist-CE3a05qT.js");
5
+ const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-DxM2DSns.js");
6
+ const require_dist = require("../dist-wzWBZ26C.js");
7
7
  let node_fs = require("node:fs");
8
8
  node_fs = require_model_download_service_C_IHWnXx.__toESM(node_fs);
9
9
  let node_path = require("node:path");
@@ -1,6 +1,6 @@
1
1
  import { t as __require } from "../chunk-BdkLduGY.mjs";
2
- import { A as nodePin, B as BaseAddon, C as detectionPipelineCapability, J as hydrateSchema, L as supportedRuntimes$1, P as runtimeDevices$1, T as hfModelUrl, W as EventCategory, Z as parseJsonUnknown, a as COCO_TO_MACRO, et as sleep, i as COCO_80_LABELS, j as pipelineExecutorCapability, p as YAMNET_TO_MACRO, q as createEvent, r as AUDIO_MACRO_LABELS, t as APPLE_SA_TO_MACRO, w as evaluateZoneRules, x as defaultDeviceFor$1, z as errMsg } from "../dist-pd_-3C0T.mjs";
3
- import { a as isModelDownloaded, i as ensureModel, n as deleteModelFromDisk } from "../model-download-service-C-IHWnXx-3Mmeob3l.mjs";
2
+ import { B as errMsg, E as hfModelUrl, F as runtimeDevices$1, G as EventCategory, J as createEvent, M as pipelineExecutorCapability, Q as parseJsonUnknown, R as supportedRuntimes$1, S as defaultDeviceFor$1, T as evaluateZoneRules, V as BaseAddon, Y as hydrateSchema, a as COCO_TO_MACRO, i as COCO_80_LABELS, j as nodePin, m as YAMNET_TO_MACRO, r as AUDIO_MACRO_LABELS, t as APPLE_SA_TO_MACRO, tt as sleep, w as detectionPipelineCapability } from "../dist-BecXbIzJ.mjs";
3
+ import { a as isModelDownloaded, i as ensureModel, n as deleteModelFromDisk } from "../model-download-service-C-IHWnXx-BPy6aoAx.mjs";
4
4
  import * as fs from "node:fs";
5
5
  import * as path$1 from "node:path";
6
6
  import * as os from "node:os";
@@ -6923,6 +6923,25 @@ var ConvertResultSchema = object({
6923
6923
  })).readonly()
6924
6924
  });
6925
6925
  /**
6926
+ * THE canonical event-clip pad: the time window a single-timestamp analytics
6927
+ * event expands to when joined with footage (clip window = `[timestamp - preMs,
6928
+ * timestamp + postMs]`). Matches the admin-ui clip window (−5s/+10s).
6929
+ *
6930
+ * Every consumer derives from this ONE constant so event↔footage boundaries
6931
+ * agree everywhere (C1):
6932
+ * - the `videoclips` default provider (addon-post-analysis) pads its clip
6933
+ * windows with it;
6934
+ * - the recorder's ephemeral in-RAM `EventMap` markers (addon-pipeline) pad
6935
+ * their `startMs/endMs` with it.
6936
+ *
6937
+ * NOTE: this is a UI/JOIN convention, NOT the `events`-band keep/discard gate —
6938
+ * that uses the per-device `preBufferSec`/`postBufferSec` config.
6939
+ */
6940
+ var EVENT_PAD_MS = {
6941
+ preMs: 5e3,
6942
+ postMs: 1e4
6943
+ };
6944
+ /**
6926
6945
  * Numeric day-of-week: 0 = Sunday … 6 = Saturday (matches `Date.getDay`).
6927
6946
  * Named `RecordingWeekday` to avoid collision with the string-union
6928
6947
  * `Weekday` exported from `interfaces/timezones.ts`.
@@ -10813,6 +10832,31 @@ var ReportMotionInputSchema = object({
10813
10832
  regions: array(MotionRegionSchema).readonly().optional()
10814
10833
  });
10815
10834
  /**
10835
+ * Where a runner gets a camera's decoded frames (cross-node Phase 2,
10836
+ * restream-owner model — P2c).
10837
+ *
10838
+ * - `local-broker` (DEFAULT): today's path — subscribe to the co-located
10839
+ * stream-broker's shm frame plane. Every pre-P2c attach payload (no
10840
+ * `frameSource` key) parses to this, so the field is additive with zero
10841
+ * behavior change.
10842
+ * - `remote-restream`: the detect node is NOT the camera's source-owner.
10843
+ * The runner acquires the owner's COMPRESSED passthrough restream
10844
+ * (`streamBroker.getStreamWithCodec({video:'copy'})` — refcounted, the
10845
+ * double-pull guard) and decodes LOCALLY via a satellite frame plane +
10846
+ * pull-mode decoder session pinned to its own node. The shm ring stays
10847
+ * node-local; only H.264/H.265 packets cross the wire.
10848
+ * `hubHostnameOverride` mirrors the recorder's `recordingHubHostname`:
10849
+ * when set it overrides the `CAMSTACK_HUB_URL`-derived host the runner
10850
+ * dials for the owner's restream.
10851
+ */
10852
+ var RunnerFrameSourceSchema = discriminatedUnion("kind", [object({ kind: literal("local-broker") }), object({
10853
+ kind: literal("remote-restream"),
10854
+ /** The camera's source-owner node (slice 1: always the hub). */
10855
+ ownerNodeId: string(),
10856
+ /** Operator override for the owner host the runner dials. */
10857
+ hubHostnameOverride: string().optional()
10858
+ })]).describe("Per-camera frame-source mode for the runner (P2c)");
10859
+ /**
10816
10860
  * Camera assignment payload sent by `addon-pipeline-orchestrator` to a
10817
10861
  * specific runner instance via `attachCamera`. Carries everything the
10818
10862
  * runner needs to subscribe to the local broker and execute inference.
@@ -10910,7 +10954,15 @@ var RunnerCameraConfigSchema = object({
10910
10954
  */
10911
10955
  onboardMotionDrivesAnalyzer: boolean().default(true),
10912
10956
  occupancyRecheckSec: number().min(occupancyRecheckSecField.min).max(occupancyRecheckSecField.max).default(occupancyRecheckSecField.default),
10913
- occupancyRecheckFrames: number().min(occupancyRecheckFramesField.min).max(occupancyRecheckFramesField.max).default(occupancyRecheckFramesField.default)
10957
+ occupancyRecheckFrames: number().min(occupancyRecheckFramesField.min).max(occupancyRecheckFramesField.max).default(occupancyRecheckFramesField.default),
10958
+ /**
10959
+ * Where this runner gets the camera's decoded frames (P2c). Defaulted so
10960
+ * every existing payload behaves as `local-broker` — the pre-Phase-2 path.
10961
+ * Populated with `remote-restream` by the orchestrator ONLY when the
10962
+ * camera's detect node differs from its source-owner (P2d, gated by the
10963
+ * `remoteSourcingNodes` rollout setting).
10964
+ */
10965
+ frameSource: RunnerFrameSourceSchema.default({ kind: "local-broker" })
10914
10966
  });
10915
10967
  motionFpsField.min, motionFpsField.max, motionFpsField.step, motionFpsField.default, detectionFpsField.min, detectionFpsField.max, detectionFpsField.step, detectionFpsField.default, motionCooldownMsField.min, motionCooldownMsField.max, motionCooldownMsField.step, motionCooldownMsField.default, occupancyRecheckSecField.min, occupancyRecheckSecField.max, occupancyRecheckSecField.step, occupancyRecheckSecField.default, occupancyRecheckFramesField.min, occupancyRecheckFramesField.max, occupancyRecheckFramesField.step, occupancyRecheckFramesField.default;
10916
10968
  /**
@@ -18144,6 +18196,16 @@ DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, method(object({ deviceI
18144
18196
  kind: "mutation",
18145
18197
  auth: "admin"
18146
18198
  });
18199
+ /**
18200
+ * `recording` cap — footage availability + HLS playback manifests + per-device
18201
+ * recording config. NOTE on events (source of truth, R5/C3): this cap carries
18202
+ * NO event surface — `getPlaybackManifest` returns playlist URLs only. Timeline
18203
+ * events (motion/object/audio) come from `pipelineAnalytics` (durable SQLite
18204
+ * rows); the recorder's internal EventMap markers are ephemeral in-RAM
18205
+ * annotations that are not exposed here and must not be treated as an event
18206
+ * feed. Event<->footage joins are by time, padded with the shared `EVENT_PAD_MS`
18207
+ * (`interfaces/recording-config.ts`).
18208
+ */
18147
18209
  var RecordingStatusSchema = object({
18148
18210
  deviceId: number(),
18149
18211
  enabled: boolean(),
@@ -23203,4 +23265,4 @@ function defaultDeviceFor(id) {
23203
23265
  return def(id).defaultDevice;
23204
23266
  }
23205
23267
  //#endregion
23206
- export { selectAssignedProfileSlots as $, nodePin as A, BaseAddon as B, detectionPipelineCapability as C, maskUrlCredentials as D, mapAudioLabelToMacro as E, storageEvictableCapability as F, asJsonObject as G, DeviceFeature as H, streamBrokerCapability as I, hydrateSchema as J, createDurableState as K, supportedRuntimes as L, pipelineRunnerCapability as M, recordingCapability as N, migrateConfigToBands as O, runtimeDevices as P, parseProfileBrokerId as Q, webrtcSessionCapability as R, defineCustomActions as S, hfModelUrl as T, DeviceType as U, CAM_PROFILE_ORDER as V, EventCategory as W, makeSourceBrokerId as X, makeProfileBrokerId as Y, parseJsonUnknown as Z, audioCodecCapability as _, COCO_TO_MACRO as a, lazy as at, decoderCapability as b, EncodeProfileSchema as c, object as ct, RecordingConfigSchema as d, union as dt, sleep as et, RingBuffer as f, audioAnalyzerCapability as g, audioAnalysisCapability as h, COCO_80_LABELS as i, discriminatedUnion as it, pipelineExecutorCapability as j, motionDetectionCapability as k, HF_BASE_URL as l, record as lt, addonWidgetsSourceCapability as m, AUDIO_BACKEND_CHOICES as n, array as nt, DEFAULT_AUDIO_ANALYZER_CONFIG as o, literal as ot, YAMNET_TO_MACRO as p, createEvent as q, AUDIO_MACRO_LABELS as r, boolean as rt, DEFAULT_DECODER_HWACCEL_CONFIG as s, number as st, APPLE_SA_TO_MACRO as t, _enum as tt, HWACCEL_OPTIONS as u, string as ut, cameraStreamsCapability as v, evaluateZoneRules as w, defaultDeviceFor as x, customAction as y, errMsg as z };
23268
+ export { parseProfileBrokerId as $, motionDetectionCapability as A, errMsg as B, defineCustomActions as C, mapAudioLabelToMacro as D, hfModelUrl as E, runtimeDevices as F, EventCategory as G, CAM_PROFILE_ORDER as H, storageEvictableCapability as I, createEvent as J, asJsonObject as K, streamBrokerCapability as L, pipelineExecutorCapability as M, pipelineRunnerCapability as N, maskUrlCredentials as O, recordingCapability as P, parseJsonUnknown as Q, supportedRuntimes as R, defaultDeviceFor as S, evaluateZoneRules as T, DeviceFeature as U, BaseAddon as V, DeviceType as W, makeProfileBrokerId as X, hydrateSchema as Y, makeSourceBrokerId as Z, audioAnalyzerCapability as _, COCO_TO_MACRO as a, discriminatedUnion as at, customAction as b, EVENT_PAD_MS as c, number as ct, HWACCEL_OPTIONS as d, string as dt, selectAssignedProfileSlots as et, RecordingConfigSchema as f, union as ft, audioAnalysisCapability as g, addonWidgetsSourceCapability as h, COCO_80_LABELS as i, boolean as it, nodePin as j, migrateConfigToBands as k, EncodeProfileSchema as l, object as lt, YAMNET_TO_MACRO as m, AUDIO_BACKEND_CHOICES as n, _enum as nt, DEFAULT_AUDIO_ANALYZER_CONFIG as o, lazy as ot, RingBuffer as p, createDurableState as q, AUDIO_MACRO_LABELS as r, array as rt, DEFAULT_DECODER_HWACCEL_CONFIG as s, literal as st, APPLE_SA_TO_MACRO as t, sleep as tt, HF_BASE_URL as u, record as ut, audioCodecCapability as v, detectionPipelineCapability as w, decoderCapability as x, cameraStreamsCapability as y, webrtcSessionCapability as z };
@@ -6923,6 +6923,25 @@ var ConvertResultSchema = object({
6923
6923
  })).readonly()
6924
6924
  });
6925
6925
  /**
6926
+ * THE canonical event-clip pad: the time window a single-timestamp analytics
6927
+ * event expands to when joined with footage (clip window = `[timestamp - preMs,
6928
+ * timestamp + postMs]`). Matches the admin-ui clip window (−5s/+10s).
6929
+ *
6930
+ * Every consumer derives from this ONE constant so event↔footage boundaries
6931
+ * agree everywhere (C1):
6932
+ * - the `videoclips` default provider (addon-post-analysis) pads its clip
6933
+ * windows with it;
6934
+ * - the recorder's ephemeral in-RAM `EventMap` markers (addon-pipeline) pad
6935
+ * their `startMs/endMs` with it.
6936
+ *
6937
+ * NOTE: this is a UI/JOIN convention, NOT the `events`-band keep/discard gate —
6938
+ * that uses the per-device `preBufferSec`/`postBufferSec` config.
6939
+ */
6940
+ var EVENT_PAD_MS = {
6941
+ preMs: 5e3,
6942
+ postMs: 1e4
6943
+ };
6944
+ /**
6926
6945
  * Numeric day-of-week: 0 = Sunday … 6 = Saturday (matches `Date.getDay`).
6927
6946
  * Named `RecordingWeekday` to avoid collision with the string-union
6928
6947
  * `Weekday` exported from `interfaces/timezones.ts`.
@@ -10813,6 +10832,31 @@ var ReportMotionInputSchema = object({
10813
10832
  regions: array(MotionRegionSchema).readonly().optional()
10814
10833
  });
10815
10834
  /**
10835
+ * Where a runner gets a camera's decoded frames (cross-node Phase 2,
10836
+ * restream-owner model — P2c).
10837
+ *
10838
+ * - `local-broker` (DEFAULT): today's path — subscribe to the co-located
10839
+ * stream-broker's shm frame plane. Every pre-P2c attach payload (no
10840
+ * `frameSource` key) parses to this, so the field is additive with zero
10841
+ * behavior change.
10842
+ * - `remote-restream`: the detect node is NOT the camera's source-owner.
10843
+ * The runner acquires the owner's COMPRESSED passthrough restream
10844
+ * (`streamBroker.getStreamWithCodec({video:'copy'})` — refcounted, the
10845
+ * double-pull guard) and decodes LOCALLY via a satellite frame plane +
10846
+ * pull-mode decoder session pinned to its own node. The shm ring stays
10847
+ * node-local; only H.264/H.265 packets cross the wire.
10848
+ * `hubHostnameOverride` mirrors the recorder's `recordingHubHostname`:
10849
+ * when set it overrides the `CAMSTACK_HUB_URL`-derived host the runner
10850
+ * dials for the owner's restream.
10851
+ */
10852
+ var RunnerFrameSourceSchema = discriminatedUnion("kind", [object({ kind: literal("local-broker") }), object({
10853
+ kind: literal("remote-restream"),
10854
+ /** The camera's source-owner node (slice 1: always the hub). */
10855
+ ownerNodeId: string(),
10856
+ /** Operator override for the owner host the runner dials. */
10857
+ hubHostnameOverride: string().optional()
10858
+ })]).describe("Per-camera frame-source mode for the runner (P2c)");
10859
+ /**
10816
10860
  * Camera assignment payload sent by `addon-pipeline-orchestrator` to a
10817
10861
  * specific runner instance via `attachCamera`. Carries everything the
10818
10862
  * runner needs to subscribe to the local broker and execute inference.
@@ -10910,7 +10954,15 @@ var RunnerCameraConfigSchema = object({
10910
10954
  */
10911
10955
  onboardMotionDrivesAnalyzer: boolean().default(true),
10912
10956
  occupancyRecheckSec: number().min(occupancyRecheckSecField.min).max(occupancyRecheckSecField.max).default(occupancyRecheckSecField.default),
10913
- occupancyRecheckFrames: number().min(occupancyRecheckFramesField.min).max(occupancyRecheckFramesField.max).default(occupancyRecheckFramesField.default)
10957
+ occupancyRecheckFrames: number().min(occupancyRecheckFramesField.min).max(occupancyRecheckFramesField.max).default(occupancyRecheckFramesField.default),
10958
+ /**
10959
+ * Where this runner gets the camera's decoded frames (P2c). Defaulted so
10960
+ * every existing payload behaves as `local-broker` — the pre-Phase-2 path.
10961
+ * Populated with `remote-restream` by the orchestrator ONLY when the
10962
+ * camera's detect node differs from its source-owner (P2d, gated by the
10963
+ * `remoteSourcingNodes` rollout setting).
10964
+ */
10965
+ frameSource: RunnerFrameSourceSchema.default({ kind: "local-broker" })
10914
10966
  });
10915
10967
  motionFpsField.min, motionFpsField.max, motionFpsField.step, motionFpsField.default, detectionFpsField.min, detectionFpsField.max, detectionFpsField.step, detectionFpsField.default, motionCooldownMsField.min, motionCooldownMsField.max, motionCooldownMsField.step, motionCooldownMsField.default, occupancyRecheckSecField.min, occupancyRecheckSecField.max, occupancyRecheckSecField.step, occupancyRecheckSecField.default, occupancyRecheckFramesField.min, occupancyRecheckFramesField.max, occupancyRecheckFramesField.step, occupancyRecheckFramesField.default;
10916
10968
  /**
@@ -18144,6 +18196,16 @@ DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, method(object({ deviceI
18144
18196
  kind: "mutation",
18145
18197
  auth: "admin"
18146
18198
  });
18199
+ /**
18200
+ * `recording` cap — footage availability + HLS playback manifests + per-device
18201
+ * recording config. NOTE on events (source of truth, R5/C3): this cap carries
18202
+ * NO event surface — `getPlaybackManifest` returns playlist URLs only. Timeline
18203
+ * events (motion/object/audio) come from `pipelineAnalytics` (durable SQLite
18204
+ * rows); the recorder's internal EventMap markers are ephemeral in-RAM
18205
+ * annotations that are not exposed here and must not be treated as an event
18206
+ * feed. Event<->footage joins are by time, padded with the shared `EVENT_PAD_MS`
18207
+ * (`interfaces/recording-config.ts`).
18208
+ */
18147
18209
  var RecordingStatusSchema = object({
18148
18210
  deviceId: number(),
18149
18211
  enabled: boolean(),
@@ -23269,6 +23331,12 @@ Object.defineProperty(exports, "DeviceType", {
23269
23331
  return DeviceType;
23270
23332
  }
23271
23333
  });
23334
+ Object.defineProperty(exports, "EVENT_PAD_MS", {
23335
+ enumerable: true,
23336
+ get: function() {
23337
+ return EVENT_PAD_MS;
23338
+ }
23339
+ });
23272
23340
  Object.defineProperty(exports, "EncodeProfileSchema", {
23273
23341
  enumerable: true,
23274
23342
  get: function() {