@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
@@ -0,0 +1,49 @@
1
+ //#region src/shared/hub-hostname.ts
2
+ /**
3
+ * Resolve the host a REMOTE node should use to dial the hub-only
4
+ * stream-broker's restream port.
5
+ *
6
+ * The broker mints `127.0.0.1` restream URLs (hub-local). A cross-node
7
+ * consumer — the recorder on a designated remote recording node, or the
8
+ * pipeline-runner's remote-source leg (cross-node Phase 2) — must pull from
9
+ * the hub, so it needs the hub's cluster-resolvable host. Resolution order:
10
+ *
11
+ * 1. `override` — an explicit operator setting (the recorder's
12
+ * `recordingHubHostname` global, or the runner attach payload's
13
+ * `frameSource.hubHostnameOverride`), when the auto-derived host isn't
14
+ * reachable for RTSP on the consuming interface.
15
+ * 2. `hubUrl` — derived from the node's `CAMSTACK_HUB_URL` (the address it
16
+ * already uses to reach the hub). The RTSP restream port differs from
17
+ * the hub API port, but the HOST is the same — only the host is
18
+ * extracted.
19
+ *
20
+ * Returns `undefined` when neither yields a host (the caller logs / fails the
21
+ * acquire; a `127.0.0.1` URL on a remote node would dial the wrong box).
22
+ *
23
+ * Pure: the env string is passed in (not read here) so it can be unit-tested.
24
+ * Promoted from `recorder/addon/recording-hub-host.ts` in P2d so both
25
+ * cross-node consumers share ONE resolver.
26
+ */
27
+ function resolveHubHostname(override, hubUrl) {
28
+ const trimmedOverride = override?.trim();
29
+ if (trimmedOverride !== void 0 && trimmedOverride.length > 0) return trimmedOverride;
30
+ return extractHost(hubUrl);
31
+ }
32
+ /** Extract the bare host from a URL/authority string, scheme + port optional. */
33
+ function extractHost(raw) {
34
+ const value = raw?.trim();
35
+ if (value === void 0 || value.length === 0) return void 0;
36
+ try {
37
+ const parsed = new URL(value);
38
+ if (parsed.hostname.length > 0) return parsed.hostname;
39
+ } catch {}
40
+ const authority = value.replace(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//, "").split("/")[0] ?? "";
41
+ if (authority.startsWith("[")) {
42
+ const end = authority.indexOf("]");
43
+ if (end > 0) return authority.slice(0, end + 1);
44
+ }
45
+ const host = authority.split(":")[0] ?? "";
46
+ return host.length > 0 ? host : void 0;
47
+ }
48
+ //#endregion
49
+ export { resolveHubHostname as t };
@@ -19,12 +19,11 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
19
19
  value: mod,
20
20
  enumerable: true
21
21
  }) : target, mod));
22
- //#endregion
22
+ require("node:crypto");
23
23
  let node_fs = require("node:fs");
24
24
  node_fs = __toESM(node_fs, 1);
25
25
  let node_path = require("node:path");
26
26
  node_path = __toESM(node_path, 1);
27
- require("node:crypto");
28
27
  //#region ../system/dist/model-download-service-C-IHWnXx.mjs
29
28
  /**
30
29
  * Map a rel path to one candidate absolute path PER root, keeping only roots the
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-CE3a05qT.js");
5
+ const require_dist = require("../dist-wzWBZ26C.js");
6
6
  let node_fs = require("node:fs");
7
7
  let node_path = require("node:path");
8
8
  //#region src/motion-wasm/wasm-motion-detector.ts
@@ -1,4 +1,4 @@
1
- import { B as BaseAddon, J as hydrateSchema, U as DeviceType, k as motionDetectionCapability, w as evaluateZoneRules } from "../dist-pd_-3C0T.mjs";
1
+ import { A as motionDetectionCapability, T as evaluateZoneRules, V as BaseAddon, W as DeviceType, Y as hydrateSchema } from "../dist-BecXbIzJ.mjs";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  //#region src/motion-wasm/wasm-motion-detector.ts
@@ -2,7 +2,9 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-CE3a05qT.js");
5
+ const require_dist = require("../dist-wzWBZ26C.js");
6
+ const require_frame_handle_plane = require("../frame-handle-plane-BAKiW6t4.js");
7
+ const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
6
8
  let _camstack_shm_ring = require("@camstack/shm-ring");
7
9
  //#region src/pipeline-runner/bench-actions.ts
8
10
  /**
@@ -240,6 +242,26 @@ var INITIAL_SUBSCRIBE_RETRY_BACKOFF_MS = 250;
240
242
  */
241
243
  var MAX_SUBSCRIBE_RETRY_BACKOFF_MS = 5e3;
242
244
  /**
245
+ * The default source: the broker's shm plane over tRPC — today's path. The
246
+ * methods return the underlying client promises DIRECTLY (no async wrapper)
247
+ * so the poller's scheduling is microtask-identical to the pre-seam code.
248
+ */
249
+ function brokerFrameHandleSource(api, brokerId) {
250
+ return {
251
+ subscribe: (input) => api.streamBroker.subscribeFrames.mutate({
252
+ brokerId,
253
+ format: input.format,
254
+ maxFps: input.maxFps,
255
+ tag: input.tag
256
+ }),
257
+ pullHandles: (input) => api.streamBroker.pullFrameHandles.query({
258
+ subscriptionId: input.subscriptionId,
259
+ maxCount: input.maxCount
260
+ }),
261
+ unsubscribe: (input) => api.streamBroker.unsubscribeFrames.mutate({ subscriptionId: input.subscriptionId }).then(() => void 0)
262
+ };
263
+ }
264
+ /**
243
265
  * Subscribe to a broker's shm frame stream and start the poll loop. Always
244
266
  * resolves to a teardown closure — when the broker is not yet registered the
245
267
  * closure cancels the ongoing retry loop; when polling is active it stops the
@@ -271,19 +293,19 @@ async function startFrameHandlePoller(options) {
271
293
  */
272
294
  async function subscribeWithRetry(options, lifecycle) {
273
295
  const { api, brokerId, format, maxFps, tag, logger } = options;
296
+ const source = options.source ?? brokerFrameHandleSource(api, brokerId);
274
297
  let backoffMs = INITIAL_SUBSCRIBE_RETRY_BACKOFF_MS;
275
298
  let attempt = 0;
276
299
  while (!lifecycle.stopped) {
277
300
  attempt += 1;
278
301
  try {
279
- const result = await api.streamBroker.subscribeFrames.mutate({
280
- brokerId,
302
+ const result = await source.subscribe({
281
303
  format,
282
304
  maxFps,
283
305
  tag
284
306
  });
285
307
  if (lifecycle.stopped) {
286
- await api.streamBroker.unsubscribeFrames.mutate({ subscriptionId: result.subscriptionId }).catch((err) => {
308
+ await source.unsubscribe({ subscriptionId: result.subscriptionId }).catch((err) => {
287
309
  logger.warn("frame-handle poller: late unsubscribe failed", { meta: {
288
310
  brokerId,
289
311
  subscriptionId: result.subscriptionId,
@@ -292,7 +314,7 @@ async function subscribeWithRetry(options, lifecycle) {
292
314
  });
293
315
  return;
294
316
  }
295
- lifecycle.activeTeardown = startPolling(options, result.subscriptionId, result.maxFps, lifecycle);
317
+ lifecycle.activeTeardown = startPolling(options, source, result.subscriptionId, result.maxFps, lifecycle);
296
318
  return;
297
319
  } catch (err) {
298
320
  if (lifecycle.stopped) return;
@@ -333,8 +355,8 @@ function sleep(ms, lifecycle) {
333
355
  * Returns the teardown closure that stops the loop and releases the
334
356
  * subscription.
335
357
  */
336
- function startPolling(options, subscriptionId, resolvedMaxFps, lifecycle) {
337
- const { api, brokerId, onFrame, logger, ownNodeId, getRemoteFrame, tag } = options;
358
+ function startPolling(options, source, subscriptionId, resolvedMaxFps, lifecycle) {
359
+ const { brokerId, onFrame, logger, ownNodeId, getRemoteFrame, tag } = options;
338
360
  const readers = new _camstack_shm_ring.FrameRingReaderCache(logger);
339
361
  const throttle = new ForeignHandleThrottle();
340
362
  const pollIntervalMs = resolvedMaxFps > 0 ? Math.max(MIN_POLL_INTERVAL_MS, Math.round(1e3 / resolvedMaxFps)) : FALLBACK_POLL_INTERVAL_MS;
@@ -342,7 +364,7 @@ function startPolling(options, subscriptionId, resolvedMaxFps, lifecycle) {
342
364
  const tick = async () => {
343
365
  if (lifecycle.stopped) return;
344
366
  try {
345
- const handles = await api.streamBroker.pullFrameHandles.query({
367
+ const handles = await source.pullHandles({
346
368
  subscriptionId,
347
369
  maxCount: PULL_MAX_COUNT
348
370
  });
@@ -374,8 +396,8 @@ function startPolling(options, subscriptionId, resolvedMaxFps, lifecycle) {
374
396
  timer = void 0;
375
397
  }
376
398
  readers.close();
377
- api.streamBroker.unsubscribeFrames.mutate({ subscriptionId }).catch((err) => {
378
- logger.warn("frame-handle poller: unsubscribeFrames failed", { meta: {
399
+ source.unsubscribe({ subscriptionId }).catch((err) => {
400
+ logger.warn("frame-handle poller: unsubscribe failed", { meta: {
379
401
  brokerId,
380
402
  subscriptionId,
381
403
  error: require_dist.errMsg(err)
@@ -384,6 +406,221 @@ function startPolling(options, subscriptionId, resolvedMaxFps, lifecycle) {
384
406
  };
385
407
  }
386
408
  //#endregion
409
+ //#region src/pipeline-runner/remote-source-plane.ts
410
+ /**
411
+ * Runner-side mode selection (pure): whether an attach payload routes this
412
+ * camera through the remote-source leg. Absent `frameSource` (a pre-P2c
413
+ * payload) and `local-broker` both take today's co-located broker path —
414
+ * the rollout-OFF safety invariant.
415
+ */
416
+ function isRemoteRestream(frameSource) {
417
+ return frameSource !== void 0 && frameSource.kind === "remote-restream";
418
+ }
419
+ /**
420
+ * Resolve WHICH broker profile a camStream id feeds for a device (pure).
421
+ *
422
+ * `getStreamWithCodec` targets a PROFILE (its assigned camStream), while the
423
+ * runner is dispatched with camStream ids (`motionStreamId` /
424
+ * `detectionStreamId`). The broker's profile slots carry the mapping
425
+ * (`sourceCamStreamId`); a stream no slot feeds returns `null` — the acquire
426
+ * fails fast and the poller's backoff retries (the assignment may still be
427
+ * propagating).
428
+ */
429
+ function profileForStreamId(slots, deviceId, streamId) {
430
+ for (const slot of slots) if (slot.deviceId === deviceId && slot.sourceCamStreamId === streamId) return slot.profile;
431
+ return null;
432
+ }
433
+ var RemoteSourcePlane = class {
434
+ api;
435
+ logger;
436
+ deviceId;
437
+ ownNodeId;
438
+ hostname;
439
+ /** Single-flight entry creation per camStream id. */
440
+ entries = /* @__PURE__ */ new Map();
441
+ disposed = false;
442
+ constructor(options) {
443
+ this.api = options.api;
444
+ this.logger = options.logger.withTags({ deviceId: options.deviceId });
445
+ this.deviceId = options.deviceId;
446
+ this.ownNodeId = options.ownNodeId;
447
+ this.hostname = options.hostname;
448
+ }
449
+ /**
450
+ * A {@link FrameHandleSource} for one camStream id, pluggable into
451
+ * `startFrameHandlePoller`. Motion (`gray`) and detection (`rgb`)
452
+ * subscriptions of the same stream share ONE acquired restream and ONE
453
+ * satellite plane (per-format decoder sessions inside it — hub parity).
454
+ */
455
+ makeSource(streamId) {
456
+ return {
457
+ subscribe: async (input) => {
458
+ if (this.disposed) throw new Error("RemoteSourcePlane: subscribe after dispose");
459
+ const entry = await this.ensureEntry(streamId);
460
+ const result = await entry.plane.subscribe({
461
+ format: input.format,
462
+ maxFps: input.maxFps,
463
+ tag: input.tag
464
+ });
465
+ entry.subscriptionIds.add(result.subscriptionId);
466
+ return result;
467
+ },
468
+ pullHandles: async (input) => {
469
+ const entry = await this.peekEntry(streamId);
470
+ if (!entry) return [];
471
+ return entry.plane.pullHandles(input.subscriptionId, input.maxCount);
472
+ },
473
+ unsubscribe: async (input) => {
474
+ const entry = await this.peekEntry(streamId);
475
+ if (!entry) return;
476
+ entry.subscriptionIds.delete(input.subscriptionId);
477
+ await entry.plane.unsubscribe(input.subscriptionId).catch(() => {});
478
+ if (entry.subscriptionIds.size === 0) {
479
+ this.entries.delete(streamId);
480
+ await this.teardownEntry(entry, streamId);
481
+ }
482
+ }
483
+ };
484
+ }
485
+ /** Dispose every entry: satellite planes destroyed, restreams released. Idempotent. */
486
+ async dispose() {
487
+ if (this.disposed) return;
488
+ this.disposed = true;
489
+ const pending = [...this.entries.entries()];
490
+ this.entries.clear();
491
+ for (const [streamId, entryPromise] of pending) try {
492
+ const entry = await entryPromise;
493
+ await this.teardownEntry(entry, streamId);
494
+ } catch {}
495
+ }
496
+ /** The live entry for a stream, or `null` (never creates one). */
497
+ async peekEntry(streamId) {
498
+ const pending = this.entries.get(streamId);
499
+ if (!pending) return null;
500
+ try {
501
+ return await pending;
502
+ } catch {
503
+ return null;
504
+ }
505
+ }
506
+ /** Single-flight: acquire the restream + build the satellite plane once per stream. */
507
+ ensureEntry(streamId) {
508
+ const existing = this.entries.get(streamId);
509
+ if (existing) return existing;
510
+ const creation = this.createEntry(streamId);
511
+ this.entries.set(streamId, creation);
512
+ creation.catch(() => {
513
+ if (this.entries.get(streamId) === creation) this.entries.delete(streamId);
514
+ });
515
+ return creation;
516
+ }
517
+ async createEntry(streamId) {
518
+ const { api, deviceId, ownNodeId } = this;
519
+ 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)");
520
+ const profile = profileForStreamId(await api.streamBroker.listAllProfileSlots.query(), deviceId, streamId);
521
+ if (profile === null) throw new Error(`RemoteSourcePlane: no broker profile is fed by camStream '${streamId}' of device ${deviceId} — cannot acquire the owner restream`);
522
+ const rtpSource = await api.streamBroker.getStreamWithCodec.mutate({
523
+ deviceId,
524
+ video: "copy",
525
+ audio: "none",
526
+ profile,
527
+ hostname: this.hostname,
528
+ tag: `runner:${ownNodeId}:remote-src:${deviceId}/${streamId}`
529
+ });
530
+ const codec = rtpSource.videoCodec === "H265" ? "h265" : "h264";
531
+ const plane = new require_frame_handle_plane.FrameHandlePlane({
532
+ decoderApi: this.buildPinnedDecoderCapApi(),
533
+ logger: this.logger,
534
+ localNodeId: ownNodeId,
535
+ resolveStreamInfo: () => ({
536
+ codec,
537
+ numericDeviceId: deviceId,
538
+ tag: `runner-remote:${deviceId}/${streamId}`,
539
+ pullModeUrl: rtpSource.url
540
+ })
541
+ });
542
+ this.logger.info("remote-source plane: owner restream acquired", { meta: {
543
+ streamId,
544
+ profile,
545
+ codec,
546
+ pipelineKey: rtpSource.pipelineKey,
547
+ hostname: this.hostname
548
+ } });
549
+ return {
550
+ plane,
551
+ pipelineKey: rtpSource.pipelineKey,
552
+ subscriptionIds: /* @__PURE__ */ new Set(),
553
+ released: false
554
+ };
555
+ }
556
+ /** Destroy an entry's satellite plane and release its restream refcount once. */
557
+ async teardownEntry(entry, streamId) {
558
+ await entry.plane.dispose().catch((err) => {
559
+ this.logger.warn("remote-source plane: satellite plane dispose failed", { meta: {
560
+ streamId,
561
+ error: require_dist.errMsg(err)
562
+ } });
563
+ });
564
+ if (entry.released) return;
565
+ entry.released = true;
566
+ await this.api.streamBroker.releaseStreamWithCodec.mutate({ pipelineKey: entry.pipelineKey }).catch((err) => {
567
+ this.logger.debug("remote-source plane: releaseStreamWithCodec failed (benign on hub restart)", { meta: {
568
+ streamId,
569
+ pipelineKey: entry.pipelineKey,
570
+ error: require_dist.errMsg(err)
571
+ } });
572
+ });
573
+ }
574
+ /**
575
+ * Every decoder call pinned to THIS runner's node — mirror of the hub
576
+ * broker's wrapper (`stream-broker-manager.buildDecoderCapApi`): the
577
+ * `decoder` cap is a singleton whose UNPINNED calls resolve to the
578
+ * cluster-elected node, which is generally NOT this agent. Pinning create
579
+ * AND every session op to the same local node keeps the session + its shm
580
+ * ring here, where this plane's readers live.
581
+ */
582
+ buildPinnedDecoderCapApi() {
583
+ const api = this.api;
584
+ const pin = require_dist.nodePin(this.ownNodeId);
585
+ const toWireBytes = (bytes) => {
586
+ const copy = new Uint8Array(new ArrayBuffer(bytes.byteLength));
587
+ copy.set(bytes);
588
+ return copy;
589
+ };
590
+ return {
591
+ supportsCodec: async (input) => api.decoder.supportsCodec.query({ ...input }, pin),
592
+ getInfo: async () => api.decoder.getInfo.query(void 0, pin),
593
+ createSession: async (config) => api.decoder.createSession.query({ ...config }, pin),
594
+ pushPacket: async (input) => {
595
+ await api.decoder.pushPacket.query({
596
+ sessionId: input.sessionId,
597
+ packet: {
598
+ ...input.packet,
599
+ data: toWireBytes(input.packet.data)
600
+ }
601
+ }, pin);
602
+ },
603
+ pullFrames: async (input) => {
604
+ return (await api.decoder.pullFrames.query({ ...input }, pin)).map((frame) => ({
605
+ ...frame,
606
+ data: Buffer.from(frame.data)
607
+ }));
608
+ },
609
+ pullHandles: async (input) => api.decoder.pullHandles.query({ ...input }, pin),
610
+ destroySession: async (input) => {
611
+ await api.decoder.destroySession.query({ ...input }, pin);
612
+ },
613
+ openStream: async (input) => {
614
+ await api.decoder.openStream.query({ ...input }, pin);
615
+ },
616
+ updateConfig: async (input) => {
617
+ await api.decoder.updateConfig.query({ ...input }, pin);
618
+ },
619
+ getStats: async (input) => api.decoder.getStats.query({ ...input }, pin)
620
+ };
621
+ }
622
+ };
623
+ //#endregion
387
624
  //#region src/pipeline-runner/frame-queue.ts
388
625
  /**
389
626
  * Latest-frame-only buffer. Keeps only the most recent item, dropping all
@@ -1157,6 +1394,14 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
1157
1394
  */
1158
1395
  onboardAnalyzerTeardownTimers = /* @__PURE__ */ new Map();
1159
1396
  /**
1397
+ * REMOTE-SOURCE legs (cross-node Phase 2, P2d) — one per attached camera
1398
+ * whose `frameSource.kind === 'remote-restream'`: the owner's compressed
1399
+ * restream + a local satellite frame plane. DORMANT with today's default
1400
+ * payloads (`local-broker` / absent) — the map stays empty and every code
1401
+ * path is identical to pre-Phase-2. Torn down on detach / shutdown.
1402
+ */
1403
+ remotePlanes = /* @__PURE__ */ new Map();
1404
+ /**
1160
1405
  * Snapshot-equality cache for metrics-snapshot defer. The runner
1161
1406
  * fires per-camera metrics every `METRICS_SNAPSHOT_INTERVAL_MS`;
1162
1407
  * for an idle camera (no inference, queue empty, fps=0) every tick
@@ -1252,6 +1497,9 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
1252
1497
  }
1253
1498
  this.lastAnalyzerDetected.clear();
1254
1499
  for (const deviceId of [...this.onboardAnalyzerTeardownTimers.keys(), ...this.onboardAnalyzerSubs.keys()]) this.clearOnboardAnalyzer(deviceId);
1500
+ const remotePlanes = [...this.remotePlanes.values()];
1501
+ this.remotePlanes.clear();
1502
+ await Promise.all(remotePlanes.map((plane) => plane.dispose().catch(() => {})));
1255
1503
  if (this.runner) {
1256
1504
  this.runner.stop();
1257
1505
  this.runner = null;
@@ -1647,6 +1895,16 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
1647
1895
  this.clearOnboardAnalyzer(deviceId);
1648
1896
  attachment.motionUnsubscribe?.();
1649
1897
  attachment.detectionUnsubscribe?.();
1898
+ const remotePlane = this.remotePlanes.get(deviceId);
1899
+ if (remotePlane) {
1900
+ this.remotePlanes.delete(deviceId);
1901
+ remotePlane.dispose().catch((err) => {
1902
+ this.ctx?.logger.debug("remote-source plane dispose failed on detach", {
1903
+ tags: { deviceId },
1904
+ meta: { error: require_dist.errMsg(err) }
1905
+ });
1906
+ });
1907
+ }
1650
1908
  this.attached.delete(deviceId);
1651
1909
  this.lastMotionAt.delete(deviceId);
1652
1910
  this.lastEmittedCameraMetrics.delete(deviceId);
@@ -1788,6 +2046,39 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
1788
2046
  timestamp: remote.timestamp
1789
2047
  };
1790
2048
  }
2049
+ /**
2050
+ * The remote-source leg for a camera, or `null` for today's co-located
2051
+ * broker path (P2d). Created lazily on the FIRST subscription of a
2052
+ * `remote-restream` camera and shared by its motion + detection pollers;
2053
+ * `detachInternal` / `onShutdown` dispose it. With rollout OFF every
2054
+ * attach payload is `local-broker` (or absent) → always `null` → the
2055
+ * subscribe paths below are byte-identical to pre-Phase-2.
2056
+ */
2057
+ remoteSourceFor(config) {
2058
+ const frameSource = config.frameSource;
2059
+ if (!isRemoteRestream(frameSource)) return null;
2060
+ const existing = this.remotePlanes.get(config.deviceId);
2061
+ if (existing) return existing;
2062
+ const api = this.ctx.api;
2063
+ if (!api) return null;
2064
+ const hostname = require_hub_hostname.resolveHubHostname(frameSource.hubHostnameOverride, process.env["CAMSTACK_HUB_URL"]);
2065
+ const plane = new RemoteSourcePlane({
2066
+ api,
2067
+ logger: this.ctx.logger,
2068
+ deviceId: config.deviceId,
2069
+ ownNodeId: this.nodeId,
2070
+ hostname
2071
+ });
2072
+ this.ctx.logger.info("remote-source leg armed for camera", {
2073
+ tags: { deviceId: config.deviceId },
2074
+ meta: {
2075
+ ownerNodeId: frameSource.ownerNodeId,
2076
+ hostname: hostname ?? "(unresolved — acquires will fail until set)"
2077
+ }
2078
+ });
2079
+ this.remotePlanes.set(config.deviceId, plane);
2080
+ return plane;
2081
+ }
1791
2082
  async subscribeMotionFrames(config) {
1792
2083
  const ctx = this.ctx;
1793
2084
  const runner = this.runner;
@@ -1798,6 +2089,7 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
1798
2089
  log.warn("subscribeMotionFrames: this.ctx.api not available");
1799
2090
  return null;
1800
2091
  }
2092
+ const remote = this.remoteSourceFor(config);
1801
2093
  return startFrameHandlePoller({
1802
2094
  api,
1803
2095
  brokerId: require_dist.makeSourceBrokerId(config.deviceId, config.motionStreamId),
@@ -1807,6 +2099,7 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
1807
2099
  logger: log,
1808
2100
  ownNodeId: this.nodeId,
1809
2101
  getRemoteFrame: (handle) => this.getRemoteFrame(handle),
2102
+ ...remote ? { source: remote.makeSource(config.motionStreamId) } : {},
1810
2103
  onFrame: (frame, _handle) => {
1811
2104
  runner.enqueueMotionFrame(config.deviceId, frame);
1812
2105
  }
@@ -1883,6 +2176,7 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
1883
2176
  log.warn("subscribeDetectionFrames: this.ctx.api not available");
1884
2177
  return null;
1885
2178
  }
2179
+ const remote = this.remoteSourceFor(config);
1886
2180
  return startFrameHandlePoller({
1887
2181
  api,
1888
2182
  brokerId: require_dist.makeSourceBrokerId(config.deviceId, config.detectionStreamId),
@@ -1892,6 +2186,7 @@ var PipelineRunnerAddon = class extends require_dist.BaseAddon {
1892
2186
  logger: log,
1893
2187
  ownNodeId: this.nodeId,
1894
2188
  getRemoteFrame: (handle) => this.getRemoteFrame(handle),
2189
+ ...remote ? { source: remote.makeSource(config.detectionStreamId) } : {},
1895
2190
  onFrame: (frame, handle) => {
1896
2191
  runner.enqueueDetectionFrame(config.deviceId, frame, handle);
1897
2192
  }