@camstack/addon-pipeline 1.2.307 → 1.2.309

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 (33) hide show
  1. package/dist/audio-analyzer/index.js +2 -2
  2. package/dist/audio-analyzer/index.mjs +2 -2
  3. package/dist/{default-detection-model-DgdiQtQb.js → default-detection-model-BZMS04Wn.js} +1 -1
  4. package/dist/{default-detection-model-BV9bIuOo.mjs → default-detection-model-ClQ1gcZ9.mjs} +1 -1
  5. package/dist/detection-pipeline/index.js +4 -4
  6. package/dist/detection-pipeline/index.mjs +4 -4
  7. package/dist/{dist-DdVMFlJ6.mjs → dist-B3_sbBDM.mjs} +305 -48
  8. package/dist/{dist-CyJWk-_f.js → dist-BLf0YjAa.js} +310 -47
  9. package/dist/motion-wasm/index.js +1 -1
  10. package/dist/motion-wasm/index.mjs +1 -1
  11. package/dist/{node-COu_tHy4.mjs → node-DJFKTJ0B.mjs} +1 -53
  12. package/dist/{node-HsWG5fGp.js → node-Vwf2PuIT.js} +1 -53
  13. package/dist/pipeline-runner/index.js +3 -3
  14. package/dist/pipeline-runner/index.mjs +3 -3
  15. package/dist/{process-memory-Bz3oIMzl.mjs → process-memory-BvwauBlX.mjs} +1 -1
  16. package/dist/{process-memory-TQXG7qrJ.js → process-memory-DGTVVYlX.js} +1 -1
  17. package/dist/recorder/index.js +147 -22
  18. package/dist/recorder/index.mjs +147 -22
  19. package/dist/{segment-demux-js-BrKa7tpr.js → segment-demux-js-BUAAuSVF.js} +22 -4
  20. package/dist/{segment-demux-js-CEJn4BR0.mjs → segment-demux-js-C0oz7IVm.mjs} +17 -5
  21. package/dist/stream-broker/_stub.js +2 -2
  22. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DdYUVQSA.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-7H0Oxf23.mjs} +2 -2
  23. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-C6EaWavG.mjs +26 -0
  24. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BhO-szf8.mjs +26 -0
  25. package/dist/stream-broker/demux-worker-child.js +1 -1
  26. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  27. package/dist/stream-broker/{hostInit-D--a9rVM.mjs → hostInit-DEY-Dqqo.mjs} +2 -2
  28. package/dist/stream-broker/index.js +434 -111
  29. package/dist/stream-broker/index.mjs +434 -111
  30. package/dist/stream-broker/remoteEntry.js +1 -1
  31. package/package.json +1 -1
  32. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DPHv0v3Q.mjs +0 -26
  33. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CdKbuxqG.mjs +0 -26
@@ -1,4 +1,4 @@
1
- import { kt as parseProcStatus } from "./dist-DdVMFlJ6.mjs";
1
+ import { At as parseProcStatus } from "./dist-B3_sbBDM.mjs";
2
2
  import * as fs from "node:fs";
3
3
  import { execFile } from "node:child_process";
4
4
  //#region src/shared/node-topology-platform.ts
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("./chunk-emK7D4bc.js");
2
- const require_dist = require("./dist-CyJWk-_f.js");
2
+ const require_dist = require("./dist-BLf0YjAa.js");
3
3
  let node_fs = require("node:fs");
4
4
  node_fs = require_chunk.__toESM(node_fs);
5
5
  let node_child_process = require("node:child_process");
@@ -3,8 +3,8 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-emK7D4bc.js");
6
- const require_dist = require("../dist-CyJWk-_f.js");
7
- const require_node = require("../node-HsWG5fGp.js");
6
+ const require_dist = require("../dist-BLf0YjAa.js");
7
+ const require_node = require("../node-Vwf2PuIT.js");
8
8
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
9
9
  const require_restream_intent = require("../restream-intent-7uDNqMKs.js");
10
10
  const require_retire_root_keys = require("../retire-root-keys-Csb8z2f3.js");
@@ -3450,7 +3450,20 @@ var StillFrameService = class {
3450
3450
  return null;
3451
3451
  }
3452
3452
  try {
3453
- const jpeg = await this.decodeFrame(abs, source.offsetSec);
3453
+ let bin;
3454
+ try {
3455
+ bin = await this.deps.resolveFfmpeg();
3456
+ } catch (err) {
3457
+ this.deps.logger.error("still: refused — no pinned ffmpeg on this node (D540)", {
3458
+ tags: { deviceId },
3459
+ meta: {
3460
+ epochMs,
3461
+ error: err instanceof Error ? err.message : String(err)
3462
+ }
3463
+ });
3464
+ return null;
3465
+ }
3466
+ const jpeg = await this.decodeFrame(bin, abs, source.offsetSec);
3454
3467
  if (jpeg === null || jpeg.byteLength === 0) return null;
3455
3468
  return {
3456
3469
  jpeg,
@@ -3461,7 +3474,7 @@ var StillFrameService = class {
3461
3474
  }
3462
3475
  }
3463
3476
  /** Run ONE ffmpeg input-seek + single-frame decode; JPEG bytes on stdout. */
3464
- decodeFrame(absPath, offsetSec) {
3477
+ decodeFrame(bin, absPath, offsetSec) {
3465
3478
  const args = [
3466
3479
  "-hide_banner",
3467
3480
  "-loglevel",
@@ -3477,7 +3490,6 @@ var StillFrameService = class {
3477
3490
  "pipe:1"
3478
3491
  ];
3479
3492
  const spawn = this.deps.spawn ?? node_child_process.spawn;
3480
- const bin = this.deps.ffmpegPath ?? "ffmpeg";
3481
3493
  return new Promise((resolve) => {
3482
3494
  let child;
3483
3495
  try {
@@ -7549,15 +7561,32 @@ var ExportEngine = class {
7549
7561
  } catch {}
7550
7562
  }
7551
7563
  }
7552
- runFfmpeg(id, deviceId, args, progress) {
7564
+ async runFfmpeg(id, deviceId, args, progress) {
7565
+ let ffmpegPath;
7566
+ try {
7567
+ ffmpegPath = await this.deps.resolveFfmpeg();
7568
+ } catch (err) {
7569
+ const reason = `no pinned ffmpeg on this node (D540): ${err instanceof Error ? err.message : String(err)}`;
7570
+ this.deps.logger.error("export render refused — no pinned ffmpeg on this node (D540)", {
7571
+ tags: { deviceId },
7572
+ meta: {
7573
+ exportId: id,
7574
+ error: reason
7575
+ }
7576
+ });
7577
+ return {
7578
+ code: null,
7579
+ stderrTail: [reason]
7580
+ };
7581
+ }
7553
7582
  this.deps.logger.debug("export ffmpeg argv", { meta: {
7554
7583
  exportId: id,
7555
7584
  args
7556
7585
  } });
7557
7586
  const stallMs = this.deps.stallMs ?? 9e4;
7558
7587
  const faststartStallMs = this.deps.faststartStallMs ?? 9e5;
7559
- return new Promise((resolve) => {
7560
- const proc = this.deps.spawn(this.deps.ffmpegPath ?? "ffmpeg", args);
7588
+ return await new Promise((resolve) => {
7589
+ const proc = this.deps.spawn(ffmpegPath, args);
7561
7590
  this.active.set(id, proc);
7562
7591
  const stderrTail = [];
7563
7592
  let stallTimer = null;
@@ -9459,6 +9488,17 @@ async function renderFootage(renderDeps, input, ext, buildArgs) {
9459
9488
  const toMs = input.aroundMs + input.postRollSec * 1e3;
9460
9489
  if (!Number.isFinite(fromMs) || !Number.isFinite(toMs) || toMs <= fromMs) throw new Error(`invalid ${ext} window [${fromMs}, ${toMs}) for device ${input.deviceId} — aroundMs=${input.aroundMs} preRollSec=${input.preRollSec} postRollSec=${input.postRollSec}`);
9461
9490
  const renderId = `${ext}-${now()}-${Math.floor(Math.random() * 1e6)}`;
9491
+ let ffmpegPath;
9492
+ try {
9493
+ ffmpegPath = await renderDeps.resolveFfmpeg();
9494
+ } catch (err) {
9495
+ const reason = err instanceof Error ? err.message : String(err);
9496
+ renderDeps.deps.logger.error(`${ext} render refused — no pinned ffmpeg on this node (D540)`, {
9497
+ tags: { deviceId: input.deviceId },
9498
+ meta: { error: reason }
9499
+ });
9500
+ throw new Error(`${ext} render refused for device ${input.deviceId}: no pinned ffmpeg — ${reason}`, { cause: err });
9501
+ }
9462
9502
  const availableEnd = latestFinalizedEnd(renderDeps, input.deviceId);
9463
9503
  if (availableEnd === null || availableEnd < toMs) throw new Error(`${FOOTAGE_NOT_READY}: [${fromMs}, ${toMs}) for device ${input.deviceId} — finalized footage ends at ${availableEnd ?? "never"}`);
9464
9504
  const window = {
@@ -9477,7 +9517,7 @@ async function renderFootage(renderDeps, input, ext, buildArgs) {
9477
9517
  if (playlist === null) throw new Error(`no footage covers [${window.fromMs}, ${window.toMs}) for device ${input.deviceId}`);
9478
9518
  const outPath = node_path.default.join(node_path.default.dirname(playlist.path), `${renderId}.${ext}`);
9479
9519
  try {
9480
- await runFfmpeg(renderDeps, buildArgs(playlist.path, outPath), ext);
9520
+ await runFfmpeg(renderDeps, ffmpegPath, buildArgs(playlist.path, outPath), ext);
9481
9521
  const bytes = await node_fs.promises.readFile(outPath);
9482
9522
  if (bytes.byteLength === 0) throw new Error(`${ext} render produced an empty file`);
9483
9523
  return {
@@ -9514,9 +9554,9 @@ function latestFinalizedEnd(renderDeps, deviceId) {
9514
9554
  * throws ESRCH, which is swallowed, so the cost of being wrong is nothing while
9515
9555
  * the cost of NOT killing is a zombie per render.
9516
9556
  */
9517
- function runFfmpeg(renderDeps, args, ext) {
9557
+ function runFfmpeg(renderDeps, ffmpegPath, args, ext) {
9518
9558
  return new Promise((resolve, reject) => {
9519
- const proc = renderDeps.spawn(renderDeps.ffmpegPath ?? "ffmpeg", args);
9559
+ const proc = renderDeps.spawn(ffmpegPath, args);
9520
9560
  let done = false;
9521
9561
  let timer = null;
9522
9562
  const settle = (err) => {
@@ -15658,6 +15698,26 @@ function buildPassthroughArgs(a) {
15658
15698
  ];
15659
15699
  }
15660
15700
  //#endregion
15701
+ //#region src/recorder/addon/dial-refusal.ts
15702
+ var METHOD_FAILED = /\bmethod ([A-Z_]+) failed: (\d{3})\b/;
15703
+ function isHandshakeMethod(method) {
15704
+ return method === "OPTIONS" || method === "DESCRIBE" || method === "SETUP" || method === "PLAY";
15705
+ }
15706
+ /** The refusal named in this generation's stderr tail, or `null` when there is none. */
15707
+ function readDialRefusal(stderrTail) {
15708
+ for (let i = stderrTail.length - 1; i >= 0; i--) {
15709
+ const match = METHOD_FAILED.exec(stderrTail[i] ?? "");
15710
+ if (match === null) continue;
15711
+ const method = match[1] ?? "";
15712
+ if (!isHandshakeMethod(method)) return null;
15713
+ return {
15714
+ method,
15715
+ status: Number(match[2])
15716
+ };
15717
+ }
15718
+ return null;
15719
+ }
15720
+ //#endregion
15661
15721
  //#region src/recorder/addon/segment-writer.ts
15662
15722
  /** Max CONSECUTIVE rapid restarts before giving up (reset by a stable run). */
15663
15723
  var MAX_RESTARTS = 10;
@@ -15716,6 +15776,12 @@ var SegmentWriter = class {
15716
15776
  * half-pruned window.
15717
15777
  */
15718
15778
  restartWindow = [];
15779
+ /**
15780
+ * Consecutive generations the source refused at the handshake (D685). Reset
15781
+ * by any generation that was not refused. Only read for the log line — the
15782
+ * rapid breaker is what bounds a source that never comes back.
15783
+ */
15784
+ refusedInARow = 0;
15719
15785
  restartTimer = null;
15720
15786
  startedAt = 0;
15721
15787
  exitWait = null;
@@ -15748,7 +15814,7 @@ var SegmentWriter = class {
15748
15814
  start() {
15749
15815
  if (this.stopped || this.proc) return;
15750
15816
  const args = buildPassthroughArgs(this.cfg);
15751
- const proc = this.deps.spawn(this.deps.ffmpegPath ?? "ffmpeg", args);
15817
+ const proc = this.deps.spawn(this.deps.ffmpegPath, args);
15752
15818
  this.proc = proc;
15753
15819
  this.exitWait = new Promise((resolve) => {
15754
15820
  this.resolveExit = resolve;
@@ -15805,10 +15871,13 @@ var SegmentWriter = class {
15805
15871
  if (this.stopped) return;
15806
15872
  const now = Date.now();
15807
15873
  const ranMs = now - this.startedAt;
15808
- if (ranMs >= STABLE_RUN_MS) this.restarts = 0;
15809
- this.restartWindow = [...this.restartWindow.filter((at) => at > now - RESTART_WINDOW_MS), now];
15874
+ const refusal = termination.reason === "exit" ? readDialRefusal(stderrTail) : null;
15875
+ this.refusedInARow = refusal === null ? 0 : this.refusedInARow + 1;
15876
+ if (ranMs >= STABLE_RUN_MS && refusal === null) this.restarts = 0;
15877
+ const liveWindow = this.restartWindow.filter((at) => at > now - RESTART_WINDOW_MS);
15878
+ this.restartWindow = refusal === null ? [...liveWindow, now] : liveWindow;
15810
15879
  const restartsInWindow = this.restartWindow.length;
15811
- if (termination.reason === "exit") this.deps.logger.warn("SegmentWriter ffmpeg exited", {
15880
+ if (termination.reason === "exit" && refusal === null) this.deps.logger.warn("SegmentWriter ffmpeg exited", {
15812
15881
  tags: { deviceId: this.deps.deviceId },
15813
15882
  meta: {
15814
15883
  outDir: this.cfg.outDir,
@@ -15880,6 +15949,39 @@ var SegmentWriter = class {
15880
15949
  }
15881
15950
  this.restarts++;
15882
15951
  const delayMs = Math.min(RESTART_BASE_MS * 2 ** (this.restarts - 1), RESTART_MAX_MS);
15952
+ if (refusal !== null) this.logDeferredRedial(refusal, delayMs, ranMs, restartsInWindow);
15953
+ else this.logRestarting(termination, delayMs, ranMs, restartsInWindow);
15954
+ this.restartTimer = setTimeout(() => {
15955
+ this.restartTimer = null;
15956
+ this.start();
15957
+ }, delayMs);
15958
+ }
15959
+ /**
15960
+ * A refused dial, named — every one of them, per camera. The FIRST of a run
15961
+ * is a WARN (the source just went away under a live writer); the rest are
15962
+ * the same outage continuing and go out at INFO with the running count.
15963
+ */
15964
+ logDeferredRedial(refusal, delayMs, ranMs, restartsInWindow) {
15965
+ const payload = {
15966
+ tags: { deviceId: this.deps.deviceId },
15967
+ meta: {
15968
+ outDir: this.cfg.outDir,
15969
+ method: refusal.method,
15970
+ status: refusal.status,
15971
+ refusedInARow: this.refusedInARow,
15972
+ attempt: this.restarts,
15973
+ delayMs,
15974
+ ranMs,
15975
+ restartsInWindow
15976
+ }
15977
+ };
15978
+ if (this.refusedInARow === 1) {
15979
+ this.deps.logger.warn("SegmentWriter dial refused — source not published, re-dialling", payload);
15980
+ return;
15981
+ }
15982
+ this.deps.logger.info("SegmentWriter dial still refused — re-dial deferred", payload);
15983
+ }
15984
+ logRestarting(termination, delayMs, ranMs, restartsInWindow) {
15883
15985
  this.deps.logger.info("SegmentWriter restarting", {
15884
15986
  tags: { deviceId: this.deps.deviceId },
15885
15987
  meta: {
@@ -15891,10 +15993,6 @@ var SegmentWriter = class {
15891
15993
  reason: termination.reason
15892
15994
  }
15893
15995
  });
15894
- this.restartTimer = setTimeout(() => {
15895
- this.restartTimer = null;
15896
- this.start();
15897
- }, delayMs);
15898
15996
  }
15899
15997
  stop() {
15900
15998
  this.stopAndWait();
@@ -17495,6 +17593,19 @@ var RecordingController = class {
17495
17593
  */
17496
17594
  async attachProfile(deviceId, profile, segmentSeconds, audio) {
17497
17595
  const attachProfileStartMs = this.now();
17596
+ let ffmpegPath;
17597
+ try {
17598
+ ffmpegPath = await this.deps.resolveFfmpeg();
17599
+ } catch (err) {
17600
+ this.deps.logger.error("recorder: writer refused — no pinned ffmpeg on this node (D540)", {
17601
+ tags: { deviceId },
17602
+ meta: {
17603
+ profile,
17604
+ error: require_dist.errMsg(err)
17605
+ }
17606
+ });
17607
+ throw new Error(`recorder: no pinned ffmpeg for device ${deviceId}/${profile}`);
17608
+ }
17498
17609
  const placement = await this.resolvePlacement(deviceId, profile);
17499
17610
  const placementDoneMs = this.now();
17500
17611
  const outDir = node_path.default.join(placement.root, STAGING_DIR_NAME, String(deviceId), profile);
@@ -17530,6 +17641,7 @@ var RecordingController = class {
17530
17641
  consumerTag
17531
17642
  }, {
17532
17643
  spawn: this.deps.spawn,
17644
+ ffmpegPath,
17533
17645
  logger: deviceLog,
17534
17646
  deviceId,
17535
17647
  claimCost: (pid) => this.deps.costRegistry?.claim({
@@ -20823,6 +20935,14 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
20823
20935
  * because those children were this process's children.
20824
20936
  */
20825
20937
  costRegistry = new require_node.ChildCostRegistry();
20938
+ /**
20939
+ * This node's PINNED ffmpeg (D540), asked once and shared by every child the
20940
+ * recorder spawns: segment writers, exports, gif/clip renders and stills.
20941
+ * Each of those used to take an optional path that no one passed, and fell
20942
+ * back to a bare `'ffmpeg'` — the HOST's binary. It rejects rather than
20943
+ * naming PATH; each site logs its refusal per camera.
20944
+ */
20945
+ pinnedFfmpeg = require_dist.memoizePinnedFfmpeg(() => this.ctx.deps.ensureFfmpeg());
20826
20946
  /** True when THIS node is the designated recording node (`recordingNodeId`).
20827
20947
  * Every other node stays fully inert — registers no providers, serves no
20828
20948
  * playback, runs no controller. Resolved in `onInitialize`. */
@@ -21216,6 +21336,7 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
21216
21336
  now: () => Date.now()
21217
21337
  });
21218
21338
  this.controller = new RecordingController({
21339
+ resolveFfmpeg: this.pinnedFfmpeg,
21219
21340
  ...this.stagingGuard !== null ? { stagingGuard: this.stagingGuard } : {},
21220
21341
  recoverStagingDirectory: (input) => this.recoverReapedDirectory(input),
21221
21342
  heapProbe: this.heapProbe,
@@ -21363,12 +21484,14 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
21363
21484
  gifRender: (input) => renderGif({
21364
21485
  deps: recordingDeps,
21365
21486
  exportsDir: this.exportsDirOrThrow(),
21366
- spawn: node_child_process.spawn
21487
+ spawn: node_child_process.spawn,
21488
+ resolveFfmpeg: this.pinnedFfmpeg
21367
21489
  }, input),
21368
21490
  clipRender: (input) => renderClip({
21369
21491
  deps: recordingDeps,
21370
21492
  exportsDir: this.exportsDirOrThrow(),
21371
- spawn: node_child_process.spawn
21493
+ spawn: node_child_process.spawn,
21494
+ resolveFfmpeg: this.pinnedFfmpeg
21372
21495
  }, input),
21373
21496
  locations: () => this.resolvedLocations,
21374
21497
  refreshLocations: () => this.refreshLocations(),
@@ -21497,6 +21620,7 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
21497
21620
  };
21498
21621
  this.exportEngine = new ExportEngine({
21499
21622
  spawn: node_child_process.spawn,
21623
+ resolveFfmpeg: this.pinnedFfmpeg,
21500
21624
  logger: this.ctx.logger.child("export"),
21501
21625
  state: exportsState,
21502
21626
  resolvePlaylist: async (rec) => {
@@ -21649,7 +21773,8 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
21649
21773
  const stillService = new StillFrameService({
21650
21774
  lowSegmentsAt: (deviceId, epochMs) => this.index.segmentsOverlapping(deviceId, "low", epochMs, epochMs + 1),
21651
21775
  resolveAbsPath: (row) => this.resolveSegmentAbsPath(row),
21652
- logger: this.ctx.logger.child("Still")
21776
+ logger: this.ctx.logger.child("Still"),
21777
+ resolveFfmpeg: this.pinnedFfmpeg
21653
21778
  });
21654
21779
  const served = await this.ctx.dataPlane?.serve({
21655
21780
  prefix: STILL_PREFIX,
@@ -1,5 +1,5 @@
1
- import { $t as videoclipsCapability, An as array, B as VISIT_MERGE_GAP_MS, Ct as mayReadLocation, Dn as selectAssignedProfileSlots, Et as modeMayWrite, F as RECORDING_SOURCE_CAMSTACK_ADDON, Fn as number, Gt as resolveRecordingProfiles, I as RecordingConfigSchema, In as object, It as recordingArchiveCapability, J as clipExportFailure, Jt as storageEvictableCapability, K as batteryCapability, L as RecordingSignalStatusSchema, Ln as preprocess, Lt as recordingCapability, N as RECORDING_EXPORT_MAX_READ_BYTES, On as sleep$1, P as RECORDING_SOURCE_CAMSTACK, Rn as record, Rt as recordingExportCapability, S as ExportRecordSchema, Tt as migrateLegacyDeviceSignalConfig, Ut as resolveLocationMode, Vn as EventCategory, Vt as resolveCreateExportSubject, Y as clipExportFailureOf, Yt as storageOccupancyCapability, Zt as stripRetiredBandPreBufferSec, _n as customAction, a as BatteryStatusSchema, an as evaluateSensorEdge, b as EVENT_PAD_MS, bn as hydrateSchema, cn as DeviceFeature, ct as evictionPolicyForMode, et as deriveBatteryPresence, fn as BaseAddon, g as DEFAULT_EVENTS_BAND_BUFFER_SEC, jn as boolean, k as OpsLogEntrySchema, kn as _enum, ln as DeviceType, lt as evictionPolicyOfLocation, nt as deriveRecordingMode, o as CLIP_BROKER_PACED_RATES, pt as gbToBytes, un as errMsg$1, ut as exportSubjectOf, vn as defineCustomActions, vt as isFirstLevelMacroClass, wn as nodePin, wt as mayWriteToLocation, y as EVENT_OWNER_TYPES, yt as loadContributionCapability, z as VIDEOCLIPS_MAX_OFFER_BYTES, zn as string, zt as recordingSignalCapability } from "../dist-DdVMFlJ6.mjs";
2
- import { a as physicalRootOf, i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-COu_tHy4.mjs";
1
+ import { An as _enum, B as VISIT_MERGE_GAP_MS, Bn as string, Bt as recordingSignalCapability, Ct as mayReadLocation, Dt as modeMayWrite, Et as migrateLegacyDeviceSignalConfig, F as RECORDING_SOURCE_CAMSTACK_ADDON, Hn as EventCategory, Ht as resolveCreateExportSubject, I as RecordingConfigSchema, In as number, J as clipExportFailure, K as batteryCapability, Kt as resolveRecordingProfiles, L as RecordingSignalStatusSchema, Ln as object, Lt as recordingArchiveCapability, Mn as boolean, N as RECORDING_EXPORT_MAX_READ_BYTES, On as selectAssignedProfileSlots, P as RECORDING_SOURCE_CAMSTACK, Qt as stripRetiredBandPreBufferSec, Rn as preprocess, Rt as recordingCapability, S as ExportRecordSchema, Tn as nodePin, Tt as memoizePinnedFfmpeg, Wt as resolveLocationMode, Xt as storageOccupancyCapability, Y as clipExportFailureOf, Yt as storageEvictableCapability, a as BatteryStatusSchema, b as EVENT_PAD_MS, ct as evictionPolicyForMode, dn as errMsg$1, en as videoclipsCapability, et as deriveBatteryPresence, g as DEFAULT_EVENTS_BAND_BUFFER_SEC, jn as array, k as OpsLogEntrySchema, kn as sleep$1, ln as DeviceFeature, lt as evictionPolicyOfLocation, nt as deriveRecordingMode, o as CLIP_BROKER_PACED_RATES, on as evaluateSensorEdge, pn as BaseAddon, pt as gbToBytes, un as DeviceType, ut as exportSubjectOf, vn as customAction, vt as isFirstLevelMacroClass, wt as mayWriteToLocation, xn as hydrateSchema, y as EVENT_OWNER_TYPES, yn as defineCustomActions, yt as loadContributionCapability, z as VIDEOCLIPS_MAX_OFFER_BYTES, zn as record, zt as recordingExportCapability } from "../dist-B3_sbBDM.mjs";
2
+ import { a as physicalRootOf, i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-DJFKTJ0B.mjs";
3
3
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
4
4
  import { d as PREBUFFER_MAX_MS, l as withRecordingIntent, r as RECORDING_PRE_ROLL_REQUEST_MS } from "../restream-intent-DyGmLAU9.mjs";
5
5
  import { n as RowMapStore, t as retireRootKeys } from "../retire-root-keys-DZvlso2l.mjs";
@@ -3443,7 +3443,20 @@ var StillFrameService = class {
3443
3443
  return null;
3444
3444
  }
3445
3445
  try {
3446
- const jpeg = await this.decodeFrame(abs, source.offsetSec);
3446
+ let bin;
3447
+ try {
3448
+ bin = await this.deps.resolveFfmpeg();
3449
+ } catch (err) {
3450
+ this.deps.logger.error("still: refused — no pinned ffmpeg on this node (D540)", {
3451
+ tags: { deviceId },
3452
+ meta: {
3453
+ epochMs,
3454
+ error: err instanceof Error ? err.message : String(err)
3455
+ }
3456
+ });
3457
+ return null;
3458
+ }
3459
+ const jpeg = await this.decodeFrame(bin, abs, source.offsetSec);
3447
3460
  if (jpeg === null || jpeg.byteLength === 0) return null;
3448
3461
  return {
3449
3462
  jpeg,
@@ -3454,7 +3467,7 @@ var StillFrameService = class {
3454
3467
  }
3455
3468
  }
3456
3469
  /** Run ONE ffmpeg input-seek + single-frame decode; JPEG bytes on stdout. */
3457
- decodeFrame(absPath, offsetSec) {
3470
+ decodeFrame(bin, absPath, offsetSec) {
3458
3471
  const args = [
3459
3472
  "-hide_banner",
3460
3473
  "-loglevel",
@@ -3470,7 +3483,6 @@ var StillFrameService = class {
3470
3483
  "pipe:1"
3471
3484
  ];
3472
3485
  const spawn$1 = this.deps.spawn ?? spawn;
3473
- const bin = this.deps.ffmpegPath ?? "ffmpeg";
3474
3486
  return new Promise((resolve) => {
3475
3487
  let child;
3476
3488
  try {
@@ -7541,15 +7553,32 @@ var ExportEngine = class {
7541
7553
  } catch {}
7542
7554
  }
7543
7555
  }
7544
- runFfmpeg(id, deviceId, args, progress) {
7556
+ async runFfmpeg(id, deviceId, args, progress) {
7557
+ let ffmpegPath;
7558
+ try {
7559
+ ffmpegPath = await this.deps.resolveFfmpeg();
7560
+ } catch (err) {
7561
+ const reason = `no pinned ffmpeg on this node (D540): ${err instanceof Error ? err.message : String(err)}`;
7562
+ this.deps.logger.error("export render refused — no pinned ffmpeg on this node (D540)", {
7563
+ tags: { deviceId },
7564
+ meta: {
7565
+ exportId: id,
7566
+ error: reason
7567
+ }
7568
+ });
7569
+ return {
7570
+ code: null,
7571
+ stderrTail: [reason]
7572
+ };
7573
+ }
7545
7574
  this.deps.logger.debug("export ffmpeg argv", { meta: {
7546
7575
  exportId: id,
7547
7576
  args
7548
7577
  } });
7549
7578
  const stallMs = this.deps.stallMs ?? 9e4;
7550
7579
  const faststartStallMs = this.deps.faststartStallMs ?? 9e5;
7551
- return new Promise((resolve) => {
7552
- const proc = this.deps.spawn(this.deps.ffmpegPath ?? "ffmpeg", args);
7580
+ return await new Promise((resolve) => {
7581
+ const proc = this.deps.spawn(ffmpegPath, args);
7553
7582
  this.active.set(id, proc);
7554
7583
  const stderrTail = [];
7555
7584
  let stallTimer = null;
@@ -9451,6 +9480,17 @@ async function renderFootage(renderDeps, input, ext, buildArgs) {
9451
9480
  const toMs = input.aroundMs + input.postRollSec * 1e3;
9452
9481
  if (!Number.isFinite(fromMs) || !Number.isFinite(toMs) || toMs <= fromMs) throw new Error(`invalid ${ext} window [${fromMs}, ${toMs}) for device ${input.deviceId} — aroundMs=${input.aroundMs} preRollSec=${input.preRollSec} postRollSec=${input.postRollSec}`);
9453
9482
  const renderId = `${ext}-${now()}-${Math.floor(Math.random() * 1e6)}`;
9483
+ let ffmpegPath;
9484
+ try {
9485
+ ffmpegPath = await renderDeps.resolveFfmpeg();
9486
+ } catch (err) {
9487
+ const reason = err instanceof Error ? err.message : String(err);
9488
+ renderDeps.deps.logger.error(`${ext} render refused — no pinned ffmpeg on this node (D540)`, {
9489
+ tags: { deviceId: input.deviceId },
9490
+ meta: { error: reason }
9491
+ });
9492
+ throw new Error(`${ext} render refused for device ${input.deviceId}: no pinned ffmpeg — ${reason}`, { cause: err });
9493
+ }
9454
9494
  const availableEnd = latestFinalizedEnd(renderDeps, input.deviceId);
9455
9495
  if (availableEnd === null || availableEnd < toMs) throw new Error(`${FOOTAGE_NOT_READY}: [${fromMs}, ${toMs}) for device ${input.deviceId} — finalized footage ends at ${availableEnd ?? "never"}`);
9456
9496
  const window = {
@@ -9469,7 +9509,7 @@ async function renderFootage(renderDeps, input, ext, buildArgs) {
9469
9509
  if (playlist === null) throw new Error(`no footage covers [${window.fromMs}, ${window.toMs}) for device ${input.deviceId}`);
9470
9510
  const outPath = path.join(path.dirname(playlist.path), `${renderId}.${ext}`);
9471
9511
  try {
9472
- await runFfmpeg(renderDeps, buildArgs(playlist.path, outPath), ext);
9512
+ await runFfmpeg(renderDeps, ffmpegPath, buildArgs(playlist.path, outPath), ext);
9473
9513
  const bytes = await promises.readFile(outPath);
9474
9514
  if (bytes.byteLength === 0) throw new Error(`${ext} render produced an empty file`);
9475
9515
  return {
@@ -9506,9 +9546,9 @@ function latestFinalizedEnd(renderDeps, deviceId) {
9506
9546
  * throws ESRCH, which is swallowed, so the cost of being wrong is nothing while
9507
9547
  * the cost of NOT killing is a zombie per render.
9508
9548
  */
9509
- function runFfmpeg(renderDeps, args, ext) {
9549
+ function runFfmpeg(renderDeps, ffmpegPath, args, ext) {
9510
9550
  return new Promise((resolve, reject) => {
9511
- const proc = renderDeps.spawn(renderDeps.ffmpegPath ?? "ffmpeg", args);
9551
+ const proc = renderDeps.spawn(ffmpegPath, args);
9512
9552
  let done = false;
9513
9553
  let timer = null;
9514
9554
  const settle = (err) => {
@@ -15650,6 +15690,26 @@ function buildPassthroughArgs(a) {
15650
15690
  ];
15651
15691
  }
15652
15692
  //#endregion
15693
+ //#region src/recorder/addon/dial-refusal.ts
15694
+ var METHOD_FAILED = /\bmethod ([A-Z_]+) failed: (\d{3})\b/;
15695
+ function isHandshakeMethod(method) {
15696
+ return method === "OPTIONS" || method === "DESCRIBE" || method === "SETUP" || method === "PLAY";
15697
+ }
15698
+ /** The refusal named in this generation's stderr tail, or `null` when there is none. */
15699
+ function readDialRefusal(stderrTail) {
15700
+ for (let i = stderrTail.length - 1; i >= 0; i--) {
15701
+ const match = METHOD_FAILED.exec(stderrTail[i] ?? "");
15702
+ if (match === null) continue;
15703
+ const method = match[1] ?? "";
15704
+ if (!isHandshakeMethod(method)) return null;
15705
+ return {
15706
+ method,
15707
+ status: Number(match[2])
15708
+ };
15709
+ }
15710
+ return null;
15711
+ }
15712
+ //#endregion
15653
15713
  //#region src/recorder/addon/segment-writer.ts
15654
15714
  /** Max CONSECUTIVE rapid restarts before giving up (reset by a stable run). */
15655
15715
  var MAX_RESTARTS = 10;
@@ -15708,6 +15768,12 @@ var SegmentWriter = class {
15708
15768
  * half-pruned window.
15709
15769
  */
15710
15770
  restartWindow = [];
15771
+ /**
15772
+ * Consecutive generations the source refused at the handshake (D685). Reset
15773
+ * by any generation that was not refused. Only read for the log line — the
15774
+ * rapid breaker is what bounds a source that never comes back.
15775
+ */
15776
+ refusedInARow = 0;
15711
15777
  restartTimer = null;
15712
15778
  startedAt = 0;
15713
15779
  exitWait = null;
@@ -15740,7 +15806,7 @@ var SegmentWriter = class {
15740
15806
  start() {
15741
15807
  if (this.stopped || this.proc) return;
15742
15808
  const args = buildPassthroughArgs(this.cfg);
15743
- const proc = this.deps.spawn(this.deps.ffmpegPath ?? "ffmpeg", args);
15809
+ const proc = this.deps.spawn(this.deps.ffmpegPath, args);
15744
15810
  this.proc = proc;
15745
15811
  this.exitWait = new Promise((resolve) => {
15746
15812
  this.resolveExit = resolve;
@@ -15797,10 +15863,13 @@ var SegmentWriter = class {
15797
15863
  if (this.stopped) return;
15798
15864
  const now = Date.now();
15799
15865
  const ranMs = now - this.startedAt;
15800
- if (ranMs >= STABLE_RUN_MS) this.restarts = 0;
15801
- this.restartWindow = [...this.restartWindow.filter((at) => at > now - RESTART_WINDOW_MS), now];
15866
+ const refusal = termination.reason === "exit" ? readDialRefusal(stderrTail) : null;
15867
+ this.refusedInARow = refusal === null ? 0 : this.refusedInARow + 1;
15868
+ if (ranMs >= STABLE_RUN_MS && refusal === null) this.restarts = 0;
15869
+ const liveWindow = this.restartWindow.filter((at) => at > now - RESTART_WINDOW_MS);
15870
+ this.restartWindow = refusal === null ? [...liveWindow, now] : liveWindow;
15802
15871
  const restartsInWindow = this.restartWindow.length;
15803
- if (termination.reason === "exit") this.deps.logger.warn("SegmentWriter ffmpeg exited", {
15872
+ if (termination.reason === "exit" && refusal === null) this.deps.logger.warn("SegmentWriter ffmpeg exited", {
15804
15873
  tags: { deviceId: this.deps.deviceId },
15805
15874
  meta: {
15806
15875
  outDir: this.cfg.outDir,
@@ -15872,6 +15941,39 @@ var SegmentWriter = class {
15872
15941
  }
15873
15942
  this.restarts++;
15874
15943
  const delayMs = Math.min(RESTART_BASE_MS * 2 ** (this.restarts - 1), RESTART_MAX_MS);
15944
+ if (refusal !== null) this.logDeferredRedial(refusal, delayMs, ranMs, restartsInWindow);
15945
+ else this.logRestarting(termination, delayMs, ranMs, restartsInWindow);
15946
+ this.restartTimer = setTimeout(() => {
15947
+ this.restartTimer = null;
15948
+ this.start();
15949
+ }, delayMs);
15950
+ }
15951
+ /**
15952
+ * A refused dial, named — every one of them, per camera. The FIRST of a run
15953
+ * is a WARN (the source just went away under a live writer); the rest are
15954
+ * the same outage continuing and go out at INFO with the running count.
15955
+ */
15956
+ logDeferredRedial(refusal, delayMs, ranMs, restartsInWindow) {
15957
+ const payload = {
15958
+ tags: { deviceId: this.deps.deviceId },
15959
+ meta: {
15960
+ outDir: this.cfg.outDir,
15961
+ method: refusal.method,
15962
+ status: refusal.status,
15963
+ refusedInARow: this.refusedInARow,
15964
+ attempt: this.restarts,
15965
+ delayMs,
15966
+ ranMs,
15967
+ restartsInWindow
15968
+ }
15969
+ };
15970
+ if (this.refusedInARow === 1) {
15971
+ this.deps.logger.warn("SegmentWriter dial refused — source not published, re-dialling", payload);
15972
+ return;
15973
+ }
15974
+ this.deps.logger.info("SegmentWriter dial still refused — re-dial deferred", payload);
15975
+ }
15976
+ logRestarting(termination, delayMs, ranMs, restartsInWindow) {
15875
15977
  this.deps.logger.info("SegmentWriter restarting", {
15876
15978
  tags: { deviceId: this.deps.deviceId },
15877
15979
  meta: {
@@ -15883,10 +15985,6 @@ var SegmentWriter = class {
15883
15985
  reason: termination.reason
15884
15986
  }
15885
15987
  });
15886
- this.restartTimer = setTimeout(() => {
15887
- this.restartTimer = null;
15888
- this.start();
15889
- }, delayMs);
15890
15988
  }
15891
15989
  stop() {
15892
15990
  this.stopAndWait();
@@ -17487,6 +17585,19 @@ var RecordingController = class {
17487
17585
  */
17488
17586
  async attachProfile(deviceId, profile, segmentSeconds, audio) {
17489
17587
  const attachProfileStartMs = this.now();
17588
+ let ffmpegPath;
17589
+ try {
17590
+ ffmpegPath = await this.deps.resolveFfmpeg();
17591
+ } catch (err) {
17592
+ this.deps.logger.error("recorder: writer refused — no pinned ffmpeg on this node (D540)", {
17593
+ tags: { deviceId },
17594
+ meta: {
17595
+ profile,
17596
+ error: errMsg$1(err)
17597
+ }
17598
+ });
17599
+ throw new Error(`recorder: no pinned ffmpeg for device ${deviceId}/${profile}`);
17600
+ }
17490
17601
  const placement = await this.resolvePlacement(deviceId, profile);
17491
17602
  const placementDoneMs = this.now();
17492
17603
  const outDir = path.join(placement.root, STAGING_DIR_NAME, String(deviceId), profile);
@@ -17522,6 +17633,7 @@ var RecordingController = class {
17522
17633
  consumerTag
17523
17634
  }, {
17524
17635
  spawn: this.deps.spawn,
17636
+ ffmpegPath,
17525
17637
  logger: deviceLog,
17526
17638
  deviceId,
17527
17639
  claimCost: (pid) => this.deps.costRegistry?.claim({
@@ -20815,6 +20927,14 @@ var RecorderV2Addon = class extends BaseAddon {
20815
20927
  * because those children were this process's children.
20816
20928
  */
20817
20929
  costRegistry = new ChildCostRegistry();
20930
+ /**
20931
+ * This node's PINNED ffmpeg (D540), asked once and shared by every child the
20932
+ * recorder spawns: segment writers, exports, gif/clip renders and stills.
20933
+ * Each of those used to take an optional path that no one passed, and fell
20934
+ * back to a bare `'ffmpeg'` — the HOST's binary. It rejects rather than
20935
+ * naming PATH; each site logs its refusal per camera.
20936
+ */
20937
+ pinnedFfmpeg = memoizePinnedFfmpeg(() => this.ctx.deps.ensureFfmpeg());
20818
20938
  /** True when THIS node is the designated recording node (`recordingNodeId`).
20819
20939
  * Every other node stays fully inert — registers no providers, serves no
20820
20940
  * playback, runs no controller. Resolved in `onInitialize`. */
@@ -21208,6 +21328,7 @@ var RecorderV2Addon = class extends BaseAddon {
21208
21328
  now: () => Date.now()
21209
21329
  });
21210
21330
  this.controller = new RecordingController({
21331
+ resolveFfmpeg: this.pinnedFfmpeg,
21211
21332
  ...this.stagingGuard !== null ? { stagingGuard: this.stagingGuard } : {},
21212
21333
  recoverStagingDirectory: (input) => this.recoverReapedDirectory(input),
21213
21334
  heapProbe: this.heapProbe,
@@ -21355,12 +21476,14 @@ var RecorderV2Addon = class extends BaseAddon {
21355
21476
  gifRender: (input) => renderGif({
21356
21477
  deps: recordingDeps,
21357
21478
  exportsDir: this.exportsDirOrThrow(),
21358
- spawn
21479
+ spawn,
21480
+ resolveFfmpeg: this.pinnedFfmpeg
21359
21481
  }, input),
21360
21482
  clipRender: (input) => renderClip({
21361
21483
  deps: recordingDeps,
21362
21484
  exportsDir: this.exportsDirOrThrow(),
21363
- spawn
21485
+ spawn,
21486
+ resolveFfmpeg: this.pinnedFfmpeg
21364
21487
  }, input),
21365
21488
  locations: () => this.resolvedLocations,
21366
21489
  refreshLocations: () => this.refreshLocations(),
@@ -21489,6 +21612,7 @@ var RecorderV2Addon = class extends BaseAddon {
21489
21612
  };
21490
21613
  this.exportEngine = new ExportEngine({
21491
21614
  spawn,
21615
+ resolveFfmpeg: this.pinnedFfmpeg,
21492
21616
  logger: this.ctx.logger.child("export"),
21493
21617
  state: exportsState,
21494
21618
  resolvePlaylist: async (rec) => {
@@ -21641,7 +21765,8 @@ var RecorderV2Addon = class extends BaseAddon {
21641
21765
  const stillService = new StillFrameService({
21642
21766
  lowSegmentsAt: (deviceId, epochMs) => this.index.segmentsOverlapping(deviceId, "low", epochMs, epochMs + 1),
21643
21767
  resolveAbsPath: (row) => this.resolveSegmentAbsPath(row),
21644
- logger: this.ctx.logger.child("Still")
21768
+ logger: this.ctx.logger.child("Still"),
21769
+ resolveFfmpeg: this.pinnedFfmpeg
21645
21770
  });
21646
21771
  const served = await this.ctx.dataPlane?.serve({
21647
21772
  prefix: STILL_PREFIX,