@camstack/addon-pipeline 1.1.42 → 1.1.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audio-analyzer/index.js +5 -4
- package/dist/audio-analyzer/index.mjs +1 -1
- package/dist/chunk-D6vf50IK.js +28 -0
- package/dist/detection-pipeline/index.js +535 -115
- package/dist/detection-pipeline/index.mjs +529 -110
- package/dist/{dist-GFd8M6KO.mjs → dist-CgBGV988.mjs} +214 -186
- package/dist/{dist-NvwN60Fq.js → dist-CySHUkXY.js} +219 -185
- package/dist/{model-download-service-C-IHWnXx-DxM2DSns.js → model-download-service-C-IHWnXx-BLXzxpRB.js} +3 -29
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +122 -287
- package/dist/pipeline-runner/index.mjs +118 -285
- package/dist/recorder/index.js +78 -31
- package/dist/recorder/index.mjs +75 -29
- package/dist/{frame-handle-plane-E_AsR77T.mjs → remote-source-plane-B4hRwchu.mjs} +219 -2
- package/dist/{frame-handle-plane-B3Fxww8H.js → remote-source-plane-D9m0z1N9.js} +236 -1
- package/dist/session-decode/decode-worker-child.js +118 -12
- package/dist/session-decode/decode-worker-child.mjs +115 -12
- package/dist/stream-broker/_stub.js +550 -523
- 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
- 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
- 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
- package/dist/stream-broker/{hostInit-DmjqskOR.mjs → hostInit-BSzP3BIj.mjs} +2 -2
- package/dist/stream-broker/index.js +840 -196
- package/dist/stream-broker/index.mjs +831 -188
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-CEsPjwzT.js → MaskShapeCanvas-DI4BY7W2-D2ML5TPi.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-D43fIRgB.js → MotionZonesSettings-NcxxQN8r-TTWGGaqv.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-C3wJxG8V.js → PrivacyMaskSettings-APgPLF7p-CJqHU2nU.js} +1 -1
- package/embed-dist/assets/index-C1oaL9s7.js +81 -0
- package/embed-dist/assets/{index-CSFtK41z.css → index-CA4kv43T.css} +1 -1
- package/embed-dist/index.html +2 -2
- package/package.json +2 -2
- 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
- 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
- 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
|
|
6
|
-
const require_dist = require("../dist-
|
|
7
|
-
const
|
|
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 =
|
|
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 =
|
|
14
|
+
node_fs = require_chunk.__toESM(node_fs);
|
|
14
15
|
let node_path = require("node:path");
|
|
15
|
-
node_path =
|
|
16
|
+
node_path = require_chunk.__toESM(node_path);
|
|
16
17
|
let node_os = require("node:os");
|
|
17
|
-
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 =
|
|
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", {
|
|
5263
|
-
brokerId,
|
|
5264
|
-
|
|
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", {
|
|
5273
|
-
brokerId,
|
|
5274
|
-
|
|
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
|
|
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(),
|
|
@@ -11875,10 +11884,13 @@ var TranscodePipelineManager = class {
|
|
|
11875
11884
|
const sourceRes = sourceStream.resolution;
|
|
11876
11885
|
const resMatchesTarget = input.targetResolution === void 0 || sourceRes !== void 0 && sourceRes.width === input.targetResolution.width && sourceRes.height === input.targetResolution.height;
|
|
11877
11886
|
if (input.video === "copy" || sourceCodec !== null && videoTargetToCodec(input.video) === sourceCodec && wantedAudio === "copy" && outputArgs.length === 0 && resMatchesTarget) {
|
|
11878
|
-
if (sourceCodec === null) this.logger.warn("copy-passthrough requested but source codec is unknown — reporting H264", {
|
|
11879
|
-
deviceId: input.deviceId,
|
|
11880
|
-
|
|
11881
|
-
|
|
11887
|
+
if (sourceCodec === null) this.logger.warn("copy-passthrough requested but source codec is unknown — reporting H264", {
|
|
11888
|
+
tags: { deviceId: input.deviceId },
|
|
11889
|
+
meta: {
|
|
11890
|
+
deviceId: input.deviceId,
|
|
11891
|
+
camStreamId: sourceStream.camStreamId
|
|
11892
|
+
}
|
|
11893
|
+
});
|
|
11882
11894
|
const passVideoCodec = sourceCodec ?? "H264";
|
|
11883
11895
|
const url = this.resolveSourceUrl(input.deviceId, sourceStream.camStreamId);
|
|
11884
11896
|
const key = pipelineKeyFor(input.deviceId, sourceStream.camStreamId, "copy", wantedAudio, outResolution, outputArgs, false);
|
|
@@ -11967,13 +11979,16 @@ var TranscodePipelineManager = class {
|
|
|
11967
11979
|
releaseTimer: null
|
|
11968
11980
|
};
|
|
11969
11981
|
this.entries.set(opts.key, entry);
|
|
11970
|
-
this.logger.info("Transcode pipeline started", {
|
|
11971
|
-
|
|
11972
|
-
|
|
11973
|
-
|
|
11974
|
-
|
|
11975
|
-
|
|
11976
|
-
|
|
11982
|
+
this.logger.info("Transcode pipeline started", {
|
|
11983
|
+
tags: { deviceId: opts.deviceId },
|
|
11984
|
+
meta: {
|
|
11985
|
+
key: opts.key,
|
|
11986
|
+
encoder,
|
|
11987
|
+
decodeHwAccel,
|
|
11988
|
+
sourceUrl,
|
|
11989
|
+
url
|
|
11990
|
+
}
|
|
11991
|
+
});
|
|
11977
11992
|
return source;
|
|
11978
11993
|
}
|
|
11979
11994
|
firstPublished(deviceId) {
|
|
@@ -12151,6 +12166,168 @@ var TranscodePipelineManager = class {
|
|
|
12151
12166
|
}
|
|
12152
12167
|
};
|
|
12153
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
|
|
12154
12331
|
//#region src/stream-broker/stream-broker/stream-broker-manager.ts
|
|
12155
12332
|
/**
|
|
12156
12333
|
* Resolve a derived stream's source to a physical camStreamId, if it is pinned
|
|
@@ -12287,6 +12464,19 @@ function findProfileForStream(map, camStreamId) {
|
|
|
12287
12464
|
return null;
|
|
12288
12465
|
}
|
|
12289
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
|
+
/**
|
|
12290
12480
|
* Watchdog parameters for the stream-health emitter. Brokers that have
|
|
12291
12481
|
* received zero video packets for STREAM_STALE_TIMEOUT_MS are reported
|
|
12292
12482
|
* as `stream.offline`; the next packet flips them back to `stream.online`.
|
|
@@ -12325,7 +12515,7 @@ function parseBrokerId(brokerId) {
|
|
|
12325
12515
|
camStreamId
|
|
12326
12516
|
};
|
|
12327
12517
|
}
|
|
12328
|
-
var StreamBrokerManager = class {
|
|
12518
|
+
var StreamBrokerManager = class StreamBrokerManager {
|
|
12329
12519
|
/**
|
|
12330
12520
|
* brokers keyed by brokerId = `${deviceId}/${camStreamId}`.
|
|
12331
12521
|
*
|
|
@@ -12394,7 +12584,6 @@ var StreamBrokerManager = class {
|
|
|
12394
12584
|
* it down on idle / retract. Populated by {@link attachDerivedFeeder}.
|
|
12395
12585
|
*/
|
|
12396
12586
|
derivedFeeders = /* @__PURE__ */ new Map();
|
|
12397
|
-
restreamers = [];
|
|
12398
12587
|
staticDecoders;
|
|
12399
12588
|
logger;
|
|
12400
12589
|
/** Public accessor for same-process callers (e.g. `CameraStreamsProvider.pickStream`)
|
|
@@ -12413,7 +12602,6 @@ var StreamBrokerManager = class {
|
|
|
12413
12602
|
* next pull after a hub restart. Keyed by `brokerIdFor(deviceId, camStreamId)`.
|
|
12414
12603
|
*/
|
|
12415
12604
|
lastProbedByBroker = /* @__PURE__ */ new Map();
|
|
12416
|
-
capabilities = null;
|
|
12417
12605
|
/**
|
|
12418
12606
|
* tRPC api proxy injected from the host addon. Used to resolve the
|
|
12419
12607
|
* `decoder` capability (and any future cross-cap consumers) via the
|
|
@@ -12442,9 +12630,66 @@ var StreamBrokerManager = class {
|
|
|
12442
12630
|
* node. `null` until `setApiAccess` wires it (falls back to unpinned).
|
|
12443
12631
|
*/
|
|
12444
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();
|
|
12445
12643
|
defaultPreBufferSec = 10;
|
|
12446
12644
|
webrtcServer = null;
|
|
12447
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
|
+
/**
|
|
12448
12693
|
* Transcode-pipeline manager — backs `getStreamWithCodec`. Lazily
|
|
12449
12694
|
* created on first acquire, lives until `destroyAll`.
|
|
12450
12695
|
*/
|
|
@@ -12671,9 +12916,6 @@ var StreamBrokerManager = class {
|
|
|
12671
12916
|
}
|
|
12672
12917
|
};
|
|
12673
12918
|
}
|
|
12674
|
-
setCapabilitiesAccess(access) {
|
|
12675
|
-
this.capabilities = access;
|
|
12676
|
-
}
|
|
12677
12919
|
/**
|
|
12678
12920
|
* Wire the addon's `ctx.api` proxy. Required for the broker to
|
|
12679
12921
|
* resolve the `decoder` capability via tRPC — see
|
|
@@ -12718,6 +12960,61 @@ var StreamBrokerManager = class {
|
|
|
12718
12960
|
this.eventBus = bus;
|
|
12719
12961
|
this.startStreamHealthWatchdog();
|
|
12720
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
|
+
}
|
|
12721
13018
|
}
|
|
12722
13019
|
/**
|
|
12723
13020
|
* Subscribe to the cap-scoped bus events that drive the per-broker
|
|
@@ -12761,14 +13058,242 @@ var StreamBrokerManager = class {
|
|
|
12761
13058
|
}
|
|
12762
13059
|
setWebrtcServer(server) {
|
|
12763
13060
|
this.webrtcServer = server;
|
|
12764
|
-
server.setProfileTierResolver((brokerId) => {
|
|
12765
|
-
const parsed = this.parseBrokerId(
|
|
13061
|
+
server.setProfileTierResolver((brokerId) => resolveBrokerTier(brokerId, (id) => {
|
|
13062
|
+
const parsed = this.parseBrokerId(id);
|
|
12766
13063
|
if (!parsed) return null;
|
|
12767
13064
|
return findProfileForStream(this.assignments.get(parsed.deviceId)?.map ?? {}, parsed.camStreamId);
|
|
12768
|
-
});
|
|
13065
|
+
}, this.remoteBrokerTiers));
|
|
12769
13066
|
server.setSourceRestreamUrlResolver((brokerId) => this.rtspProvider.getEntry(brokerId)?.url ?? null);
|
|
13067
|
+
server.setEnsureAdaptiveTierBrokerHook((deviceId, tier) => this.ensureRemoteAdaptiveTierBroker(deviceId, tier));
|
|
12770
13068
|
for (const [brokerId, broker] of this.brokers) server.registerBroker(brokerId, broker);
|
|
12771
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
|
+
}
|
|
12772
13297
|
setProfileMapPersister(persister) {
|
|
12773
13298
|
this.profileMapPersister = persister;
|
|
12774
13299
|
}
|
|
@@ -12776,19 +13301,6 @@ var StreamBrokerManager = class {
|
|
|
12776
13301
|
this.assignments.clear();
|
|
12777
13302
|
for (const [k, v] of entries) this.assignments.set(k, v);
|
|
12778
13303
|
}
|
|
12779
|
-
getLiveRestreamers() {
|
|
12780
|
-
const live = this.capabilities?.getCollection?.("restreamer");
|
|
12781
|
-
if (live && live.length > 0) return live;
|
|
12782
|
-
return this.restreamers;
|
|
12783
|
-
}
|
|
12784
|
-
setRestreamers(restreamers) {
|
|
12785
|
-
this.restreamers = restreamers;
|
|
12786
|
-
for (const broker of this.brokers.values()) broker.setRestreamers(restreamers);
|
|
12787
|
-
this.logger.info("Updated restreamers", { meta: { count: restreamers.length } });
|
|
12788
|
-
}
|
|
12789
|
-
getRestreamers() {
|
|
12790
|
-
return this.restreamers;
|
|
12791
|
-
}
|
|
12792
13304
|
getRtspRestreamProvider() {
|
|
12793
13305
|
return this.rtspProvider;
|
|
12794
13306
|
}
|
|
@@ -13318,12 +13830,16 @@ var StreamBrokerManager = class {
|
|
|
13318
13830
|
await broker.rotateDecoderSession(reason, agentNodeId);
|
|
13319
13831
|
rotated++;
|
|
13320
13832
|
} catch (err) {
|
|
13321
|
-
|
|
13322
|
-
|
|
13323
|
-
|
|
13324
|
-
|
|
13325
|
-
|
|
13326
|
-
|
|
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
|
+
});
|
|
13327
13843
|
}
|
|
13328
13844
|
}
|
|
13329
13845
|
return rotated;
|
|
@@ -13413,6 +13929,7 @@ var StreamBrokerManager = class {
|
|
|
13413
13929
|
clearInterval(this.streamHealthTimer);
|
|
13414
13930
|
this.streamHealthTimer = void 0;
|
|
13415
13931
|
}
|
|
13932
|
+
await this.lastOwnershipReconcile.catch(() => void 0);
|
|
13416
13933
|
if (this.transcodeManager) {
|
|
13417
13934
|
this.transcodeManager.shutdownAll();
|
|
13418
13935
|
this.transcodeManager = null;
|
|
@@ -13509,15 +14026,7 @@ var StreamBrokerManager = class {
|
|
|
13509
14026
|
packetCount: stats.preBufferPackets
|
|
13510
14027
|
};
|
|
13511
14028
|
}
|
|
13512
|
-
async getStreamUrl(
|
|
13513
|
-
const prefix = input.streamId.split("/")[0];
|
|
13514
|
-
if (!prefix) return { url: "" };
|
|
13515
|
-
const deviceId = Number(prefix);
|
|
13516
|
-
if (!Number.isFinite(deviceId)) return { url: "" };
|
|
13517
|
-
for (const restreamer of this.getLiveRestreamers()) {
|
|
13518
|
-
const match = restreamer.getExposedResources(deviceId).find((r) => r.format === input.format);
|
|
13519
|
-
if (match) return { url: match.value };
|
|
13520
|
-
}
|
|
14029
|
+
async getStreamUrl(_input) {
|
|
13521
14030
|
return { url: "" };
|
|
13522
14031
|
}
|
|
13523
14032
|
async getRtspPort() {
|
|
@@ -13609,13 +14118,24 @@ var StreamBrokerManager = class {
|
|
|
13609
14118
|
return this.rtspProvider.getEntry(input.brokerId, input.hostname);
|
|
13610
14119
|
}
|
|
13611
14120
|
async regenerateRtspToken(input) {
|
|
13612
|
-
|
|
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
|
+
});
|
|
13613
14127
|
const result = this.rtspProvider.regenerateToken(input.brokerId);
|
|
13614
|
-
if (result) this.logger.info("regenerateRtspToken succeeded", {
|
|
13615
|
-
|
|
13616
|
-
|
|
13617
|
-
|
|
13618
|
-
|
|
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
|
+
});
|
|
13619
14139
|
return result;
|
|
13620
14140
|
}
|
|
13621
14141
|
async setRtspEnabled(input) {
|
|
@@ -14506,10 +15026,13 @@ var StreamBrokerManager = class {
|
|
|
14506
15026
|
const key = `${cam.deviceId}:${cam.camStreamId}`;
|
|
14507
15027
|
if (!this.sourceParserFellBackWarned.has(key)) {
|
|
14508
15028
|
this.sourceParserFellBackWarned.add(key);
|
|
14509
|
-
this.logger.warn("source parser native-udp requested but not yet implemented — falling back to native-tcp", {
|
|
14510
|
-
deviceId: cam.deviceId,
|
|
14511
|
-
|
|
14512
|
-
|
|
15029
|
+
this.logger.warn("source parser native-udp requested but not yet implemented — falling back to native-tcp", {
|
|
15030
|
+
tags: { deviceId: cam.deviceId },
|
|
15031
|
+
meta: {
|
|
15032
|
+
deviceId: cam.deviceId,
|
|
15033
|
+
camStreamId: cam.camStreamId
|
|
15034
|
+
}
|
|
15035
|
+
});
|
|
14513
15036
|
}
|
|
14514
15037
|
}
|
|
14515
15038
|
if (resolved.engine !== "ffmpeg") return {};
|
|
@@ -14651,18 +15174,69 @@ var StreamBrokerManager = class {
|
|
|
14651
15174
|
* registered. Tied to publish lifecycle: `publishCameraStream` calls
|
|
14652
15175
|
* this once per stream; `retractCameraStream` releases it.
|
|
14653
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
|
+
}
|
|
14654
15215
|
async ensureBroker(deviceId, camStreamId) {
|
|
14655
15216
|
const cam = this.cameraStreams.get(deviceId)?.get(camStreamId);
|
|
14656
15217
|
if (!cam) return;
|
|
14657
15218
|
const brokerId = brokerIdFor(deviceId, camStreamId);
|
|
14658
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
|
+
}
|
|
14659
15234
|
const brokerLogger = this.logger.child(camStreamId).withTags({
|
|
14660
15235
|
deviceId,
|
|
14661
15236
|
camStreamId
|
|
14662
15237
|
});
|
|
14663
15238
|
const source = this.buildSourceFromCamStream(cam);
|
|
14664
15239
|
const broker = new StreamBroker(brokerId, this.buildDecoderCapApi(deviceId), brokerLogger, buildAudioCodecCapApi(this.api), this.localNodeId);
|
|
14665
|
-
broker.setRestreamers(this.restreamers);
|
|
14666
15240
|
broker.setRestreamUrlResolver(() => this.rtspProvider.getEntry(brokerId)?.mutedUrl ?? null);
|
|
14667
15241
|
broker.setFfmpegBinaryPath(this.ffmpegConfig.binaryPath);
|
|
14668
15242
|
this.applyPreBufferConfig(broker, deviceId, camStreamId);
|
|
@@ -14725,7 +15299,7 @@ var StreamBrokerManager = class {
|
|
|
14725
15299
|
if (!broker) return;
|
|
14726
15300
|
if (this.derivedFeeders.has(brokerId)) return;
|
|
14727
15301
|
const controller = new DerivedFeederController({
|
|
14728
|
-
logger: this.logger.child(`derived-feeder:${camStreamId}`),
|
|
15302
|
+
logger: this.logger.child(`derived-feeder:${camStreamId}`).withTags({ deviceId }),
|
|
14729
15303
|
spawn: (onTeardown) => this.spawnDerivedFeeder(deviceId, camStreamId, source, encodeProfile, broker, onTeardown)
|
|
14730
15304
|
});
|
|
14731
15305
|
const unsubscribe = broker.onDemandChange((active) => controller.onDemand(active));
|
|
@@ -14776,21 +15350,27 @@ var StreamBrokerManager = class {
|
|
|
14776
15350
|
const feeder = new DerivedStreamFeeder({
|
|
14777
15351
|
ffmpegPath: this.ffmpegConfig.binaryPath,
|
|
14778
15352
|
args,
|
|
14779
|
-
logger: this.logger.child(`derived-ffmpeg:${camStreamId}`)
|
|
15353
|
+
logger: this.logger.child(`derived-ffmpeg:${camStreamId}`).withTags({ deviceId })
|
|
14780
15354
|
}, {
|
|
14781
15355
|
onEncodedPacket: (packet) => broker.pushEncodedPacket(packet),
|
|
14782
15356
|
onAudioInfo: (info) => broker.pushAudioInfo(info),
|
|
14783
15357
|
onPlaying: () => {
|
|
14784
|
-
this.logger.info("derived feeder streaming", {
|
|
14785
|
-
|
|
14786
|
-
|
|
14787
|
-
|
|
15358
|
+
this.logger.info("derived feeder streaming", {
|
|
15359
|
+
tags: { deviceId },
|
|
15360
|
+
meta: {
|
|
15361
|
+
camStreamId,
|
|
15362
|
+
loopbackUrl
|
|
15363
|
+
}
|
|
15364
|
+
});
|
|
14788
15365
|
},
|
|
14789
15366
|
onError: (error) => {
|
|
14790
|
-
this.logger.warn("derived feeder error", {
|
|
14791
|
-
|
|
14792
|
-
|
|
14793
|
-
|
|
15367
|
+
this.logger.warn("derived feeder error", {
|
|
15368
|
+
tags: { deviceId },
|
|
15369
|
+
meta: {
|
|
15370
|
+
camStreamId,
|
|
15371
|
+
error: error.message
|
|
15372
|
+
}
|
|
15373
|
+
});
|
|
14794
15374
|
},
|
|
14795
15375
|
onTeardown
|
|
14796
15376
|
});
|
|
@@ -14844,44 +15424,10 @@ var StreamBrokerManager = class {
|
|
|
14844
15424
|
broker.setPreBufferEnabled(defaultWarm);
|
|
14845
15425
|
broker.setPreBufferDuration(defaultWarm ? this.defaultPreBufferSec : 0);
|
|
14846
15426
|
}
|
|
14847
|
-
async fanOutRegistration(
|
|
14848
|
-
const log = this.logger.withTags({
|
|
14849
|
-
deviceId,
|
|
14850
|
-
brokerId,
|
|
14851
|
-
camStreamId: cam.camStreamId
|
|
14852
|
-
});
|
|
14853
|
-
const localStreamUrl = broker.getLocalStreamUrl();
|
|
14854
|
-
const codec = "h264";
|
|
14855
|
-
const label = cam.label ?? brokerId;
|
|
14856
|
-
for (const restreamer of this.getLiveRestreamers()) try {
|
|
14857
|
-
await restreamer.registerDevice(deviceId, [{
|
|
14858
|
-
streamId: brokerId,
|
|
14859
|
-
label,
|
|
14860
|
-
codec,
|
|
14861
|
-
type: "video",
|
|
14862
|
-
sourceUrl: localStreamUrl
|
|
14863
|
-
}]);
|
|
14864
|
-
} catch (err) {
|
|
14865
|
-
log.error("Failed to register with restreamer", { meta: {
|
|
14866
|
-
restreamerId: restreamer.id,
|
|
14867
|
-
error: require_dist.errMsg(err)
|
|
14868
|
-
} });
|
|
14869
|
-
}
|
|
15427
|
+
async fanOutRegistration(_deviceId, brokerId, _cam, broker) {
|
|
14870
15428
|
if (this.webrtcServer) this.webrtcServer.registerBroker(brokerId, broker);
|
|
14871
15429
|
}
|
|
14872
|
-
async fanOutUnregistration(
|
|
14873
|
-
const log = this.logger.withTags({
|
|
14874
|
-
deviceId,
|
|
14875
|
-
brokerId
|
|
14876
|
-
});
|
|
14877
|
-
for (const restreamer of this.getLiveRestreamers()) try {
|
|
14878
|
-
await restreamer.unregisterDevice(deviceId);
|
|
14879
|
-
} catch (err) {
|
|
14880
|
-
log.error("Failed to unregister from restreamer", { meta: {
|
|
14881
|
-
restreamerId: restreamer.id,
|
|
14882
|
-
error: require_dist.errMsg(err)
|
|
14883
|
-
} });
|
|
14884
|
-
}
|
|
15430
|
+
async fanOutUnregistration(_deviceId, brokerId) {
|
|
14885
15431
|
if (this.webrtcServer) this.webrtcServer.unregisterBroker(brokerId);
|
|
14886
15432
|
}
|
|
14887
15433
|
brokerStatusToSlotStatus(status) {
|
|
@@ -16303,6 +16849,15 @@ var BrokerWebrtcServer = class {
|
|
|
16303
16849
|
* returns null when no restream is dialed → the feed falls back to stdin.
|
|
16304
16850
|
*/
|
|
16305
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;
|
|
16306
16861
|
/** sessionId → session entry. */
|
|
16307
16862
|
sessions = /* @__PURE__ */ new Map();
|
|
16308
16863
|
/**
|
|
@@ -16346,7 +16901,11 @@ var BrokerWebrtcServer = class {
|
|
|
16346
16901
|
}
|
|
16347
16902
|
registerBroker(brokerId, broker) {
|
|
16348
16903
|
this.brokers.set(brokerId, broker);
|
|
16349
|
-
|
|
16904
|
+
const did = deviceIdFromBrokerId(brokerId);
|
|
16905
|
+
this.logger.info("WebRTC broker registered", {
|
|
16906
|
+
...did !== -1 ? { tags: { deviceId: did } } : {},
|
|
16907
|
+
meta: { brokerId }
|
|
16908
|
+
});
|
|
16350
16909
|
}
|
|
16351
16910
|
/**
|
|
16352
16911
|
* Provide a resolver that maps brokerId → current profile tier. The
|
|
@@ -16365,13 +16924,27 @@ var BrokerWebrtcServer = class {
|
|
|
16365
16924
|
setSourceRestreamUrlResolver(resolver) {
|
|
16366
16925
|
this.sourceRestreamUrlResolver = resolver;
|
|
16367
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
|
+
}
|
|
16368
16937
|
unregisterBroker(brokerId) {
|
|
16369
16938
|
this.brokers.delete(brokerId);
|
|
16370
16939
|
for (const [sid, entry] of this.sessions) if (entry.brokerId === brokerId) {
|
|
16371
16940
|
this.cleanupSession(sid);
|
|
16372
16941
|
entry.session.close().catch(() => {});
|
|
16373
16942
|
}
|
|
16374
|
-
|
|
16943
|
+
const unregisterDid = deviceIdFromBrokerId(brokerId);
|
|
16944
|
+
this.logger.info("WebRTC broker unregistered", {
|
|
16945
|
+
...unregisterDid !== -1 ? { tags: { deviceId: unregisterDid } } : {},
|
|
16946
|
+
meta: { brokerId }
|
|
16947
|
+
});
|
|
16375
16948
|
}
|
|
16376
16949
|
supportsStream(streamId) {
|
|
16377
16950
|
if (this.brokers.has(streamId)) return true;
|
|
@@ -16465,11 +17038,21 @@ var BrokerWebrtcServer = class {
|
|
|
16465
17038
|
*/
|
|
16466
17039
|
async setupSessionForBroker(streamId, hints, opts) {
|
|
16467
17040
|
if (this.stopped) throw new Error("Server stopped");
|
|
16468
|
-
const
|
|
17041
|
+
const isAdaptiveStream = streamId.endsWith("/adaptive");
|
|
17042
|
+
const intent = isAdaptiveStream && opts.sessionId !== void 0 ? this.adaptiveIntent.get(opts.sessionId) : void 0;
|
|
16469
17043
|
const resolveHints = intent !== void 0 ? {
|
|
16470
17044
|
...hints,
|
|
16471
17045
|
prefersTier: intent.prefersTier
|
|
16472
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
|
+
}
|
|
16473
17056
|
const brokerId = this.resolveBrokerId(streamId, resolveHints);
|
|
16474
17057
|
const broker = this.brokers.get(brokerId);
|
|
16475
17058
|
if (!broker) throw new Error(`No broker for stream "${streamId}" (resolved: "${brokerId}")`);
|
|
@@ -16937,10 +17520,13 @@ var BrokerWebrtcServer = class {
|
|
|
16937
17520
|
const entry = this.sessions.get(sessionId);
|
|
16938
17521
|
if (!entry) return;
|
|
16939
17522
|
await entry.session.addIceCandidate(candidate).catch((err) => {
|
|
16940
|
-
this.logger.warn("addIceCandidate threw", {
|
|
16941
|
-
|
|
16942
|
-
|
|
16943
|
-
|
|
17523
|
+
this.logger.warn("addIceCandidate threw", {
|
|
17524
|
+
tags: { deviceId: deviceIdFromBrokerId(entry.brokerId) },
|
|
17525
|
+
meta: {
|
|
17526
|
+
sessionId,
|
|
17527
|
+
error: require_dist.errMsg(err)
|
|
17528
|
+
}
|
|
17529
|
+
});
|
|
16944
17530
|
});
|
|
16945
17531
|
}
|
|
16946
17532
|
/** Trickle ICE — the server's gathered candidates for a session (poll). */
|
|
@@ -16957,10 +17543,13 @@ var BrokerWebrtcServer = class {
|
|
|
16957
17543
|
if (!entry) return;
|
|
16958
17544
|
this.cleanupSession(sessionId);
|
|
16959
17545
|
await entry.session.close().catch(() => {});
|
|
16960
|
-
this.logger.info("WebRTC session closed", {
|
|
16961
|
-
|
|
16962
|
-
|
|
16963
|
-
|
|
17546
|
+
this.logger.info("WebRTC session closed", {
|
|
17547
|
+
tags: { deviceId: deviceIdFromBrokerId(entry.brokerId) },
|
|
17548
|
+
meta: {
|
|
17549
|
+
sessionId,
|
|
17550
|
+
deviceId: deviceIdFromBrokerId(entry.brokerId)
|
|
17551
|
+
}
|
|
17552
|
+
});
|
|
16964
17553
|
}
|
|
16965
17554
|
async stop() {
|
|
16966
17555
|
if (this.stopped) return;
|
|
@@ -17061,13 +17650,16 @@ var BrokerWebrtcServer = class {
|
|
|
17061
17650
|
target: { kind: "adaptive" },
|
|
17062
17651
|
epoch
|
|
17063
17652
|
};
|
|
17064
|
-
this.logger.info("adaptive: scheduling renegotiation", {
|
|
17065
|
-
|
|
17066
|
-
|
|
17067
|
-
|
|
17068
|
-
|
|
17069
|
-
|
|
17070
|
-
|
|
17653
|
+
this.logger.info("adaptive: scheduling renegotiation", {
|
|
17654
|
+
tags: { deviceId },
|
|
17655
|
+
meta: {
|
|
17656
|
+
sessionId,
|
|
17657
|
+
deviceId,
|
|
17658
|
+
tier,
|
|
17659
|
+
forceTranscodeDown,
|
|
17660
|
+
epoch
|
|
17661
|
+
}
|
|
17662
|
+
});
|
|
17071
17663
|
}
|
|
17072
17664
|
cleanupSession(sessionId) {
|
|
17073
17665
|
const entry = this.sessions.get(sessionId);
|
|
@@ -17132,13 +17724,17 @@ var BrokerWebrtcServer = class {
|
|
|
17132
17724
|
bestScore = score;
|
|
17133
17725
|
}
|
|
17134
17726
|
}
|
|
17135
|
-
|
|
17136
|
-
|
|
17137
|
-
|
|
17138
|
-
|
|
17139
|
-
|
|
17140
|
-
|
|
17141
|
-
|
|
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
|
+
});
|
|
17142
17738
|
return bestId;
|
|
17143
17739
|
}
|
|
17144
17740
|
async resolveIceServers() {
|
|
@@ -17301,10 +17897,13 @@ var CameraStreamsProvider = class {
|
|
|
17301
17897
|
async pickStream(input) {
|
|
17302
17898
|
const streams = await this.manager.getCameraStreamsForDevice(input.deviceId);
|
|
17303
17899
|
if (streams.length === 0) {
|
|
17304
|
-
this.manager.getLogger?.()?.info("camera-streams: pickStream — no streams for device", {
|
|
17305
|
-
deviceId: input.deviceId,
|
|
17306
|
-
|
|
17307
|
-
|
|
17900
|
+
this.manager.getLogger?.()?.info("camera-streams: pickStream — no streams for device", {
|
|
17901
|
+
tags: { deviceId: input.deviceId },
|
|
17902
|
+
meta: {
|
|
17903
|
+
deviceId: input.deviceId,
|
|
17904
|
+
requirements: input.requirements
|
|
17905
|
+
}
|
|
17906
|
+
});
|
|
17308
17907
|
return null;
|
|
17309
17908
|
}
|
|
17310
17909
|
const requirements = input.requirements;
|
|
@@ -17321,15 +17920,18 @@ var CameraStreamsProvider = class {
|
|
|
17321
17920
|
return true;
|
|
17322
17921
|
});
|
|
17323
17922
|
if (candidates.length === 0) {
|
|
17324
|
-
this.manager.getLogger?.()?.info("camera-streams: pickStream — no matching candidates", {
|
|
17325
|
-
deviceId: input.deviceId,
|
|
17326
|
-
|
|
17327
|
-
|
|
17328
|
-
|
|
17329
|
-
|
|
17330
|
-
|
|
17331
|
-
|
|
17332
|
-
|
|
17923
|
+
this.manager.getLogger?.()?.info("camera-streams: pickStream — no matching candidates", {
|
|
17924
|
+
tags: { deviceId: input.deviceId },
|
|
17925
|
+
meta: {
|
|
17926
|
+
deviceId: input.deviceId,
|
|
17927
|
+
requirements: input.requirements,
|
|
17928
|
+
streams: streams.map((s) => ({
|
|
17929
|
+
camStreamId: s.camStreamId,
|
|
17930
|
+
codec: s.codec,
|
|
17931
|
+
resolution: s.resolution
|
|
17932
|
+
}))
|
|
17933
|
+
}
|
|
17934
|
+
});
|
|
17333
17935
|
return null;
|
|
17334
17936
|
}
|
|
17335
17937
|
const requireSibling = requirements.requireSiblingCodec ?? [];
|
|
@@ -17338,14 +17940,17 @@ var CameraStreamsProvider = class {
|
|
|
17338
17940
|
if (s.camStreamId.startsWith("derived:")) return false;
|
|
17339
17941
|
return codecMatches(s.codec, requireSibling);
|
|
17340
17942
|
})) {
|
|
17341
|
-
this.manager.getLogger?.()?.info("camera-streams: pickStream — no sibling codec, skipping bypass", {
|
|
17342
|
-
deviceId: input.deviceId,
|
|
17343
|
-
|
|
17344
|
-
|
|
17345
|
-
|
|
17346
|
-
|
|
17347
|
-
|
|
17348
|
-
|
|
17943
|
+
this.manager.getLogger?.()?.info("camera-streams: pickStream — no sibling codec, skipping bypass", {
|
|
17944
|
+
tags: { deviceId: input.deviceId },
|
|
17945
|
+
meta: {
|
|
17946
|
+
deviceId: input.deviceId,
|
|
17947
|
+
requireSibling,
|
|
17948
|
+
streams: streams.map((s) => ({
|
|
17949
|
+
camStreamId: s.camStreamId,
|
|
17950
|
+
codec: s.codec
|
|
17951
|
+
}))
|
|
17952
|
+
}
|
|
17953
|
+
});
|
|
17349
17954
|
return null;
|
|
17350
17955
|
}
|
|
17351
17956
|
}
|
|
@@ -17447,6 +18052,34 @@ var WebrtcSessionProvider = class WebrtcSessionProvider {
|
|
|
17447
18052
|
if (camStreamId === void 0) return WEBRTC_FALLBACK_PROFILE;
|
|
17448
18053
|
return withSourceResolution(WEBRTC_FALLBACK_PROFILE, this.manager.getCameraStreamProbedParams(deviceId, camStreamId));
|
|
17449
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
|
+
}
|
|
17450
18083
|
async listStreams(input) {
|
|
17451
18084
|
const { deviceId } = input;
|
|
17452
18085
|
const assignedSlots = this.manager.getProfileSlotsForDevice(deviceId).filter((s) => s.sourceCamStreamId !== null);
|
|
@@ -17508,32 +18141,44 @@ var WebrtcSessionProvider = class WebrtcSessionProvider {
|
|
|
17508
18141
|
}
|
|
17509
18142
|
async createSession(input) {
|
|
17510
18143
|
const streamId = this.resolveTargetToStreamId(input.deviceId, input.target);
|
|
17511
|
-
const streamingDebug = this.resolveSessionDebug(input.deviceId);
|
|
17512
18144
|
const camStreamId = this.resolveCamStreamId(input.deviceId, input.target);
|
|
18145
|
+
await this.ensureBrokerAvailable(input.deviceId, streamId, camStreamId);
|
|
18146
|
+
const streamingDebug = this.resolveSessionDebug(input.deviceId);
|
|
17513
18147
|
const transcodeProfile = this.resolveTranscodeProfile(input.deviceId, camStreamId);
|
|
17514
|
-
|
|
17515
|
-
|
|
17516
|
-
|
|
17517
|
-
|
|
17518
|
-
|
|
17519
|
-
|
|
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
|
+
}
|
|
17520
18159
|
}
|
|
17521
18160
|
async handleOffer(input) {
|
|
17522
18161
|
const target = input.target ?? { kind: "adaptive" };
|
|
17523
18162
|
const streamId = this.resolveTargetToStreamId(input.deviceId, target);
|
|
17524
|
-
const streamingDebug = this.resolveSessionDebug(input.deviceId);
|
|
17525
18163
|
const camStreamId = this.resolveCamStreamId(input.deviceId, target);
|
|
18164
|
+
await this.ensureBrokerAvailable(input.deviceId, streamId, camStreamId);
|
|
18165
|
+
const streamingDebug = this.resolveSessionDebug(input.deviceId);
|
|
17526
18166
|
const transcodeProfile = this.resolveTranscodeProfile(input.deviceId, camStreamId);
|
|
17527
|
-
|
|
17528
|
-
|
|
17529
|
-
|
|
17530
|
-
|
|
17531
|
-
|
|
17532
|
-
|
|
17533
|
-
|
|
17534
|
-
|
|
17535
|
-
|
|
17536
|
-
|
|
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
|
+
}
|
|
17537
18182
|
}
|
|
17538
18183
|
async handleAnswer(input) {
|
|
17539
18184
|
await this.webrtcServer.handleAnswer(input.sessionId, input.sdpAnswer);
|
|
@@ -17757,7 +18402,6 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
|
|
|
17757
18402
|
const localNode = this.ctx.kernel.localNodeId;
|
|
17758
18403
|
const localNodeId = localNode ? localNode.includes("/") ? localNode.split("/")[0] : localNode : void 0;
|
|
17759
18404
|
this.brokerManager.setApiAccess(this.ctx.api, localNodeId, this.ctx.kernel.hwaccel);
|
|
17760
|
-
if (this.capabilities) this.brokerManager.setCapabilitiesAccess(this.capabilities);
|
|
17761
18405
|
await this.loadFfmpegConfig();
|
|
17762
18406
|
const rtspProvider = this.brokerManager.getRtspRestreamProvider();
|
|
17763
18407
|
const brokerManager = this.brokerManager;
|
|
@@ -19486,7 +20130,7 @@ function fromBase64Url(s) {
|
|
|
19486
20130
|
//#region src/stream-broker/index.ts
|
|
19487
20131
|
var stream_broker_default = StreamBrokerAddon;
|
|
19488
20132
|
//#endregion
|
|
19489
|
-
exports.DecoderSessionProxy =
|
|
20133
|
+
exports.DecoderSessionProxy = require_remote_source_plane.DecoderSessionProxy;
|
|
19490
20134
|
exports.FfmpegDecoderProvider = FfmpegDecoderProvider;
|
|
19491
20135
|
exports.InProcessTransport = InProcessTransport;
|
|
19492
20136
|
exports.LengthPrefixCodec = LengthPrefixCodec;
|