@camstack/addon-pipeline 1.1.42 → 1.1.44

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/audio-analyzer/index.js +5 -4
  2. package/dist/audio-analyzer/index.mjs +1 -1
  3. package/dist/chunk-D6vf50IK.js +28 -0
  4. package/dist/detection-pipeline/index.js +535 -115
  5. package/dist/detection-pipeline/index.mjs +529 -110
  6. package/dist/{dist-GFd8M6KO.mjs → dist-CgBGV988.mjs} +214 -186
  7. package/dist/{dist-NvwN60Fq.js → dist-CySHUkXY.js} +219 -185
  8. package/dist/{model-download-service-C-IHWnXx-DxM2DSns.js → model-download-service-C-IHWnXx-BLXzxpRB.js} +3 -29
  9. package/dist/motion-wasm/index.js +1 -1
  10. package/dist/motion-wasm/index.mjs +1 -1
  11. package/dist/pipeline-runner/index.js +122 -287
  12. package/dist/pipeline-runner/index.mjs +118 -285
  13. package/dist/recorder/index.js +78 -31
  14. package/dist/recorder/index.mjs +75 -29
  15. package/dist/{frame-handle-plane-E_AsR77T.mjs → remote-source-plane-B4hRwchu.mjs} +219 -2
  16. package/dist/{frame-handle-plane-B3Fxww8H.js → remote-source-plane-D9m0z1N9.js} +236 -1
  17. package/dist/session-decode/decode-worker-child.js +118 -12
  18. package/dist/session-decode/decode-worker-child.mjs +115 -12
  19. package/dist/stream-broker/_stub.js +550 -523
  20. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CeielP5P.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-G6wtUb3G.mjs} +2 -2
  21. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Dlagq9X_.mjs +26 -0
  22. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BUpF966l.mjs +26 -0
  23. package/dist/stream-broker/{hostInit-DmjqskOR.mjs → hostInit-BSzP3BIj.mjs} +2 -2
  24. package/dist/stream-broker/index.js +840 -196
  25. package/dist/stream-broker/index.mjs +831 -188
  26. package/dist/stream-broker/remoteEntry.js +1 -1
  27. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-CEsPjwzT.js → MaskShapeCanvas-DI4BY7W2-D2ML5TPi.js} +1 -1
  28. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-D43fIRgB.js → MotionZonesSettings-NcxxQN8r-TTWGGaqv.js} +1 -1
  29. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-C3wJxG8V.js → PrivacyMaskSettings-APgPLF7p-CJqHU2nU.js} +1 -1
  30. package/embed-dist/assets/index-C1oaL9s7.js +81 -0
  31. package/embed-dist/assets/{index-CSFtK41z.css → index-CA4kv43T.css} +1 -1
  32. package/embed-dist/index.html +2 -2
  33. package/package.json +2 -2
  34. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DqJvWBKS.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-dGka8yZY.mjs +0 -26
  36. package/embed-dist/assets/index-CX-rILhw.js +0 -81
@@ -1,10 +1,11 @@
1
- const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-DxM2DSns.js");
2
- const require_dist = require("../dist-NvwN60Fq.js");
1
+ const require_chunk = require("../chunk-D6vf50IK.js");
2
+ const require_dist = require("../dist-CySHUkXY.js");
3
3
  const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
4
+ const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-BLXzxpRB.js");
4
5
  let node_child_process = require("node:child_process");
5
6
  let node_fs = require("node:fs");
6
7
  let node_path = require("node:path");
7
- node_path = require_model_download_service_C_IHWnXx.__toESM(node_path);
8
+ node_path = require_chunk.__toESM(node_path);
8
9
  //#region src/recorder/segment-path.ts
9
10
  function segmentRelPath(deviceId, profile, startMs, durMs, bytes) {
10
11
  if (profile.includes("/")) throw new Error(`segmentRelPath: profile must not contain '/': ${profile}`);
@@ -224,7 +225,10 @@ var SegmentStore = class SegmentStore {
224
225
  try {
225
226
  bytes = await this.deps.statBytes(input.flatAbsPath);
226
227
  } catch (err) {
227
- this.deps.logger.warn("segment stat failed", err);
228
+ this.deps.logger.warn("segment stat failed", {
229
+ tags: { deviceId: input.deviceId },
230
+ error: err
231
+ });
228
232
  return null;
229
233
  }
230
234
  if (bytes <= 0) return null;
@@ -233,7 +237,10 @@ var SegmentStore = class SegmentStore {
233
237
  try {
234
238
  await this.deps.moveFile(input.flatAbsPath, toAbs);
235
239
  } catch (err) {
236
- this.deps.logger.warn("segment relocate failed", err);
240
+ this.deps.logger.warn("segment relocate failed", {
241
+ tags: { deviceId: input.deviceId },
242
+ error: err
243
+ });
237
244
  return null;
238
245
  }
239
246
  const row = {
@@ -283,7 +290,10 @@ var SegmentStore = class SegmentStore {
283
290
  }
284
291
  continue;
285
292
  } catch (err) {
286
- this.deps.logger.warn("bucket removeDir failed — falling back to per-file deletes", err);
293
+ this.deps.logger.warn("bucket removeDir failed — falling back to per-file deletes", {
294
+ tags: { deviceId: bucketRows[0]?.deviceId },
295
+ error: err
296
+ });
287
297
  }
288
298
  for (const r of bucketRows) try {
289
299
  await this.deps.deletePath({
@@ -293,7 +303,10 @@ var SegmentStore = class SegmentStore {
293
303
  removed.push(r.path);
294
304
  reclaimed += r.bytes;
295
305
  } catch (err) {
296
- this.deps.logger.warn("segment delete failed", err);
306
+ this.deps.logger.warn("segment delete failed", {
307
+ tags: { deviceId: r.deviceId },
308
+ error: err
309
+ });
297
310
  }
298
311
  }
299
312
  if (removed.length > 0) this.deps.index.removeSegments(removed);
@@ -448,13 +461,16 @@ async function runRedundancyJanitor(deps) {
448
461
  files += vrows.length;
449
462
  bytes += reclaimed;
450
463
  } catch (err) {
451
- deps.logger.warn("janitor: evict failed (skipping)", { meta: {
452
- deviceId,
453
- profile,
454
- location,
455
- count: vrows.length,
456
- error: err instanceof Error ? err.message : String(err)
457
- } });
464
+ deps.logger.warn("janitor: evict failed (skipping)", {
465
+ tags: { deviceId },
466
+ meta: {
467
+ deviceId,
468
+ profile,
469
+ location,
470
+ count: vrows.length,
471
+ error: err instanceof Error ? err.message : String(err)
472
+ }
473
+ });
458
474
  }
459
475
  }
460
476
  }
@@ -1091,9 +1107,12 @@ function buildRecordingProvider(deps) {
1091
1107
  return require_dist.hydrateSchema(buildRecordingDeviceSchema(), {});
1092
1108
  },
1093
1109
  getDeviceLiveContribution: async () => null,
1094
- applyDeviceSettingsPatch: async ({ patch }) => {
1110
+ applyDeviceSettingsPatch: async ({ deviceId, patch }) => {
1095
1111
  const keys = Object.keys(patch ?? {});
1096
- if (keys.length > 0) deps.logger.debug("recorder: ignoring legacy settings patch (widget owns settings)", { meta: { keys } });
1112
+ if (keys.length > 0) deps.logger.debug("recorder: ignoring legacy settings patch (widget owns settings)", {
1113
+ tags: { deviceId },
1114
+ meta: { keys }
1115
+ });
1097
1116
  return { success: true };
1098
1117
  }
1099
1118
  };
@@ -1857,7 +1876,7 @@ var RecordingController = class {
1857
1876
  * assigned sources is ignored (record every assigned source — minimum of 1).
1858
1877
  */
1859
1878
  async resolveProfiles(deviceId, override) {
1860
- const assigned = require_dist.selectAssignedProfileSlots(await this.deps.brokerCall(() => this.deps.api.streamBroker.listAllProfileSlots.query()), deviceId).map((slot) => slot.profile);
1879
+ const assigned = require_dist.selectAssignedProfileSlots(await this.deps.brokerCall(() => this.deps.api.streamBroker.listAllProfileSlots.query(void 0, require_dist.nodePin(this.deps.ownerNodeId))), deviceId).map((slot) => slot.profile);
1861
1880
  if (!override || override.length === 0) return assigned;
1862
1881
  const selected = assigned.filter((p) => override.includes(p));
1863
1882
  return selected.length > 0 ? selected : assigned;
@@ -1877,16 +1896,17 @@ var RecordingController = class {
1877
1896
  profile,
1878
1897
  ...this.deps.consumerHostname !== void 0 ? { hostname: this.deps.consumerHostname } : {},
1879
1898
  tag: `recorder:${deviceId}/${profile}`
1880
- }));
1899
+ }, require_dist.nodePin(this.deps.ownerNodeId)));
1881
1900
  const outDir = node_path.default.join(placement.root, STAGING_DIR_NAME, String(deviceId), profile);
1882
1901
  await node_fs.promises.mkdir(outDir, { recursive: true });
1902
+ const deviceLog = this.deps.logger.withTags({ deviceId });
1883
1903
  const writer = new SegmentWriter({
1884
1904
  rtspUrl: source.url,
1885
1905
  outDir,
1886
1906
  segmentSeconds
1887
1907
  }, {
1888
1908
  spawn: this.deps.spawn,
1889
- logger: this.deps.logger,
1909
+ logger: deviceLog,
1890
1910
  onGaveUp: () => {
1891
1911
  this.recoverWriterGaveUp(deviceId);
1892
1912
  }
@@ -1901,7 +1921,7 @@ var RecordingController = class {
1901
1921
  onFinalized: (startMs, durMs, flatAbsPath) => {
1902
1922
  this.handleFinalizedSegment(deviceId, profile, placement, startMs, durMs, flatAbsPath);
1903
1923
  },
1904
- logger: this.deps.logger
1924
+ logger: deviceLog
1905
1925
  }),
1906
1926
  pipelineKey: source.pipelineKey,
1907
1927
  outDir
@@ -1962,7 +1982,7 @@ var RecordingController = class {
1962
1982
  r.writer.stop();
1963
1983
  } catch {}
1964
1984
  try {
1965
- await this.deps.api.streamBroker.releaseStreamWithCodec.mutate({ pipelineKey: r.pipelineKey });
1985
+ await this.deps.api.streamBroker.releaseStreamWithCodec.mutate({ pipelineKey: r.pipelineKey }, require_dist.nodePin(this.deps.ownerNodeId));
1966
1986
  } catch (err) {
1967
1987
  this.deps.logger.warn("recorder: releaseStreamWithCodec failed", {
1968
1988
  tags: { deviceId },
@@ -2073,9 +2093,13 @@ var MARKER_PAD_POST_MS = require_dist.EVENT_PAD_MS.postMs;
2073
2093
  * form the hub reverse-proxy client path `/addon/recorder/playback`. */
2074
2094
  var RECORDER_ADDON_ID = "recorder";
2075
2095
  var PLAYBACK_PREFIX = "playback";
2076
- /** Node the hub-only `stream-broker` runs on. Every recorder on the hub or on
2077
- * a designated agent gates on, and pulls from, THIS node's broker. */
2078
- var BROKER_HOST_NODE_ID = "hub";
2096
+ /** Narrows the `unknown` extras `SegmentStore`'s logger contract accepts down
2097
+ * to the structured `{ tags, error }` shape the device-scoped call sites
2098
+ * (stat/relocate failures) pass — as opposed to the raw caught error most
2099
+ * other call sites still pass directly. */
2100
+ function isSegmentStoreLogExtras(extras) {
2101
+ return typeof extras === "object" && extras !== null && ("tags" in extras || "error" in extras);
2102
+ }
2079
2103
  var RecorderV2Addon = class extends require_dist.BaseAddon {
2080
2104
  nodeId = "hub";
2081
2105
  /** True when THIS node is the designated recording node (`recordingNodeId`).
@@ -2110,14 +2134,15 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
2110
2134
  } });
2111
2135
  return { providers: [] };
2112
2136
  }
2113
- const consumerHostname = this.nodeId === BROKER_HOST_NODE_ID ? void 0 : resolveRecordingHubHostname(this.config.recordingHubHostname, process.env["CAMSTACK_HUB_URL"]);
2114
- if (this.nodeId !== BROKER_HOST_NODE_ID) {
2137
+ const ingestOwner = await this.resolveIngestOwner();
2138
+ const consumerHostname = this.nodeId === ingestOwner.ownerNodeId ? void 0 : ingestOwner.reachableHost ?? resolveRecordingHubHostname(this.config.recordingHubHostname, process.env["CAMSTACK_HUB_URL"]);
2139
+ if (this.nodeId !== ingestOwner.ownerNodeId) {
2115
2140
  this.ctx.logger.info("recorder is a REMOTE recording node — pulling broker streams cross-node", { meta: {
2116
2141
  ownNodeId: this.nodeId,
2117
- brokerHost: BROKER_HOST_NODE_ID,
2142
+ brokerHost: ingestOwner.ownerNodeId,
2118
2143
  consumerHostname: consumerHostname ?? "(unresolved)"
2119
2144
  } });
2120
- if (consumerHostname === void 0) this.ctx.logger.warn("recorder: could not resolve the hub host for cross-node pull — set the `recordingHubHostname` global setting or CAMSTACK_HUB_URL; recording will not reach this node", { meta: { ownNodeId: this.nodeId } });
2145
+ if (consumerHostname === void 0) this.ctx.logger.warn("recorder: could not resolve the ingest owner host for cross-node pull — set the `recordingHubHostname` global setting or CAMSTACK_HUB_URL; recording will not reach this node", { meta: { ownNodeId: this.nodeId } });
2121
2146
  }
2122
2147
  const segmentStoreDeps = {
2123
2148
  index: this.index,
@@ -2142,14 +2167,21 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
2142
2167
  force: true
2143
2168
  });
2144
2169
  },
2145
- logger: { warn: (message, extras) => this.ctx.logger.warn(message, { meta: { extras } }) }
2170
+ logger: { warn: (message, extras) => {
2171
+ if (isSegmentStoreLogExtras(extras)) this.ctx.logger.warn(message, {
2172
+ tags: extras.tags,
2173
+ meta: { error: extras.error }
2174
+ });
2175
+ else this.ctx.logger.warn(message, { meta: { extras } });
2176
+ } }
2146
2177
  };
2147
2178
  this.segmentStore = new SegmentStore(segmentStoreDeps);
2148
- const brokerHandle = this.ctx.useCapability("stream-broker", brokerScope(BROKER_HOST_NODE_ID));
2179
+ const brokerHandle = this.ctx.useCapability("stream-broker", brokerScope(ingestOwner.ownerNodeId));
2149
2180
  this.controller = new RecordingController({
2150
2181
  api: this.ctx.api,
2151
2182
  logger: this.ctx.logger,
2152
2183
  nodeId: this.nodeId,
2184
+ ownerNodeId: ingestOwner.ownerNodeId,
2153
2185
  segmentStore: this.segmentStore,
2154
2186
  loadConfig: (deviceId) => loadDeviceConfig(this.configStore(), deviceId),
2155
2187
  enabledDeviceIds: async () => [...(await readDeviceConfigs(this.configStore())).keys()],
@@ -2158,7 +2190,7 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
2158
2190
  segmentSeconds: this.config.segmentSeconds,
2159
2191
  watchIntervalMs: this.config.watchIntervalMs,
2160
2192
  ...consumerHostname !== void 0 ? { consumerHostname } : {},
2161
- onBrokerReady: (handler) => this.ctx.onCapabilityStateChange("stream-broker", brokerScope(BROKER_HOST_NODE_ID), (state) => {
2193
+ onBrokerReady: (handler) => this.ctx.onCapabilityStateChange("stream-broker", brokerScope(ingestOwner.ownerNodeId), (state) => {
2162
2194
  if (state === "ready") handler();
2163
2195
  }),
2164
2196
  brokerCall: makeBrokerCall(brokerHandle)
@@ -2284,6 +2316,21 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
2284
2316
  return this.resolvedLocations;
2285
2317
  }
2286
2318
  /**
2319
+ * Resolve the cluster's camera-source owner. Graceful degrade: the
2320
+ * `pipeline-orchestrator` cap may not be mounted yet (pre-image-release),
2321
+ * in which case we fall back to `{ ownerNodeId: 'hub' }` — the same default
2322
+ * `getIngestOwner` itself returns absent an explicit `clusterRoles.ingestNode`,
2323
+ * so an unresolved owner is byte-identical to today's hub-only behaviour.
2324
+ */
2325
+ async resolveIngestOwner() {
2326
+ try {
2327
+ return await this.ctx.api.pipelineOrchestrator.getIngestOwner.query();
2328
+ } catch (err) {
2329
+ this.ctx.logger.warn("recorder: getIngestOwner unavailable — defaulting to hub (cap not mounted yet?)", { meta: { error: require_dist.errMsg(err) } });
2330
+ return { ownerNodeId: "hub" };
2331
+ }
2332
+ }
2333
+ /**
2287
2334
  * Roots the playback data-plane handler searches, IN ORDER: `<dataDir>/playback`
2288
2335
  * FIRST (the freshly-written master/variant `.m3u8` playlists), then every
2289
2336
  * recordings location root (the segment `.m4s` files at
@@ -1,4 +1,4 @@
1
- import { A as recordingCapability, B as DeviceType, I as errMsg, J as selectAssignedProfileSlots, L as BaseAddon, M as storageEvictableCapability, T as migrateConfigToBands, U as hydrateSchema, V as EventCategory, at as string, it as record, s as EVENT_PAD_MS, u as RecordingConfigSchema } from "../dist-GFd8M6KO.mjs";
1
+ import { A as recordingCapability, B as DeviceType, D as nodePin, I as errMsg, L as BaseAddon, M as storageEvictableCapability, T as migrateConfigToBands, U as hydrateSchema, V as EventCategory, Y as selectAssignedProfileSlots, at as record, ot as string, s as EVENT_PAD_MS, u as RecordingConfigSchema } from "../dist-CgBGV988.mjs";
2
2
  import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
3
3
  import { t as createFileDataPlaneHandler } from "../model-download-service-C-IHWnXx-BPy6aoAx.mjs";
4
4
  import { spawn } from "node:child_process";
@@ -223,7 +223,10 @@ var SegmentStore = class SegmentStore {
223
223
  try {
224
224
  bytes = await this.deps.statBytes(input.flatAbsPath);
225
225
  } catch (err) {
226
- this.deps.logger.warn("segment stat failed", err);
226
+ this.deps.logger.warn("segment stat failed", {
227
+ tags: { deviceId: input.deviceId },
228
+ error: err
229
+ });
227
230
  return null;
228
231
  }
229
232
  if (bytes <= 0) return null;
@@ -232,7 +235,10 @@ var SegmentStore = class SegmentStore {
232
235
  try {
233
236
  await this.deps.moveFile(input.flatAbsPath, toAbs);
234
237
  } catch (err) {
235
- this.deps.logger.warn("segment relocate failed", err);
238
+ this.deps.logger.warn("segment relocate failed", {
239
+ tags: { deviceId: input.deviceId },
240
+ error: err
241
+ });
236
242
  return null;
237
243
  }
238
244
  const row = {
@@ -282,7 +288,10 @@ var SegmentStore = class SegmentStore {
282
288
  }
283
289
  continue;
284
290
  } catch (err) {
285
- this.deps.logger.warn("bucket removeDir failed — falling back to per-file deletes", err);
291
+ this.deps.logger.warn("bucket removeDir failed — falling back to per-file deletes", {
292
+ tags: { deviceId: bucketRows[0]?.deviceId },
293
+ error: err
294
+ });
286
295
  }
287
296
  for (const r of bucketRows) try {
288
297
  await this.deps.deletePath({
@@ -292,7 +301,10 @@ var SegmentStore = class SegmentStore {
292
301
  removed.push(r.path);
293
302
  reclaimed += r.bytes;
294
303
  } catch (err) {
295
- this.deps.logger.warn("segment delete failed", err);
304
+ this.deps.logger.warn("segment delete failed", {
305
+ tags: { deviceId: r.deviceId },
306
+ error: err
307
+ });
296
308
  }
297
309
  }
298
310
  if (removed.length > 0) this.deps.index.removeSegments(removed);
@@ -447,13 +459,16 @@ async function runRedundancyJanitor(deps) {
447
459
  files += vrows.length;
448
460
  bytes += reclaimed;
449
461
  } catch (err) {
450
- deps.logger.warn("janitor: evict failed (skipping)", { meta: {
451
- deviceId,
452
- profile,
453
- location,
454
- count: vrows.length,
455
- error: err instanceof Error ? err.message : String(err)
456
- } });
462
+ deps.logger.warn("janitor: evict failed (skipping)", {
463
+ tags: { deviceId },
464
+ meta: {
465
+ deviceId,
466
+ profile,
467
+ location,
468
+ count: vrows.length,
469
+ error: err instanceof Error ? err.message : String(err)
470
+ }
471
+ });
457
472
  }
458
473
  }
459
474
  }
@@ -1090,9 +1105,12 @@ function buildRecordingProvider(deps) {
1090
1105
  return hydrateSchema(buildRecordingDeviceSchema(), {});
1091
1106
  },
1092
1107
  getDeviceLiveContribution: async () => null,
1093
- applyDeviceSettingsPatch: async ({ patch }) => {
1108
+ applyDeviceSettingsPatch: async ({ deviceId, patch }) => {
1094
1109
  const keys = Object.keys(patch ?? {});
1095
- if (keys.length > 0) deps.logger.debug("recorder: ignoring legacy settings patch (widget owns settings)", { meta: { keys } });
1110
+ if (keys.length > 0) deps.logger.debug("recorder: ignoring legacy settings patch (widget owns settings)", {
1111
+ tags: { deviceId },
1112
+ meta: { keys }
1113
+ });
1096
1114
  return { success: true };
1097
1115
  }
1098
1116
  };
@@ -1856,7 +1874,7 @@ var RecordingController = class {
1856
1874
  * assigned sources is ignored (record every assigned source — minimum of 1).
1857
1875
  */
1858
1876
  async resolveProfiles(deviceId, override) {
1859
- const assigned = selectAssignedProfileSlots(await this.deps.brokerCall(() => this.deps.api.streamBroker.listAllProfileSlots.query()), deviceId).map((slot) => slot.profile);
1877
+ const assigned = selectAssignedProfileSlots(await this.deps.brokerCall(() => this.deps.api.streamBroker.listAllProfileSlots.query(void 0, nodePin(this.deps.ownerNodeId))), deviceId).map((slot) => slot.profile);
1860
1878
  if (!override || override.length === 0) return assigned;
1861
1879
  const selected = assigned.filter((p) => override.includes(p));
1862
1880
  return selected.length > 0 ? selected : assigned;
@@ -1876,16 +1894,17 @@ var RecordingController = class {
1876
1894
  profile,
1877
1895
  ...this.deps.consumerHostname !== void 0 ? { hostname: this.deps.consumerHostname } : {},
1878
1896
  tag: `recorder:${deviceId}/${profile}`
1879
- }));
1897
+ }, nodePin(this.deps.ownerNodeId)));
1880
1898
  const outDir = path.join(placement.root, STAGING_DIR_NAME, String(deviceId), profile);
1881
1899
  await promises.mkdir(outDir, { recursive: true });
1900
+ const deviceLog = this.deps.logger.withTags({ deviceId });
1882
1901
  const writer = new SegmentWriter({
1883
1902
  rtspUrl: source.url,
1884
1903
  outDir,
1885
1904
  segmentSeconds
1886
1905
  }, {
1887
1906
  spawn: this.deps.spawn,
1888
- logger: this.deps.logger,
1907
+ logger: deviceLog,
1889
1908
  onGaveUp: () => {
1890
1909
  this.recoverWriterGaveUp(deviceId);
1891
1910
  }
@@ -1900,7 +1919,7 @@ var RecordingController = class {
1900
1919
  onFinalized: (startMs, durMs, flatAbsPath) => {
1901
1920
  this.handleFinalizedSegment(deviceId, profile, placement, startMs, durMs, flatAbsPath);
1902
1921
  },
1903
- logger: this.deps.logger
1922
+ logger: deviceLog
1904
1923
  }),
1905
1924
  pipelineKey: source.pipelineKey,
1906
1925
  outDir
@@ -1961,7 +1980,7 @@ var RecordingController = class {
1961
1980
  r.writer.stop();
1962
1981
  } catch {}
1963
1982
  try {
1964
- await this.deps.api.streamBroker.releaseStreamWithCodec.mutate({ pipelineKey: r.pipelineKey });
1983
+ await this.deps.api.streamBroker.releaseStreamWithCodec.mutate({ pipelineKey: r.pipelineKey }, nodePin(this.deps.ownerNodeId));
1965
1984
  } catch (err) {
1966
1985
  this.deps.logger.warn("recorder: releaseStreamWithCodec failed", {
1967
1986
  tags: { deviceId },
@@ -2072,9 +2091,13 @@ var MARKER_PAD_POST_MS = EVENT_PAD_MS.postMs;
2072
2091
  * form the hub reverse-proxy client path `/addon/recorder/playback`. */
2073
2092
  var RECORDER_ADDON_ID = "recorder";
2074
2093
  var PLAYBACK_PREFIX = "playback";
2075
- /** Node the hub-only `stream-broker` runs on. Every recorder on the hub or on
2076
- * a designated agent gates on, and pulls from, THIS node's broker. */
2077
- var BROKER_HOST_NODE_ID = "hub";
2094
+ /** Narrows the `unknown` extras `SegmentStore`'s logger contract accepts down
2095
+ * to the structured `{ tags, error }` shape the device-scoped call sites
2096
+ * (stat/relocate failures) pass — as opposed to the raw caught error most
2097
+ * other call sites still pass directly. */
2098
+ function isSegmentStoreLogExtras(extras) {
2099
+ return typeof extras === "object" && extras !== null && ("tags" in extras || "error" in extras);
2100
+ }
2078
2101
  var RecorderV2Addon = class extends BaseAddon {
2079
2102
  nodeId = "hub";
2080
2103
  /** True when THIS node is the designated recording node (`recordingNodeId`).
@@ -2109,14 +2132,15 @@ var RecorderV2Addon = class extends BaseAddon {
2109
2132
  } });
2110
2133
  return { providers: [] };
2111
2134
  }
2112
- const consumerHostname = this.nodeId === BROKER_HOST_NODE_ID ? void 0 : resolveRecordingHubHostname(this.config.recordingHubHostname, process.env["CAMSTACK_HUB_URL"]);
2113
- if (this.nodeId !== BROKER_HOST_NODE_ID) {
2135
+ const ingestOwner = await this.resolveIngestOwner();
2136
+ const consumerHostname = this.nodeId === ingestOwner.ownerNodeId ? void 0 : ingestOwner.reachableHost ?? resolveRecordingHubHostname(this.config.recordingHubHostname, process.env["CAMSTACK_HUB_URL"]);
2137
+ if (this.nodeId !== ingestOwner.ownerNodeId) {
2114
2138
  this.ctx.logger.info("recorder is a REMOTE recording node — pulling broker streams cross-node", { meta: {
2115
2139
  ownNodeId: this.nodeId,
2116
- brokerHost: BROKER_HOST_NODE_ID,
2140
+ brokerHost: ingestOwner.ownerNodeId,
2117
2141
  consumerHostname: consumerHostname ?? "(unresolved)"
2118
2142
  } });
2119
- if (consumerHostname === void 0) this.ctx.logger.warn("recorder: could not resolve the hub host for cross-node pull — set the `recordingHubHostname` global setting or CAMSTACK_HUB_URL; recording will not reach this node", { meta: { ownNodeId: this.nodeId } });
2143
+ if (consumerHostname === void 0) this.ctx.logger.warn("recorder: could not resolve the ingest owner host for cross-node pull — set the `recordingHubHostname` global setting or CAMSTACK_HUB_URL; recording will not reach this node", { meta: { ownNodeId: this.nodeId } });
2120
2144
  }
2121
2145
  const segmentStoreDeps = {
2122
2146
  index: this.index,
@@ -2141,14 +2165,21 @@ var RecorderV2Addon = class extends BaseAddon {
2141
2165
  force: true
2142
2166
  });
2143
2167
  },
2144
- logger: { warn: (message, extras) => this.ctx.logger.warn(message, { meta: { extras } }) }
2168
+ logger: { warn: (message, extras) => {
2169
+ if (isSegmentStoreLogExtras(extras)) this.ctx.logger.warn(message, {
2170
+ tags: extras.tags,
2171
+ meta: { error: extras.error }
2172
+ });
2173
+ else this.ctx.logger.warn(message, { meta: { extras } });
2174
+ } }
2145
2175
  };
2146
2176
  this.segmentStore = new SegmentStore(segmentStoreDeps);
2147
- const brokerHandle = this.ctx.useCapability("stream-broker", brokerScope(BROKER_HOST_NODE_ID));
2177
+ const brokerHandle = this.ctx.useCapability("stream-broker", brokerScope(ingestOwner.ownerNodeId));
2148
2178
  this.controller = new RecordingController({
2149
2179
  api: this.ctx.api,
2150
2180
  logger: this.ctx.logger,
2151
2181
  nodeId: this.nodeId,
2182
+ ownerNodeId: ingestOwner.ownerNodeId,
2152
2183
  segmentStore: this.segmentStore,
2153
2184
  loadConfig: (deviceId) => loadDeviceConfig(this.configStore(), deviceId),
2154
2185
  enabledDeviceIds: async () => [...(await readDeviceConfigs(this.configStore())).keys()],
@@ -2157,7 +2188,7 @@ var RecorderV2Addon = class extends BaseAddon {
2157
2188
  segmentSeconds: this.config.segmentSeconds,
2158
2189
  watchIntervalMs: this.config.watchIntervalMs,
2159
2190
  ...consumerHostname !== void 0 ? { consumerHostname } : {},
2160
- onBrokerReady: (handler) => this.ctx.onCapabilityStateChange("stream-broker", brokerScope(BROKER_HOST_NODE_ID), (state) => {
2191
+ onBrokerReady: (handler) => this.ctx.onCapabilityStateChange("stream-broker", brokerScope(ingestOwner.ownerNodeId), (state) => {
2161
2192
  if (state === "ready") handler();
2162
2193
  }),
2163
2194
  brokerCall: makeBrokerCall(brokerHandle)
@@ -2283,6 +2314,21 @@ var RecorderV2Addon = class extends BaseAddon {
2283
2314
  return this.resolvedLocations;
2284
2315
  }
2285
2316
  /**
2317
+ * Resolve the cluster's camera-source owner. Graceful degrade: the
2318
+ * `pipeline-orchestrator` cap may not be mounted yet (pre-image-release),
2319
+ * in which case we fall back to `{ ownerNodeId: 'hub' }` — the same default
2320
+ * `getIngestOwner` itself returns absent an explicit `clusterRoles.ingestNode`,
2321
+ * so an unresolved owner is byte-identical to today's hub-only behaviour.
2322
+ */
2323
+ async resolveIngestOwner() {
2324
+ try {
2325
+ return await this.ctx.api.pipelineOrchestrator.getIngestOwner.query();
2326
+ } catch (err) {
2327
+ this.ctx.logger.warn("recorder: getIngestOwner unavailable — defaulting to hub (cap not mounted yet?)", { meta: { error: errMsg(err) } });
2328
+ return { ownerNodeId: "hub" };
2329
+ }
2330
+ }
2331
+ /**
2286
2332
  * Roots the playback data-plane handler searches, IN ORDER: `<dataDir>/playback`
2287
2333
  * FIRST (the freshly-written master/variant `.m3u8` playlists), then every
2288
2334
  * recordings location root (the segment `.m4s` files at