@camstack/addon-pipeline 1.2.369 → 1.2.371

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 (37) hide show
  1. package/dist/audio-analyzer/index.js +3 -3
  2. package/dist/audio-analyzer/index.mjs +3 -3
  3. package/dist/{custom-model-catalog-uHzR2xWY.mjs → custom-model-catalog-C6C2cSwd.mjs} +2 -2
  4. package/dist/{custom-model-catalog-C3PYjN8c.js → custom-model-catalog-n9v8MUL6.js} +2 -2
  5. package/dist/detection-pipeline/index.js +4 -4
  6. package/dist/detection-pipeline/index.mjs +4 -4
  7. package/dist/{dist-DKSwif2M.mjs → dist-8AEiKOFO.mjs} +386 -30
  8. package/dist/{dist-D3PtiAhd.js → dist-DmuX7IFJ.js} +403 -29
  9. package/dist/{model-catalogs-CVd4TcTQ.mjs → model-catalogs-BSWAHZJh.mjs} +1 -1
  10. package/dist/{model-catalogs-kGqNkIuV.js → model-catalogs-DO4aA1qo.js} +1 -1
  11. package/dist/motion-wasm/index.js +1 -1
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/{node-C_Fqti3A.mjs → node-Bd4va7c-.mjs} +1 -1
  14. package/dist/{node--15m1sN3.js → node-DqA6nD1z.js} +1 -1
  15. package/dist/pipeline-runner/index.js +3 -3
  16. package/dist/pipeline-runner/index.mjs +3 -3
  17. package/dist/{process-memory-DELcKfDf.js → process-memory-2OQ2oxrz.js} +1 -1
  18. package/dist/{process-memory-Dq_Omilr.mjs → process-memory-BvS2Z88w.mjs} +1 -1
  19. package/dist/recorder/index.js +184 -54
  20. package/dist/recorder/index.mjs +184 -54
  21. package/dist/{segment-demux-js-DSYbEqvj.js → segment-demux-js-BkGK1rhu.js} +1 -1
  22. package/dist/{segment-demux-js-BTUbXt7b.mjs → segment-demux-js-By9MLn_g.mjs} +1 -1
  23. package/dist/stream-broker/_stub.js +1188 -861
  24. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CMHYV8dG.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DjoAY42S.mjs} +3 -3
  25. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-C3ssSNKh.mjs +26 -0
  26. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-nua-KuDg.mjs +26 -0
  27. package/dist/stream-broker/demux-worker-child.js +1 -1
  28. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  29. package/dist/stream-broker/{hostInit-Cz1kTtGe.mjs → hostInit-DPiGNc5u.mjs} +3 -3
  30. package/dist/stream-broker/index.js +302 -90
  31. package/dist/stream-broker/index.mjs +302 -90
  32. package/dist/stream-broker/remoteEntry.js +1 -1
  33. package/i18n/en.json +8 -0
  34. package/i18n/it.json +8 -0
  35. package/package.json +6 -1
  36. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DBku2n1V.mjs +0 -26
  37. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DNC4tvrD.mjs +0 -26
@@ -1,5 +1,5 @@
1
- import { $t as recordingExportCapability, An as BaseAddon, At as isFirstLevelMacroClass, B as RECORDING_SOURCE_CAMSTACK_ADDON, Bt as modeMayWrite, C as EVENT_PAD_MS, Cn as evaluateSensorEdge, Ct as exportSubjectOf, Dn as DeviceType, En as DeviceFeature, Et as gbToBytes, G as VIDEOCLIPS_MAX_OFFER_BYTES, H as RecordingConfigSchema, It as mayReadLocation, Jn as selectAssignedProfileSlots, K as VISIT_MERGE_GAP_MS, Kn as nodePin, Ln as customAction, Lt as mayWriteToLocation, N as OpsLogEntrySchema, Nt as loadContributionCapability, On as errMsg$1, Qn as boolean, Qt as recordingCapability, R as RECORDING_EXPORT_MAX_READ_BYTES, Rn as defineCustomActions, Rt as memoizePinnedFfmpeg, S as EVENT_OWNER_TYPES, St as evictionPolicyOfLocation, T as ExportRecordSchema, U as RecordingSignalStatusSchema, Vn as hydrateSchema, Xn as _enum, Yn as sleep$1, Zn as array, Zt as recordingArchiveCapability, _n as videoclipsCapability, ar as record, at as clipExportFailureOf, cr as EventCategory, en as recordingSignalCapability, et as batteryCapability, fn as storageEvictableCapability, ft as deriveBatteryPresence, hn as stripRetiredBandPreBufferSec, in as resolveLocationMode, ir as preprocess, it as clipExportFailure, mt as deriveRecordingMode, nn as resolveCreateExportSubject, nr as number, o as BatteryStatusSchema, or as string, pn as storageOccupancyCapability, rr as object, s as CLIP_BROKER_PACED_RATES, sn as resolveRecordingProfiles, v as DEFAULT_EVENTS_BAND_BUFFER_SEC, xt as evictionPolicyForMode, z as RECORDING_SOURCE_CAMSTACK, zt as migrateLegacyDeviceSignalConfig } from "../dist-DKSwif2M.mjs";
2
- import { a as physicalRootOf, i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-C_Fqti3A.mjs";
1
+ import { $n as _enum, $t as recordingArchiveCapability, An as errMsg$1, B as RECORDING_SOURCE_CAMSTACK_ADDON, Bn as defineCustomActions, Bt as memoizePinnedFfmpeg, C as EVENT_PAD_MS, Ct as evictionPolicyForMode, Ft as loadContributionCapability, G as VIDEOCLIPS_MAX_OFFER_BYTES, H as RecordingConfigSchema, Ht as modeMayWrite, Jn as nodePin, K as VISIT_MERGE_GAP_MS, Mn as BaseAddon, Mt as isFirstLevelMacroClass, N as OpsLogEntrySchema, On as DeviceFeature, Ot as gbToBytes, Qn as userError, R as RECORDING_EXPORT_MAX_READ_BYTES, Rt as mayReadLocation, S as EVENT_OWNER_TYPES, T as ExportRecordSchema, Tn as evaluateSensorEdge, Tt as exportSubjectOf, U as RecordingSignalStatusSchema, Un as hydrateSchema, Vt as migrateLegacyDeviceSignalConfig, Xn as selectAssignedProfileSlots, Z as armRemovalReconcile, Zn as sleep$1, _n as stripRetiredBandPreBufferSec, ar as number, at as clipExportFailure, cr as record, dr as EventCategory, en as recordingCapability, er as array, hn as storageOccupancyCapability, ht as deriveRecordingMode, in as resolveCreateExportSubject, kn as DeviceType, ln as resolveRecordingProfiles, lr as string, mn as storageEvictableCapability, nn as recordingSignalCapability, o as BatteryStatusSchema, on as resolveLocationMode, or as object, ot as clipExportFailureOf, pt as deriveBatteryPresence, s as CLIP_BROKER_PACED_RATES, sr as preprocess, tn as recordingExportCapability, tr as boolean, tt as batteryCapability, v as DEFAULT_EVENTS_BAND_BUFFER_SEC, vt as deviceRemovalHookCapability, wt as evictionPolicyOfLocation, yn as videoclipsCapability, z as RECORDING_SOURCE_CAMSTACK, zn as customAction, zt as mayWriteToLocation } from "../dist-8AEiKOFO.mjs";
2
+ import { a as physicalRootOf, i as NO_COST_CLAIM, t as ChildCostRegistry } from "../node-Bd4va7c-.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";
@@ -4989,9 +4989,10 @@ function archiveUsageComplete(ledger, locationIds) {
4989
4989
  /**
4990
4990
  * Per-device recording-config persistence for the recorder addon.
4991
4991
  *
4992
- * ONE ROW PER CAMERA in `recorder:recording-configs`, through
4993
- * {@link RowMapStore}. Asking what camera 617 records is a primary-key lookup;
4994
- * saving its config writes that row and touches no other.
4992
+ * A camera's config is its PER-DEVICE SETTINGS (D865), key `recordingConfig`,
4993
+ * through `ctx.settings.*DeviceStore`. Asking what camera 617 records is one
4994
+ * scope read; saving its config writes that key and touches no other camera.
4995
+ * Deleting the camera clears the scope centrally.
4995
4996
  *
4996
4997
  * ## What this replaced, and why
4997
4998
  *
@@ -5012,54 +5013,79 @@ function archiveUsageComplete(ledger, locationIds) {
5012
5013
  * empty fallback (= the whole fleet silently unconfigured).
5013
5014
  */
5014
5015
  /**
5015
- * @durable class=config owner=recorder
5016
- * write="one row per camera, written by `saveDeviceConfig` (the
5017
- * `recording-archive` cap's setDeviceConfig / band authoring) — the only
5018
- * writer"
5019
- * retention="none by age. A row goes only when the operator deletes the camera's
5020
- * configuration; an unconfigured camera has no row and reads as DEFAULT_DEVICE_CONFIG"
5016
+ * READ is forgiving where the wire contract is strict — see the header.
5017
+ * `.strip()` keeps a row written before a key was retired readable.
5018
+ *
5019
+ * The preprocess is the D371→D380 migration, applied on EVERY read so a row
5020
+ * behaves correctly from the first read of the boot, before
5021
+ * {@link migrateLegacyDeviceSignalRows} has persisted anything. Pure and
5022
+ * idempotent — a row with no legacy key passes through by reference.
5021
5023
  */
5022
- var RECORDING_CONFIGS_COLLECTION = "recorder:recording-configs";
5023
- var RECORDING_CONFIGS_SPEC = {
5024
- collection: RECORDING_CONFIGS_COLLECTION,
5025
- schema: preprocess((raw) => migrateLegacyDeviceSignalConfig(raw).value, RecordingConfigSchema.strip()).transform((config) => stripRetiredBandPreBufferSec(config).config),
5026
- columns: [
5027
- {
5028
- name: "deviceId",
5029
- type: "INTEGER",
5030
- notNull: true
5031
- },
5032
- {
5033
- name: "enabled",
5034
- type: "BOOLEAN",
5035
- notNull: true
5036
- },
5037
- {
5038
- name: "mode",
5039
- type: "TEXT"
5024
+ var StoredRecordingConfigSchema = preprocess((raw) => migrateLegacyDeviceSignalConfig(raw).value, RecordingConfigSchema.strip()).transform((config) => stripRetiredBandPreBufferSec(config).config);
5025
+ /** The per-device settings key a camera's recording config lives under. */
5026
+ var RECORDING_CONFIG_KEY = "recordingConfig";
5027
+ var DeviceSettingsConfigsState = class {
5028
+ deps;
5029
+ constructor(deps) {
5030
+ this.deps = deps;
5031
+ }
5032
+ async declare() {}
5033
+ parse(deviceId, raw) {
5034
+ const parsed = StoredRecordingConfigSchema.safeParse(raw);
5035
+ if (parsed.success) return parsed.data;
5036
+ this.deps.logger.warn("recording config row skipped — it does not parse", {
5037
+ tags: { deviceId },
5038
+ meta: { error: parsed.error.message }
5039
+ });
5040
+ return null;
5041
+ }
5042
+ async read(key) {
5043
+ const deviceId = Number(key);
5044
+ if (!Number.isInteger(deviceId)) return null;
5045
+ const held = (await this.deps.settings.readDeviceStore(deviceId))[RECORDING_CONFIG_KEY];
5046
+ return held === void 0 ? null : this.parse(deviceId, held);
5047
+ }
5048
+ async readAllRaw() {
5049
+ const out = /* @__PURE__ */ new Map();
5050
+ const list = this.deps.settings.listDeviceStoreIds;
5051
+ if (list === void 0) throw new Error("the per-device settings cannot list their devices on this view");
5052
+ const ids = await list.call(this.deps.settings);
5053
+ const batch = this.deps.settings.readDeviceStoreBatch;
5054
+ const blobs = batch === void 0 ? new Map(await Promise.all(ids.map(async (id) => [id, await this.deps.settings.readDeviceStore(id)]))) : await batch.call(this.deps.settings, ids);
5055
+ for (const [id, blob] of blobs) {
5056
+ const held = blob[RECORDING_CONFIG_KEY];
5057
+ if (held !== void 0) out.set(String(id), held);
5040
5058
  }
5041
- ],
5042
- project: (key, value) => ({
5043
- deviceId: Number(key),
5044
- enabled: value.enabled,
5045
- mode: value.mode ?? null
5046
- }),
5047
- deviceIdColumn: "deviceId",
5048
- loadLimit: 2e4
5059
+ return out;
5060
+ }
5061
+ async readAll() {
5062
+ const out = /* @__PURE__ */ new Map();
5063
+ for (const [key, raw] of await this.readAllRaw()) {
5064
+ const parsed = this.parse(Number(key), raw);
5065
+ if (parsed !== null) out.set(key, parsed);
5066
+ }
5067
+ return out;
5068
+ }
5069
+ async write(key, value) {
5070
+ const deviceId = Number(key);
5071
+ if (!Number.isInteger(deviceId)) throw new Error(`recording config: bad device id "${key}"`);
5072
+ await this.deps.settings.writeDeviceStore(deviceId, { [RECORDING_CONFIG_KEY]: value });
5073
+ }
5074
+ async remove(key) {
5075
+ const deviceId = Number(key);
5076
+ if (!Number.isInteger(deviceId)) return;
5077
+ await this.deps.settings.writeDeviceStore(deviceId, { [RECORDING_CONFIG_KEY]: void 0 });
5078
+ }
5049
5079
  };
5080
+ /** Build the handle over the standard per-device settings. */
5081
+ function recordingConfigsState(deps) {
5082
+ return new DeviceSettingsConfigsState(deps);
5083
+ }
5050
5084
  /** Cold-start default for a camera that has never been configured: disabled, no bands. */
5051
5085
  var DEFAULT_DEVICE_CONFIG = {
5052
5086
  enabled: false,
5053
5087
  bands: []
5054
5088
  };
5055
- /** Build the handle. `declare()` MUST run once at boot before any read/write. */
5056
- function recordingConfigsState(store, logger) {
5057
- return new RowMapStore({
5058
- spec: RECORDING_CONFIGS_SPEC,
5059
- store,
5060
- logger
5061
- });
5062
- }
5063
5089
  /** Read every persisted device config, keyed by numeric deviceId. */
5064
5090
  async function readDeviceConfigs(state) {
5065
5091
  const rows = await state.readAll();
@@ -6186,6 +6212,7 @@ Object.freeze({
6186
6212
  "device-export": ["listExposedDevices", "listSupportedDeviceKinds"],
6187
6213
  "device-extension": ["listSlots", "listSources"],
6188
6214
  "device-provider": ["discoverDevices", "getDevices"],
6215
+ "device-removal-hook": ["listDeviceData"],
6189
6216
  "failure-contribution": ["list"],
6190
6217
  "llm": [
6191
6218
  "getDefaults",
@@ -11098,7 +11125,7 @@ function buildRecordingProvider(deps) {
11098
11125
  limit
11099
11126
  }) ?? [],
11100
11127
  renderClip: async (input) => {
11101
- if (!deps.clipRender) throw new Error("clip rendering unavailable on this node");
11128
+ if (!deps.clipRender) throw userError("recording.clip-render-unavailable", void 0, "clip rendering unavailable on this node");
11102
11129
  return deps.clipRender(input);
11103
11130
  },
11104
11131
  renderGif: async (input) => {
@@ -20071,7 +20098,7 @@ async function hasFootage(index, deviceId, profile, fromMs, toMs) {
20071
20098
  /** Load a record or throw a clear NOT_FOUND-style error. */
20072
20099
  async function requireRecord(state, exportId) {
20073
20100
  const rec = await getExportRecord(state, exportId);
20074
- if (!rec) throw new Error(`recording-export: unknown export "${exportId}"`);
20101
+ if (!rec) throw userError("recording.export-unknown", { id: exportId }, `recording-export: unknown export "${exportId}"`);
20075
20102
  return rec;
20076
20103
  }
20077
20104
  /** Build the `recordingExport` provider over the injected deps. */
@@ -20184,11 +20211,14 @@ function buildRecordingExportProvider(deps) {
20184
20211
  if (subject.kind === "clip") return await createClipExport(subject, input.options);
20185
20212
  const { deviceId, profiles, fromMs, toMs } = subject;
20186
20213
  const { options } = input;
20187
- if (!(fromMs < toMs)) throw new Error(`recording-export: invalid range ${fromMs}..${toMs}`);
20214
+ if (!(fromMs < toMs)) throw userError("recording.export-invalid-range", {
20215
+ fromMs,
20216
+ toMs
20217
+ }, `recording-export: invalid range ${fromMs}..${toMs}`);
20188
20218
  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}`);
20189
20219
  const now = deps.now();
20190
20220
  const [head, ...rest] = profiles;
20191
- if (head === void 0) throw new Error("recording-export: profiles must contain at least one profile");
20221
+ if (head === void 0) throw userError("recording.export-no-profiles", void 0, "recording-export: profiles must contain at least one profile");
20192
20222
  const first = await queueOne({
20193
20223
  deviceId,
20194
20224
  profile: head,
@@ -20239,7 +20269,10 @@ function buildRecordingExportProvider(deps) {
20239
20269
  state: rec.state
20240
20270
  }
20241
20271
  });
20242
- throw new Error(`recording-export: export "${exportId}" is not ready (${rec.state})`);
20272
+ throw userError("recording.export-not-ready", {
20273
+ id: exportId,
20274
+ state: rec.state
20275
+ }, `recording-export: export "${exportId}" is not ready (${rec.state})`);
20243
20276
  }
20244
20277
  if (deps.now() >= rec.expiresAt) {
20245
20278
  deps.logger.warn("export byte read refused: export has expired", {
@@ -20249,7 +20282,7 @@ function buildRecordingExportProvider(deps) {
20249
20282
  expiresAt: rec.expiresAt
20250
20283
  }
20251
20284
  });
20252
- throw new Error(`recording-export: export "${exportId}" has expired`);
20285
+ throw userError("recording.export-expired", { id: exportId }, `recording-export: export "${exportId}" has expired`);
20253
20286
  }
20254
20287
  const file = deps.outPath(rec);
20255
20288
  let size;
@@ -20263,7 +20296,7 @@ function buildRecordingExportProvider(deps) {
20263
20296
  error: String(err)
20264
20297
  }
20265
20298
  });
20266
- throw new Error(`recording-export: export "${exportId}" has no file on disk`, { cause: err });
20299
+ throw Object.assign(userError("recording.export-no-file", { id: exportId }, `recording-export: export "${exportId}" has no file on disk`), { cause: err });
20267
20300
  }
20268
20301
  if (size > 52428800) {
20269
20302
  const shrink = deps.shrinkToBound;
@@ -20344,6 +20377,75 @@ function buildRecordingExportProvider(deps) {
20344
20377
  * the host extracted from `CAMSTACK_HUB_URL`).
20345
20378
  */
20346
20379
  var resolveRecordingHubHostname = resolveHubHostname;
20380
+ async function nextVictims(deps, deviceId) {
20381
+ const archive = deps.archive?.() ?? null;
20382
+ if (archive === null) return null;
20383
+ const victims = [];
20384
+ for await (const page of archive.streamSegments({ deviceId })) {
20385
+ victims.push(...page);
20386
+ if (victims.length >= 2e4) break;
20387
+ }
20388
+ return victims;
20389
+ }
20390
+ /** Delete every segment the archive holds for `deviceId`, across every location of this node. */
20391
+ async function purgeDeviceFootage(deps, deviceId) {
20392
+ let deletedBuckets = 0;
20393
+ let reclaimedBytes = 0;
20394
+ for (let pass = 0; pass < 200; pass++) {
20395
+ const victims = await nextVictims(deps, deviceId);
20396
+ if (victims === null) throw new Error("no durable hour ledger on this node — footage cannot be enumerated");
20397
+ if (victims.length === 0) break;
20398
+ const freed = await evictVictims(deps, deviceId, victims);
20399
+ deletedBuckets += freed.deletedBuckets;
20400
+ reclaimedBytes += freed.reclaimedBytes;
20401
+ if (freed.deletedBuckets === 0) break;
20402
+ }
20403
+ const census = await deps.archive?.()?.census({ deviceId });
20404
+ if (census === null || census === void 0 || census.segments > 0) throw new Error(`footage of device ${deviceId} is not fully removed (${census?.segments ?? "unknown"} segments left)`);
20405
+ if (deletedBuckets > 0) await deps.opsLog?.append({
20406
+ op: "prune",
20407
+ reason: "orphaned-device",
20408
+ deviceId,
20409
+ itemsAffected: deletedBuckets,
20410
+ bytesReclaimed: reclaimedBytes,
20411
+ detail: "the device was deleted — its footage was removed with it"
20412
+ });
20413
+ return {
20414
+ deletedBuckets,
20415
+ reclaimedBytes
20416
+ };
20417
+ }
20418
+ function createRecorderRemovalHook(deps) {
20419
+ const removeDevice = async (input) => {
20420
+ const { deviceId } = input;
20421
+ const footage = await purgeDeviceFootage(deps, deviceId);
20422
+ const counts = { footage: footage.deletedBuckets };
20423
+ if (footage.deletedBuckets > 0) deps.logger.info("recorder: removed the data of a deleted device", {
20424
+ tags: { deviceId },
20425
+ meta: {
20426
+ counts,
20427
+ reclaimedBytes: footage.reclaimedBytes,
20428
+ deletedAt: input.deletedAt
20429
+ }
20430
+ });
20431
+ return { counts };
20432
+ };
20433
+ return {
20434
+ onDeviceRemoved: removeDevice,
20435
+ listDeviceData: async () => {
20436
+ const out = [];
20437
+ const locationIds = new Set(deps.locations().map((l) => l.id));
20438
+ const usage = deps.archiveDeviceUsage?.(locationIds) ?? null;
20439
+ if (usage === null) throw new Error("no durable hour ledger on this node — footage cannot be enumerated");
20440
+ for (const [deviceId, u] of usage) if (u.segments > 0) out.push({
20441
+ store: "footage",
20442
+ deviceId,
20443
+ count: u.segments
20444
+ });
20445
+ return out;
20446
+ }
20447
+ };
20448
+ }
20347
20449
  //#endregion
20348
20450
  //#region src/recorder/addon/recording-source-provider.ts
20349
20451
  /** What the picker calls us. */
@@ -20850,7 +20952,7 @@ var RECORDER_RETIRED_ROOT_KEYS = [
20850
20952
  },
20851
20953
  {
20852
20954
  key: "recordingConfigs",
20853
- successor: RECORDING_CONFIGS_COLLECTION,
20955
+ successor: "the recorder per-device settings (key recordingConfig)",
20854
20956
  reason: "one row per camera since the flatten; as a blob, asking what camera 617 records parsed every camera in the installation"
20855
20957
  }
20856
20958
  ];
@@ -23205,6 +23307,8 @@ var RecorderV2Addon = class extends BaseAddon {
23205
23307
  * Already ALIAS-GUARDED: a writable same-type duplicate of another
23206
23308
  * location's physical root arrives here demoted to read-only. */
23207
23309
  resolvedLocations = [];
23310
+ /** The `device-removal-hook` provider (D865); its reconcile is armed in `onHubReachable`. */
23311
+ removalHook = null;
23208
23312
  /** Attach-boundary placement (assignment + mount probe + audit). Null on
23209
23313
  * non-recording nodes / before `onInitialize`. */
23210
23314
  placement = null;
@@ -23908,6 +24012,8 @@ var RecorderV2Addon = class extends BaseAddon {
23908
24012
  migrateLegacyDeviceSignalRows(this.configStore(), this.ctx.logger).catch((err) => {
23909
24013
  this.ctx.logger.warn("recorder: device-signal config migration failed", { meta: { error: errMsg$1(err) } });
23910
24014
  }).then(() => warnLostBands(this.configStore(), this.ctx.logger));
24015
+ const removalHook = createRecorderRemovalHook(recordingDeps);
24016
+ this.removalHook = removalHook;
23911
24017
  const providers = [
23912
24018
  {
23913
24019
  capability: recordingArchiveCapability,
@@ -23939,6 +24045,10 @@ var RecorderV2Addon = class extends BaseAddon {
23939
24045
  {
23940
24046
  capability: loadContributionCapability,
23941
24047
  provider: { list: () => this.costRegistry.contributions() }
24048
+ },
24049
+ {
24050
+ capability: deviceRemovalHookCapability,
24051
+ provider: removalHook
23942
24052
  }
23943
24053
  ];
23944
24054
  this.footageDiscard = createFootageDiscardJob({
@@ -24070,6 +24180,12 @@ var RecorderV2Addon = class extends BaseAddon {
24070
24180
  async onHubReachable() {
24071
24181
  if (!this.isRecordingNode) return;
24072
24182
  await this.refreshLocations();
24183
+ const removalHook = this.removalHook;
24184
+ if (removalHook !== null) this.ctx.addDisposer(armRemovalReconcile({
24185
+ api: this.ctx.api,
24186
+ logger: this.ctx.logger,
24187
+ remove: (input) => removalHook.onDeviceRemoved(input)
24188
+ }));
24073
24189
  try {
24074
24190
  this.exportsRoot = await this.ctx.api.storage.resolve.query({
24075
24191
  location: ARTIFACTS_MEDIA_LOCATION,
@@ -25051,19 +25167,25 @@ var RecorderV2Addon = class extends BaseAddon {
25051
25167
  return this.schema({ sections: [{
25052
25168
  id: "cluster-recording",
25053
25169
  title: "Cluster recording",
25170
+ titleKey: "pipeline:globalSettings.s_cluster-recording.title",
25054
25171
  description: "Which node runs recording (continuous + events write path). A SINGLE node — no multi-node balancing; every other node stays fully inert. Default: hub.",
25172
+ descriptionKey: "pipeline:globalSettings.s_cluster-recording.description",
25055
25173
  columns: 1,
25056
25174
  fields: [{
25057
25175
  type: "text",
25058
25176
  key: "recordingNodeId",
25059
25177
  label: "Recording node",
25178
+ labelKey: "pipeline:globalSettings.s_cluster-recording.recordingNodeId.label",
25060
25179
  description: "Node id that records + serves playback (e.g. \"hub\"). Leave \"hub\" unless you intentionally move recording to an agent. Takes effect on addon restart.",
25180
+ descriptionKey: "pipeline:globalSettings.s_cluster-recording.recordingNodeId.description",
25061
25181
  default: RECORDING_NODE_DEFAULT
25062
25182
  }, {
25063
25183
  type: "text",
25064
25184
  key: "recordingHubHostname",
25065
25185
  label: "Hub host (cross-node)",
25186
+ labelKey: "pipeline:globalSettings.s_cluster-recording.recordingHubHostname.label",
25066
25187
  description: "Advanced: only used when the recording node is an AGENT. The hub's host the agent dials to pull broker streams (the RTSP restream port must be reachable there). Leave empty to auto-derive from the agent's hub connection.",
25188
+ descriptionKey: "pipeline:globalSettings.s_cluster-recording.recordingHubHostname.description",
25067
25189
  default: ""
25068
25190
  }]
25069
25191
  }] });
@@ -25137,8 +25259,16 @@ var RecorderV2Addon = class extends BaseAddon {
25137
25259
  this.censusedSettingsStoreClient ??= censusStoreClient(this.ctx.api.settingsStore, this.readCensus);
25138
25260
  return this.censusedSettingsStoreClient;
25139
25261
  }
25262
+ configsState = null;
25263
+ /** A camera's recording config — its per-device settings (D865), migrated on read. */
25140
25264
  configStore() {
25141
- return recordingConfigsState(this.censusedSettingsStore(), this.ctx.logger);
25265
+ const settings = this.ctx.settings;
25266
+ if (settings === void 0) throw new Error("recorder: device settings are not available");
25267
+ this.configsState ??= recordingConfigsState({
25268
+ settings,
25269
+ logger: this.ctx.logger
25270
+ });
25271
+ return this.configsState;
25142
25272
  }
25143
25273
  /** Durable handle over the exportId→ExportRecord blob (same `BaseAddon.state()`
25144
25274
  * wrapper as the config store). */
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("./chunk-emK7D4bc.js");
2
- const require_dist = require("./dist-D3PtiAhd.js");
2
+ const require_dist = require("./dist-DmuX7IFJ.js");
3
3
  let node_child_process = require("node:child_process");
4
4
  //#region src/stream-broker/recorded/avcc-annexb.ts
5
5
  /**
@@ -1,5 +1,5 @@
1
1
  import { r as __toESM, t as __commonJSMin } from "./chunk-DnnnRqeS.mjs";
2
- import { On as errMsg } from "./dist-DKSwif2M.mjs";
2
+ import { An as errMsg } from "./dist-8AEiKOFO.mjs";
3
3
  import { spawn } from "node:child_process";
4
4
  //#region src/stream-broker/recorded/avcc-annexb.ts
5
5
  /**