@camstack/addon-pipeline 1.2.74 → 1.2.76

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 (36) hide show
  1. package/dist/{addon-utils-CQs-AjTJ.js → addon-utils-CTpQfjSR.js} +4 -4
  2. package/dist/audio-analyzer/index.js +5 -6
  3. package/dist/audio-analyzer/index.mjs +3 -3
  4. package/dist/detection-pipeline/index.js +9 -10
  5. package/dist/detection-pipeline/index.mjs +5 -5
  6. package/dist/{dist-CgJ8MRAy.js → dist-CZLjObZZ.js} +1996 -29
  7. package/dist/{dist-CKY0efl3.mjs → dist-zksfWnEA.mjs} +1961 -29
  8. package/dist/{event-loop-stall-monitor-CIapFIez.mjs → event-loop-stall-monitor-BuZA3loB.mjs} +1 -1
  9. package/dist/{event-loop-stall-monitor-Dx-zXcNg.js → event-loop-stall-monitor-CcBQAI28.js} +1 -1
  10. package/dist/lazy-sharp-6oymT_yf.mjs +341 -0
  11. package/dist/lazy-sharp-SWR_D1Um.js +360 -0
  12. package/dist/motion-wasm/index.js +3 -3
  13. package/dist/motion-wasm/index.mjs +3 -3
  14. package/dist/pipeline-runner/index.js +280 -18
  15. package/dist/pipeline-runner/index.mjs +278 -15
  16. package/dist/recorder/index.js +138 -32
  17. package/dist/recorder/index.mjs +137 -30
  18. package/dist/session-decode/decode-worker-child.js +311 -126
  19. package/dist/session-decode/decode-worker-child.mjs +311 -123
  20. package/dist/stream-broker/_stub.js +94 -94
  21. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DVJ4gKkk.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Dd6XOtVn.mjs} +2 -2
  22. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-RYTOS6B-.mjs +26 -0
  23. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DfsLsn2e.mjs +26 -0
  24. package/dist/stream-broker/{hostInit-5SFdfUyc.mjs → hostInit-COc_Q_xX.mjs} +2 -2
  25. package/dist/stream-broker/index.js +12 -13
  26. package/dist/stream-broker/index.mjs +3 -3
  27. package/dist/stream-broker/remoteEntry.js +1 -1
  28. package/dist/{worker-protocol-D44DCkcs.js → worker-protocol-DAQ1iZFK.js} +1 -1
  29. package/dist/{worker-protocol-BzAQPUR-.mjs → worker-protocol-DyX_HbaJ.mjs} +1 -1
  30. package/package.json +1 -1
  31. package/dist/chunk-emK7D4bc.js +0 -35
  32. package/dist/lazy-sharp-B-mb8uWx.mjs +0 -20
  33. package/dist/lazy-sharp-CcYuXtsa.js +0 -25
  34. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-C_plRUtD.mjs +0 -26
  35. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CExO0piw.mjs +0 -26
  36. package/dist/{addon-utils-CZ2xo67g.mjs → addon-utils-A2S9D7pu.mjs} +2 -2
@@ -1,10 +1,10 @@
1
- import { At as string, Dt as number, J as recordingExportCapability, Mt as EventCategory, N as deriveRecordingMode, Ot as object, St as array, Z as storageEvictableCapability, _ as OpsLogEntrySchema, b as RECORDING_EXPORT_MAX_READ_BYTES, c as DEFAULT_EVENTS_BAND_BUFFER_SEC, d as EVENT_PAD_MS, ft as hydrateSchema, gt as nodePin, kt as record, p as ExportRecordSchema, q as recordingCapability, st as BaseAddon, tt as errMsg, ut as DeviceType, x as RecordingConfigSchema, yt as selectAssignedProfileSlots } from "../dist-CKY0efl3.mjs";
1
+ import { At as string, Dt as number, J as recordingExportCapability, Mt as EventCategory, N as deriveRecordingMode, Ot as object, St as array, Z as storageEvictableCapability, _ as OpsLogEntrySchema, b as RECORDING_EXPORT_MAX_READ_BYTES, c as DEFAULT_EVENTS_BAND_BUFFER_SEC, d as EVENT_PAD_MS, ft as hydrateSchema, gt as nodePin, kt as record, p as ExportRecordSchema, q as recordingCapability, st as BaseAddon, tt as errMsg, ut as DeviceType, x as RecordingConfigSchema, yt as selectAssignedProfileSlots } from "../dist-zksfWnEA.mjs";
2
2
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
3
- import { n as createFileDataPlaneHandler, s as parseRangeHeader, t as contentTypeFor } from "../addon-utils-CZ2xo67g.mjs";
3
+ import { n as createFileDataPlaneHandler, s as parseRangeHeader, t as contentTypeFor } from "../addon-utils-A2S9D7pu.mjs";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { spawn } from "node:child_process";
6
- import { createReadStream, promises, realpathSync } from "node:fs";
7
6
  import path from "node:path";
7
+ import { createReadStream, promises, realpathSync } from "node:fs";
8
8
  import { open } from "node:fs/promises";
9
9
  //#region src/recorder/event-capture.ts
10
10
  function markerFromMotion(data, pad) {
@@ -2249,7 +2249,7 @@ var DEFAULT_DEVICE_CONFIG = {
2249
2249
  enabled: false,
2250
2250
  bands: []
2251
2251
  };
2252
- function serialize(map) {
2252
+ function serialize$1(map) {
2253
2253
  const out = {};
2254
2254
  for (const [id, config] of map) out[String(id)] = config;
2255
2255
  return out;
@@ -2304,7 +2304,7 @@ async function saveDeviceConfig(state, deviceId, config) {
2304
2304
  };
2305
2305
  const map = await readDeviceConfigs(state);
2306
2306
  map.set(deviceId, validated);
2307
- await state.set(serialize(map));
2307
+ await state.set(serialize$1(map));
2308
2308
  return validated;
2309
2309
  }
2310
2310
  /**
@@ -2642,6 +2642,33 @@ function buildExportArgs(input) {
2642
2642
  var RECORDING_EXPORTS_KEY = "recordingExports";
2643
2643
  /** Persisted shape: export id → full ExportRecord. */
2644
2644
  var RecordingExportsBlobSchema = record(string(), ExportRecordSchema);
2645
+ /**
2646
+ * One in-flight mutation chain per durable handle.
2647
+ *
2648
+ * EVERY mutation here is a read-modify-write of the SAME blob, and both halves
2649
+ * are async — so two overlapping cycles read the same snapshot and the second
2650
+ * `set` discards whatever the first one added. Last writer wins, silently.
2651
+ *
2652
+ * That is not a hypothetical. The engine patches `progressPct` roughly once a
2653
+ * second for an in-flight render, unawaited; on 2026-08-14 a `createExport` for
2654
+ * camera 615 landed between one such patch's read and its write, the row was
2655
+ * erased, and five seconds later the poll answered `unknown export
2656
+ * "547ea182-…"`. The camera's whole night was lost to it — the scheduler read a
2657
+ * vanished row as a failed render and re-queued, forever.
2658
+ *
2659
+ * Serialising per handle is the smallest fix that makes the loss impossible:
2660
+ * the cycles queue instead of interleaving, and the queue is per state object
2661
+ * so two recorders in one process never contend. A `WeakMap` keeps a disposed
2662
+ * handle collectable. Reads stay OUTSIDE the chain — a blob replacement is
2663
+ * atomic, so a reader always sees one consistent generation.
2664
+ */
2665
+ var mutationChains = /* @__PURE__ */ new WeakMap();
2666
+ /** Run `work` after every mutation already queued for `state`, never before. */
2667
+ async function serialize(state, work) {
2668
+ const next = (mutationChains.get(state) ?? Promise.resolve()).then(work, work);
2669
+ mutationChains.set(state, next.catch(() => void 0));
2670
+ return next;
2671
+ }
2645
2672
  /** Read every persisted export row, keyed by export id. */
2646
2673
  async function readExports(state) {
2647
2674
  const blob = await state.get();
@@ -2650,26 +2677,30 @@ async function readExports(state) {
2650
2677
  /** Insert or replace one export row. Validates at the boundary. Returns it. */
2651
2678
  async function upsertExport(state, record) {
2652
2679
  const validated = ExportRecordSchema.parse(record);
2653
- const map = await readExports(state);
2654
- map.set(validated.id, validated);
2655
- await state.set(Object.fromEntries(map));
2656
- return validated;
2680
+ return serialize(state, async () => {
2681
+ const map = await readExports(state);
2682
+ map.set(validated.id, validated);
2683
+ await state.set(Object.fromEntries(map));
2684
+ return validated;
2685
+ });
2657
2686
  }
2658
2687
  /**
2659
2688
  * Shallow-merge a patch into one export row and persist. Returns the merged
2660
2689
  * record, or null when the id is unknown.
2661
2690
  */
2662
2691
  async function patchExport(state, id, patch) {
2663
- const map = await readExports(state);
2664
- const existing = map.get(id);
2665
- if (!existing) return null;
2666
- const merged = ExportRecordSchema.parse({
2667
- ...existing,
2668
- ...patch
2692
+ return serialize(state, async () => {
2693
+ const map = await readExports(state);
2694
+ const existing = map.get(id);
2695
+ if (!existing) return null;
2696
+ const merged = ExportRecordSchema.parse({
2697
+ ...existing,
2698
+ ...patch
2699
+ });
2700
+ map.set(id, merged);
2701
+ await state.set(Object.fromEntries(map));
2702
+ return merged;
2669
2703
  });
2670
- map.set(id, merged);
2671
- await state.set(Object.fromEntries(map));
2672
- return merged;
2673
2704
  }
2674
2705
  /** Read one export row by id, or null. */
2675
2706
  async function getExportRecord(state, id) {
@@ -3000,6 +3031,80 @@ async function markDownloaded(id, now, deps, scheduleDelete, graceMs) {
3000
3031
  }, graceMs);
3001
3032
  }
3002
3033
  //#endregion
3034
+ //#region src/recorder/addon/export-reconcile.ts
3035
+ /**
3036
+ * States that can only mean "a render was owed and the process went away".
3037
+ *
3038
+ * There is no live render at reconcile time by construction: the engine is
3039
+ * built fresh with an empty queue, and this runs before anything is enqueued.
3040
+ */
3041
+ function isInterrupted(record) {
3042
+ return record.state === "queued" || record.state === "rendering";
3043
+ }
3044
+ /**
3045
+ * Hand every interrupted row back to the engine, once, at boot.
3046
+ *
3047
+ * A row past its `expiresAt` is FAILED rather than re-rendered: hours of
3048
+ * footage re-encoded for a file the janitor is about to drop costs a recording
3049
+ * node an ffmpeg for nothing. Failing it is also what makes the outcome
3050
+ * visible — a row that silently stays `queued` forever is the defect this
3051
+ * module exists to remove, and swapping it for a quieter one would not be a
3052
+ * fix.
3053
+ */
3054
+ async function reconcileInterruptedExports(deps) {
3055
+ const now = deps.now();
3056
+ const interrupted = (await listExportsFor(deps.state)).filter(isInterrupted);
3057
+ if (interrupted.length === 0) return {
3058
+ requeued: 0,
3059
+ expired: 0
3060
+ };
3061
+ let requeued = 0;
3062
+ let expired = 0;
3063
+ for (const row of interrupted) {
3064
+ const tags = { deviceId: row.deviceId };
3065
+ const meta = {
3066
+ exportId: row.id,
3067
+ wasState: row.state,
3068
+ fromMs: row.fromMs,
3069
+ toMs: row.toMs,
3070
+ createdAt: row.createdAt
3071
+ };
3072
+ if (row.expiresAt <= now) {
3073
+ expired += 1;
3074
+ await patchExport(deps.state, row.id, {
3075
+ state: "failed",
3076
+ progressPct: null,
3077
+ error: "interrupted by a restart and expired before it could be rendered"
3078
+ });
3079
+ deps.logger.warn("export abandoned: interrupted by a restart, and already expired", {
3080
+ tags,
3081
+ meta
3082
+ });
3083
+ continue;
3084
+ }
3085
+ requeued += 1;
3086
+ await patchExport(deps.state, row.id, {
3087
+ state: "queued",
3088
+ progressPct: null,
3089
+ error: null
3090
+ });
3091
+ deps.logger.warn("export re-queued: it was interrupted by a restart", {
3092
+ tags,
3093
+ meta
3094
+ });
3095
+ deps.engine.enqueue(row.id);
3096
+ }
3097
+ deps.logger.info("recording exports reconciled after restart", { meta: {
3098
+ interrupted: interrupted.length,
3099
+ requeued,
3100
+ expired
3101
+ } });
3102
+ return {
3103
+ requeued,
3104
+ expired
3105
+ };
3106
+ }
3107
+ //#endregion
3003
3108
  //#region src/recorder/playlist.ts
3004
3109
  /** A VOD variant playlist with an absolute wall-clock anchor per segment. */
3005
3110
  function buildVariantPlaylist(segments) {
@@ -7477,23 +7582,25 @@ var RecorderV2Addon = class extends BaseAddon {
7477
7582
  this.retentionSweeper?.start();
7478
7583
  }
7479
7584
  /**
7480
- * Resume interrupted exports and arm the expiry sweep. A row left `rendering`
7481
- * by a crash/restart has no live ffmpeg, so demote it to `queued` and re-enqueue
7482
- * it alongside any still-`queued` rows. The sweep runs immediately then on an
7483
- * interval; both are best-effort (a failure never blocks boot).
7585
+ * Resume interrupted exports and arm the expiry sweep.
7586
+ *
7587
+ * The resume itself lives in `export-reconcile.ts` pure, tested, and loud
7588
+ * about every row it moves. It used to be an untested loop inlined here that
7589
+ * said nothing at all, so "why is this export still queued an hour later" had
7590
+ * no answer in the log, and a row whose life had already run out was
7591
+ * re-rendered anyway. Both halves are best-effort: a failure never blocks
7592
+ * boot.
7484
7593
  */
7485
7594
  async bootstrapExports() {
7486
7595
  const engine = this.exportEngine;
7487
7596
  if (!engine) return;
7488
- const state = this.exportsStore();
7489
7597
  try {
7490
- for (const rec of await listExportsFor(state)) {
7491
- if (rec.state === "rendering") await patchExport(state, rec.id, {
7492
- state: "queued",
7493
- progressPct: null
7494
- });
7495
- if (rec.state === "queued" || rec.state === "rendering") engine.enqueue(rec.id);
7496
- }
7598
+ await reconcileInterruptedExports({
7599
+ state: this.exportsStore(),
7600
+ engine,
7601
+ now: () => Date.now(),
7602
+ logger: this.ctx.logger.child("export")
7603
+ });
7497
7604
  } catch (err) {
7498
7605
  this.ctx.logger.warn("recorder: export boot re-queue failed", { meta: { error: errMsg(err) } });
7499
7606
  }