@camstack/addon-pipeline 1.2.87 → 1.2.89

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 (28) hide show
  1. package/dist/{addon-utils-D4cfzq_G.js → addon-utils-yc-chXuC.js} +1 -1
  2. package/dist/audio-analyzer/index.js +2 -2
  3. package/dist/audio-analyzer/index.mjs +1 -1
  4. package/dist/detection-pipeline/index.js +4 -4
  5. package/dist/detection-pipeline/index.mjs +2 -2
  6. package/dist/{dist-DHQjHc58.mjs → dist-BJYwWEZ9.mjs} +62 -12
  7. package/dist/{dist-Jyy7mZzp.js → dist-n4Nbnxx5.js} +62 -12
  8. package/dist/{event-loop-stall-monitor-Dh7uvmRd.mjs → event-loop-stall-monitor-BiPffk0D.mjs} +1 -1
  9. package/dist/{event-loop-stall-monitor-DwOjLeEM.js → event-loop-stall-monitor-DG7QY00k.js} +1 -1
  10. package/dist/{lazy-sharp-C8FjN01L.js → lazy-sharp-EoSOHgoj.js} +1 -1
  11. package/dist/motion-wasm/index.js +2 -2
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/pipeline-runner/index.js +4 -4
  14. package/dist/pipeline-runner/index.mjs +3 -3
  15. package/dist/recorder/index.js +189 -51
  16. package/dist/recorder/index.mjs +188 -50
  17. package/dist/session-decode/decode-worker-child.js +2 -2
  18. package/dist/session-decode/decode-worker-child.mjs +1 -1
  19. package/dist/stream-broker/_stub.js +1 -1
  20. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D-4L5i5Y.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CsrMZYLB.mjs} +3 -3
  21. package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-B2scqOH7.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DNSELPGi.mjs} +1 -1
  22. package/dist/stream-broker/{hostInit-Dsc30ipV.mjs → hostInit-DoFovQmc.mjs} +3 -3
  23. package/dist/stream-broker/index.js +3 -2
  24. package/dist/stream-broker/index.mjs +3 -2
  25. package/dist/stream-broker/remoteEntry.js +1 -1
  26. package/dist/{worker-protocol-DgKV4U9F.js → worker-protocol-DjuxhGo3.js} +1 -1
  27. package/dist/{worker-protocol-bhtyPfnr.mjs → worker-protocol-XZ1e36rM.mjs} +1 -1
  28. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
- const require_dist = require("../dist-Jyy7mZzp.js");
1
+ const require_dist = require("../dist-n4Nbnxx5.js");
2
2
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
3
- const require_addon_utils = require("../addon-utils-D4cfzq_G.js");
3
+ const require_addon_utils = require("../addon-utils-yc-chXuC.js");
4
4
  let node_crypto = require("node:crypto");
5
5
  let node_child_process = require("node:child_process");
6
6
  let node_path = require("node:path");
@@ -2634,6 +2634,28 @@ function withStreamTimeDenseRanges(input) {
2634
2634
  }
2635
2635
  //#endregion
2636
2636
  //#region src/recorder/addon/export-ffmpeg-args.ts
2637
+ /**
2638
+ * ffmpeg argv builder for recording EXPORTS (render a footage range → one MP4).
2639
+ *
2640
+ * Input is the export's OWN source playlist for the range — a per-export file
2641
+ * whose segment URIs are absolute on-disk paths (`export-source-playlist.ts`).
2642
+ * It is deliberately NOT the playback manifest's `localMasterPath`: those URIs
2643
+ * resolve only through the data-plane's multi-root search, which ffmpeg does not
2644
+ * have. Output is a single faststart MP4. Three shapes, from `ExportOptions`:
2645
+ * - 1× + audio → pure remux (`-c copy`), zero re-encode.
2646
+ * - speed ≠ 1 → `setpts=PTS/speed` video re-encode; audio kept
2647
+ * only for 0.5–2× (single `atempo`), else dropped.
2648
+ * - timelapse → sample one frame per `everyMs`, output at
2649
+ * `outputFps`, always silent.
2650
+ * Distinct from `buildPassthroughArgs` (live segmentation) — do not conflate.
2651
+ *
2652
+ * A **timelapse of a long HLS concat** is two argv sets, not one. Pass 1
2653
+ * (`buildConcatPassArgs`) remuxes the playlist to a single mpegts with `-c
2654
+ * copy`. Pass 2 (`buildExportArgs` with that file as `-i`) then selects
2655
+ * frames. Selecting WHILE walking `#EXT-X-DISCONTINUITY` (one per 10s
2656
+ * segment from `-reset_timestamps`) is what parked a 30h salone job at
2657
+ * `rendering 0%` / 0% CPU — ffmpeg never left the concat demuxer.
2658
+ */
2637
2659
  /** Widest [min,max] speed a single ffmpeg `atempo` filter accepts. Outside this
2638
2660
  * band audio is dropped rather than chained (kept simple + predictable). */
2639
2661
  var ATEMPO_MIN = .5;
@@ -2697,6 +2719,29 @@ function selectPredicate(everySec, dense) {
2697
2719
  if (dense === void 0 || dense.ranges.length === 0) return `${seed}+${elapsed(everySec)}`;
2698
2720
  return `${seed}+if(${dense.ranges.map((r) => `between(t,${sec(r.fromSec)},${sec(r.toSec)})`).join("+")},${elapsed(dense.everyMs / 1e3)},${elapsed(everySec)})`;
2699
2721
  }
2722
+ /**
2723
+ * Sidecar for timelapse pass 1: same directory as the final MP4, `.concat.ts`.
2724
+ * mpegts (not MP4) because `-c copy` across HLS discontinuities is reliable
2725
+ * in a transport stream and often is not in a fragmented MP4.
2726
+ */
2727
+ function concatPassOutPath(outPath) {
2728
+ const dir = node_path.default.dirname(outPath);
2729
+ const base = node_path.default.basename(outPath, node_path.default.extname(outPath));
2730
+ return node_path.default.join(dir, `${base}.concat.ts`);
2731
+ }
2732
+ /** Pass 1 of a timelapse: remux the HLS playlist into one continuous file. */
2733
+ function buildConcatPassArgs(input) {
2734
+ return [
2735
+ "-i",
2736
+ input.inputPlaylist,
2737
+ "-c",
2738
+ "copy",
2739
+ "-an",
2740
+ "-f",
2741
+ "mpegts",
2742
+ input.outPath
2743
+ ];
2744
+ }
2700
2745
  /** Build the ffmpeg argv for one export render. */
2701
2746
  function buildExportArgs(input) {
2702
2747
  const { inputPlaylist, outPath, options } = input;
@@ -2983,12 +3028,6 @@ var ExportEngine = class {
2983
3028
  /** Spawn ffmpeg for one render and resolve when it has exited + finalized. */
2984
3029
  async spawnRender(id, rec, playlist, options) {
2985
3030
  const outPath = this.deps.outPath(rec);
2986
- const args = buildExportArgs({
2987
- inputPlaylist: playlist,
2988
- outPath,
2989
- options
2990
- });
2991
- const totalSec = expectedOutSeconds(rec);
2992
3031
  this.deps.logger.info("export render starting", {
2993
3032
  tags: { deviceId: rec.deviceId },
2994
3033
  meta: {
@@ -2999,11 +3038,67 @@ var ExportEngine = class {
2999
3038
  outPath
3000
3039
  }
3001
3040
  });
3041
+ if (options.timelapse) {
3042
+ await this.spawnTimelapse(id, rec, playlist, outPath, options);
3043
+ return;
3044
+ }
3045
+ const args = buildExportArgs({
3046
+ inputPlaylist: playlist,
3047
+ outPath,
3048
+ options
3049
+ });
3050
+ const ran = await this.runFfmpeg(id, args, {
3051
+ totalSec: expectedOutSeconds(rec),
3052
+ floor: 0,
3053
+ ceil: 99
3054
+ });
3055
+ await this.finish(id, ran.code, outPath, ran.stderrTail);
3056
+ }
3057
+ /**
3058
+ * Concat the HLS pieces into one continuous file, THEN select frames.
3059
+ * The select filter on a discontinuity-heavy playlist is what parked a
3060
+ * 30h job at `rendering 0%` — pass 1 is `-c copy` into mpegts so pass 2
3061
+ * sees a single timestamp line.
3062
+ */
3063
+ async spawnTimelapse(id, rec, playlist, outPath, options) {
3064
+ const concatPath = concatPassOutPath(outPath);
3065
+ try {
3066
+ const concatArgs = buildConcatPassArgs({
3067
+ inputPlaylist: playlist,
3068
+ outPath: concatPath
3069
+ });
3070
+ const concatRan = await this.runFfmpeg(id, concatArgs, {
3071
+ totalSec: Math.max(.001, (rec.toMs - rec.fromMs) / 1e3),
3072
+ floor: 0,
3073
+ ceil: 49
3074
+ });
3075
+ if (this.cancelled.has(id) || concatRan.code !== 0) {
3076
+ await this.finish(id, concatRan.code, concatPath, concatRan.stderrTail);
3077
+ return;
3078
+ }
3079
+ const selectArgs = buildExportArgs({
3080
+ inputPlaylist: concatPath,
3081
+ outPath,
3082
+ options
3083
+ });
3084
+ const selectRan = await this.runFfmpeg(id, selectArgs, {
3085
+ totalSec: expectedOutSeconds(rec),
3086
+ floor: 50,
3087
+ ceil: 99
3088
+ });
3089
+ await this.finish(id, selectRan.code, outPath, selectRan.stderrTail);
3090
+ } finally {
3091
+ try {
3092
+ await this.deps.removeFile(concatPath);
3093
+ } catch {}
3094
+ }
3095
+ }
3096
+ runFfmpeg(id, args, progress) {
3002
3097
  this.deps.logger.debug("export ffmpeg argv", { meta: {
3003
3098
  exportId: id,
3004
3099
  args
3005
3100
  } });
3006
- await new Promise((resolve) => {
3101
+ return new Promise((resolve) => {
3007
3102
  const proc = this.deps.spawn(this.deps.ffmpegPath ?? "ffmpeg", args);
3008
3103
  this.activeProc = proc;
3009
3104
  const stderrTail = [];
@@ -3018,7 +3113,8 @@ var ExportEngine = class {
3018
3113
  const m = TIME_RE.exec(line);
3019
3114
  if (m) {
3020
3115
  const sec = Number(m[1]) * 3600 + Number(m[2]) * 60 + Number(m[3]);
3021
- const pct = Math.min(99, Math.round(sec / totalSec * 100));
3116
+ const span = progress.ceil - progress.floor;
3117
+ const pct = Math.min(progress.ceil, progress.floor + Math.round(sec / progress.totalSec * span));
3022
3118
  patchExport(this.deps.state, id, { progressPct: pct });
3023
3119
  this.deps.emit(require_dist.EventCategory.RecordingExportProgress, {
3024
3120
  exportId: id,
@@ -3029,7 +3125,10 @@ var ExportEngine = class {
3029
3125
  });
3030
3126
  proc.on("exit", (code) => {
3031
3127
  this.activeProc = null;
3032
- this.finish(id, typeof code === "number" ? code : null, outPath, stderrTail).then(resolve);
3128
+ resolve({
3129
+ code: typeof code === "number" ? code : null,
3130
+ stderrTail
3131
+ });
3033
3132
  });
3034
3133
  });
3035
3134
  }
@@ -3080,9 +3179,15 @@ var ExportEngine = class {
3080
3179
  outPath,
3081
3180
  bytes
3082
3181
  } });
3182
+ const rec = await getExportRecord(this.deps.state, id);
3083
3183
  this.deps.emit(require_dist.EventCategory.RecordingExportCompleted, {
3084
3184
  exportId: id,
3085
- fileBytes: bytes
3185
+ fileBytes: bytes,
3186
+ ...rec !== null ? {
3187
+ deviceId: rec.deviceId,
3188
+ ...rec.options.title !== void 0 ? { title: rec.options.title } : {},
3189
+ ...rec.options.notifyTargetIds !== void 0 && rec.options.notifyTargetIds.length > 0 ? { notifyTargetIds: rec.options.notifyTargetIds } : {}
3190
+ } : {}
3086
3191
  });
3087
3192
  }
3088
3193
  async fail(id, error) {
@@ -5759,8 +5864,19 @@ function startPeriodicPass(deps) {
5759
5864
  isRunning: () => startedAt !== null
5760
5865
  };
5761
5866
  }
5762
- //#endregion
5763
- //#region src/recorder/addon/readiness-restore.ts
5867
+ async function runBounded(items, concurrency, worker) {
5868
+ if (items.length === 0) return;
5869
+ const queue = [...items];
5870
+ const lanes = Math.max(1, Math.min(concurrency, queue.length));
5871
+ const lane = async () => {
5872
+ for (;;) {
5873
+ const item = queue.shift();
5874
+ if (item === void 0) return;
5875
+ await worker(item);
5876
+ }
5877
+ };
5878
+ await Promise.all(Array.from({ length: lanes }, () => lane()));
5879
+ }
5764
5880
  var ReadinessRestore = class {
5765
5881
  deps;
5766
5882
  pending = /* @__PURE__ */ new Map();
@@ -5770,6 +5886,9 @@ var ReadinessRestore = class {
5770
5886
  constructor(deps) {
5771
5887
  this.deps = deps;
5772
5888
  }
5889
+ get concurrency() {
5890
+ return Math.max(1, this.deps.concurrency ?? 8);
5891
+ }
5773
5892
  /** Seed the pending set, subscribe to readiness, and drain once immediately
5774
5893
  * (covers the already-ready case). Returns when the first drain settles. */
5775
5894
  async start(items) {
@@ -5800,7 +5919,8 @@ var ReadinessRestore = class {
5800
5919
  return this.pending.has(key);
5801
5920
  }
5802
5921
  /** Attempt every still-pending item; repeat once if a readiness signal arrived
5803
- * mid-flight. Re-entrant and self-coalescing. */
5922
+ * mid-flight. Re-entrant and self-coalescing. Overlapping up to
5923
+ * {@link ReadinessRestoreDeps.concurrency}. */
5804
5924
  async drain() {
5805
5925
  if (this.draining) {
5806
5926
  this.rerun = true;
@@ -5811,12 +5931,15 @@ var ReadinessRestore = class {
5811
5931
  try {
5812
5932
  do {
5813
5933
  this.rerun = false;
5814
- for (const [key, value] of Array.from(this.pending)) try {
5815
- await this.deps.attempt(key, value);
5816
- this.pending.delete(key);
5817
- } catch (err) {
5818
- this.deps.onDefer?.(key, value, err);
5819
- }
5934
+ await runBounded(Array.from(this.pending), this.concurrency, async ([key, value]) => {
5935
+ if (!this.pending.has(key)) return;
5936
+ try {
5937
+ await this.deps.attempt(key, value);
5938
+ this.pending.delete(key);
5939
+ } catch (err) {
5940
+ this.deps.onDefer?.(key, value, err);
5941
+ }
5942
+ });
5820
5943
  } while (this.rerun && this.pending.size > 0);
5821
5944
  } finally {
5822
5945
  this.draining = false;
@@ -6511,14 +6634,16 @@ var RecordingController = class {
6511
6634
  } catch (err) {
6512
6635
  this.deps.logger.warn("recorder controller: placement recompute failed", { meta: { error: require_dist.errMsg(err) } });
6513
6636
  }
6514
- for (const id of ids) try {
6515
- await this.evaluateDevice(id);
6516
- } catch (err) {
6517
- this.deps.logger.warn("recorder controller: convergence evaluate failed", {
6518
- tags: { deviceId: id },
6519
- meta: { error: require_dist.errMsg(err) }
6520
- });
6521
- }
6637
+ await runBounded([...ids], 8, async (id) => {
6638
+ try {
6639
+ await this.evaluateDevice(id);
6640
+ } catch (err) {
6641
+ this.deps.logger.warn("recorder controller: convergence evaluate failed", {
6642
+ tags: { deviceId: id },
6643
+ meta: { error: require_dist.errMsg(err) }
6644
+ });
6645
+ }
6646
+ });
6522
6647
  }
6523
6648
  /**
6524
6649
  * Re-evaluate one device after a config change (called from `setDeviceConfig`).
@@ -6942,6 +7067,12 @@ function exportFileName(rec) {
6942
7067
  async function hasFootage(index, deviceId, profile, fromMs, toMs) {
6943
7068
  return (await index.segmentsInWindow(deviceId, profile, fromMs, toMs)).some((s) => s.startMs < toMs && s.startMs + s.durMs > fromMs);
6944
7069
  }
7070
+ /** Canonical `profiles[]`, falling back to the legacy singular `profile`. */
7071
+ function resolveExportProfiles(input) {
7072
+ if (input.profiles !== void 0 && input.profiles.length > 0) return [...input.profiles];
7073
+ if (typeof input.profile === "string" && input.profile.length > 0) return [input.profile];
7074
+ return [];
7075
+ }
6945
7076
  /** Load a record or throw a clear NOT_FOUND-style error. */
6946
7077
  async function requireRecord(state, exportId) {
6947
7078
  const rec = await getExportRecord(state, exportId);
@@ -6951,30 +7082,37 @@ async function requireRecord(state, exportId) {
6951
7082
  /** Build the `recordingExport` provider over the injected deps. */
6952
7083
  function buildRecordingExportProvider(deps) {
6953
7084
  return {
6954
- createExport: async ({ deviceId, profile, fromMs, toMs, options }) => {
7085
+ createExport: async (input) => {
7086
+ const { deviceId, fromMs, toMs, options } = input;
7087
+ const profiles = resolveExportProfiles(input);
7088
+ if (profiles.length < 1) throw new Error("recording-export: profiles must contain at least one profile");
6955
7089
  if (!(fromMs < toMs)) throw new Error(`recording-export: invalid range ${fromMs}..${toMs}`);
6956
- if (!await hasFootage(deps.index, deviceId, profile, fromMs, toMs)) throw new Error(`recording-export: no footage for ${deviceId}/${profile} in ${fromMs}..${toMs}`);
7090
+ for (const profile of profiles) if (!await hasFootage(deps.index, deviceId, profile, fromMs, toMs)) throw new Error(`recording-export: no footage for ${deviceId}/${profile} in ${fromMs}..${toMs}`);
6957
7091
  const now = deps.now();
6958
- const record = {
6959
- id: deps.newId(),
6960
- deviceId,
6961
- profile,
6962
- fromMs,
6963
- toMs,
6964
- options,
6965
- state: "queued",
6966
- progressPct: null,
6967
- fileBytes: null,
6968
- expiresAt: now + options.maxLifeMs,
6969
- deleteAfterDownload: options.deleteAfterDownload,
6970
- downloadedAt: null,
6971
- createdAt: now,
6972
- createdBy: deps.createdBy(),
6973
- error: null
6974
- };
6975
- const saved = await upsertExport(deps.state, record);
6976
- deps.engine.enqueue(saved.id);
6977
- return saved;
7092
+ let first = null;
7093
+ for (const profile of profiles) {
7094
+ const record = {
7095
+ id: deps.newId(),
7096
+ deviceId,
7097
+ profile,
7098
+ fromMs,
7099
+ toMs,
7100
+ options,
7101
+ state: "queued",
7102
+ progressPct: null,
7103
+ fileBytes: null,
7104
+ expiresAt: now + options.maxLifeMs,
7105
+ deleteAfterDownload: options.deleteAfterDownload,
7106
+ downloadedAt: null,
7107
+ createdAt: now,
7108
+ createdBy: deps.createdBy(),
7109
+ error: null
7110
+ };
7111
+ const saved = await upsertExport(deps.state, record);
7112
+ deps.engine.enqueue(saved.id);
7113
+ first ??= saved;
7114
+ }
7115
+ return first;
6978
7116
  },
6979
7117
  listExports: ({ deviceId }) => listExportsFor(deps.state, deviceId),
6980
7118
  getExport: ({ exportId }) => requireRecord(deps.state, exportId),