@camstack/addon-pipeline 1.1.24 → 1.1.25

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 (41) hide show
  1. package/dist/audio-analyzer/index.js +2 -2
  2. package/dist/audio-analyzer/index.mjs +2 -2
  3. package/dist/audio-codec-ffmpeg/index.js +1 -1
  4. package/dist/audio-codec-ffmpeg/index.mjs +1 -1
  5. package/dist/decoder-ffmpeg/index.js +84 -12
  6. package/dist/decoder-ffmpeg/index.mjs +84 -12
  7. package/dist/detection-pipeline/index.js +2 -2
  8. package/dist/detection-pipeline/index.mjs +2 -2
  9. package/dist/{dist-pd_-3C0T.mjs → dist-BecXbIzJ.mjs} +64 -2
  10. package/dist/{dist-CE3a05qT.js → dist-wzWBZ26C.js} +69 -1
  11. package/dist/frame-handle-plane-BAKiW6t4.js +647 -0
  12. package/dist/frame-handle-plane-DGg0Aevs.mjs +636 -0
  13. package/dist/hub-hostname-DAJXlOgV.js +54 -0
  14. package/dist/hub-hostname-cCknRYKj.mjs +49 -0
  15. package/dist/{model-download-service-C-IHWnXx-BnQ_awK4.js → model-download-service-C-IHWnXx-DxM2DSns.js} +1 -2
  16. package/dist/motion-wasm/index.js +1 -1
  17. package/dist/motion-wasm/index.mjs +1 -1
  18. package/dist/pipeline-runner/index.js +305 -10
  19. package/dist/pipeline-runner/index.mjs +305 -10
  20. package/dist/recorder/index.js +267 -65
  21. package/dist/recorder/index.mjs +267 -65
  22. package/dist/stream-broker/_stub.js +39 -39
  23. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BVFAEkIB.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CWPU9tbs.mjs} +2 -2
  24. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-wfWYFiTT.mjs +26 -0
  25. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-lpFwIH9e.mjs +26 -0
  26. package/dist/stream-broker/{hostInit-DHej3Ep9.mjs → hostInit-B7Fx3R_6.mjs} +2 -2
  27. package/dist/stream-broker/index.js +7 -639
  28. package/dist/stream-broker/index.mjs +5 -637
  29. package/dist/stream-broker/remoteEntry.js +1 -1
  30. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-BqXU75it.js → MaskShapeCanvas-DI4BY7W2-CPkQfKnu.js} +1 -1
  31. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-Ct2fpgCd.js → MotionZonesSettings-NcxxQN8r-DlLMQBag.js} +1 -1
  32. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-a9eFVPPl.js → PrivacyMaskSettings-APgPLF7p-kNcauIAN.js} +1 -1
  33. package/embed-dist/assets/index-DrJ0ee3f.css +2 -0
  34. package/embed-dist/assets/index-xpPLFfsT.js +80 -0
  35. package/embed-dist/index.html +2 -2
  36. package/package.json +1 -1
  37. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BFeBlYRQ.mjs +0 -26
  38. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BC1Jt6G8.mjs +0 -26
  39. package/embed-dist/assets/index-C7SSikPl.css +0 -2
  40. package/embed-dist/assets/index-DhdVxWXL.js +0 -80
  41. package/dist/{model-download-service-C-IHWnXx-3Mmeob3l.mjs → model-download-service-C-IHWnXx-BPy6aoAx.mjs} +1 -1
@@ -1,4 +1,6 @@
1
- import { A as nodePin, B as BaseAddon, M as pipelineRunnerCapability, S as defineCustomActions, W as EventCategory, X as makeSourceBrokerId, at as lazy, ct as object, nt as array, q as createEvent, rt as boolean, st as number, tt as _enum, ut as string, y as customAction, z as errMsg } from "../dist-pd_-3C0T.mjs";
1
+ import { B as errMsg, C as defineCustomActions, G as EventCategory, J as createEvent, N as pipelineRunnerCapability, V as BaseAddon, Z as makeSourceBrokerId, b as customAction, ct as number, dt as string, it as boolean, j as nodePin, lt as object, nt as _enum, ot as lazy, rt as array } from "../dist-BecXbIzJ.mjs";
2
+ import { t as FrameHandlePlane } from "../frame-handle-plane-DGg0Aevs.mjs";
3
+ import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
2
4
  import { FrameRingReaderCache } from "@camstack/shm-ring";
3
5
  //#region src/pipeline-runner/bench-actions.ts
4
6
  /**
@@ -236,6 +238,26 @@ var INITIAL_SUBSCRIBE_RETRY_BACKOFF_MS = 250;
236
238
  */
237
239
  var MAX_SUBSCRIBE_RETRY_BACKOFF_MS = 5e3;
238
240
  /**
241
+ * The default source: the broker's shm plane over tRPC — today's path. The
242
+ * methods return the underlying client promises DIRECTLY (no async wrapper)
243
+ * so the poller's scheduling is microtask-identical to the pre-seam code.
244
+ */
245
+ function brokerFrameHandleSource(api, brokerId) {
246
+ return {
247
+ subscribe: (input) => api.streamBroker.subscribeFrames.mutate({
248
+ brokerId,
249
+ format: input.format,
250
+ maxFps: input.maxFps,
251
+ tag: input.tag
252
+ }),
253
+ pullHandles: (input) => api.streamBroker.pullFrameHandles.query({
254
+ subscriptionId: input.subscriptionId,
255
+ maxCount: input.maxCount
256
+ }),
257
+ unsubscribe: (input) => api.streamBroker.unsubscribeFrames.mutate({ subscriptionId: input.subscriptionId }).then(() => void 0)
258
+ };
259
+ }
260
+ /**
239
261
  * Subscribe to a broker's shm frame stream and start the poll loop. Always
240
262
  * resolves to a teardown closure — when the broker is not yet registered the
241
263
  * closure cancels the ongoing retry loop; when polling is active it stops the
@@ -267,19 +289,19 @@ async function startFrameHandlePoller(options) {
267
289
  */
268
290
  async function subscribeWithRetry(options, lifecycle) {
269
291
  const { api, brokerId, format, maxFps, tag, logger } = options;
292
+ const source = options.source ?? brokerFrameHandleSource(api, brokerId);
270
293
  let backoffMs = INITIAL_SUBSCRIBE_RETRY_BACKOFF_MS;
271
294
  let attempt = 0;
272
295
  while (!lifecycle.stopped) {
273
296
  attempt += 1;
274
297
  try {
275
- const result = await api.streamBroker.subscribeFrames.mutate({
276
- brokerId,
298
+ const result = await source.subscribe({
277
299
  format,
278
300
  maxFps,
279
301
  tag
280
302
  });
281
303
  if (lifecycle.stopped) {
282
- await api.streamBroker.unsubscribeFrames.mutate({ subscriptionId: result.subscriptionId }).catch((err) => {
304
+ await source.unsubscribe({ subscriptionId: result.subscriptionId }).catch((err) => {
283
305
  logger.warn("frame-handle poller: late unsubscribe failed", { meta: {
284
306
  brokerId,
285
307
  subscriptionId: result.subscriptionId,
@@ -288,7 +310,7 @@ async function subscribeWithRetry(options, lifecycle) {
288
310
  });
289
311
  return;
290
312
  }
291
- lifecycle.activeTeardown = startPolling(options, result.subscriptionId, result.maxFps, lifecycle);
313
+ lifecycle.activeTeardown = startPolling(options, source, result.subscriptionId, result.maxFps, lifecycle);
292
314
  return;
293
315
  } catch (err) {
294
316
  if (lifecycle.stopped) return;
@@ -329,8 +351,8 @@ function sleep(ms, lifecycle) {
329
351
  * Returns the teardown closure that stops the loop and releases the
330
352
  * subscription.
331
353
  */
332
- function startPolling(options, subscriptionId, resolvedMaxFps, lifecycle) {
333
- const { api, brokerId, onFrame, logger, ownNodeId, getRemoteFrame, tag } = options;
354
+ function startPolling(options, source, subscriptionId, resolvedMaxFps, lifecycle) {
355
+ const { brokerId, onFrame, logger, ownNodeId, getRemoteFrame, tag } = options;
334
356
  const readers = new FrameRingReaderCache(logger);
335
357
  const throttle = new ForeignHandleThrottle();
336
358
  const pollIntervalMs = resolvedMaxFps > 0 ? Math.max(MIN_POLL_INTERVAL_MS, Math.round(1e3 / resolvedMaxFps)) : FALLBACK_POLL_INTERVAL_MS;
@@ -338,7 +360,7 @@ function startPolling(options, subscriptionId, resolvedMaxFps, lifecycle) {
338
360
  const tick = async () => {
339
361
  if (lifecycle.stopped) return;
340
362
  try {
341
- const handles = await api.streamBroker.pullFrameHandles.query({
363
+ const handles = await source.pullHandles({
342
364
  subscriptionId,
343
365
  maxCount: PULL_MAX_COUNT
344
366
  });
@@ -370,8 +392,8 @@ function startPolling(options, subscriptionId, resolvedMaxFps, lifecycle) {
370
392
  timer = void 0;
371
393
  }
372
394
  readers.close();
373
- api.streamBroker.unsubscribeFrames.mutate({ subscriptionId }).catch((err) => {
374
- logger.warn("frame-handle poller: unsubscribeFrames failed", { meta: {
395
+ source.unsubscribe({ subscriptionId }).catch((err) => {
396
+ logger.warn("frame-handle poller: unsubscribe failed", { meta: {
375
397
  brokerId,
376
398
  subscriptionId,
377
399
  error: errMsg(err)
@@ -380,6 +402,221 @@ function startPolling(options, subscriptionId, resolvedMaxFps, lifecycle) {
380
402
  };
381
403
  }
382
404
  //#endregion
405
+ //#region src/pipeline-runner/remote-source-plane.ts
406
+ /**
407
+ * Runner-side mode selection (pure): whether an attach payload routes this
408
+ * camera through the remote-source leg. Absent `frameSource` (a pre-P2c
409
+ * payload) and `local-broker` both take today's co-located broker path —
410
+ * the rollout-OFF safety invariant.
411
+ */
412
+ function isRemoteRestream(frameSource) {
413
+ return frameSource !== void 0 && frameSource.kind === "remote-restream";
414
+ }
415
+ /**
416
+ * Resolve WHICH broker profile a camStream id feeds for a device (pure).
417
+ *
418
+ * `getStreamWithCodec` targets a PROFILE (its assigned camStream), while the
419
+ * runner is dispatched with camStream ids (`motionStreamId` /
420
+ * `detectionStreamId`). The broker's profile slots carry the mapping
421
+ * (`sourceCamStreamId`); a stream no slot feeds returns `null` — the acquire
422
+ * fails fast and the poller's backoff retries (the assignment may still be
423
+ * propagating).
424
+ */
425
+ function profileForStreamId(slots, deviceId, streamId) {
426
+ for (const slot of slots) if (slot.deviceId === deviceId && slot.sourceCamStreamId === streamId) return slot.profile;
427
+ return null;
428
+ }
429
+ var RemoteSourcePlane = class {
430
+ api;
431
+ logger;
432
+ deviceId;
433
+ ownNodeId;
434
+ hostname;
435
+ /** Single-flight entry creation per camStream id. */
436
+ entries = /* @__PURE__ */ new Map();
437
+ disposed = false;
438
+ constructor(options) {
439
+ this.api = options.api;
440
+ this.logger = options.logger.withTags({ deviceId: options.deviceId });
441
+ this.deviceId = options.deviceId;
442
+ this.ownNodeId = options.ownNodeId;
443
+ this.hostname = options.hostname;
444
+ }
445
+ /**
446
+ * A {@link FrameHandleSource} for one camStream id, pluggable into
447
+ * `startFrameHandlePoller`. Motion (`gray`) and detection (`rgb`)
448
+ * subscriptions of the same stream share ONE acquired restream and ONE
449
+ * satellite plane (per-format decoder sessions inside it — hub parity).
450
+ */
451
+ makeSource(streamId) {
452
+ return {
453
+ subscribe: async (input) => {
454
+ if (this.disposed) throw new Error("RemoteSourcePlane: subscribe after dispose");
455
+ const entry = await this.ensureEntry(streamId);
456
+ const result = await entry.plane.subscribe({
457
+ format: input.format,
458
+ maxFps: input.maxFps,
459
+ tag: input.tag
460
+ });
461
+ entry.subscriptionIds.add(result.subscriptionId);
462
+ return result;
463
+ },
464
+ pullHandles: async (input) => {
465
+ const entry = await this.peekEntry(streamId);
466
+ if (!entry) return [];
467
+ return entry.plane.pullHandles(input.subscriptionId, input.maxCount);
468
+ },
469
+ unsubscribe: async (input) => {
470
+ const entry = await this.peekEntry(streamId);
471
+ if (!entry) return;
472
+ entry.subscriptionIds.delete(input.subscriptionId);
473
+ await entry.plane.unsubscribe(input.subscriptionId).catch(() => {});
474
+ if (entry.subscriptionIds.size === 0) {
475
+ this.entries.delete(streamId);
476
+ await this.teardownEntry(entry, streamId);
477
+ }
478
+ }
479
+ };
480
+ }
481
+ /** Dispose every entry: satellite planes destroyed, restreams released. Idempotent. */
482
+ async dispose() {
483
+ if (this.disposed) return;
484
+ this.disposed = true;
485
+ const pending = [...this.entries.entries()];
486
+ this.entries.clear();
487
+ for (const [streamId, entryPromise] of pending) try {
488
+ const entry = await entryPromise;
489
+ await this.teardownEntry(entry, streamId);
490
+ } catch {}
491
+ }
492
+ /** The live entry for a stream, or `null` (never creates one). */
493
+ async peekEntry(streamId) {
494
+ const pending = this.entries.get(streamId);
495
+ if (!pending) return null;
496
+ try {
497
+ return await pending;
498
+ } catch {
499
+ return null;
500
+ }
501
+ }
502
+ /** Single-flight: acquire the restream + build the satellite plane once per stream. */
503
+ ensureEntry(streamId) {
504
+ const existing = this.entries.get(streamId);
505
+ if (existing) return existing;
506
+ const creation = this.createEntry(streamId);
507
+ this.entries.set(streamId, creation);
508
+ creation.catch(() => {
509
+ if (this.entries.get(streamId) === creation) this.entries.delete(streamId);
510
+ });
511
+ return creation;
512
+ }
513
+ async createEntry(streamId) {
514
+ const { api, deviceId, ownNodeId } = this;
515
+ if (this.hostname === void 0) throw new Error("RemoteSourcePlane: owner hostname unresolvable — set frameSource.hubHostnameOverride or CAMSTACK_HUB_URL (a 127.0.0.1 restream URL would dial the wrong box)");
516
+ const profile = profileForStreamId(await api.streamBroker.listAllProfileSlots.query(), deviceId, streamId);
517
+ if (profile === null) throw new Error(`RemoteSourcePlane: no broker profile is fed by camStream '${streamId}' of device ${deviceId} — cannot acquire the owner restream`);
518
+ const rtpSource = await api.streamBroker.getStreamWithCodec.mutate({
519
+ deviceId,
520
+ video: "copy",
521
+ audio: "none",
522
+ profile,
523
+ hostname: this.hostname,
524
+ tag: `runner:${ownNodeId}:remote-src:${deviceId}/${streamId}`
525
+ });
526
+ const codec = rtpSource.videoCodec === "H265" ? "h265" : "h264";
527
+ const plane = new FrameHandlePlane({
528
+ decoderApi: this.buildPinnedDecoderCapApi(),
529
+ logger: this.logger,
530
+ localNodeId: ownNodeId,
531
+ resolveStreamInfo: () => ({
532
+ codec,
533
+ numericDeviceId: deviceId,
534
+ tag: `runner-remote:${deviceId}/${streamId}`,
535
+ pullModeUrl: rtpSource.url
536
+ })
537
+ });
538
+ this.logger.info("remote-source plane: owner restream acquired", { meta: {
539
+ streamId,
540
+ profile,
541
+ codec,
542
+ pipelineKey: rtpSource.pipelineKey,
543
+ hostname: this.hostname
544
+ } });
545
+ return {
546
+ plane,
547
+ pipelineKey: rtpSource.pipelineKey,
548
+ subscriptionIds: /* @__PURE__ */ new Set(),
549
+ released: false
550
+ };
551
+ }
552
+ /** Destroy an entry's satellite plane and release its restream refcount once. */
553
+ async teardownEntry(entry, streamId) {
554
+ await entry.plane.dispose().catch((err) => {
555
+ this.logger.warn("remote-source plane: satellite plane dispose failed", { meta: {
556
+ streamId,
557
+ error: errMsg(err)
558
+ } });
559
+ });
560
+ if (entry.released) return;
561
+ entry.released = true;
562
+ await this.api.streamBroker.releaseStreamWithCodec.mutate({ pipelineKey: entry.pipelineKey }).catch((err) => {
563
+ this.logger.debug("remote-source plane: releaseStreamWithCodec failed (benign on hub restart)", { meta: {
564
+ streamId,
565
+ pipelineKey: entry.pipelineKey,
566
+ error: errMsg(err)
567
+ } });
568
+ });
569
+ }
570
+ /**
571
+ * Every decoder call pinned to THIS runner's node — mirror of the hub
572
+ * broker's wrapper (`stream-broker-manager.buildDecoderCapApi`): the
573
+ * `decoder` cap is a singleton whose UNPINNED calls resolve to the
574
+ * cluster-elected node, which is generally NOT this agent. Pinning create
575
+ * AND every session op to the same local node keeps the session + its shm
576
+ * ring here, where this plane's readers live.
577
+ */
578
+ buildPinnedDecoderCapApi() {
579
+ const api = this.api;
580
+ const pin = nodePin(this.ownNodeId);
581
+ const toWireBytes = (bytes) => {
582
+ const copy = new Uint8Array(new ArrayBuffer(bytes.byteLength));
583
+ copy.set(bytes);
584
+ return copy;
585
+ };
586
+ return {
587
+ supportsCodec: async (input) => api.decoder.supportsCodec.query({ ...input }, pin),
588
+ getInfo: async () => api.decoder.getInfo.query(void 0, pin),
589
+ createSession: async (config) => api.decoder.createSession.query({ ...config }, pin),
590
+ pushPacket: async (input) => {
591
+ await api.decoder.pushPacket.query({
592
+ sessionId: input.sessionId,
593
+ packet: {
594
+ ...input.packet,
595
+ data: toWireBytes(input.packet.data)
596
+ }
597
+ }, pin);
598
+ },
599
+ pullFrames: async (input) => {
600
+ return (await api.decoder.pullFrames.query({ ...input }, pin)).map((frame) => ({
601
+ ...frame,
602
+ data: Buffer.from(frame.data)
603
+ }));
604
+ },
605
+ pullHandles: async (input) => api.decoder.pullHandles.query({ ...input }, pin),
606
+ destroySession: async (input) => {
607
+ await api.decoder.destroySession.query({ ...input }, pin);
608
+ },
609
+ openStream: async (input) => {
610
+ await api.decoder.openStream.query({ ...input }, pin);
611
+ },
612
+ updateConfig: async (input) => {
613
+ await api.decoder.updateConfig.query({ ...input }, pin);
614
+ },
615
+ getStats: async (input) => api.decoder.getStats.query({ ...input }, pin)
616
+ };
617
+ }
618
+ };
619
+ //#endregion
383
620
  //#region src/pipeline-runner/frame-queue.ts
384
621
  /**
385
622
  * Latest-frame-only buffer. Keeps only the most recent item, dropping all
@@ -1153,6 +1390,14 @@ var PipelineRunnerAddon = class extends BaseAddon {
1153
1390
  */
1154
1391
  onboardAnalyzerTeardownTimers = /* @__PURE__ */ new Map();
1155
1392
  /**
1393
+ * REMOTE-SOURCE legs (cross-node Phase 2, P2d) — one per attached camera
1394
+ * whose `frameSource.kind === 'remote-restream'`: the owner's compressed
1395
+ * restream + a local satellite frame plane. DORMANT with today's default
1396
+ * payloads (`local-broker` / absent) — the map stays empty and every code
1397
+ * path is identical to pre-Phase-2. Torn down on detach / shutdown.
1398
+ */
1399
+ remotePlanes = /* @__PURE__ */ new Map();
1400
+ /**
1156
1401
  * Snapshot-equality cache for metrics-snapshot defer. The runner
1157
1402
  * fires per-camera metrics every `METRICS_SNAPSHOT_INTERVAL_MS`;
1158
1403
  * for an idle camera (no inference, queue empty, fps=0) every tick
@@ -1248,6 +1493,9 @@ var PipelineRunnerAddon = class extends BaseAddon {
1248
1493
  }
1249
1494
  this.lastAnalyzerDetected.clear();
1250
1495
  for (const deviceId of [...this.onboardAnalyzerTeardownTimers.keys(), ...this.onboardAnalyzerSubs.keys()]) this.clearOnboardAnalyzer(deviceId);
1496
+ const remotePlanes = [...this.remotePlanes.values()];
1497
+ this.remotePlanes.clear();
1498
+ await Promise.all(remotePlanes.map((plane) => plane.dispose().catch(() => {})));
1251
1499
  if (this.runner) {
1252
1500
  this.runner.stop();
1253
1501
  this.runner = null;
@@ -1643,6 +1891,16 @@ var PipelineRunnerAddon = class extends BaseAddon {
1643
1891
  this.clearOnboardAnalyzer(deviceId);
1644
1892
  attachment.motionUnsubscribe?.();
1645
1893
  attachment.detectionUnsubscribe?.();
1894
+ const remotePlane = this.remotePlanes.get(deviceId);
1895
+ if (remotePlane) {
1896
+ this.remotePlanes.delete(deviceId);
1897
+ remotePlane.dispose().catch((err) => {
1898
+ this.ctx?.logger.debug("remote-source plane dispose failed on detach", {
1899
+ tags: { deviceId },
1900
+ meta: { error: errMsg(err) }
1901
+ });
1902
+ });
1903
+ }
1646
1904
  this.attached.delete(deviceId);
1647
1905
  this.lastMotionAt.delete(deviceId);
1648
1906
  this.lastEmittedCameraMetrics.delete(deviceId);
@@ -1784,6 +2042,39 @@ var PipelineRunnerAddon = class extends BaseAddon {
1784
2042
  timestamp: remote.timestamp
1785
2043
  };
1786
2044
  }
2045
+ /**
2046
+ * The remote-source leg for a camera, or `null` for today's co-located
2047
+ * broker path (P2d). Created lazily on the FIRST subscription of a
2048
+ * `remote-restream` camera and shared by its motion + detection pollers;
2049
+ * `detachInternal` / `onShutdown` dispose it. With rollout OFF every
2050
+ * attach payload is `local-broker` (or absent) → always `null` → the
2051
+ * subscribe paths below are byte-identical to pre-Phase-2.
2052
+ */
2053
+ remoteSourceFor(config) {
2054
+ const frameSource = config.frameSource;
2055
+ if (!isRemoteRestream(frameSource)) return null;
2056
+ const existing = this.remotePlanes.get(config.deviceId);
2057
+ if (existing) return existing;
2058
+ const api = this.ctx.api;
2059
+ if (!api) return null;
2060
+ const hostname = resolveHubHostname(frameSource.hubHostnameOverride, process.env["CAMSTACK_HUB_URL"]);
2061
+ const plane = new RemoteSourcePlane({
2062
+ api,
2063
+ logger: this.ctx.logger,
2064
+ deviceId: config.deviceId,
2065
+ ownNodeId: this.nodeId,
2066
+ hostname
2067
+ });
2068
+ this.ctx.logger.info("remote-source leg armed for camera", {
2069
+ tags: { deviceId: config.deviceId },
2070
+ meta: {
2071
+ ownerNodeId: frameSource.ownerNodeId,
2072
+ hostname: hostname ?? "(unresolved — acquires will fail until set)"
2073
+ }
2074
+ });
2075
+ this.remotePlanes.set(config.deviceId, plane);
2076
+ return plane;
2077
+ }
1787
2078
  async subscribeMotionFrames(config) {
1788
2079
  const ctx = this.ctx;
1789
2080
  const runner = this.runner;
@@ -1794,6 +2085,7 @@ var PipelineRunnerAddon = class extends BaseAddon {
1794
2085
  log.warn("subscribeMotionFrames: this.ctx.api not available");
1795
2086
  return null;
1796
2087
  }
2088
+ const remote = this.remoteSourceFor(config);
1797
2089
  return startFrameHandlePoller({
1798
2090
  api,
1799
2091
  brokerId: makeSourceBrokerId(config.deviceId, config.motionStreamId),
@@ -1803,6 +2095,7 @@ var PipelineRunnerAddon = class extends BaseAddon {
1803
2095
  logger: log,
1804
2096
  ownNodeId: this.nodeId,
1805
2097
  getRemoteFrame: (handle) => this.getRemoteFrame(handle),
2098
+ ...remote ? { source: remote.makeSource(config.motionStreamId) } : {},
1806
2099
  onFrame: (frame, _handle) => {
1807
2100
  runner.enqueueMotionFrame(config.deviceId, frame);
1808
2101
  }
@@ -1879,6 +2172,7 @@ var PipelineRunnerAddon = class extends BaseAddon {
1879
2172
  log.warn("subscribeDetectionFrames: this.ctx.api not available");
1880
2173
  return null;
1881
2174
  }
2175
+ const remote = this.remoteSourceFor(config);
1882
2176
  return startFrameHandlePoller({
1883
2177
  api,
1884
2178
  brokerId: makeSourceBrokerId(config.deviceId, config.detectionStreamId),
@@ -1888,6 +2182,7 @@ var PipelineRunnerAddon = class extends BaseAddon {
1888
2182
  logger: log,
1889
2183
  ownNodeId: this.nodeId,
1890
2184
  getRemoteFrame: (handle) => this.getRemoteFrame(handle),
2185
+ ...remote ? { source: remote.makeSource(config.detectionStreamId) } : {},
1891
2186
  onFrame: (frame, handle) => {
1892
2187
  runner.enqueueDetectionFrame(config.deviceId, frame, handle);
1893
2188
  }