@camstack/addon-pipeline 1.1.43 → 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 +526 -109
  5. package/dist/detection-pipeline/index.mjs +520 -104
  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 +60 -20
  14. package/dist/recorder/index.mjs +57 -18
  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 +86 -2
  18. package/dist/session-decode/decode-worker-child.mjs +83 -2
  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 +746 -135
  25. package/dist/stream-broker/index.mjs +737 -127
  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
@@ -2,23 +2,24 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-DxM2DSns.js");
6
- const require_dist = require("../dist-NvwN60Fq.js");
7
- const require_frame_handle_plane = require("../frame-handle-plane-B3Fxww8H.js");
5
+ const require_chunk = require("../chunk-D6vf50IK.js");
6
+ const require_dist = require("../dist-CySHUkXY.js");
7
+ const require_remote_source_plane = require("../remote-source-plane-D9m0z1N9.js");
8
+ const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-BLXzxpRB.js");
8
9
  let node_crypto = require("node:crypto");
9
- node_crypto = require_model_download_service_C_IHWnXx.__toESM(node_crypto);
10
+ node_crypto = require_chunk.__toESM(node_crypto);
10
11
  let node_child_process = require("node:child_process");
11
12
  let node_url = require("node:url");
12
13
  let node_fs = require("node:fs");
13
- node_fs = require_model_download_service_C_IHWnXx.__toESM(node_fs);
14
+ node_fs = require_chunk.__toESM(node_fs);
14
15
  let node_path = require("node:path");
15
- node_path = require_model_download_service_C_IHWnXx.__toESM(node_path);
16
+ node_path = require_chunk.__toESM(node_path);
16
17
  let node_os = require("node:os");
17
- node_os = require_model_download_service_C_IHWnXx.__toESM(node_os);
18
+ node_os = require_chunk.__toESM(node_os);
18
19
  let node_dns_promises = require("node:dns/promises");
19
20
  let node_module = require("node:module");
20
21
  let node_net = require("node:net");
21
- node_net = require_model_download_service_C_IHWnXx.__toESM(node_net);
22
+ node_net = require_chunk.__toESM(node_net);
22
23
  let net = require("net");
23
24
  let events = require("events");
24
25
  let node_fs_promises = require("node:fs/promises");
@@ -5177,6 +5178,16 @@ var RtspListenServer = class RtspListenServer {
5177
5178
  //#endregion
5178
5179
  //#region src/stream-broker/rtsp/rtsp-restream-provider.ts
5179
5180
  /**
5181
+ * Parse the numeric deviceId out of a canonical brokerId
5182
+ * (`${deviceId}/${camStreamId}` or `${deviceId}/${profile}`). Returns
5183
+ * `undefined` for a malformed id — so every device-scoped log line here
5184
+ * can carry `tags.deviceId` without risking a bogus tag value.
5185
+ */
5186
+ function deviceIdFromBrokerId$1(brokerId) {
5187
+ const parsed = Number(brokerId.split("/")[0]);
5188
+ return Number.isFinite(parsed) ? parsed : void 0;
5189
+ }
5190
+ /**
5180
5191
  * Internal sub-object of `StreamBrokerManager` that implements the RTSP
5181
5192
  * restream bookkeeping. No longer registered as a separate capability —
5182
5193
  * accessed via passthrough methods on `StreamBrokerManager`.
@@ -5259,20 +5270,26 @@ var RtspRestreamProviderImpl = class {
5259
5270
  const token = this.server.regenerateToken(brokerId);
5260
5271
  if (token) {
5261
5272
  this.persistTokens();
5262
- this.logger.info("RTSP token regenerated", { meta: {
5263
- brokerId,
5264
- tokenPrefix: token.slice(0, 8)
5265
- } });
5273
+ this.logger.info("RTSP token regenerated", {
5274
+ tags: { deviceId: deviceIdFromBrokerId$1(brokerId) },
5275
+ meta: {
5276
+ brokerId,
5277
+ tokenPrefix: token.slice(0, 8)
5278
+ }
5279
+ });
5266
5280
  }
5267
5281
  return token;
5268
5282
  }
5269
5283
  setEnabled(brokerId, enabled) {
5270
5284
  this.server.setEnabled(brokerId, enabled);
5271
5285
  this.persistEnabled();
5272
- this.logger.info("RTSP restream state changed", { meta: {
5273
- brokerId,
5274
- enabled
5275
- } });
5286
+ this.logger.info("RTSP restream state changed", {
5287
+ tags: { deviceId: deviceIdFromBrokerId$1(brokerId) },
5288
+ meta: {
5289
+ brokerId,
5290
+ enabled
5291
+ }
5292
+ });
5276
5293
  }
5277
5294
  isEnabled(brokerId) {
5278
5295
  return this.server.isEnabled(brokerId);
@@ -9437,7 +9454,6 @@ var StreamBroker = class StreamBroker {
9437
9454
  static RTP_ACTIVITY_TIMEOUT_MS = 8e3;
9438
9455
  manualStop = false;
9439
9456
  stopping = false;
9440
- restreamers = [];
9441
9457
  pipeServer;
9442
9458
  rtspRestreamer;
9443
9459
  preBuffer;
@@ -9614,9 +9630,6 @@ var StreamBroker = class StreamBroker {
9614
9630
  url
9615
9631
  };
9616
9632
  }
9617
- setRestreamers(restreamers) {
9618
- this.restreamers = restreamers;
9619
- }
9620
9633
  /**
9621
9634
  * Inject a manager-owned resolver for this broker's OWN RTSP restream URL
9622
9635
  * (`rtsp://127.0.0.1:<port>/<token>[/muted]`). When the source is an RTP
@@ -10057,10 +10070,6 @@ var StreamBroker = class StreamBroker {
10057
10070
  this.logger?.info("First keyframe received — decoder can start");
10058
10071
  }
10059
10072
  }
10060
- for (const restreamer of this.restreamers) {
10061
- const streamId = `${this.deviceId}/video`;
10062
- restreamer.pushPacket(streamId, packet);
10063
- }
10064
10073
  if (packet.type === "video" && this.firstKeyframeReceived) {
10065
10074
  const decoderPacket = this.decoderPacketWithParamSets(packet);
10066
10075
  this.decoderSeedBuffer.push(decoderPacket);
@@ -10456,7 +10465,7 @@ var StreamBroker = class StreamBroker {
10456
10465
  this.logger?.warn("subscribeFrameHandles: no decoder API — frame handles unavailable");
10457
10466
  return null;
10458
10467
  }
10459
- this.frameHandlePlane = new require_frame_handle_plane.FrameHandlePlane({
10468
+ this.frameHandlePlane = new require_remote_source_plane.FrameHandlePlane({
10460
10469
  decoderApi: this.decoderApi,
10461
10470
  logger: this.logger?.child("frame-handle-plane"),
10462
10471
  resolveStreamInfo: () => this.resolveFrameHandleStreamInfo(),
@@ -12157,6 +12166,168 @@ var TranscodePipelineManager = class {
12157
12166
  }
12158
12167
  };
12159
12168
  //#endregion
12169
+ //#region src/stream-broker/webrtc/remote-encoded-source-broker.ts
12170
+ var RemoteEncodedSourceBroker = class {
12171
+ api;
12172
+ logger;
12173
+ deviceId;
12174
+ streamId;
12175
+ ownerNodeId;
12176
+ hostname;
12177
+ createBroker;
12178
+ /** Single-flight acquire. */
12179
+ pending = null;
12180
+ disposed = false;
12181
+ constructor(options) {
12182
+ this.api = options.api;
12183
+ this.logger = options.logger.withTags({
12184
+ deviceId: options.deviceId,
12185
+ streamId: options.streamId
12186
+ });
12187
+ this.deviceId = options.deviceId;
12188
+ this.streamId = options.streamId;
12189
+ this.ownerNodeId = options.ownerNodeId;
12190
+ this.hostname = options.reachableHost;
12191
+ this.createBroker = options.createBroker;
12192
+ }
12193
+ /**
12194
+ * Single-flight: acquire the owner restream + construct/start the inner
12195
+ * broker once, sharing the same instance across concurrent callers. A
12196
+ * rejected acquire is retryable — the next `getBroker()` re-attempts from
12197
+ * scratch (fresh URL/token).
12198
+ */
12199
+ async getBroker() {
12200
+ if (this.disposed) throw new Error("RemoteEncodedSourceBroker: getBroker after dispose");
12201
+ if (!this.pending) {
12202
+ const creation = this.createEntry();
12203
+ this.pending = creation;
12204
+ creation.catch(() => {
12205
+ if (this.pending === creation) this.pending = null;
12206
+ });
12207
+ }
12208
+ return (await this.pending).broker;
12209
+ }
12210
+ /** Stop the inner broker and release the acquired restream once. Idempotent. */
12211
+ async dispose() {
12212
+ if (this.disposed) return;
12213
+ this.disposed = true;
12214
+ const pending = this.pending;
12215
+ this.pending = null;
12216
+ if (!pending) return;
12217
+ let entry;
12218
+ try {
12219
+ entry = await pending;
12220
+ } catch {
12221
+ return;
12222
+ }
12223
+ await entry.broker.stop().catch((err) => {
12224
+ this.logger.warn("remote-encoded source broker: inner broker stop failed", { meta: {
12225
+ streamId: this.streamId,
12226
+ error: require_dist.errMsg(err)
12227
+ } });
12228
+ });
12229
+ await this.api.streamBroker.releaseStreamWithCodec.mutate({ pipelineKey: entry.pipelineKey }, require_dist.nodePin(this.ownerNodeId)).catch((err) => {
12230
+ this.logger.debug("remote-encoded source broker: releaseStreamWithCodec failed (benign on hub restart)", { meta: {
12231
+ streamId: this.streamId,
12232
+ pipelineKey: entry.pipelineKey,
12233
+ error: require_dist.errMsg(err)
12234
+ } });
12235
+ });
12236
+ }
12237
+ async createEntry() {
12238
+ const { api, deviceId, streamId, ownerNodeId } = this;
12239
+ if (this.hostname === void 0) throw new Error("RemoteEncodedSourceBroker: owner hostname unresolvable — set frameSource.hubHostnameOverride or CAMSTACK_HUB_URL (a 127.0.0.1 restream URL would dial the wrong box)");
12240
+ const profile = require_remote_source_plane.profileForStreamId(await api.streamBroker.listAllProfileSlots.query(void 0, require_dist.nodePin(ownerNodeId)), deviceId, streamId);
12241
+ if (profile === null) throw new Error(`RemoteEncodedSourceBroker: no broker profile is fed by camStream '${streamId}' of device ${deviceId} — cannot acquire the owner restream`);
12242
+ const rtpSource = await api.streamBroker.getStreamWithCodec.mutate({
12243
+ deviceId,
12244
+ video: "copy",
12245
+ audio: "none",
12246
+ profile,
12247
+ hostname: this.hostname,
12248
+ tag: `webrtc:remote-encoded:${deviceId}/${streamId}`
12249
+ }, require_dist.nodePin(ownerNodeId));
12250
+ const source = {
12251
+ type: "rtsp",
12252
+ url: rtpSource.url,
12253
+ videoCodec: rtpSource.videoCodec
12254
+ };
12255
+ const brokerId = `${deviceId}/${profile}`;
12256
+ const broker = this.createBroker({
12257
+ brokerId,
12258
+ logger: this.logger.child(profile)
12259
+ });
12260
+ broker.setSourceProvider(() => source);
12261
+ await broker.start(source);
12262
+ this.logger.info("remote-encoded source broker: owner restream acquired", { meta: {
12263
+ streamId,
12264
+ profile,
12265
+ codec: rtpSource.videoCodec,
12266
+ pipelineKey: rtpSource.pipelineKey,
12267
+ hostname: this.hostname
12268
+ } });
12269
+ return {
12270
+ broker,
12271
+ pipelineKey: rtpSource.pipelineKey
12272
+ };
12273
+ }
12274
+ };
12275
+ //#endregion
12276
+ //#region src/stream-broker/webrtc/resolve-tier-slot.ts
12277
+ /**
12278
+ * `resolveTierSlot` — pure helper: given an owner's profile slots, a
12279
+ * deviceId, and a preferred tier, resolve WHICH assigned tier camStream to
12280
+ * consume.
12281
+ *
12282
+ * Slice 3b (ADAPTIVE-target WebRTC live-view for remote-owned cameras,
12283
+ * lazy-per-tier). The exact-tier slot wins when it is assigned; otherwise
12284
+ * falls back through the canonical `CAM_PROFILE_ORDER` (high→mid→low) — the
12285
+ * same single-source tier order used by `stream-broker-manager` and the
12286
+ * analogous no-hints tier-selection fallback in `broker-webrtc-server.ts`.
12287
+ */
12288
+ /**
12289
+ * Resolve the camStream feeding `deviceId` at `preferredTier`. Prefers an
12290
+ * exact-tier match; when that tier is unassigned, falls back through
12291
+ * `TIER_PREFERENCE` order to the first assigned tier. Returns `null` when
12292
+ * the device has no assigned slot at all. Pure; never mutates `slots`.
12293
+ */
12294
+ function resolveTierSlot(slots, deviceId, preferredTier) {
12295
+ const deviceSlots = slots.filter((slot) => slot.deviceId === deviceId);
12296
+ const exact = deviceSlots.find((slot) => slot.profile === preferredTier && slot.sourceCamStreamId !== null);
12297
+ if (exact && exact.sourceCamStreamId !== null) return {
12298
+ camStreamId: exact.sourceCamStreamId,
12299
+ profile: exact.profile
12300
+ };
12301
+ for (const tier of require_dist.CAM_PROFILE_ORDER) {
12302
+ const match = deviceSlots.find((slot) => slot.profile === tier && slot.sourceCamStreamId !== null);
12303
+ if (match && match.sourceCamStreamId !== null) return {
12304
+ camStreamId: match.sourceCamStreamId,
12305
+ profile: match.profile
12306
+ };
12307
+ }
12308
+ return null;
12309
+ }
12310
+ //#endregion
12311
+ //#region src/stream-broker/webrtc/webrtc-source-mode.ts
12312
+ /**
12313
+ * Decide how a WebRTC live-view node sources a camera. Owner==self → the
12314
+ * local broker path (unchanged). Owner≠self + reachable → a remote-encoded
12315
+ * adapter dialing the owner restream. Owner≠self + no reachableHost →
12316
+ * unresolvable (fail fast; a 127.0.0.1 restream URL would dial the wrong box).
12317
+ */
12318
+ function resolveWebrtcSourceMode(input) {
12319
+ if (input.ownerNodeId === input.ownNodeId) return { kind: "local" };
12320
+ if (input.reachableHost === void 0) return {
12321
+ kind: "unresolvable",
12322
+ ownerNodeId: input.ownerNodeId
12323
+ };
12324
+ return {
12325
+ kind: "remote-adapter",
12326
+ ownerNodeId: input.ownerNodeId,
12327
+ reachableHost: input.reachableHost
12328
+ };
12329
+ }
12330
+ //#endregion
12160
12331
  //#region src/stream-broker/stream-broker/stream-broker-manager.ts
12161
12332
  /**
12162
12333
  * Resolve a derived stream's source to a physical camStreamId, if it is pinned
@@ -12293,6 +12464,19 @@ function findProfileForStream(map, camStreamId) {
12293
12464
  return null;
12294
12465
  }
12295
12466
  /**
12467
+ * Pure lookup behind `StreamBrokerManager`'s `profileTierResolver` closure
12468
+ * (Slice 3b Task 2). `localLookup` mirrors today's behavior — parse the
12469
+ * brokerId and walk the LOCAL device's `this.assignments` map — which is
12470
+ * always empty for a remote-owned camera, so it returns null for every
12471
+ * remote-ensured adapter's brokerId. `remoteBrokerTiers` is the fallback
12472
+ * populated at the remote-adapter ensure site (Task 3) so `selectBestBroker`
12473
+ * can still tier-match a remote-owned camera's broker. Local wins when both
12474
+ * resolve a tier — a local broker is never shadowed by a stale remote entry.
12475
+ */
12476
+ function resolveBrokerTier(brokerId, localLookup, remoteBrokerTiers) {
12477
+ return localLookup(brokerId) ?? remoteBrokerTiers.get(brokerId) ?? null;
12478
+ }
12479
+ /**
12296
12480
  * Watchdog parameters for the stream-health emitter. Brokers that have
12297
12481
  * received zero video packets for STREAM_STALE_TIMEOUT_MS are reported
12298
12482
  * as `stream.offline`; the next packet flips them back to `stream.online`.
@@ -12331,7 +12515,7 @@ function parseBrokerId(brokerId) {
12331
12515
  camStreamId
12332
12516
  };
12333
12517
  }
12334
- var StreamBrokerManager = class {
12518
+ var StreamBrokerManager = class StreamBrokerManager {
12335
12519
  /**
12336
12520
  * brokers keyed by brokerId = `${deviceId}/${camStreamId}`.
12337
12521
  *
@@ -12400,7 +12584,6 @@ var StreamBrokerManager = class {
12400
12584
  * it down on idle / retract. Populated by {@link attachDerivedFeeder}.
12401
12585
  */
12402
12586
  derivedFeeders = /* @__PURE__ */ new Map();
12403
- restreamers = [];
12404
12587
  staticDecoders;
12405
12588
  logger;
12406
12589
  /** Public accessor for same-process callers (e.g. `CameraStreamsProvider.pickStream`)
@@ -12419,7 +12602,6 @@ var StreamBrokerManager = class {
12419
12602
  * next pull after a hub restart. Keyed by `brokerIdFor(deviceId, camStreamId)`.
12420
12603
  */
12421
12604
  lastProbedByBroker = /* @__PURE__ */ new Map();
12422
- capabilities = null;
12423
12605
  /**
12424
12606
  * tRPC api proxy injected from the host addon. Used to resolve the
12425
12607
  * `decoder` capability (and any future cross-cap consumers) via the
@@ -12448,9 +12630,66 @@ var StreamBrokerManager = class {
12448
12630
  * node. `null` until `setApiAccess` wires it (falls back to unpinned).
12449
12631
  */
12450
12632
  localNodeId = null;
12633
+ /**
12634
+ * Short-TTL cache of `clusterRoles.ingestNode` (via `getIngestOwner`) for the
12635
+ * `ensureBroker` dormancy gate — the reconcile calls `ensureBroker` per
12636
+ * camera, so a per-call cross-node query would be wasteful. Refreshed lazily
12637
+ * when older than {@link INGEST_OWNER_CACHE_TTL_MS}. `null` = never resolved.
12638
+ */
12639
+ cachedIngestOwnerNodeId = null;
12640
+ cachedIngestOwnerAt = 0;
12641
+ static INGEST_OWNER_CACHE_TTL_MS = 6e4;
12642
+ lastOwnershipReconcile = Promise.resolve();
12451
12643
  defaultPreBufferSec = 10;
12452
12644
  webrtcServer = null;
12453
12645
  /**
12646
+ * brokerId → the remote-encoded adapter backing a non-owned camera's WebRTC
12647
+ * session (Slice 3 Task 3 — cross-node ingest wiring). Mirrors `this.brokers`
12648
+ * for the cross-node leg: `ensureBrokerForWebrtcSession` get-or-creates one
12649
+ * when `getIngestOwner` resolves to a DIFFERENT node than this one, and
12650
+ * tears it down on the adapter's inner broker's own `onDemandChange(false)`
12651
+ * edge (last-session-idle) — the same hook `attachDerivedFeeder` uses to
12652
+ * tear down a derived feeder.
12653
+ */
12654
+ remoteBrokerAdapters = /* @__PURE__ */ new Map();
12655
+ /**
12656
+ * brokerId → in-flight `ensureBrokerForWebrtcSession` get-or-create for
12657
+ * that broker's remote adapter. Manager-level single-flight guard: two
12658
+ * concurrent WebRTC session creations for the SAME non-owned brokerId
12659
+ * (a real race — first-session-ever for a camera whose owner is a
12660
+ * different node) both pass the `remoteBrokerAdapters.has()` early-return
12661
+ * with the map still empty, because the check and the eventual `.set()`
12662
+ * straddle an `await getIngestOwner.query()`. Without this guard both
12663
+ * callers would construct their own `RemoteEncodedSourceBroker`, both
12664
+ * `.set()` into `remoteBrokerAdapters` (second overwrites first), and
12665
+ * both acquire the owner's restream (`getStreamWithCodec`) — the first
12666
+ * adapter becomes unreachable from the map so its `onDemandChange(false)`
12667
+ * teardown never runs and its restream refcount leaks forever. Concurrent
12668
+ * callers instead await the SAME promise, so exactly one adapter is
12669
+ * constructed, registered, and refcounted per brokerId.
12670
+ */
12671
+ remoteAdapterEnsures = /* @__PURE__ */ new Map();
12672
+ /**
12673
+ * brokerId → the profile tier a remote-ensured adapter was created for
12674
+ * (Slice 3b Task 2). `this.assignments` only tracks LOCALLY-owned devices,
12675
+ * so the `profileTierResolver` closure below always misses for a remote
12676
+ * adapter's brokerId — this map is the fallback so `selectBestBroker` can
12677
+ * still tier-match it. Populated at the ensure site alongside
12678
+ * `remoteBrokerAdapters.set(brokerId, ...)` (Task 3 threads the tier in)
12679
+ * and cleared at every `remoteBrokerAdapters.delete(brokerId)` site so no
12680
+ * stale tier lingers past the adapter's own lifetime.
12681
+ */
12682
+ remoteBrokerTiers = /* @__PURE__ */ new Map();
12683
+ /**
12684
+ * Injected factory for the inner broker a `RemoteEncodedSourceBroker`
12685
+ * drives. Production wraps the real `StreamBroker` — this leg never decodes
12686
+ * locally, so the decoder/audio-codec caps and local-node pin are unused
12687
+ * (mirrors the recommendation in `RemoteEncodedSourceBroker`'s doc comment).
12688
+ * Tests substitute a fake via `setRemoteEncodedBrokerFactory` so no real
12689
+ * RTSP dial happens.
12690
+ */
12691
+ createRemoteEncodedBroker = (deps) => new StreamBroker(deps.brokerId, null, deps.logger, null, null);
12692
+ /**
12454
12693
  * Transcode-pipeline manager — backs `getStreamWithCodec`. Lazily
12455
12694
  * created on first acquire, lives until `destroyAll`.
12456
12695
  */
@@ -12677,9 +12916,6 @@ var StreamBrokerManager = class {
12677
12916
  }
12678
12917
  };
12679
12918
  }
12680
- setCapabilitiesAccess(access) {
12681
- this.capabilities = access;
12682
- }
12683
12919
  /**
12684
12920
  * Wire the addon's `ctx.api` proxy. Required for the broker to
12685
12921
  * resolve the `decoder` capability via tRPC — see
@@ -12724,6 +12960,61 @@ var StreamBrokerManager = class {
12724
12960
  this.eventBus = bus;
12725
12961
  this.startStreamHealthWatchdog();
12726
12962
  this.subscribePlaceholderStateSources(bus);
12963
+ this.subscribeIngestOwnerChanges(bus);
12964
+ }
12965
+ /**
12966
+ * Track the cluster camera-source owner from the orchestrator's pushed
12967
+ * `PipelineIngestOwnerChanged` events instead of polling `getIngestOwner`
12968
+ * on every `ensureBroker`. The orchestrator re-emits on every node-capability
12969
+ * refresh (boot / agent online-offline / ingest flip), so this keeps the
12970
+ * ingest-owner gate current and prompt; the query in {@link isLocalIngestOwner}
12971
+ * survives only as a cold-start + long-backstop reconcile (loss-safety).
12972
+ */
12973
+ subscribeIngestOwnerChanges(bus) {
12974
+ bus.subscribe({ category: require_dist.EventCategory.PipelineIngestOwnerChanged }, (event) => {
12975
+ if (!require_dist.isEvent(event, require_dist.EventCategory.PipelineIngestOwnerChanged)) return;
12976
+ const { ownerNodeId } = event.data;
12977
+ const changed = this.cachedIngestOwnerNodeId !== ownerNodeId;
12978
+ this.cachedIngestOwnerNodeId = ownerNodeId;
12979
+ this.cachedIngestOwnerAt = Date.now();
12980
+ if (changed) this.logger.debug("ingest owner changed (pushed)", { meta: {
12981
+ ownerNodeId,
12982
+ self: this.localNodeId
12983
+ } });
12984
+ this.lastOwnershipReconcile = this.reconcileOwnershipTeardown();
12985
+ });
12986
+ }
12987
+ /**
12988
+ * Tear down every LOCAL physical source broker when this node is no longer the
12989
+ * cluster ingest owner. `this.brokers` holds only physical source + derived
12990
+ * brokers (the `RemoteEncodedSourceBroker` restream adapters a non-owner uses
12991
+ * live in the separate `remoteBrokerAdapters` map, so they are untouched).
12992
+ * Re-creation is blocked by the ingest-owner gate on this node; the node's
12993
+ * consumers re-source from the new owner's restream via the orchestrator's
12994
+ * re-dispatch. No-op when this node IS the owner or holds no brokers, so a
12995
+ * repeated same-owner re-emit is cheap.
12996
+ */
12997
+ async reconcileOwnershipTeardown() {
12998
+ if (this.localNodeId === null) return;
12999
+ if (this.cachedIngestOwnerNodeId === null) return;
13000
+ if (this.cachedIngestOwnerNodeId === this.localNodeId) return;
13001
+ if (this.brokers.size === 0) return;
13002
+ const brokerIds = [...this.brokers.keys()];
13003
+ this.logger.info("ingest owner moved away — tearing down local physical brokers", { meta: {
13004
+ owner: this.cachedIngestOwnerNodeId,
13005
+ self: this.localNodeId,
13006
+ count: brokerIds.length
13007
+ } });
13008
+ for (const brokerId of brokerIds) {
13009
+ const parsed = this.parseBrokerId(brokerId);
13010
+ if (!parsed) continue;
13011
+ await this.releaseBroker(parsed.deviceId, parsed.camStreamId).catch((err) => {
13012
+ this.logger.warn("reverse-flip teardown: releaseBroker failed", { meta: {
13013
+ brokerId,
13014
+ error: require_dist.errMsg(err)
13015
+ } });
13016
+ });
13017
+ }
12727
13018
  }
12728
13019
  /**
12729
13020
  * Subscribe to the cap-scoped bus events that drive the per-broker
@@ -12767,14 +13058,242 @@ var StreamBrokerManager = class {
12767
13058
  }
12768
13059
  setWebrtcServer(server) {
12769
13060
  this.webrtcServer = server;
12770
- server.setProfileTierResolver((brokerId) => {
12771
- const parsed = this.parseBrokerId(brokerId);
13061
+ server.setProfileTierResolver((brokerId) => resolveBrokerTier(brokerId, (id) => {
13062
+ const parsed = this.parseBrokerId(id);
12772
13063
  if (!parsed) return null;
12773
13064
  return findProfileForStream(this.assignments.get(parsed.deviceId)?.map ?? {}, parsed.camStreamId);
12774
- });
13065
+ }, this.remoteBrokerTiers));
12775
13066
  server.setSourceRestreamUrlResolver((brokerId) => this.rtspProvider.getEntry(brokerId)?.url ?? null);
13067
+ server.setEnsureAdaptiveTierBrokerHook((deviceId, tier) => this.ensureRemoteAdaptiveTierBroker(deviceId, tier));
12776
13068
  for (const [brokerId, broker] of this.brokers) server.registerBroker(brokerId, broker);
12777
13069
  }
13070
+ /**
13071
+ * Test/injection seam for the inner broker a `RemoteEncodedSourceBroker`
13072
+ * drives (Slice 3 Task 3). Production never calls this — the field default
13073
+ * already wraps the real `StreamBroker`. Mirrors the `createBroker`
13074
+ * injection point `RemoteEncodedSourceBroker` itself exposes.
13075
+ */
13076
+ setRemoteEncodedBrokerFactory(factory) {
13077
+ this.createRemoteEncodedBroker = factory;
13078
+ }
13079
+ /**
13080
+ * Ensure a broker exists for `brokerId` before a WebRTC session subscribes
13081
+ * to it (Slice 3 Task 3 — cross-node ingest wiring). Called by
13082
+ * `WebrtcSessionProvider.createSession`/`handleOffer` right before they
13083
+ * delegate to `BrokerWebrtcServer`.
13084
+ *
13085
+ * No-op when a LOCAL broker already covers `brokerId` — the
13086
+ * default/dormant state while every camera's `ingestNode` is the hub, so
13087
+ * this returns on the very first check with ZERO extra calls (byte-
13088
+ * identical to today). Otherwise resolves the cluster's camera-source
13089
+ * owner (`getIngestOwner`) and branches via `resolveWebrtcSourceMode`:
13090
+ *
13091
+ * - `local` → this node IS the owner but hasn't published/registered a
13092
+ * broker for this brokerId (a genuinely missing/misconfigured
13093
+ * stream) — do nothing and let `BrokerWebrtcServer` throw its normal
13094
+ * "No broker for stream" error, unchanged.
13095
+ * - `remote-adapter` → get-or-create a `RemoteEncodedSourceBroker` that
13096
+ * dials the owner's compressed passthrough restream, register its
13097
+ * inner broker with the WebRTC server exactly like a local broker (so
13098
+ * `setupSessionForBroker`'s resolution, tier logic, and everything
13099
+ * downstream see an ordinary `IStreamBroker` — this leg never touches
13100
+ * `broker-webrtc-server.ts`), and tear the adapter down on its own
13101
+ * `onDemandChange(false)` (last-session-idle) edge, mirroring
13102
+ * `attachDerivedFeeder`'s local-broker teardown hook.
13103
+ * - `unresolvable` → the owner is set but has no reachable host —
13104
+ * throw a descriptive error rather than let the session hang.
13105
+ *
13106
+ * `camStreamId === undefined` (an `adaptive` target with no cam stream
13107
+ * pinned yet) can't resolve a single owner profile to acquire — falls
13108
+ * through to the natural local-only resolution/error, same as today.
13109
+ *
13110
+ * `tier` (Slice 3b Task 3) is an optional hint from
13111
+ * `ensureRemoteAdaptiveTierBroker` — the profile the caller already
13112
+ * resolved `camStreamId` from via `resolveTierSlot`. When set, it is
13113
+ * recorded into `remoteBrokerTiers` alongside the adapter registration so
13114
+ * `resolveBrokerTier`/`selectBestBroker` can tier-match this brokerId even
13115
+ * though it's absent from `this.assignments` (remote-owned). Callers that
13116
+ * don't have a resolved tier (the plain WebRTC session path) omit it —
13117
+ * unchanged behavior.
13118
+ */
13119
+ async ensureBrokerForWebrtcSession(deviceId, brokerId, camStreamId, tier) {
13120
+ if (this.brokers.has(brokerId)) return;
13121
+ if (this.remoteBrokerAdapters.has(brokerId)) return;
13122
+ if (camStreamId === void 0) return;
13123
+ if (!this.api || this.localNodeId === null) return;
13124
+ const api = this.api;
13125
+ const ownNodeId = this.localNodeId;
13126
+ const existingEnsure = this.remoteAdapterEnsures.get(brokerId);
13127
+ if (existingEnsure) {
13128
+ await existingEnsure;
13129
+ return;
13130
+ }
13131
+ const ensure = this.createRemoteBrokerAdapterForSession(api, ownNodeId, deviceId, brokerId, camStreamId, tier);
13132
+ this.remoteAdapterEnsures.set(brokerId, ensure);
13133
+ try {
13134
+ await ensure;
13135
+ } finally {
13136
+ this.remoteAdapterEnsures.delete(brokerId);
13137
+ }
13138
+ }
13139
+ /**
13140
+ * Single-flight body for `ensureBrokerForWebrtcSession`'s remote-adapter
13141
+ * get-or-create — resolves the ingest owner, and for `remote-adapter` mode
13142
+ * constructs the `RemoteEncodedSourceBroker`, registers it with the WebRTC
13143
+ * server, and wires its idle-teardown hook. Only ever run once per
13144
+ * brokerId at a time; see `remoteAdapterEnsures`.
13145
+ */
13146
+ async createRemoteBrokerAdapterForSession(api, ownNodeId, deviceId, brokerId, camStreamId, tier) {
13147
+ let owner;
13148
+ try {
13149
+ owner = await api.pipelineOrchestrator.getIngestOwner.query();
13150
+ } catch (err) {
13151
+ this.logger.debug("webrtc-session: getIngestOwner unavailable — defaulting to local (cap not mounted yet?)", {
13152
+ tags: { deviceId },
13153
+ meta: { error: require_dist.errMsg(err) }
13154
+ });
13155
+ owner = { ownerNodeId: ownNodeId };
13156
+ }
13157
+ const mode = resolveWebrtcSourceMode({
13158
+ ownerNodeId: owner.ownerNodeId,
13159
+ ownNodeId,
13160
+ reachableHost: owner.reachableHost
13161
+ });
13162
+ if (mode.kind === "local") return;
13163
+ if (mode.kind === "unresolvable") {
13164
+ const configIssueSuffix = owner.configIssue ? ` (${owner.configIssue})` : "";
13165
+ throw new Error(`webrtc-session: device ${deviceId} is owned by node "${mode.ownerNodeId}" but no reachable host is configured for it — cannot source its WebRTC stream remotely${configIssueSuffix}`);
13166
+ }
13167
+ const adapter = new RemoteEncodedSourceBroker({
13168
+ api,
13169
+ logger: this.logger.child("remote-encoded").withTags({ deviceId }),
13170
+ deviceId,
13171
+ streamId: camStreamId,
13172
+ ownerNodeId: mode.ownerNodeId,
13173
+ reachableHost: mode.reachableHost,
13174
+ createBroker: this.createRemoteEncodedBroker
13175
+ });
13176
+ this.remoteBrokerAdapters.set(brokerId, adapter);
13177
+ if (tier) this.remoteBrokerTiers.set(brokerId, tier);
13178
+ let broker;
13179
+ try {
13180
+ broker = await adapter.getBroker();
13181
+ } catch (err) {
13182
+ this.remoteBrokerAdapters.delete(brokerId);
13183
+ this.remoteBrokerTiers.delete(brokerId);
13184
+ throw err;
13185
+ }
13186
+ this.webrtcServer?.registerBroker(brokerId, broker);
13187
+ let unsubscribeDemand = () => {};
13188
+ unsubscribeDemand = broker.onDemandChange((active) => {
13189
+ if (active) return;
13190
+ unsubscribeDemand();
13191
+ this.remoteBrokerAdapters.delete(brokerId);
13192
+ this.remoteBrokerTiers.delete(brokerId);
13193
+ this.webrtcServer?.unregisterBroker(brokerId);
13194
+ adapter.dispose().catch((disposeErr) => {
13195
+ this.logger.warn("webrtc-session: remote-encoded adapter dispose failed", {
13196
+ tags: { deviceId },
13197
+ meta: {
13198
+ brokerId,
13199
+ error: require_dist.errMsg(disposeErr)
13200
+ }
13201
+ });
13202
+ });
13203
+ });
13204
+ }
13205
+ /**
13206
+ * Idempotent cleanup for a remote adapter registered by
13207
+ * `ensureBrokerForWebrtcSession`. `WebrtcSessionProvider.createSession`/
13208
+ * `handleOffer` call this when the downstream `BrokerWebrtcServer`
13209
+ * delegate throws AFTER the adapter was already registered.
13210
+ *
13211
+ * Without this, teardown is driven ONLY by the inner broker's
13212
+ * `onDemandChange(true→false)` edge (last-WebRTC-subscriber-leaves). If
13213
+ * the delegate never succeeds, demand never reaches `true`, that edge
13214
+ * never fires, and the adapter + its WebRTC registration + the owner
13215
+ * restream refcount (`releaseStreamWithCodec`) leak permanently.
13216
+ *
13217
+ * A cheap no-op when no remote adapter exists for `brokerId` — the
13218
+ * owner==self / local-broker path never registers one, so this call adds
13219
+ * zero behavior there. Also safe if the adapter was already torn down
13220
+ * (e.g. a racing `onDemandChange(false)` edge) — `RemoteEncodedSourceBroker
13221
+ * .dispose()` is itself idempotent. Dispose failures are logged and
13222
+ * swallowed; this method never throws, so callers can always re-throw the
13223
+ * original delegate error afterward.
13224
+ */
13225
+ async disposeRemoteBrokerAdapterIfPresent(brokerId) {
13226
+ const adapter = this.remoteBrokerAdapters.get(brokerId);
13227
+ if (!adapter) return;
13228
+ this.remoteBrokerAdapters.delete(brokerId);
13229
+ this.remoteBrokerTiers.delete(brokerId);
13230
+ this.webrtcServer?.unregisterBroker(brokerId);
13231
+ try {
13232
+ await adapter.dispose();
13233
+ } catch (disposeErr) {
13234
+ this.logger.warn("webrtc-session: remote-encoded adapter dispose failed (delegate-failure cleanup)", { meta: {
13235
+ brokerId,
13236
+ error: require_dist.errMsg(disposeErr)
13237
+ } });
13238
+ }
13239
+ }
13240
+ /**
13241
+ * Lazily ensure a remote adapter exists for a device's PREFERRED tier's
13242
+ * camStream (Slice 3b Task 3 — adaptive WebRTC live-view for remote-owned
13243
+ * cameras, lazy-per-tier). Adaptive-target sessions don't carry a
13244
+ * session-pinned camStreamId, so unlike `ensureBrokerForWebrtcSession`
13245
+ * this method's own job is resolving WHICH camStreamId the preferred tier
13246
+ * maps to on the owning node — via `resolveTierSlot` walking the owner's
13247
+ * `listAllProfileSlots` — before delegating to
13248
+ * `ensureBrokerForWebrtcSession` for the actual get-or-create (reusing its
13249
+ * single-flight guard and idle-teardown wiring unchanged).
13250
+ *
13251
+ * No-op for a LOCALLY-owned device — `this.assignments` already tracks it
13252
+ * and the normal (non-adaptive) resolve path handles it; this returns on
13253
+ * the very first check with ZERO extra calls, byte-identical to today.
13254
+ *
13255
+ * Mirrors `ensureBrokerForWebrtcSession`'s owner-resolution: graceful
13256
+ * `getIngestOwner` (defaults to local on failure — cap not mounted yet) +
13257
+ * `resolveWebrtcSourceMode`. `local` → return (nothing to do; the normal
13258
+ * resolve path already covers this node being the owner). `unresolvable`
13259
+ * → throw the same descriptive error rather than let a session hang.
13260
+ *
13261
+ * When the preferred tier (and every fallback tier) has no assigned slot
13262
+ * on the owner, `resolveTierSlot` returns null — this method returns
13263
+ * without throwing; the normal WebRTC resolve path will surface its own
13264
+ * "no broker for stream" error naturally once a session actually tries to
13265
+ * subscribe.
13266
+ */
13267
+ async ensureRemoteAdaptiveTierBroker(deviceId, preferredTier) {
13268
+ if (this.assignments.has(deviceId)) return;
13269
+ if (!this.api || this.localNodeId === null) return;
13270
+ const api = this.api;
13271
+ const ownNodeId = this.localNodeId;
13272
+ let owner;
13273
+ try {
13274
+ owner = await api.pipelineOrchestrator.getIngestOwner.query();
13275
+ } catch (err) {
13276
+ this.logger.debug("webrtc-adaptive-tier: getIngestOwner unavailable — defaulting to local (cap not mounted yet?)", {
13277
+ tags: { deviceId },
13278
+ meta: { error: require_dist.errMsg(err) }
13279
+ });
13280
+ owner = { ownerNodeId: ownNodeId };
13281
+ }
13282
+ const mode = resolveWebrtcSourceMode({
13283
+ ownerNodeId: owner.ownerNodeId,
13284
+ ownNodeId,
13285
+ reachableHost: owner.reachableHost
13286
+ });
13287
+ if (mode.kind === "local") return;
13288
+ if (mode.kind === "unresolvable") {
13289
+ const configIssueSuffix = owner.configIssue ? ` (${owner.configIssue})` : "";
13290
+ throw new Error(`webrtc-adaptive-tier: device ${deviceId} is owned by node "${mode.ownerNodeId}" but no reachable host is configured for it — cannot source its WebRTC stream remotely${configIssueSuffix}`);
13291
+ }
13292
+ const hit = resolveTierSlot(await api.streamBroker.listAllProfileSlots.query(void 0, require_dist.nodePin(mode.ownerNodeId)), deviceId, preferredTier);
13293
+ if (hit === null) return;
13294
+ const brokerId = require_dist.makeSourceBrokerId(deviceId, hit.camStreamId);
13295
+ await this.ensureBrokerForWebrtcSession(deviceId, brokerId, hit.camStreamId, hit.profile);
13296
+ }
12778
13297
  setProfileMapPersister(persister) {
12779
13298
  this.profileMapPersister = persister;
12780
13299
  }
@@ -12782,19 +13301,6 @@ var StreamBrokerManager = class {
12782
13301
  this.assignments.clear();
12783
13302
  for (const [k, v] of entries) this.assignments.set(k, v);
12784
13303
  }
12785
- getLiveRestreamers() {
12786
- const live = this.capabilities?.getCollection?.("restreamer");
12787
- if (live && live.length > 0) return live;
12788
- return this.restreamers;
12789
- }
12790
- setRestreamers(restreamers) {
12791
- this.restreamers = restreamers;
12792
- for (const broker of this.brokers.values()) broker.setRestreamers(restreamers);
12793
- this.logger.info("Updated restreamers", { meta: { count: restreamers.length } });
12794
- }
12795
- getRestreamers() {
12796
- return this.restreamers;
12797
- }
12798
13304
  getRtspRestreamProvider() {
12799
13305
  return this.rtspProvider;
12800
13306
  }
@@ -13324,12 +13830,16 @@ var StreamBrokerManager = class {
13324
13830
  await broker.rotateDecoderSession(reason, agentNodeId);
13325
13831
  rotated++;
13326
13832
  } catch (err) {
13327
- this.logger.warn("decoder rotation failed", { meta: {
13328
- brokerId: broker.deviceId,
13329
- agentNodeId,
13330
- reason,
13331
- error: require_dist.errMsg(err)
13332
- } });
13833
+ const parsedBrokerId = parseBrokerId(broker.deviceId);
13834
+ this.logger.warn("decoder rotation failed", {
13835
+ ...parsedBrokerId ? { tags: { deviceId: parsedBrokerId.deviceId } } : {},
13836
+ meta: {
13837
+ brokerId: broker.deviceId,
13838
+ agentNodeId,
13839
+ reason,
13840
+ error: require_dist.errMsg(err)
13841
+ }
13842
+ });
13333
13843
  }
13334
13844
  }
13335
13845
  return rotated;
@@ -13419,6 +13929,7 @@ var StreamBrokerManager = class {
13419
13929
  clearInterval(this.streamHealthTimer);
13420
13930
  this.streamHealthTimer = void 0;
13421
13931
  }
13932
+ await this.lastOwnershipReconcile.catch(() => void 0);
13422
13933
  if (this.transcodeManager) {
13423
13934
  this.transcodeManager.shutdownAll();
13424
13935
  this.transcodeManager = null;
@@ -13515,15 +14026,7 @@ var StreamBrokerManager = class {
13515
14026
  packetCount: stats.preBufferPackets
13516
14027
  };
13517
14028
  }
13518
- async getStreamUrl(input) {
13519
- const prefix = input.streamId.split("/")[0];
13520
- if (!prefix) return { url: "" };
13521
- const deviceId = Number(prefix);
13522
- if (!Number.isFinite(deviceId)) return { url: "" };
13523
- for (const restreamer of this.getLiveRestreamers()) {
13524
- const match = restreamer.getExposedResources(deviceId).find((r) => r.format === input.format);
13525
- if (match) return { url: match.value };
13526
- }
14029
+ async getStreamUrl(_input) {
13527
14030
  return { url: "" };
13528
14031
  }
13529
14032
  async getRtspPort() {
@@ -13615,13 +14118,24 @@ var StreamBrokerManager = class {
13615
14118
  return this.rtspProvider.getEntry(input.brokerId, input.hostname);
13616
14119
  }
13617
14120
  async regenerateRtspToken(input) {
13618
- this.logger.info("regenerateRtspToken called", { meta: { brokerId: input.brokerId } });
14121
+ const parsedBrokerId = parseBrokerId(input.brokerId);
14122
+ const tags = parsedBrokerId ? { deviceId: parsedBrokerId.deviceId } : void 0;
14123
+ this.logger.info("regenerateRtspToken called", {
14124
+ tags,
14125
+ meta: { brokerId: input.brokerId }
14126
+ });
13619
14127
  const result = this.rtspProvider.regenerateToken(input.brokerId);
13620
- if (result) this.logger.info("regenerateRtspToken succeeded", { meta: {
13621
- brokerId: input.brokerId,
13622
- newTokenPrefix: result.slice(0, 8)
13623
- } });
13624
- else this.logger.warn("regenerateRtspToken failed — broker not found", { meta: { brokerId: input.brokerId } });
14128
+ if (result) this.logger.info("regenerateRtspToken succeeded", {
14129
+ tags,
14130
+ meta: {
14131
+ brokerId: input.brokerId,
14132
+ newTokenPrefix: result.slice(0, 8)
14133
+ }
14134
+ });
14135
+ else this.logger.warn("regenerateRtspToken failed — broker not found", {
14136
+ tags,
14137
+ meta: { brokerId: input.brokerId }
14138
+ });
13625
14139
  return result;
13626
14140
  }
13627
14141
  async setRtspEnabled(input) {
@@ -14660,18 +15174,69 @@ var StreamBrokerManager = class {
14660
15174
  * registered. Tied to publish lifecycle: `publishCameraStream` calls
14661
15175
  * this once per stream; `retractCameraStream` releases it.
14662
15176
  */
15177
+ /**
15178
+ * True when THIS node owns the cluster's camera source pull
15179
+ * (`clusterRoles.ingestNode`). Backs the `ensureBroker` dormancy gate.
15180
+ *
15181
+ * Ownership is normally PUSHED by the orchestrator's
15182
+ * `PipelineIngestOwnerChanged` event ({@link subscribeIngestOwnerChanges}),
15183
+ * so the steady-state gate is a pure cache read — no per-`ensureBroker`
15184
+ * cross-process query.
15185
+ *
15186
+ * - Not yet wired (`api`/`localNodeId` absent) → `true` (unchanged behavior;
15187
+ * the manager runs as today until `setApiAccess` supplies a node id).
15188
+ * - Cached owner fresh (event-stamped, within {@link INGEST_OWNER_CACHE_TTL_MS}) →
15189
+ * pure comparison, no query.
15190
+ * - Cold start / no event for the backstop window → a single `getIngestOwner`
15191
+ * reconcile query. It fails FAST now (FIX #1 makes an unregistered-provider
15192
+ * miss a precise error, not a 30s hang), so on failure we FAIL-OPEN: use the
15193
+ * last-known owner, or ALLOW when nothing is known — a rare transient
15194
+ * double-pull self-heals on the next pushed event, whereas denying would
15195
+ * starve the owner (no prebuffer, no recording).
15196
+ */
15197
+ async isLocalIngestOwner() {
15198
+ if (!this.api || this.localNodeId === null) return true;
15199
+ const now = Date.now();
15200
+ if (this.cachedIngestOwnerNodeId !== null && now - this.cachedIngestOwnerAt < StreamBrokerManager.INGEST_OWNER_CACHE_TTL_MS) return this.cachedIngestOwnerNodeId === this.localNodeId;
15201
+ try {
15202
+ const owner = await this.api.pipelineOrchestrator.getIngestOwner.query();
15203
+ this.cachedIngestOwnerNodeId = owner.ownerNodeId;
15204
+ this.cachedIngestOwnerAt = now;
15205
+ return owner.ownerNodeId === this.localNodeId;
15206
+ } catch (err) {
15207
+ this.logger.debug("isLocalIngestOwner: getIngestOwner unavailable — fail-open", { meta: {
15208
+ lastKnown: this.cachedIngestOwnerNodeId,
15209
+ self: this.localNodeId,
15210
+ error: require_dist.errMsg(err)
15211
+ } });
15212
+ return this.cachedIngestOwnerNodeId === null ? true : this.cachedIngestOwnerNodeId === this.localNodeId;
15213
+ }
15214
+ }
14663
15215
  async ensureBroker(deviceId, camStreamId) {
14664
15216
  const cam = this.cameraStreams.get(deviceId)?.get(camStreamId);
14665
15217
  if (!cam) return;
14666
15218
  const brokerId = brokerIdFor(deviceId, camStreamId);
14667
15219
  if (this.brokers.has(brokerId)) return;
15220
+ if (!await this.isLocalIngestOwner()) {
15221
+ this.logger.debug("ensureBroker: not the ingest owner — skipping local physical broker", {
15222
+ tags: {
15223
+ deviceId,
15224
+ camStreamId
15225
+ },
15226
+ meta: {
15227
+ brokerId,
15228
+ ingestOwner: this.cachedIngestOwnerNodeId,
15229
+ self: this.localNodeId
15230
+ }
15231
+ });
15232
+ return;
15233
+ }
14668
15234
  const brokerLogger = this.logger.child(camStreamId).withTags({
14669
15235
  deviceId,
14670
15236
  camStreamId
14671
15237
  });
14672
15238
  const source = this.buildSourceFromCamStream(cam);
14673
15239
  const broker = new StreamBroker(brokerId, this.buildDecoderCapApi(deviceId), brokerLogger, buildAudioCodecCapApi(this.api), this.localNodeId);
14674
- broker.setRestreamers(this.restreamers);
14675
15240
  broker.setRestreamUrlResolver(() => this.rtspProvider.getEntry(brokerId)?.mutedUrl ?? null);
14676
15241
  broker.setFfmpegBinaryPath(this.ffmpegConfig.binaryPath);
14677
15242
  this.applyPreBufferConfig(broker, deviceId, camStreamId);
@@ -14859,44 +15424,10 @@ var StreamBrokerManager = class {
14859
15424
  broker.setPreBufferEnabled(defaultWarm);
14860
15425
  broker.setPreBufferDuration(defaultWarm ? this.defaultPreBufferSec : 0);
14861
15426
  }
14862
- async fanOutRegistration(deviceId, brokerId, cam, broker) {
14863
- const log = this.logger.withTags({
14864
- deviceId,
14865
- brokerId,
14866
- camStreamId: cam.camStreamId
14867
- });
14868
- const localStreamUrl = broker.getLocalStreamUrl();
14869
- const codec = "h264";
14870
- const label = cam.label ?? brokerId;
14871
- for (const restreamer of this.getLiveRestreamers()) try {
14872
- await restreamer.registerDevice(deviceId, [{
14873
- streamId: brokerId,
14874
- label,
14875
- codec,
14876
- type: "video",
14877
- sourceUrl: localStreamUrl
14878
- }]);
14879
- } catch (err) {
14880
- log.error("Failed to register with restreamer", { meta: {
14881
- restreamerId: restreamer.id,
14882
- error: require_dist.errMsg(err)
14883
- } });
14884
- }
15427
+ async fanOutRegistration(_deviceId, brokerId, _cam, broker) {
14885
15428
  if (this.webrtcServer) this.webrtcServer.registerBroker(brokerId, broker);
14886
15429
  }
14887
- async fanOutUnregistration(deviceId, brokerId) {
14888
- const log = this.logger.withTags({
14889
- deviceId,
14890
- brokerId
14891
- });
14892
- for (const restreamer of this.getLiveRestreamers()) try {
14893
- await restreamer.unregisterDevice(deviceId);
14894
- } catch (err) {
14895
- log.error("Failed to unregister from restreamer", { meta: {
14896
- restreamerId: restreamer.id,
14897
- error: require_dist.errMsg(err)
14898
- } });
14899
- }
15430
+ async fanOutUnregistration(_deviceId, brokerId) {
14900
15431
  if (this.webrtcServer) this.webrtcServer.unregisterBroker(brokerId);
14901
15432
  }
14902
15433
  brokerStatusToSlotStatus(status) {
@@ -16318,6 +16849,15 @@ var BrokerWebrtcServer = class {
16318
16849
  * returns null when no restream is dialed → the feed falls back to stdin.
16319
16850
  */
16320
16851
  sourceRestreamUrlResolver = null;
16852
+ /**
16853
+ * Hook the manager injects to ensure the wanted tier's ADAPTIVE broker is
16854
+ * registered locally before `resolveBrokerId` runs — the manager wires it
16855
+ * to `ensureRemoteAdaptiveTierBroker` so a remote node's broker gets a
16856
+ * local registration in time for the resolve. No-ops on local-owned
16857
+ * devices (the manager's own gate), so an unset hook or a local-owner call
16858
+ * leaves adaptive resolution byte-identical to before this hook existed.
16859
+ */
16860
+ ensureAdaptiveTierBroker = null;
16321
16861
  /** sessionId → session entry. */
16322
16862
  sessions = /* @__PURE__ */ new Map();
16323
16863
  /**
@@ -16361,7 +16901,11 @@ var BrokerWebrtcServer = class {
16361
16901
  }
16362
16902
  registerBroker(brokerId, broker) {
16363
16903
  this.brokers.set(brokerId, broker);
16364
- this.logger.info("WebRTC broker registered", { meta: { brokerId } });
16904
+ const did = deviceIdFromBrokerId(brokerId);
16905
+ this.logger.info("WebRTC broker registered", {
16906
+ ...did !== -1 ? { tags: { deviceId: did } } : {},
16907
+ meta: { brokerId }
16908
+ });
16365
16909
  }
16366
16910
  /**
16367
16911
  * Provide a resolver that maps brokerId → current profile tier. The
@@ -16380,13 +16924,27 @@ var BrokerWebrtcServer = class {
16380
16924
  setSourceRestreamUrlResolver(resolver) {
16381
16925
  this.sourceRestreamUrlResolver = resolver;
16382
16926
  }
16927
+ /**
16928
+ * Provide a hook that ensures the wanted tier's remote adaptive broker
16929
+ * exists BEFORE `resolveBrokerId` runs, for ADAPTIVE streams only. The
16930
+ * manager injects one wired to `ensureRemoteAdaptiveTierBroker` (Task 3).
16931
+ * Without it (or on a local-owner call, which the hook itself no-ops on)
16932
+ * adaptive resolution behaves exactly as before this hook existed.
16933
+ */
16934
+ setEnsureAdaptiveTierBrokerHook(hook) {
16935
+ this.ensureAdaptiveTierBroker = hook;
16936
+ }
16383
16937
  unregisterBroker(brokerId) {
16384
16938
  this.brokers.delete(brokerId);
16385
16939
  for (const [sid, entry] of this.sessions) if (entry.brokerId === brokerId) {
16386
16940
  this.cleanupSession(sid);
16387
16941
  entry.session.close().catch(() => {});
16388
16942
  }
16389
- this.logger.info("WebRTC broker unregistered", { meta: { brokerId } });
16943
+ const unregisterDid = deviceIdFromBrokerId(brokerId);
16944
+ this.logger.info("WebRTC broker unregistered", {
16945
+ ...unregisterDid !== -1 ? { tags: { deviceId: unregisterDid } } : {},
16946
+ meta: { brokerId }
16947
+ });
16390
16948
  }
16391
16949
  supportsStream(streamId) {
16392
16950
  if (this.brokers.has(streamId)) return true;
@@ -16480,11 +17038,21 @@ var BrokerWebrtcServer = class {
16480
17038
  */
16481
17039
  async setupSessionForBroker(streamId, hints, opts) {
16482
17040
  if (this.stopped) throw new Error("Server stopped");
16483
- const intent = streamId.endsWith("/adaptive") && opts.sessionId !== void 0 ? this.adaptiveIntent.get(opts.sessionId) : void 0;
17041
+ const isAdaptiveStream = streamId.endsWith("/adaptive");
17042
+ const intent = isAdaptiveStream && opts.sessionId !== void 0 ? this.adaptiveIntent.get(opts.sessionId) : void 0;
16484
17043
  const resolveHints = intent !== void 0 ? {
16485
17044
  ...hints,
16486
17045
  prefersTier: intent.prefersTier
16487
17046
  } : hints;
17047
+ if (isAdaptiveStream) {
17048
+ const adaptiveDeviceId = deviceIdFromBrokerId(streamId);
17049
+ const desiredTier = resolveHints.prefersTier === "high" || resolveHints.prefersTier === "mid" || resolveHints.prefersTier === "low" ? resolveHints.prefersTier : "high";
17050
+ try {
17051
+ await this.ensureAdaptiveTierBroker?.(adaptiveDeviceId, desiredTier);
17052
+ } catch (err) {
17053
+ throw new Error(`Failed to ensure remote adaptive-tier broker for stream "${streamId}" (tier: ${desiredTier}): ${require_dist.errMsg(err)}`);
17054
+ }
17055
+ }
16488
17056
  const brokerId = this.resolveBrokerId(streamId, resolveHints);
16489
17057
  const broker = this.brokers.get(brokerId);
16490
17058
  if (!broker) throw new Error(`No broker for stream "${streamId}" (resolved: "${brokerId}")`);
@@ -17156,13 +17724,17 @@ var BrokerWebrtcServer = class {
17156
17724
  bestScore = score;
17157
17725
  }
17158
17726
  }
17159
- this.logger.debug("Adaptive source selected", { meta: {
17160
- deviceKey,
17161
- selected: bestId,
17162
- tier: bestTier,
17163
- score: Math.round(bestScore * 100) / 100,
17164
- hints
17165
- } });
17727
+ const adaptiveDeviceId = Number(deviceKey.split("/")[0]);
17728
+ this.logger.debug("Adaptive source selected", {
17729
+ ...Number.isFinite(adaptiveDeviceId) ? { tags: { deviceId: adaptiveDeviceId } } : {},
17730
+ meta: {
17731
+ deviceKey,
17732
+ selected: bestId,
17733
+ tier: bestTier,
17734
+ score: Math.round(bestScore * 100) / 100,
17735
+ hints
17736
+ }
17737
+ });
17166
17738
  return bestId;
17167
17739
  }
17168
17740
  async resolveIceServers() {
@@ -17480,6 +18052,34 @@ var WebrtcSessionProvider = class WebrtcSessionProvider {
17480
18052
  if (camStreamId === void 0) return WEBRTC_FALLBACK_PROFILE;
17481
18053
  return withSourceResolution(WEBRTC_FALLBACK_PROFILE, this.manager.getCameraStreamProbedParams(deviceId, camStreamId));
17482
18054
  }
18055
+ /**
18056
+ * Ensure a broker is registered for `brokerId` before delegating to the
18057
+ * WebRTC server (Slice 3 Task 3 — cross-node ingest wiring). Defers to
18058
+ * `StreamBrokerManager.ensureBrokerForWebrtcSession`, guarded the same way
18059
+ * `resolveSessionDebug` guards `manager.isStreamingDebug`/`isWebrtcDebug` —
18060
+ * older/test manager doubles that don't implement the hook simply skip it,
18061
+ * leaving today's local-only resolution completely unchanged.
18062
+ */
18063
+ async ensureBrokerAvailable(deviceId, brokerId, camStreamId) {
18064
+ if (typeof this.manager.ensureBrokerForWebrtcSession !== "function") return;
18065
+ await this.manager.ensureBrokerForWebrtcSession(deviceId, brokerId, camStreamId);
18066
+ }
18067
+ /**
18068
+ * Best-effort cleanup for the remote-encoded adapter `ensureBrokerAvailable`
18069
+ * may have just registered, run when the `BrokerWebrtcServer` delegate that
18070
+ * follows it throws. Without this, a delegate failure AFTER a successful
18071
+ * remote-adapter registration (session-construction/offer error, server
18072
+ * stopping mid-request, …) leaks the adapter forever — its only other
18073
+ * teardown path is the inner broker's `onDemandChange(false)` edge, which
18074
+ * never fires because no session ever reached the `true` (demand-active)
18075
+ * edge. Guarded the same way `ensureBrokerAvailable` guards the manager
18076
+ * hook itself — older/test manager doubles without the method are a no-op,
18077
+ * so the owner==self / local-broker happy path is byte-identical.
18078
+ */
18079
+ async disposeRemoteBrokerAdapterOnDelegateFailure(brokerId) {
18080
+ if (typeof this.manager.disposeRemoteBrokerAdapterIfPresent !== "function") return;
18081
+ await this.manager.disposeRemoteBrokerAdapterIfPresent(brokerId).catch(() => {});
18082
+ }
17483
18083
  async listStreams(input) {
17484
18084
  const { deviceId } = input;
17485
18085
  const assignedSlots = this.manager.getProfileSlotsForDevice(deviceId).filter((s) => s.sourceCamStreamId !== null);
@@ -17541,32 +18141,44 @@ var WebrtcSessionProvider = class WebrtcSessionProvider {
17541
18141
  }
17542
18142
  async createSession(input) {
17543
18143
  const streamId = this.resolveTargetToStreamId(input.deviceId, input.target);
17544
- const streamingDebug = this.resolveSessionDebug(input.deviceId);
17545
18144
  const camStreamId = this.resolveCamStreamId(input.deviceId, input.target);
18145
+ await this.ensureBrokerAvailable(input.deviceId, streamId, camStreamId);
18146
+ const streamingDebug = this.resolveSessionDebug(input.deviceId);
17546
18147
  const transcodeProfile = this.resolveTranscodeProfile(input.deviceId, camStreamId);
17547
- return this.webrtcServer.createSession(streamId, input.hints, {
17548
- streamingDebug,
17549
- relayOnly: input.relayOnly === true,
17550
- transcodeProfile,
17551
- ...input.consumerAttribution !== void 0 ? { consumerAttribution: input.consumerAttribution } : {}
17552
- });
18148
+ try {
18149
+ return await this.webrtcServer.createSession(streamId, input.hints, {
18150
+ streamingDebug,
18151
+ relayOnly: input.relayOnly === true,
18152
+ transcodeProfile,
18153
+ ...input.consumerAttribution !== void 0 ? { consumerAttribution: input.consumerAttribution } : {}
18154
+ });
18155
+ } catch (err) {
18156
+ await this.disposeRemoteBrokerAdapterOnDelegateFailure(streamId);
18157
+ throw err;
18158
+ }
17553
18159
  }
17554
18160
  async handleOffer(input) {
17555
18161
  const target = input.target ?? { kind: "adaptive" };
17556
18162
  const streamId = this.resolveTargetToStreamId(input.deviceId, target);
17557
- const streamingDebug = this.resolveSessionDebug(input.deviceId);
17558
18163
  const camStreamId = this.resolveCamStreamId(input.deviceId, target);
18164
+ await this.ensureBrokerAvailable(input.deviceId, streamId, camStreamId);
18165
+ const streamingDebug = this.resolveSessionDebug(input.deviceId);
17559
18166
  const transcodeProfile = this.resolveTranscodeProfile(input.deviceId, camStreamId);
17560
- return this.webrtcServer.handleOffer(streamId, input.sdpOffer, void 0, {
17561
- streamingDebug,
17562
- sessionId: input.sessionId,
17563
- relayOnly: input.relayOnly === true,
17564
- nonTrickle: input.nonTrickle === true,
17565
- disableTurn: input.disableTurn === true,
17566
- disableIpv6: input.disableIpv6 === true,
17567
- transcodeProfile,
17568
- ...input.consumerAttribution !== void 0 ? { consumerAttribution: input.consumerAttribution } : {}
17569
- });
18167
+ try {
18168
+ return await this.webrtcServer.handleOffer(streamId, input.sdpOffer, void 0, {
18169
+ streamingDebug,
18170
+ sessionId: input.sessionId,
18171
+ relayOnly: input.relayOnly === true,
18172
+ nonTrickle: input.nonTrickle === true,
18173
+ disableTurn: input.disableTurn === true,
18174
+ disableIpv6: input.disableIpv6 === true,
18175
+ transcodeProfile,
18176
+ ...input.consumerAttribution !== void 0 ? { consumerAttribution: input.consumerAttribution } : {}
18177
+ });
18178
+ } catch (err) {
18179
+ await this.disposeRemoteBrokerAdapterOnDelegateFailure(streamId);
18180
+ throw err;
18181
+ }
17570
18182
  }
17571
18183
  async handleAnswer(input) {
17572
18184
  await this.webrtcServer.handleAnswer(input.sessionId, input.sdpAnswer);
@@ -17790,7 +18402,6 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
17790
18402
  const localNode = this.ctx.kernel.localNodeId;
17791
18403
  const localNodeId = localNode ? localNode.includes("/") ? localNode.split("/")[0] : localNode : void 0;
17792
18404
  this.brokerManager.setApiAccess(this.ctx.api, localNodeId, this.ctx.kernel.hwaccel);
17793
- if (this.capabilities) this.brokerManager.setCapabilitiesAccess(this.capabilities);
17794
18405
  await this.loadFfmpegConfig();
17795
18406
  const rtspProvider = this.brokerManager.getRtspRestreamProvider();
17796
18407
  const brokerManager = this.brokerManager;
@@ -19519,7 +20130,7 @@ function fromBase64Url(s) {
19519
20130
  //#region src/stream-broker/index.ts
19520
20131
  var stream_broker_default = StreamBrokerAddon;
19521
20132
  //#endregion
19522
- exports.DecoderSessionProxy = require_frame_handle_plane.DecoderSessionProxy;
20133
+ exports.DecoderSessionProxy = require_remote_source_plane.DecoderSessionProxy;
19523
20134
  exports.FfmpegDecoderProvider = FfmpegDecoderProvider;
19524
20135
  exports.InProcessTransport = InProcessTransport;
19525
20136
  exports.LengthPrefixCodec = LengthPrefixCodec;