@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as __require } from "../chunk-BdkLduGY.mjs";
|
|
2
|
-
import { $ as
|
|
3
|
-
import {
|
|
2
|
+
import { $ as boolean, B as DeviceType, D as nodePin, F as webrtcSessionCapability, G as makeProfileBrokerId, H as createEvent, I as errMsg, J as parseProfileBrokerId, K as makeSourceBrokerId, L as BaseAddon, N as streamBrokerCapability, Q as array, R as CAM_PROFILE_ORDER, V as EventCategory, W as isEvent, Z as _enum, at as record, c as EncodeProfileSchema, d as RingBuffer, et as discriminatedUnion, g as cameraStreamsCapability, it as object, nt as literal, ot as string, p as addonWidgetsSourceCapability, rt as number, st as union, w as maskUrlCredentials, z as DeviceFeature } from "../dist-CgBGV988.mjs";
|
|
3
|
+
import { a as DecoderSessionProxy, i as FrameHandlePlane, r as profileForStreamId } from "../remote-source-plane-B4hRwchu.mjs";
|
|
4
4
|
import { t as createFileDataPlaneHandler } from "../model-download-service-C-IHWnXx-BPy6aoAx.mjs";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import * as crypto$1 from "node:crypto";
|
|
@@ -5173,6 +5173,16 @@ var RtspListenServer = class RtspListenServer {
|
|
|
5173
5173
|
//#endregion
|
|
5174
5174
|
//#region src/stream-broker/rtsp/rtsp-restream-provider.ts
|
|
5175
5175
|
/**
|
|
5176
|
+
* Parse the numeric deviceId out of a canonical brokerId
|
|
5177
|
+
* (`${deviceId}/${camStreamId}` or `${deviceId}/${profile}`). Returns
|
|
5178
|
+
* `undefined` for a malformed id — so every device-scoped log line here
|
|
5179
|
+
* can carry `tags.deviceId` without risking a bogus tag value.
|
|
5180
|
+
*/
|
|
5181
|
+
function deviceIdFromBrokerId$1(brokerId) {
|
|
5182
|
+
const parsed = Number(brokerId.split("/")[0]);
|
|
5183
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
5184
|
+
}
|
|
5185
|
+
/**
|
|
5176
5186
|
* Internal sub-object of `StreamBrokerManager` that implements the RTSP
|
|
5177
5187
|
* restream bookkeeping. No longer registered as a separate capability —
|
|
5178
5188
|
* accessed via passthrough methods on `StreamBrokerManager`.
|
|
@@ -5255,20 +5265,26 @@ var RtspRestreamProviderImpl = class {
|
|
|
5255
5265
|
const token = this.server.regenerateToken(brokerId);
|
|
5256
5266
|
if (token) {
|
|
5257
5267
|
this.persistTokens();
|
|
5258
|
-
this.logger.info("RTSP token regenerated", {
|
|
5259
|
-
brokerId,
|
|
5260
|
-
|
|
5261
|
-
|
|
5268
|
+
this.logger.info("RTSP token regenerated", {
|
|
5269
|
+
tags: { deviceId: deviceIdFromBrokerId$1(brokerId) },
|
|
5270
|
+
meta: {
|
|
5271
|
+
brokerId,
|
|
5272
|
+
tokenPrefix: token.slice(0, 8)
|
|
5273
|
+
}
|
|
5274
|
+
});
|
|
5262
5275
|
}
|
|
5263
5276
|
return token;
|
|
5264
5277
|
}
|
|
5265
5278
|
setEnabled(brokerId, enabled) {
|
|
5266
5279
|
this.server.setEnabled(brokerId, enabled);
|
|
5267
5280
|
this.persistEnabled();
|
|
5268
|
-
this.logger.info("RTSP restream state changed", {
|
|
5269
|
-
brokerId,
|
|
5270
|
-
|
|
5271
|
-
|
|
5281
|
+
this.logger.info("RTSP restream state changed", {
|
|
5282
|
+
tags: { deviceId: deviceIdFromBrokerId$1(brokerId) },
|
|
5283
|
+
meta: {
|
|
5284
|
+
brokerId,
|
|
5285
|
+
enabled
|
|
5286
|
+
}
|
|
5287
|
+
});
|
|
5272
5288
|
}
|
|
5273
5289
|
isEnabled(brokerId) {
|
|
5274
5290
|
return this.server.isEnabled(brokerId);
|
|
@@ -9433,7 +9449,6 @@ var StreamBroker = class StreamBroker {
|
|
|
9433
9449
|
static RTP_ACTIVITY_TIMEOUT_MS = 8e3;
|
|
9434
9450
|
manualStop = false;
|
|
9435
9451
|
stopping = false;
|
|
9436
|
-
restreamers = [];
|
|
9437
9452
|
pipeServer;
|
|
9438
9453
|
rtspRestreamer;
|
|
9439
9454
|
preBuffer;
|
|
@@ -9610,9 +9625,6 @@ var StreamBroker = class StreamBroker {
|
|
|
9610
9625
|
url
|
|
9611
9626
|
};
|
|
9612
9627
|
}
|
|
9613
|
-
setRestreamers(restreamers) {
|
|
9614
|
-
this.restreamers = restreamers;
|
|
9615
|
-
}
|
|
9616
9628
|
/**
|
|
9617
9629
|
* Inject a manager-owned resolver for this broker's OWN RTSP restream URL
|
|
9618
9630
|
* (`rtsp://127.0.0.1:<port>/<token>[/muted]`). When the source is an RTP
|
|
@@ -10053,10 +10065,6 @@ var StreamBroker = class StreamBroker {
|
|
|
10053
10065
|
this.logger?.info("First keyframe received — decoder can start");
|
|
10054
10066
|
}
|
|
10055
10067
|
}
|
|
10056
|
-
for (const restreamer of this.restreamers) {
|
|
10057
|
-
const streamId = `${this.deviceId}/video`;
|
|
10058
|
-
restreamer.pushPacket(streamId, packet);
|
|
10059
|
-
}
|
|
10060
10068
|
if (packet.type === "video" && this.firstKeyframeReceived) {
|
|
10061
10069
|
const decoderPacket = this.decoderPacketWithParamSets(packet);
|
|
10062
10070
|
this.decoderSeedBuffer.push(decoderPacket);
|
|
@@ -11871,10 +11879,13 @@ var TranscodePipelineManager = class {
|
|
|
11871
11879
|
const sourceRes = sourceStream.resolution;
|
|
11872
11880
|
const resMatchesTarget = input.targetResolution === void 0 || sourceRes !== void 0 && sourceRes.width === input.targetResolution.width && sourceRes.height === input.targetResolution.height;
|
|
11873
11881
|
if (input.video === "copy" || sourceCodec !== null && videoTargetToCodec(input.video) === sourceCodec && wantedAudio === "copy" && outputArgs.length === 0 && resMatchesTarget) {
|
|
11874
|
-
if (sourceCodec === null) this.logger.warn("copy-passthrough requested but source codec is unknown — reporting H264", {
|
|
11875
|
-
deviceId: input.deviceId,
|
|
11876
|
-
|
|
11877
|
-
|
|
11882
|
+
if (sourceCodec === null) this.logger.warn("copy-passthrough requested but source codec is unknown — reporting H264", {
|
|
11883
|
+
tags: { deviceId: input.deviceId },
|
|
11884
|
+
meta: {
|
|
11885
|
+
deviceId: input.deviceId,
|
|
11886
|
+
camStreamId: sourceStream.camStreamId
|
|
11887
|
+
}
|
|
11888
|
+
});
|
|
11878
11889
|
const passVideoCodec = sourceCodec ?? "H264";
|
|
11879
11890
|
const url = this.resolveSourceUrl(input.deviceId, sourceStream.camStreamId);
|
|
11880
11891
|
const key = pipelineKeyFor(input.deviceId, sourceStream.camStreamId, "copy", wantedAudio, outResolution, outputArgs, false);
|
|
@@ -11963,13 +11974,16 @@ var TranscodePipelineManager = class {
|
|
|
11963
11974
|
releaseTimer: null
|
|
11964
11975
|
};
|
|
11965
11976
|
this.entries.set(opts.key, entry);
|
|
11966
|
-
this.logger.info("Transcode pipeline started", {
|
|
11967
|
-
|
|
11968
|
-
|
|
11969
|
-
|
|
11970
|
-
|
|
11971
|
-
|
|
11972
|
-
|
|
11977
|
+
this.logger.info("Transcode pipeline started", {
|
|
11978
|
+
tags: { deviceId: opts.deviceId },
|
|
11979
|
+
meta: {
|
|
11980
|
+
key: opts.key,
|
|
11981
|
+
encoder,
|
|
11982
|
+
decodeHwAccel,
|
|
11983
|
+
sourceUrl,
|
|
11984
|
+
url
|
|
11985
|
+
}
|
|
11986
|
+
});
|
|
11973
11987
|
return source;
|
|
11974
11988
|
}
|
|
11975
11989
|
firstPublished(deviceId) {
|
|
@@ -12147,6 +12161,168 @@ var TranscodePipelineManager = class {
|
|
|
12147
12161
|
}
|
|
12148
12162
|
};
|
|
12149
12163
|
//#endregion
|
|
12164
|
+
//#region src/stream-broker/webrtc/remote-encoded-source-broker.ts
|
|
12165
|
+
var RemoteEncodedSourceBroker = class {
|
|
12166
|
+
api;
|
|
12167
|
+
logger;
|
|
12168
|
+
deviceId;
|
|
12169
|
+
streamId;
|
|
12170
|
+
ownerNodeId;
|
|
12171
|
+
hostname;
|
|
12172
|
+
createBroker;
|
|
12173
|
+
/** Single-flight acquire. */
|
|
12174
|
+
pending = null;
|
|
12175
|
+
disposed = false;
|
|
12176
|
+
constructor(options) {
|
|
12177
|
+
this.api = options.api;
|
|
12178
|
+
this.logger = options.logger.withTags({
|
|
12179
|
+
deviceId: options.deviceId,
|
|
12180
|
+
streamId: options.streamId
|
|
12181
|
+
});
|
|
12182
|
+
this.deviceId = options.deviceId;
|
|
12183
|
+
this.streamId = options.streamId;
|
|
12184
|
+
this.ownerNodeId = options.ownerNodeId;
|
|
12185
|
+
this.hostname = options.reachableHost;
|
|
12186
|
+
this.createBroker = options.createBroker;
|
|
12187
|
+
}
|
|
12188
|
+
/**
|
|
12189
|
+
* Single-flight: acquire the owner restream + construct/start the inner
|
|
12190
|
+
* broker once, sharing the same instance across concurrent callers. A
|
|
12191
|
+
* rejected acquire is retryable — the next `getBroker()` re-attempts from
|
|
12192
|
+
* scratch (fresh URL/token).
|
|
12193
|
+
*/
|
|
12194
|
+
async getBroker() {
|
|
12195
|
+
if (this.disposed) throw new Error("RemoteEncodedSourceBroker: getBroker after dispose");
|
|
12196
|
+
if (!this.pending) {
|
|
12197
|
+
const creation = this.createEntry();
|
|
12198
|
+
this.pending = creation;
|
|
12199
|
+
creation.catch(() => {
|
|
12200
|
+
if (this.pending === creation) this.pending = null;
|
|
12201
|
+
});
|
|
12202
|
+
}
|
|
12203
|
+
return (await this.pending).broker;
|
|
12204
|
+
}
|
|
12205
|
+
/** Stop the inner broker and release the acquired restream once. Idempotent. */
|
|
12206
|
+
async dispose() {
|
|
12207
|
+
if (this.disposed) return;
|
|
12208
|
+
this.disposed = true;
|
|
12209
|
+
const pending = this.pending;
|
|
12210
|
+
this.pending = null;
|
|
12211
|
+
if (!pending) return;
|
|
12212
|
+
let entry;
|
|
12213
|
+
try {
|
|
12214
|
+
entry = await pending;
|
|
12215
|
+
} catch {
|
|
12216
|
+
return;
|
|
12217
|
+
}
|
|
12218
|
+
await entry.broker.stop().catch((err) => {
|
|
12219
|
+
this.logger.warn("remote-encoded source broker: inner broker stop failed", { meta: {
|
|
12220
|
+
streamId: this.streamId,
|
|
12221
|
+
error: errMsg(err)
|
|
12222
|
+
} });
|
|
12223
|
+
});
|
|
12224
|
+
await this.api.streamBroker.releaseStreamWithCodec.mutate({ pipelineKey: entry.pipelineKey }, nodePin(this.ownerNodeId)).catch((err) => {
|
|
12225
|
+
this.logger.debug("remote-encoded source broker: releaseStreamWithCodec failed (benign on hub restart)", { meta: {
|
|
12226
|
+
streamId: this.streamId,
|
|
12227
|
+
pipelineKey: entry.pipelineKey,
|
|
12228
|
+
error: errMsg(err)
|
|
12229
|
+
} });
|
|
12230
|
+
});
|
|
12231
|
+
}
|
|
12232
|
+
async createEntry() {
|
|
12233
|
+
const { api, deviceId, streamId, ownerNodeId } = this;
|
|
12234
|
+
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)");
|
|
12235
|
+
const profile = profileForStreamId(await api.streamBroker.listAllProfileSlots.query(void 0, nodePin(ownerNodeId)), deviceId, streamId);
|
|
12236
|
+
if (profile === null) throw new Error(`RemoteEncodedSourceBroker: no broker profile is fed by camStream '${streamId}' of device ${deviceId} — cannot acquire the owner restream`);
|
|
12237
|
+
const rtpSource = await api.streamBroker.getStreamWithCodec.mutate({
|
|
12238
|
+
deviceId,
|
|
12239
|
+
video: "copy",
|
|
12240
|
+
audio: "none",
|
|
12241
|
+
profile,
|
|
12242
|
+
hostname: this.hostname,
|
|
12243
|
+
tag: `webrtc:remote-encoded:${deviceId}/${streamId}`
|
|
12244
|
+
}, nodePin(ownerNodeId));
|
|
12245
|
+
const source = {
|
|
12246
|
+
type: "rtsp",
|
|
12247
|
+
url: rtpSource.url,
|
|
12248
|
+
videoCodec: rtpSource.videoCodec
|
|
12249
|
+
};
|
|
12250
|
+
const brokerId = `${deviceId}/${profile}`;
|
|
12251
|
+
const broker = this.createBroker({
|
|
12252
|
+
brokerId,
|
|
12253
|
+
logger: this.logger.child(profile)
|
|
12254
|
+
});
|
|
12255
|
+
broker.setSourceProvider(() => source);
|
|
12256
|
+
await broker.start(source);
|
|
12257
|
+
this.logger.info("remote-encoded source broker: owner restream acquired", { meta: {
|
|
12258
|
+
streamId,
|
|
12259
|
+
profile,
|
|
12260
|
+
codec: rtpSource.videoCodec,
|
|
12261
|
+
pipelineKey: rtpSource.pipelineKey,
|
|
12262
|
+
hostname: this.hostname
|
|
12263
|
+
} });
|
|
12264
|
+
return {
|
|
12265
|
+
broker,
|
|
12266
|
+
pipelineKey: rtpSource.pipelineKey
|
|
12267
|
+
};
|
|
12268
|
+
}
|
|
12269
|
+
};
|
|
12270
|
+
//#endregion
|
|
12271
|
+
//#region src/stream-broker/webrtc/resolve-tier-slot.ts
|
|
12272
|
+
/**
|
|
12273
|
+
* `resolveTierSlot` — pure helper: given an owner's profile slots, a
|
|
12274
|
+
* deviceId, and a preferred tier, resolve WHICH assigned tier camStream to
|
|
12275
|
+
* consume.
|
|
12276
|
+
*
|
|
12277
|
+
* Slice 3b (ADAPTIVE-target WebRTC live-view for remote-owned cameras,
|
|
12278
|
+
* lazy-per-tier). The exact-tier slot wins when it is assigned; otherwise
|
|
12279
|
+
* falls back through the canonical `CAM_PROFILE_ORDER` (high→mid→low) — the
|
|
12280
|
+
* same single-source tier order used by `stream-broker-manager` and the
|
|
12281
|
+
* analogous no-hints tier-selection fallback in `broker-webrtc-server.ts`.
|
|
12282
|
+
*/
|
|
12283
|
+
/**
|
|
12284
|
+
* Resolve the camStream feeding `deviceId` at `preferredTier`. Prefers an
|
|
12285
|
+
* exact-tier match; when that tier is unassigned, falls back through
|
|
12286
|
+
* `TIER_PREFERENCE` order to the first assigned tier. Returns `null` when
|
|
12287
|
+
* the device has no assigned slot at all. Pure; never mutates `slots`.
|
|
12288
|
+
*/
|
|
12289
|
+
function resolveTierSlot(slots, deviceId, preferredTier) {
|
|
12290
|
+
const deviceSlots = slots.filter((slot) => slot.deviceId === deviceId);
|
|
12291
|
+
const exact = deviceSlots.find((slot) => slot.profile === preferredTier && slot.sourceCamStreamId !== null);
|
|
12292
|
+
if (exact && exact.sourceCamStreamId !== null) return {
|
|
12293
|
+
camStreamId: exact.sourceCamStreamId,
|
|
12294
|
+
profile: exact.profile
|
|
12295
|
+
};
|
|
12296
|
+
for (const tier of CAM_PROFILE_ORDER) {
|
|
12297
|
+
const match = deviceSlots.find((slot) => slot.profile === tier && slot.sourceCamStreamId !== null);
|
|
12298
|
+
if (match && match.sourceCamStreamId !== null) return {
|
|
12299
|
+
camStreamId: match.sourceCamStreamId,
|
|
12300
|
+
profile: match.profile
|
|
12301
|
+
};
|
|
12302
|
+
}
|
|
12303
|
+
return null;
|
|
12304
|
+
}
|
|
12305
|
+
//#endregion
|
|
12306
|
+
//#region src/stream-broker/webrtc/webrtc-source-mode.ts
|
|
12307
|
+
/**
|
|
12308
|
+
* Decide how a WebRTC live-view node sources a camera. Owner==self → the
|
|
12309
|
+
* local broker path (unchanged). Owner≠self + reachable → a remote-encoded
|
|
12310
|
+
* adapter dialing the owner restream. Owner≠self + no reachableHost →
|
|
12311
|
+
* unresolvable (fail fast; a 127.0.0.1 restream URL would dial the wrong box).
|
|
12312
|
+
*/
|
|
12313
|
+
function resolveWebrtcSourceMode(input) {
|
|
12314
|
+
if (input.ownerNodeId === input.ownNodeId) return { kind: "local" };
|
|
12315
|
+
if (input.reachableHost === void 0) return {
|
|
12316
|
+
kind: "unresolvable",
|
|
12317
|
+
ownerNodeId: input.ownerNodeId
|
|
12318
|
+
};
|
|
12319
|
+
return {
|
|
12320
|
+
kind: "remote-adapter",
|
|
12321
|
+
ownerNodeId: input.ownerNodeId,
|
|
12322
|
+
reachableHost: input.reachableHost
|
|
12323
|
+
};
|
|
12324
|
+
}
|
|
12325
|
+
//#endregion
|
|
12150
12326
|
//#region src/stream-broker/stream-broker/stream-broker-manager.ts
|
|
12151
12327
|
/**
|
|
12152
12328
|
* Resolve a derived stream's source to a physical camStreamId, if it is pinned
|
|
@@ -12283,6 +12459,19 @@ function findProfileForStream(map, camStreamId) {
|
|
|
12283
12459
|
return null;
|
|
12284
12460
|
}
|
|
12285
12461
|
/**
|
|
12462
|
+
* Pure lookup behind `StreamBrokerManager`'s `profileTierResolver` closure
|
|
12463
|
+
* (Slice 3b Task 2). `localLookup` mirrors today's behavior — parse the
|
|
12464
|
+
* brokerId and walk the LOCAL device's `this.assignments` map — which is
|
|
12465
|
+
* always empty for a remote-owned camera, so it returns null for every
|
|
12466
|
+
* remote-ensured adapter's brokerId. `remoteBrokerTiers` is the fallback
|
|
12467
|
+
* populated at the remote-adapter ensure site (Task 3) so `selectBestBroker`
|
|
12468
|
+
* can still tier-match a remote-owned camera's broker. Local wins when both
|
|
12469
|
+
* resolve a tier — a local broker is never shadowed by a stale remote entry.
|
|
12470
|
+
*/
|
|
12471
|
+
function resolveBrokerTier(brokerId, localLookup, remoteBrokerTiers) {
|
|
12472
|
+
return localLookup(brokerId) ?? remoteBrokerTiers.get(brokerId) ?? null;
|
|
12473
|
+
}
|
|
12474
|
+
/**
|
|
12286
12475
|
* Watchdog parameters for the stream-health emitter. Brokers that have
|
|
12287
12476
|
* received zero video packets for STREAM_STALE_TIMEOUT_MS are reported
|
|
12288
12477
|
* as `stream.offline`; the next packet flips them back to `stream.online`.
|
|
@@ -12321,7 +12510,7 @@ function parseBrokerId(brokerId) {
|
|
|
12321
12510
|
camStreamId
|
|
12322
12511
|
};
|
|
12323
12512
|
}
|
|
12324
|
-
var StreamBrokerManager = class {
|
|
12513
|
+
var StreamBrokerManager = class StreamBrokerManager {
|
|
12325
12514
|
/**
|
|
12326
12515
|
* brokers keyed by brokerId = `${deviceId}/${camStreamId}`.
|
|
12327
12516
|
*
|
|
@@ -12390,7 +12579,6 @@ var StreamBrokerManager = class {
|
|
|
12390
12579
|
* it down on idle / retract. Populated by {@link attachDerivedFeeder}.
|
|
12391
12580
|
*/
|
|
12392
12581
|
derivedFeeders = /* @__PURE__ */ new Map();
|
|
12393
|
-
restreamers = [];
|
|
12394
12582
|
staticDecoders;
|
|
12395
12583
|
logger;
|
|
12396
12584
|
/** Public accessor for same-process callers (e.g. `CameraStreamsProvider.pickStream`)
|
|
@@ -12409,7 +12597,6 @@ var StreamBrokerManager = class {
|
|
|
12409
12597
|
* next pull after a hub restart. Keyed by `brokerIdFor(deviceId, camStreamId)`.
|
|
12410
12598
|
*/
|
|
12411
12599
|
lastProbedByBroker = /* @__PURE__ */ new Map();
|
|
12412
|
-
capabilities = null;
|
|
12413
12600
|
/**
|
|
12414
12601
|
* tRPC api proxy injected from the host addon. Used to resolve the
|
|
12415
12602
|
* `decoder` capability (and any future cross-cap consumers) via the
|
|
@@ -12438,9 +12625,66 @@ var StreamBrokerManager = class {
|
|
|
12438
12625
|
* node. `null` until `setApiAccess` wires it (falls back to unpinned).
|
|
12439
12626
|
*/
|
|
12440
12627
|
localNodeId = null;
|
|
12628
|
+
/**
|
|
12629
|
+
* Short-TTL cache of `clusterRoles.ingestNode` (via `getIngestOwner`) for the
|
|
12630
|
+
* `ensureBroker` dormancy gate — the reconcile calls `ensureBroker` per
|
|
12631
|
+
* camera, so a per-call cross-node query would be wasteful. Refreshed lazily
|
|
12632
|
+
* when older than {@link INGEST_OWNER_CACHE_TTL_MS}. `null` = never resolved.
|
|
12633
|
+
*/
|
|
12634
|
+
cachedIngestOwnerNodeId = null;
|
|
12635
|
+
cachedIngestOwnerAt = 0;
|
|
12636
|
+
static INGEST_OWNER_CACHE_TTL_MS = 6e4;
|
|
12637
|
+
lastOwnershipReconcile = Promise.resolve();
|
|
12441
12638
|
defaultPreBufferSec = 10;
|
|
12442
12639
|
webrtcServer = null;
|
|
12443
12640
|
/**
|
|
12641
|
+
* brokerId → the remote-encoded adapter backing a non-owned camera's WebRTC
|
|
12642
|
+
* session (Slice 3 Task 3 — cross-node ingest wiring). Mirrors `this.brokers`
|
|
12643
|
+
* for the cross-node leg: `ensureBrokerForWebrtcSession` get-or-creates one
|
|
12644
|
+
* when `getIngestOwner` resolves to a DIFFERENT node than this one, and
|
|
12645
|
+
* tears it down on the adapter's inner broker's own `onDemandChange(false)`
|
|
12646
|
+
* edge (last-session-idle) — the same hook `attachDerivedFeeder` uses to
|
|
12647
|
+
* tear down a derived feeder.
|
|
12648
|
+
*/
|
|
12649
|
+
remoteBrokerAdapters = /* @__PURE__ */ new Map();
|
|
12650
|
+
/**
|
|
12651
|
+
* brokerId → in-flight `ensureBrokerForWebrtcSession` get-or-create for
|
|
12652
|
+
* that broker's remote adapter. Manager-level single-flight guard: two
|
|
12653
|
+
* concurrent WebRTC session creations for the SAME non-owned brokerId
|
|
12654
|
+
* (a real race — first-session-ever for a camera whose owner is a
|
|
12655
|
+
* different node) both pass the `remoteBrokerAdapters.has()` early-return
|
|
12656
|
+
* with the map still empty, because the check and the eventual `.set()`
|
|
12657
|
+
* straddle an `await getIngestOwner.query()`. Without this guard both
|
|
12658
|
+
* callers would construct their own `RemoteEncodedSourceBroker`, both
|
|
12659
|
+
* `.set()` into `remoteBrokerAdapters` (second overwrites first), and
|
|
12660
|
+
* both acquire the owner's restream (`getStreamWithCodec`) — the first
|
|
12661
|
+
* adapter becomes unreachable from the map so its `onDemandChange(false)`
|
|
12662
|
+
* teardown never runs and its restream refcount leaks forever. Concurrent
|
|
12663
|
+
* callers instead await the SAME promise, so exactly one adapter is
|
|
12664
|
+
* constructed, registered, and refcounted per brokerId.
|
|
12665
|
+
*/
|
|
12666
|
+
remoteAdapterEnsures = /* @__PURE__ */ new Map();
|
|
12667
|
+
/**
|
|
12668
|
+
* brokerId → the profile tier a remote-ensured adapter was created for
|
|
12669
|
+
* (Slice 3b Task 2). `this.assignments` only tracks LOCALLY-owned devices,
|
|
12670
|
+
* so the `profileTierResolver` closure below always misses for a remote
|
|
12671
|
+
* adapter's brokerId — this map is the fallback so `selectBestBroker` can
|
|
12672
|
+
* still tier-match it. Populated at the ensure site alongside
|
|
12673
|
+
* `remoteBrokerAdapters.set(brokerId, ...)` (Task 3 threads the tier in)
|
|
12674
|
+
* and cleared at every `remoteBrokerAdapters.delete(brokerId)` site so no
|
|
12675
|
+
* stale tier lingers past the adapter's own lifetime.
|
|
12676
|
+
*/
|
|
12677
|
+
remoteBrokerTiers = /* @__PURE__ */ new Map();
|
|
12678
|
+
/**
|
|
12679
|
+
* Injected factory for the inner broker a `RemoteEncodedSourceBroker`
|
|
12680
|
+
* drives. Production wraps the real `StreamBroker` — this leg never decodes
|
|
12681
|
+
* locally, so the decoder/audio-codec caps and local-node pin are unused
|
|
12682
|
+
* (mirrors the recommendation in `RemoteEncodedSourceBroker`'s doc comment).
|
|
12683
|
+
* Tests substitute a fake via `setRemoteEncodedBrokerFactory` so no real
|
|
12684
|
+
* RTSP dial happens.
|
|
12685
|
+
*/
|
|
12686
|
+
createRemoteEncodedBroker = (deps) => new StreamBroker(deps.brokerId, null, deps.logger, null, null);
|
|
12687
|
+
/**
|
|
12444
12688
|
* Transcode-pipeline manager — backs `getStreamWithCodec`. Lazily
|
|
12445
12689
|
* created on first acquire, lives until `destroyAll`.
|
|
12446
12690
|
*/
|
|
@@ -12667,9 +12911,6 @@ var StreamBrokerManager = class {
|
|
|
12667
12911
|
}
|
|
12668
12912
|
};
|
|
12669
12913
|
}
|
|
12670
|
-
setCapabilitiesAccess(access) {
|
|
12671
|
-
this.capabilities = access;
|
|
12672
|
-
}
|
|
12673
12914
|
/**
|
|
12674
12915
|
* Wire the addon's `ctx.api` proxy. Required for the broker to
|
|
12675
12916
|
* resolve the `decoder` capability via tRPC — see
|
|
@@ -12714,6 +12955,61 @@ var StreamBrokerManager = class {
|
|
|
12714
12955
|
this.eventBus = bus;
|
|
12715
12956
|
this.startStreamHealthWatchdog();
|
|
12716
12957
|
this.subscribePlaceholderStateSources(bus);
|
|
12958
|
+
this.subscribeIngestOwnerChanges(bus);
|
|
12959
|
+
}
|
|
12960
|
+
/**
|
|
12961
|
+
* Track the cluster camera-source owner from the orchestrator's pushed
|
|
12962
|
+
* `PipelineIngestOwnerChanged` events instead of polling `getIngestOwner`
|
|
12963
|
+
* on every `ensureBroker`. The orchestrator re-emits on every node-capability
|
|
12964
|
+
* refresh (boot / agent online-offline / ingest flip), so this keeps the
|
|
12965
|
+
* ingest-owner gate current and prompt; the query in {@link isLocalIngestOwner}
|
|
12966
|
+
* survives only as a cold-start + long-backstop reconcile (loss-safety).
|
|
12967
|
+
*/
|
|
12968
|
+
subscribeIngestOwnerChanges(bus) {
|
|
12969
|
+
bus.subscribe({ category: EventCategory.PipelineIngestOwnerChanged }, (event) => {
|
|
12970
|
+
if (!isEvent(event, EventCategory.PipelineIngestOwnerChanged)) return;
|
|
12971
|
+
const { ownerNodeId } = event.data;
|
|
12972
|
+
const changed = this.cachedIngestOwnerNodeId !== ownerNodeId;
|
|
12973
|
+
this.cachedIngestOwnerNodeId = ownerNodeId;
|
|
12974
|
+
this.cachedIngestOwnerAt = Date.now();
|
|
12975
|
+
if (changed) this.logger.debug("ingest owner changed (pushed)", { meta: {
|
|
12976
|
+
ownerNodeId,
|
|
12977
|
+
self: this.localNodeId
|
|
12978
|
+
} });
|
|
12979
|
+
this.lastOwnershipReconcile = this.reconcileOwnershipTeardown();
|
|
12980
|
+
});
|
|
12981
|
+
}
|
|
12982
|
+
/**
|
|
12983
|
+
* Tear down every LOCAL physical source broker when this node is no longer the
|
|
12984
|
+
* cluster ingest owner. `this.brokers` holds only physical source + derived
|
|
12985
|
+
* brokers (the `RemoteEncodedSourceBroker` restream adapters a non-owner uses
|
|
12986
|
+
* live in the separate `remoteBrokerAdapters` map, so they are untouched).
|
|
12987
|
+
* Re-creation is blocked by the ingest-owner gate on this node; the node's
|
|
12988
|
+
* consumers re-source from the new owner's restream via the orchestrator's
|
|
12989
|
+
* re-dispatch. No-op when this node IS the owner or holds no brokers, so a
|
|
12990
|
+
* repeated same-owner re-emit is cheap.
|
|
12991
|
+
*/
|
|
12992
|
+
async reconcileOwnershipTeardown() {
|
|
12993
|
+
if (this.localNodeId === null) return;
|
|
12994
|
+
if (this.cachedIngestOwnerNodeId === null) return;
|
|
12995
|
+
if (this.cachedIngestOwnerNodeId === this.localNodeId) return;
|
|
12996
|
+
if (this.brokers.size === 0) return;
|
|
12997
|
+
const brokerIds = [...this.brokers.keys()];
|
|
12998
|
+
this.logger.info("ingest owner moved away — tearing down local physical brokers", { meta: {
|
|
12999
|
+
owner: this.cachedIngestOwnerNodeId,
|
|
13000
|
+
self: this.localNodeId,
|
|
13001
|
+
count: brokerIds.length
|
|
13002
|
+
} });
|
|
13003
|
+
for (const brokerId of brokerIds) {
|
|
13004
|
+
const parsed = this.parseBrokerId(brokerId);
|
|
13005
|
+
if (!parsed) continue;
|
|
13006
|
+
await this.releaseBroker(parsed.deviceId, parsed.camStreamId).catch((err) => {
|
|
13007
|
+
this.logger.warn("reverse-flip teardown: releaseBroker failed", { meta: {
|
|
13008
|
+
brokerId,
|
|
13009
|
+
error: errMsg(err)
|
|
13010
|
+
} });
|
|
13011
|
+
});
|
|
13012
|
+
}
|
|
12717
13013
|
}
|
|
12718
13014
|
/**
|
|
12719
13015
|
* Subscribe to the cap-scoped bus events that drive the per-broker
|
|
@@ -12757,14 +13053,242 @@ var StreamBrokerManager = class {
|
|
|
12757
13053
|
}
|
|
12758
13054
|
setWebrtcServer(server) {
|
|
12759
13055
|
this.webrtcServer = server;
|
|
12760
|
-
server.setProfileTierResolver((brokerId) => {
|
|
12761
|
-
const parsed = this.parseBrokerId(
|
|
13056
|
+
server.setProfileTierResolver((brokerId) => resolveBrokerTier(brokerId, (id) => {
|
|
13057
|
+
const parsed = this.parseBrokerId(id);
|
|
12762
13058
|
if (!parsed) return null;
|
|
12763
13059
|
return findProfileForStream(this.assignments.get(parsed.deviceId)?.map ?? {}, parsed.camStreamId);
|
|
12764
|
-
});
|
|
13060
|
+
}, this.remoteBrokerTiers));
|
|
12765
13061
|
server.setSourceRestreamUrlResolver((brokerId) => this.rtspProvider.getEntry(brokerId)?.url ?? null);
|
|
13062
|
+
server.setEnsureAdaptiveTierBrokerHook((deviceId, tier) => this.ensureRemoteAdaptiveTierBroker(deviceId, tier));
|
|
12766
13063
|
for (const [brokerId, broker] of this.brokers) server.registerBroker(brokerId, broker);
|
|
12767
13064
|
}
|
|
13065
|
+
/**
|
|
13066
|
+
* Test/injection seam for the inner broker a `RemoteEncodedSourceBroker`
|
|
13067
|
+
* drives (Slice 3 Task 3). Production never calls this — the field default
|
|
13068
|
+
* already wraps the real `StreamBroker`. Mirrors the `createBroker`
|
|
13069
|
+
* injection point `RemoteEncodedSourceBroker` itself exposes.
|
|
13070
|
+
*/
|
|
13071
|
+
setRemoteEncodedBrokerFactory(factory) {
|
|
13072
|
+
this.createRemoteEncodedBroker = factory;
|
|
13073
|
+
}
|
|
13074
|
+
/**
|
|
13075
|
+
* Ensure a broker exists for `brokerId` before a WebRTC session subscribes
|
|
13076
|
+
* to it (Slice 3 Task 3 — cross-node ingest wiring). Called by
|
|
13077
|
+
* `WebrtcSessionProvider.createSession`/`handleOffer` right before they
|
|
13078
|
+
* delegate to `BrokerWebrtcServer`.
|
|
13079
|
+
*
|
|
13080
|
+
* No-op when a LOCAL broker already covers `brokerId` — the
|
|
13081
|
+
* default/dormant state while every camera's `ingestNode` is the hub, so
|
|
13082
|
+
* this returns on the very first check with ZERO extra calls (byte-
|
|
13083
|
+
* identical to today). Otherwise resolves the cluster's camera-source
|
|
13084
|
+
* owner (`getIngestOwner`) and branches via `resolveWebrtcSourceMode`:
|
|
13085
|
+
*
|
|
13086
|
+
* - `local` → this node IS the owner but hasn't published/registered a
|
|
13087
|
+
* broker for this brokerId (a genuinely missing/misconfigured
|
|
13088
|
+
* stream) — do nothing and let `BrokerWebrtcServer` throw its normal
|
|
13089
|
+
* "No broker for stream" error, unchanged.
|
|
13090
|
+
* - `remote-adapter` → get-or-create a `RemoteEncodedSourceBroker` that
|
|
13091
|
+
* dials the owner's compressed passthrough restream, register its
|
|
13092
|
+
* inner broker with the WebRTC server exactly like a local broker (so
|
|
13093
|
+
* `setupSessionForBroker`'s resolution, tier logic, and everything
|
|
13094
|
+
* downstream see an ordinary `IStreamBroker` — this leg never touches
|
|
13095
|
+
* `broker-webrtc-server.ts`), and tear the adapter down on its own
|
|
13096
|
+
* `onDemandChange(false)` (last-session-idle) edge, mirroring
|
|
13097
|
+
* `attachDerivedFeeder`'s local-broker teardown hook.
|
|
13098
|
+
* - `unresolvable` → the owner is set but has no reachable host —
|
|
13099
|
+
* throw a descriptive error rather than let the session hang.
|
|
13100
|
+
*
|
|
13101
|
+
* `camStreamId === undefined` (an `adaptive` target with no cam stream
|
|
13102
|
+
* pinned yet) can't resolve a single owner profile to acquire — falls
|
|
13103
|
+
* through to the natural local-only resolution/error, same as today.
|
|
13104
|
+
*
|
|
13105
|
+
* `tier` (Slice 3b Task 3) is an optional hint from
|
|
13106
|
+
* `ensureRemoteAdaptiveTierBroker` — the profile the caller already
|
|
13107
|
+
* resolved `camStreamId` from via `resolveTierSlot`. When set, it is
|
|
13108
|
+
* recorded into `remoteBrokerTiers` alongside the adapter registration so
|
|
13109
|
+
* `resolveBrokerTier`/`selectBestBroker` can tier-match this brokerId even
|
|
13110
|
+
* though it's absent from `this.assignments` (remote-owned). Callers that
|
|
13111
|
+
* don't have a resolved tier (the plain WebRTC session path) omit it —
|
|
13112
|
+
* unchanged behavior.
|
|
13113
|
+
*/
|
|
13114
|
+
async ensureBrokerForWebrtcSession(deviceId, brokerId, camStreamId, tier) {
|
|
13115
|
+
if (this.brokers.has(brokerId)) return;
|
|
13116
|
+
if (this.remoteBrokerAdapters.has(brokerId)) return;
|
|
13117
|
+
if (camStreamId === void 0) return;
|
|
13118
|
+
if (!this.api || this.localNodeId === null) return;
|
|
13119
|
+
const api = this.api;
|
|
13120
|
+
const ownNodeId = this.localNodeId;
|
|
13121
|
+
const existingEnsure = this.remoteAdapterEnsures.get(brokerId);
|
|
13122
|
+
if (existingEnsure) {
|
|
13123
|
+
await existingEnsure;
|
|
13124
|
+
return;
|
|
13125
|
+
}
|
|
13126
|
+
const ensure = this.createRemoteBrokerAdapterForSession(api, ownNodeId, deviceId, brokerId, camStreamId, tier);
|
|
13127
|
+
this.remoteAdapterEnsures.set(brokerId, ensure);
|
|
13128
|
+
try {
|
|
13129
|
+
await ensure;
|
|
13130
|
+
} finally {
|
|
13131
|
+
this.remoteAdapterEnsures.delete(brokerId);
|
|
13132
|
+
}
|
|
13133
|
+
}
|
|
13134
|
+
/**
|
|
13135
|
+
* Single-flight body for `ensureBrokerForWebrtcSession`'s remote-adapter
|
|
13136
|
+
* get-or-create — resolves the ingest owner, and for `remote-adapter` mode
|
|
13137
|
+
* constructs the `RemoteEncodedSourceBroker`, registers it with the WebRTC
|
|
13138
|
+
* server, and wires its idle-teardown hook. Only ever run once per
|
|
13139
|
+
* brokerId at a time; see `remoteAdapterEnsures`.
|
|
13140
|
+
*/
|
|
13141
|
+
async createRemoteBrokerAdapterForSession(api, ownNodeId, deviceId, brokerId, camStreamId, tier) {
|
|
13142
|
+
let owner;
|
|
13143
|
+
try {
|
|
13144
|
+
owner = await api.pipelineOrchestrator.getIngestOwner.query();
|
|
13145
|
+
} catch (err) {
|
|
13146
|
+
this.logger.debug("webrtc-session: getIngestOwner unavailable — defaulting to local (cap not mounted yet?)", {
|
|
13147
|
+
tags: { deviceId },
|
|
13148
|
+
meta: { error: errMsg(err) }
|
|
13149
|
+
});
|
|
13150
|
+
owner = { ownerNodeId: ownNodeId };
|
|
13151
|
+
}
|
|
13152
|
+
const mode = resolveWebrtcSourceMode({
|
|
13153
|
+
ownerNodeId: owner.ownerNodeId,
|
|
13154
|
+
ownNodeId,
|
|
13155
|
+
reachableHost: owner.reachableHost
|
|
13156
|
+
});
|
|
13157
|
+
if (mode.kind === "local") return;
|
|
13158
|
+
if (mode.kind === "unresolvable") {
|
|
13159
|
+
const configIssueSuffix = owner.configIssue ? ` (${owner.configIssue})` : "";
|
|
13160
|
+
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}`);
|
|
13161
|
+
}
|
|
13162
|
+
const adapter = new RemoteEncodedSourceBroker({
|
|
13163
|
+
api,
|
|
13164
|
+
logger: this.logger.child("remote-encoded").withTags({ deviceId }),
|
|
13165
|
+
deviceId,
|
|
13166
|
+
streamId: camStreamId,
|
|
13167
|
+
ownerNodeId: mode.ownerNodeId,
|
|
13168
|
+
reachableHost: mode.reachableHost,
|
|
13169
|
+
createBroker: this.createRemoteEncodedBroker
|
|
13170
|
+
});
|
|
13171
|
+
this.remoteBrokerAdapters.set(brokerId, adapter);
|
|
13172
|
+
if (tier) this.remoteBrokerTiers.set(brokerId, tier);
|
|
13173
|
+
let broker;
|
|
13174
|
+
try {
|
|
13175
|
+
broker = await adapter.getBroker();
|
|
13176
|
+
} catch (err) {
|
|
13177
|
+
this.remoteBrokerAdapters.delete(brokerId);
|
|
13178
|
+
this.remoteBrokerTiers.delete(brokerId);
|
|
13179
|
+
throw err;
|
|
13180
|
+
}
|
|
13181
|
+
this.webrtcServer?.registerBroker(brokerId, broker);
|
|
13182
|
+
let unsubscribeDemand = () => {};
|
|
13183
|
+
unsubscribeDemand = broker.onDemandChange((active) => {
|
|
13184
|
+
if (active) return;
|
|
13185
|
+
unsubscribeDemand();
|
|
13186
|
+
this.remoteBrokerAdapters.delete(brokerId);
|
|
13187
|
+
this.remoteBrokerTiers.delete(brokerId);
|
|
13188
|
+
this.webrtcServer?.unregisterBroker(brokerId);
|
|
13189
|
+
adapter.dispose().catch((disposeErr) => {
|
|
13190
|
+
this.logger.warn("webrtc-session: remote-encoded adapter dispose failed", {
|
|
13191
|
+
tags: { deviceId },
|
|
13192
|
+
meta: {
|
|
13193
|
+
brokerId,
|
|
13194
|
+
error: errMsg(disposeErr)
|
|
13195
|
+
}
|
|
13196
|
+
});
|
|
13197
|
+
});
|
|
13198
|
+
});
|
|
13199
|
+
}
|
|
13200
|
+
/**
|
|
13201
|
+
* Idempotent cleanup for a remote adapter registered by
|
|
13202
|
+
* `ensureBrokerForWebrtcSession`. `WebrtcSessionProvider.createSession`/
|
|
13203
|
+
* `handleOffer` call this when the downstream `BrokerWebrtcServer`
|
|
13204
|
+
* delegate throws AFTER the adapter was already registered.
|
|
13205
|
+
*
|
|
13206
|
+
* Without this, teardown is driven ONLY by the inner broker's
|
|
13207
|
+
* `onDemandChange(true→false)` edge (last-WebRTC-subscriber-leaves). If
|
|
13208
|
+
* the delegate never succeeds, demand never reaches `true`, that edge
|
|
13209
|
+
* never fires, and the adapter + its WebRTC registration + the owner
|
|
13210
|
+
* restream refcount (`releaseStreamWithCodec`) leak permanently.
|
|
13211
|
+
*
|
|
13212
|
+
* A cheap no-op when no remote adapter exists for `brokerId` — the
|
|
13213
|
+
* owner==self / local-broker path never registers one, so this call adds
|
|
13214
|
+
* zero behavior there. Also safe if the adapter was already torn down
|
|
13215
|
+
* (e.g. a racing `onDemandChange(false)` edge) — `RemoteEncodedSourceBroker
|
|
13216
|
+
* .dispose()` is itself idempotent. Dispose failures are logged and
|
|
13217
|
+
* swallowed; this method never throws, so callers can always re-throw the
|
|
13218
|
+
* original delegate error afterward.
|
|
13219
|
+
*/
|
|
13220
|
+
async disposeRemoteBrokerAdapterIfPresent(brokerId) {
|
|
13221
|
+
const adapter = this.remoteBrokerAdapters.get(brokerId);
|
|
13222
|
+
if (!adapter) return;
|
|
13223
|
+
this.remoteBrokerAdapters.delete(brokerId);
|
|
13224
|
+
this.remoteBrokerTiers.delete(brokerId);
|
|
13225
|
+
this.webrtcServer?.unregisterBroker(brokerId);
|
|
13226
|
+
try {
|
|
13227
|
+
await adapter.dispose();
|
|
13228
|
+
} catch (disposeErr) {
|
|
13229
|
+
this.logger.warn("webrtc-session: remote-encoded adapter dispose failed (delegate-failure cleanup)", { meta: {
|
|
13230
|
+
brokerId,
|
|
13231
|
+
error: errMsg(disposeErr)
|
|
13232
|
+
} });
|
|
13233
|
+
}
|
|
13234
|
+
}
|
|
13235
|
+
/**
|
|
13236
|
+
* Lazily ensure a remote adapter exists for a device's PREFERRED tier's
|
|
13237
|
+
* camStream (Slice 3b Task 3 — adaptive WebRTC live-view for remote-owned
|
|
13238
|
+
* cameras, lazy-per-tier). Adaptive-target sessions don't carry a
|
|
13239
|
+
* session-pinned camStreamId, so unlike `ensureBrokerForWebrtcSession`
|
|
13240
|
+
* this method's own job is resolving WHICH camStreamId the preferred tier
|
|
13241
|
+
* maps to on the owning node — via `resolveTierSlot` walking the owner's
|
|
13242
|
+
* `listAllProfileSlots` — before delegating to
|
|
13243
|
+
* `ensureBrokerForWebrtcSession` for the actual get-or-create (reusing its
|
|
13244
|
+
* single-flight guard and idle-teardown wiring unchanged).
|
|
13245
|
+
*
|
|
13246
|
+
* No-op for a LOCALLY-owned device — `this.assignments` already tracks it
|
|
13247
|
+
* and the normal (non-adaptive) resolve path handles it; this returns on
|
|
13248
|
+
* the very first check with ZERO extra calls, byte-identical to today.
|
|
13249
|
+
*
|
|
13250
|
+
* Mirrors `ensureBrokerForWebrtcSession`'s owner-resolution: graceful
|
|
13251
|
+
* `getIngestOwner` (defaults to local on failure — cap not mounted yet) +
|
|
13252
|
+
* `resolveWebrtcSourceMode`. `local` → return (nothing to do; the normal
|
|
13253
|
+
* resolve path already covers this node being the owner). `unresolvable`
|
|
13254
|
+
* → throw the same descriptive error rather than let a session hang.
|
|
13255
|
+
*
|
|
13256
|
+
* When the preferred tier (and every fallback tier) has no assigned slot
|
|
13257
|
+
* on the owner, `resolveTierSlot` returns null — this method returns
|
|
13258
|
+
* without throwing; the normal WebRTC resolve path will surface its own
|
|
13259
|
+
* "no broker for stream" error naturally once a session actually tries to
|
|
13260
|
+
* subscribe.
|
|
13261
|
+
*/
|
|
13262
|
+
async ensureRemoteAdaptiveTierBroker(deviceId, preferredTier) {
|
|
13263
|
+
if (this.assignments.has(deviceId)) return;
|
|
13264
|
+
if (!this.api || this.localNodeId === null) return;
|
|
13265
|
+
const api = this.api;
|
|
13266
|
+
const ownNodeId = this.localNodeId;
|
|
13267
|
+
let owner;
|
|
13268
|
+
try {
|
|
13269
|
+
owner = await api.pipelineOrchestrator.getIngestOwner.query();
|
|
13270
|
+
} catch (err) {
|
|
13271
|
+
this.logger.debug("webrtc-adaptive-tier: getIngestOwner unavailable — defaulting to local (cap not mounted yet?)", {
|
|
13272
|
+
tags: { deviceId },
|
|
13273
|
+
meta: { error: errMsg(err) }
|
|
13274
|
+
});
|
|
13275
|
+
owner = { ownerNodeId: ownNodeId };
|
|
13276
|
+
}
|
|
13277
|
+
const mode = resolveWebrtcSourceMode({
|
|
13278
|
+
ownerNodeId: owner.ownerNodeId,
|
|
13279
|
+
ownNodeId,
|
|
13280
|
+
reachableHost: owner.reachableHost
|
|
13281
|
+
});
|
|
13282
|
+
if (mode.kind === "local") return;
|
|
13283
|
+
if (mode.kind === "unresolvable") {
|
|
13284
|
+
const configIssueSuffix = owner.configIssue ? ` (${owner.configIssue})` : "";
|
|
13285
|
+
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}`);
|
|
13286
|
+
}
|
|
13287
|
+
const hit = resolveTierSlot(await api.streamBroker.listAllProfileSlots.query(void 0, nodePin(mode.ownerNodeId)), deviceId, preferredTier);
|
|
13288
|
+
if (hit === null) return;
|
|
13289
|
+
const brokerId = makeSourceBrokerId(deviceId, hit.camStreamId);
|
|
13290
|
+
await this.ensureBrokerForWebrtcSession(deviceId, brokerId, hit.camStreamId, hit.profile);
|
|
13291
|
+
}
|
|
12768
13292
|
setProfileMapPersister(persister) {
|
|
12769
13293
|
this.profileMapPersister = persister;
|
|
12770
13294
|
}
|
|
@@ -12772,19 +13296,6 @@ var StreamBrokerManager = class {
|
|
|
12772
13296
|
this.assignments.clear();
|
|
12773
13297
|
for (const [k, v] of entries) this.assignments.set(k, v);
|
|
12774
13298
|
}
|
|
12775
|
-
getLiveRestreamers() {
|
|
12776
|
-
const live = this.capabilities?.getCollection?.("restreamer");
|
|
12777
|
-
if (live && live.length > 0) return live;
|
|
12778
|
-
return this.restreamers;
|
|
12779
|
-
}
|
|
12780
|
-
setRestreamers(restreamers) {
|
|
12781
|
-
this.restreamers = restreamers;
|
|
12782
|
-
for (const broker of this.brokers.values()) broker.setRestreamers(restreamers);
|
|
12783
|
-
this.logger.info("Updated restreamers", { meta: { count: restreamers.length } });
|
|
12784
|
-
}
|
|
12785
|
-
getRestreamers() {
|
|
12786
|
-
return this.restreamers;
|
|
12787
|
-
}
|
|
12788
13299
|
getRtspRestreamProvider() {
|
|
12789
13300
|
return this.rtspProvider;
|
|
12790
13301
|
}
|
|
@@ -13314,12 +13825,16 @@ var StreamBrokerManager = class {
|
|
|
13314
13825
|
await broker.rotateDecoderSession(reason, agentNodeId);
|
|
13315
13826
|
rotated++;
|
|
13316
13827
|
} catch (err) {
|
|
13317
|
-
|
|
13318
|
-
|
|
13319
|
-
|
|
13320
|
-
|
|
13321
|
-
|
|
13322
|
-
|
|
13828
|
+
const parsedBrokerId = parseBrokerId(broker.deviceId);
|
|
13829
|
+
this.logger.warn("decoder rotation failed", {
|
|
13830
|
+
...parsedBrokerId ? { tags: { deviceId: parsedBrokerId.deviceId } } : {},
|
|
13831
|
+
meta: {
|
|
13832
|
+
brokerId: broker.deviceId,
|
|
13833
|
+
agentNodeId,
|
|
13834
|
+
reason,
|
|
13835
|
+
error: errMsg(err)
|
|
13836
|
+
}
|
|
13837
|
+
});
|
|
13323
13838
|
}
|
|
13324
13839
|
}
|
|
13325
13840
|
return rotated;
|
|
@@ -13409,6 +13924,7 @@ var StreamBrokerManager = class {
|
|
|
13409
13924
|
clearInterval(this.streamHealthTimer);
|
|
13410
13925
|
this.streamHealthTimer = void 0;
|
|
13411
13926
|
}
|
|
13927
|
+
await this.lastOwnershipReconcile.catch(() => void 0);
|
|
13412
13928
|
if (this.transcodeManager) {
|
|
13413
13929
|
this.transcodeManager.shutdownAll();
|
|
13414
13930
|
this.transcodeManager = null;
|
|
@@ -13505,15 +14021,7 @@ var StreamBrokerManager = class {
|
|
|
13505
14021
|
packetCount: stats.preBufferPackets
|
|
13506
14022
|
};
|
|
13507
14023
|
}
|
|
13508
|
-
async getStreamUrl(
|
|
13509
|
-
const prefix = input.streamId.split("/")[0];
|
|
13510
|
-
if (!prefix) return { url: "" };
|
|
13511
|
-
const deviceId = Number(prefix);
|
|
13512
|
-
if (!Number.isFinite(deviceId)) return { url: "" };
|
|
13513
|
-
for (const restreamer of this.getLiveRestreamers()) {
|
|
13514
|
-
const match = restreamer.getExposedResources(deviceId).find((r) => r.format === input.format);
|
|
13515
|
-
if (match) return { url: match.value };
|
|
13516
|
-
}
|
|
14024
|
+
async getStreamUrl(_input) {
|
|
13517
14025
|
return { url: "" };
|
|
13518
14026
|
}
|
|
13519
14027
|
async getRtspPort() {
|
|
@@ -13605,13 +14113,24 @@ var StreamBrokerManager = class {
|
|
|
13605
14113
|
return this.rtspProvider.getEntry(input.brokerId, input.hostname);
|
|
13606
14114
|
}
|
|
13607
14115
|
async regenerateRtspToken(input) {
|
|
13608
|
-
|
|
14116
|
+
const parsedBrokerId = parseBrokerId(input.brokerId);
|
|
14117
|
+
const tags = parsedBrokerId ? { deviceId: parsedBrokerId.deviceId } : void 0;
|
|
14118
|
+
this.logger.info("regenerateRtspToken called", {
|
|
14119
|
+
tags,
|
|
14120
|
+
meta: { brokerId: input.brokerId }
|
|
14121
|
+
});
|
|
13609
14122
|
const result = this.rtspProvider.regenerateToken(input.brokerId);
|
|
13610
|
-
if (result) this.logger.info("regenerateRtspToken succeeded", {
|
|
13611
|
-
|
|
13612
|
-
|
|
13613
|
-
|
|
13614
|
-
|
|
14123
|
+
if (result) this.logger.info("regenerateRtspToken succeeded", {
|
|
14124
|
+
tags,
|
|
14125
|
+
meta: {
|
|
14126
|
+
brokerId: input.brokerId,
|
|
14127
|
+
newTokenPrefix: result.slice(0, 8)
|
|
14128
|
+
}
|
|
14129
|
+
});
|
|
14130
|
+
else this.logger.warn("regenerateRtspToken failed — broker not found", {
|
|
14131
|
+
tags,
|
|
14132
|
+
meta: { brokerId: input.brokerId }
|
|
14133
|
+
});
|
|
13615
14134
|
return result;
|
|
13616
14135
|
}
|
|
13617
14136
|
async setRtspEnabled(input) {
|
|
@@ -14502,10 +15021,13 @@ var StreamBrokerManager = class {
|
|
|
14502
15021
|
const key = `${cam.deviceId}:${cam.camStreamId}`;
|
|
14503
15022
|
if (!this.sourceParserFellBackWarned.has(key)) {
|
|
14504
15023
|
this.sourceParserFellBackWarned.add(key);
|
|
14505
|
-
this.logger.warn("source parser native-udp requested but not yet implemented — falling back to native-tcp", {
|
|
14506
|
-
deviceId: cam.deviceId,
|
|
14507
|
-
|
|
14508
|
-
|
|
15024
|
+
this.logger.warn("source parser native-udp requested but not yet implemented — falling back to native-tcp", {
|
|
15025
|
+
tags: { deviceId: cam.deviceId },
|
|
15026
|
+
meta: {
|
|
15027
|
+
deviceId: cam.deviceId,
|
|
15028
|
+
camStreamId: cam.camStreamId
|
|
15029
|
+
}
|
|
15030
|
+
});
|
|
14509
15031
|
}
|
|
14510
15032
|
}
|
|
14511
15033
|
if (resolved.engine !== "ffmpeg") return {};
|
|
@@ -14647,18 +15169,69 @@ var StreamBrokerManager = class {
|
|
|
14647
15169
|
* registered. Tied to publish lifecycle: `publishCameraStream` calls
|
|
14648
15170
|
* this once per stream; `retractCameraStream` releases it.
|
|
14649
15171
|
*/
|
|
15172
|
+
/**
|
|
15173
|
+
* True when THIS node owns the cluster's camera source pull
|
|
15174
|
+
* (`clusterRoles.ingestNode`). Backs the `ensureBroker` dormancy gate.
|
|
15175
|
+
*
|
|
15176
|
+
* Ownership is normally PUSHED by the orchestrator's
|
|
15177
|
+
* `PipelineIngestOwnerChanged` event ({@link subscribeIngestOwnerChanges}),
|
|
15178
|
+
* so the steady-state gate is a pure cache read — no per-`ensureBroker`
|
|
15179
|
+
* cross-process query.
|
|
15180
|
+
*
|
|
15181
|
+
* - Not yet wired (`api`/`localNodeId` absent) → `true` (unchanged behavior;
|
|
15182
|
+
* the manager runs as today until `setApiAccess` supplies a node id).
|
|
15183
|
+
* - Cached owner fresh (event-stamped, within {@link INGEST_OWNER_CACHE_TTL_MS}) →
|
|
15184
|
+
* pure comparison, no query.
|
|
15185
|
+
* - Cold start / no event for the backstop window → a single `getIngestOwner`
|
|
15186
|
+
* reconcile query. It fails FAST now (FIX #1 makes an unregistered-provider
|
|
15187
|
+
* miss a precise error, not a 30s hang), so on failure we FAIL-OPEN: use the
|
|
15188
|
+
* last-known owner, or ALLOW when nothing is known — a rare transient
|
|
15189
|
+
* double-pull self-heals on the next pushed event, whereas denying would
|
|
15190
|
+
* starve the owner (no prebuffer, no recording).
|
|
15191
|
+
*/
|
|
15192
|
+
async isLocalIngestOwner() {
|
|
15193
|
+
if (!this.api || this.localNodeId === null) return true;
|
|
15194
|
+
const now = Date.now();
|
|
15195
|
+
if (this.cachedIngestOwnerNodeId !== null && now - this.cachedIngestOwnerAt < StreamBrokerManager.INGEST_OWNER_CACHE_TTL_MS) return this.cachedIngestOwnerNodeId === this.localNodeId;
|
|
15196
|
+
try {
|
|
15197
|
+
const owner = await this.api.pipelineOrchestrator.getIngestOwner.query();
|
|
15198
|
+
this.cachedIngestOwnerNodeId = owner.ownerNodeId;
|
|
15199
|
+
this.cachedIngestOwnerAt = now;
|
|
15200
|
+
return owner.ownerNodeId === this.localNodeId;
|
|
15201
|
+
} catch (err) {
|
|
15202
|
+
this.logger.debug("isLocalIngestOwner: getIngestOwner unavailable — fail-open", { meta: {
|
|
15203
|
+
lastKnown: this.cachedIngestOwnerNodeId,
|
|
15204
|
+
self: this.localNodeId,
|
|
15205
|
+
error: errMsg(err)
|
|
15206
|
+
} });
|
|
15207
|
+
return this.cachedIngestOwnerNodeId === null ? true : this.cachedIngestOwnerNodeId === this.localNodeId;
|
|
15208
|
+
}
|
|
15209
|
+
}
|
|
14650
15210
|
async ensureBroker(deviceId, camStreamId) {
|
|
14651
15211
|
const cam = this.cameraStreams.get(deviceId)?.get(camStreamId);
|
|
14652
15212
|
if (!cam) return;
|
|
14653
15213
|
const brokerId = brokerIdFor(deviceId, camStreamId);
|
|
14654
15214
|
if (this.brokers.has(brokerId)) return;
|
|
15215
|
+
if (!await this.isLocalIngestOwner()) {
|
|
15216
|
+
this.logger.debug("ensureBroker: not the ingest owner — skipping local physical broker", {
|
|
15217
|
+
tags: {
|
|
15218
|
+
deviceId,
|
|
15219
|
+
camStreamId
|
|
15220
|
+
},
|
|
15221
|
+
meta: {
|
|
15222
|
+
brokerId,
|
|
15223
|
+
ingestOwner: this.cachedIngestOwnerNodeId,
|
|
15224
|
+
self: this.localNodeId
|
|
15225
|
+
}
|
|
15226
|
+
});
|
|
15227
|
+
return;
|
|
15228
|
+
}
|
|
14655
15229
|
const brokerLogger = this.logger.child(camStreamId).withTags({
|
|
14656
15230
|
deviceId,
|
|
14657
15231
|
camStreamId
|
|
14658
15232
|
});
|
|
14659
15233
|
const source = this.buildSourceFromCamStream(cam);
|
|
14660
15234
|
const broker = new StreamBroker(brokerId, this.buildDecoderCapApi(deviceId), brokerLogger, buildAudioCodecCapApi(this.api), this.localNodeId);
|
|
14661
|
-
broker.setRestreamers(this.restreamers);
|
|
14662
15235
|
broker.setRestreamUrlResolver(() => this.rtspProvider.getEntry(brokerId)?.mutedUrl ?? null);
|
|
14663
15236
|
broker.setFfmpegBinaryPath(this.ffmpegConfig.binaryPath);
|
|
14664
15237
|
this.applyPreBufferConfig(broker, deviceId, camStreamId);
|
|
@@ -14721,7 +15294,7 @@ var StreamBrokerManager = class {
|
|
|
14721
15294
|
if (!broker) return;
|
|
14722
15295
|
if (this.derivedFeeders.has(brokerId)) return;
|
|
14723
15296
|
const controller = new DerivedFeederController({
|
|
14724
|
-
logger: this.logger.child(`derived-feeder:${camStreamId}`),
|
|
15297
|
+
logger: this.logger.child(`derived-feeder:${camStreamId}`).withTags({ deviceId }),
|
|
14725
15298
|
spawn: (onTeardown) => this.spawnDerivedFeeder(deviceId, camStreamId, source, encodeProfile, broker, onTeardown)
|
|
14726
15299
|
});
|
|
14727
15300
|
const unsubscribe = broker.onDemandChange((active) => controller.onDemand(active));
|
|
@@ -14772,21 +15345,27 @@ var StreamBrokerManager = class {
|
|
|
14772
15345
|
const feeder = new DerivedStreamFeeder({
|
|
14773
15346
|
ffmpegPath: this.ffmpegConfig.binaryPath,
|
|
14774
15347
|
args,
|
|
14775
|
-
logger: this.logger.child(`derived-ffmpeg:${camStreamId}`)
|
|
15348
|
+
logger: this.logger.child(`derived-ffmpeg:${camStreamId}`).withTags({ deviceId })
|
|
14776
15349
|
}, {
|
|
14777
15350
|
onEncodedPacket: (packet) => broker.pushEncodedPacket(packet),
|
|
14778
15351
|
onAudioInfo: (info) => broker.pushAudioInfo(info),
|
|
14779
15352
|
onPlaying: () => {
|
|
14780
|
-
this.logger.info("derived feeder streaming", {
|
|
14781
|
-
|
|
14782
|
-
|
|
14783
|
-
|
|
15353
|
+
this.logger.info("derived feeder streaming", {
|
|
15354
|
+
tags: { deviceId },
|
|
15355
|
+
meta: {
|
|
15356
|
+
camStreamId,
|
|
15357
|
+
loopbackUrl
|
|
15358
|
+
}
|
|
15359
|
+
});
|
|
14784
15360
|
},
|
|
14785
15361
|
onError: (error) => {
|
|
14786
|
-
this.logger.warn("derived feeder error", {
|
|
14787
|
-
|
|
14788
|
-
|
|
14789
|
-
|
|
15362
|
+
this.logger.warn("derived feeder error", {
|
|
15363
|
+
tags: { deviceId },
|
|
15364
|
+
meta: {
|
|
15365
|
+
camStreamId,
|
|
15366
|
+
error: error.message
|
|
15367
|
+
}
|
|
15368
|
+
});
|
|
14790
15369
|
},
|
|
14791
15370
|
onTeardown
|
|
14792
15371
|
});
|
|
@@ -14840,44 +15419,10 @@ var StreamBrokerManager = class {
|
|
|
14840
15419
|
broker.setPreBufferEnabled(defaultWarm);
|
|
14841
15420
|
broker.setPreBufferDuration(defaultWarm ? this.defaultPreBufferSec : 0);
|
|
14842
15421
|
}
|
|
14843
|
-
async fanOutRegistration(
|
|
14844
|
-
const log = this.logger.withTags({
|
|
14845
|
-
deviceId,
|
|
14846
|
-
brokerId,
|
|
14847
|
-
camStreamId: cam.camStreamId
|
|
14848
|
-
});
|
|
14849
|
-
const localStreamUrl = broker.getLocalStreamUrl();
|
|
14850
|
-
const codec = "h264";
|
|
14851
|
-
const label = cam.label ?? brokerId;
|
|
14852
|
-
for (const restreamer of this.getLiveRestreamers()) try {
|
|
14853
|
-
await restreamer.registerDevice(deviceId, [{
|
|
14854
|
-
streamId: brokerId,
|
|
14855
|
-
label,
|
|
14856
|
-
codec,
|
|
14857
|
-
type: "video",
|
|
14858
|
-
sourceUrl: localStreamUrl
|
|
14859
|
-
}]);
|
|
14860
|
-
} catch (err) {
|
|
14861
|
-
log.error("Failed to register with restreamer", { meta: {
|
|
14862
|
-
restreamerId: restreamer.id,
|
|
14863
|
-
error: errMsg(err)
|
|
14864
|
-
} });
|
|
14865
|
-
}
|
|
15422
|
+
async fanOutRegistration(_deviceId, brokerId, _cam, broker) {
|
|
14866
15423
|
if (this.webrtcServer) this.webrtcServer.registerBroker(brokerId, broker);
|
|
14867
15424
|
}
|
|
14868
|
-
async fanOutUnregistration(
|
|
14869
|
-
const log = this.logger.withTags({
|
|
14870
|
-
deviceId,
|
|
14871
|
-
brokerId
|
|
14872
|
-
});
|
|
14873
|
-
for (const restreamer of this.getLiveRestreamers()) try {
|
|
14874
|
-
await restreamer.unregisterDevice(deviceId);
|
|
14875
|
-
} catch (err) {
|
|
14876
|
-
log.error("Failed to unregister from restreamer", { meta: {
|
|
14877
|
-
restreamerId: restreamer.id,
|
|
14878
|
-
error: errMsg(err)
|
|
14879
|
-
} });
|
|
14880
|
-
}
|
|
15425
|
+
async fanOutUnregistration(_deviceId, brokerId) {
|
|
14881
15426
|
if (this.webrtcServer) this.webrtcServer.unregisterBroker(brokerId);
|
|
14882
15427
|
}
|
|
14883
15428
|
brokerStatusToSlotStatus(status) {
|
|
@@ -16299,6 +16844,15 @@ var BrokerWebrtcServer = class {
|
|
|
16299
16844
|
* returns null when no restream is dialed → the feed falls back to stdin.
|
|
16300
16845
|
*/
|
|
16301
16846
|
sourceRestreamUrlResolver = null;
|
|
16847
|
+
/**
|
|
16848
|
+
* Hook the manager injects to ensure the wanted tier's ADAPTIVE broker is
|
|
16849
|
+
* registered locally before `resolveBrokerId` runs — the manager wires it
|
|
16850
|
+
* to `ensureRemoteAdaptiveTierBroker` so a remote node's broker gets a
|
|
16851
|
+
* local registration in time for the resolve. No-ops on local-owned
|
|
16852
|
+
* devices (the manager's own gate), so an unset hook or a local-owner call
|
|
16853
|
+
* leaves adaptive resolution byte-identical to before this hook existed.
|
|
16854
|
+
*/
|
|
16855
|
+
ensureAdaptiveTierBroker = null;
|
|
16302
16856
|
/** sessionId → session entry. */
|
|
16303
16857
|
sessions = /* @__PURE__ */ new Map();
|
|
16304
16858
|
/**
|
|
@@ -16342,7 +16896,11 @@ var BrokerWebrtcServer = class {
|
|
|
16342
16896
|
}
|
|
16343
16897
|
registerBroker(brokerId, broker) {
|
|
16344
16898
|
this.brokers.set(brokerId, broker);
|
|
16345
|
-
|
|
16899
|
+
const did = deviceIdFromBrokerId(brokerId);
|
|
16900
|
+
this.logger.info("WebRTC broker registered", {
|
|
16901
|
+
...did !== -1 ? { tags: { deviceId: did } } : {},
|
|
16902
|
+
meta: { brokerId }
|
|
16903
|
+
});
|
|
16346
16904
|
}
|
|
16347
16905
|
/**
|
|
16348
16906
|
* Provide a resolver that maps brokerId → current profile tier. The
|
|
@@ -16361,13 +16919,27 @@ var BrokerWebrtcServer = class {
|
|
|
16361
16919
|
setSourceRestreamUrlResolver(resolver) {
|
|
16362
16920
|
this.sourceRestreamUrlResolver = resolver;
|
|
16363
16921
|
}
|
|
16922
|
+
/**
|
|
16923
|
+
* Provide a hook that ensures the wanted tier's remote adaptive broker
|
|
16924
|
+
* exists BEFORE `resolveBrokerId` runs, for ADAPTIVE streams only. The
|
|
16925
|
+
* manager injects one wired to `ensureRemoteAdaptiveTierBroker` (Task 3).
|
|
16926
|
+
* Without it (or on a local-owner call, which the hook itself no-ops on)
|
|
16927
|
+
* adaptive resolution behaves exactly as before this hook existed.
|
|
16928
|
+
*/
|
|
16929
|
+
setEnsureAdaptiveTierBrokerHook(hook) {
|
|
16930
|
+
this.ensureAdaptiveTierBroker = hook;
|
|
16931
|
+
}
|
|
16364
16932
|
unregisterBroker(brokerId) {
|
|
16365
16933
|
this.brokers.delete(brokerId);
|
|
16366
16934
|
for (const [sid, entry] of this.sessions) if (entry.brokerId === brokerId) {
|
|
16367
16935
|
this.cleanupSession(sid);
|
|
16368
16936
|
entry.session.close().catch(() => {});
|
|
16369
16937
|
}
|
|
16370
|
-
|
|
16938
|
+
const unregisterDid = deviceIdFromBrokerId(brokerId);
|
|
16939
|
+
this.logger.info("WebRTC broker unregistered", {
|
|
16940
|
+
...unregisterDid !== -1 ? { tags: { deviceId: unregisterDid } } : {},
|
|
16941
|
+
meta: { brokerId }
|
|
16942
|
+
});
|
|
16371
16943
|
}
|
|
16372
16944
|
supportsStream(streamId) {
|
|
16373
16945
|
if (this.brokers.has(streamId)) return true;
|
|
@@ -16461,11 +17033,21 @@ var BrokerWebrtcServer = class {
|
|
|
16461
17033
|
*/
|
|
16462
17034
|
async setupSessionForBroker(streamId, hints, opts) {
|
|
16463
17035
|
if (this.stopped) throw new Error("Server stopped");
|
|
16464
|
-
const
|
|
17036
|
+
const isAdaptiveStream = streamId.endsWith("/adaptive");
|
|
17037
|
+
const intent = isAdaptiveStream && opts.sessionId !== void 0 ? this.adaptiveIntent.get(opts.sessionId) : void 0;
|
|
16465
17038
|
const resolveHints = intent !== void 0 ? {
|
|
16466
17039
|
...hints,
|
|
16467
17040
|
prefersTier: intent.prefersTier
|
|
16468
17041
|
} : hints;
|
|
17042
|
+
if (isAdaptiveStream) {
|
|
17043
|
+
const adaptiveDeviceId = deviceIdFromBrokerId(streamId);
|
|
17044
|
+
const desiredTier = resolveHints.prefersTier === "high" || resolveHints.prefersTier === "mid" || resolveHints.prefersTier === "low" ? resolveHints.prefersTier : "high";
|
|
17045
|
+
try {
|
|
17046
|
+
await this.ensureAdaptiveTierBroker?.(adaptiveDeviceId, desiredTier);
|
|
17047
|
+
} catch (err) {
|
|
17048
|
+
throw new Error(`Failed to ensure remote adaptive-tier broker for stream "${streamId}" (tier: ${desiredTier}): ${errMsg(err)}`);
|
|
17049
|
+
}
|
|
17050
|
+
}
|
|
16469
17051
|
const brokerId = this.resolveBrokerId(streamId, resolveHints);
|
|
16470
17052
|
const broker = this.brokers.get(brokerId);
|
|
16471
17053
|
if (!broker) throw new Error(`No broker for stream "${streamId}" (resolved: "${brokerId}")`);
|
|
@@ -16933,10 +17515,13 @@ var BrokerWebrtcServer = class {
|
|
|
16933
17515
|
const entry = this.sessions.get(sessionId);
|
|
16934
17516
|
if (!entry) return;
|
|
16935
17517
|
await entry.session.addIceCandidate(candidate).catch((err) => {
|
|
16936
|
-
this.logger.warn("addIceCandidate threw", {
|
|
16937
|
-
|
|
16938
|
-
|
|
16939
|
-
|
|
17518
|
+
this.logger.warn("addIceCandidate threw", {
|
|
17519
|
+
tags: { deviceId: deviceIdFromBrokerId(entry.brokerId) },
|
|
17520
|
+
meta: {
|
|
17521
|
+
sessionId,
|
|
17522
|
+
error: errMsg(err)
|
|
17523
|
+
}
|
|
17524
|
+
});
|
|
16940
17525
|
});
|
|
16941
17526
|
}
|
|
16942
17527
|
/** Trickle ICE — the server's gathered candidates for a session (poll). */
|
|
@@ -16953,10 +17538,13 @@ var BrokerWebrtcServer = class {
|
|
|
16953
17538
|
if (!entry) return;
|
|
16954
17539
|
this.cleanupSession(sessionId);
|
|
16955
17540
|
await entry.session.close().catch(() => {});
|
|
16956
|
-
this.logger.info("WebRTC session closed", {
|
|
16957
|
-
|
|
16958
|
-
|
|
16959
|
-
|
|
17541
|
+
this.logger.info("WebRTC session closed", {
|
|
17542
|
+
tags: { deviceId: deviceIdFromBrokerId(entry.brokerId) },
|
|
17543
|
+
meta: {
|
|
17544
|
+
sessionId,
|
|
17545
|
+
deviceId: deviceIdFromBrokerId(entry.brokerId)
|
|
17546
|
+
}
|
|
17547
|
+
});
|
|
16960
17548
|
}
|
|
16961
17549
|
async stop() {
|
|
16962
17550
|
if (this.stopped) return;
|
|
@@ -17057,13 +17645,16 @@ var BrokerWebrtcServer = class {
|
|
|
17057
17645
|
target: { kind: "adaptive" },
|
|
17058
17646
|
epoch
|
|
17059
17647
|
};
|
|
17060
|
-
this.logger.info("adaptive: scheduling renegotiation", {
|
|
17061
|
-
|
|
17062
|
-
|
|
17063
|
-
|
|
17064
|
-
|
|
17065
|
-
|
|
17066
|
-
|
|
17648
|
+
this.logger.info("adaptive: scheduling renegotiation", {
|
|
17649
|
+
tags: { deviceId },
|
|
17650
|
+
meta: {
|
|
17651
|
+
sessionId,
|
|
17652
|
+
deviceId,
|
|
17653
|
+
tier,
|
|
17654
|
+
forceTranscodeDown,
|
|
17655
|
+
epoch
|
|
17656
|
+
}
|
|
17657
|
+
});
|
|
17067
17658
|
}
|
|
17068
17659
|
cleanupSession(sessionId) {
|
|
17069
17660
|
const entry = this.sessions.get(sessionId);
|
|
@@ -17128,13 +17719,17 @@ var BrokerWebrtcServer = class {
|
|
|
17128
17719
|
bestScore = score;
|
|
17129
17720
|
}
|
|
17130
17721
|
}
|
|
17131
|
-
|
|
17132
|
-
|
|
17133
|
-
|
|
17134
|
-
|
|
17135
|
-
|
|
17136
|
-
|
|
17137
|
-
|
|
17722
|
+
const adaptiveDeviceId = Number(deviceKey.split("/")[0]);
|
|
17723
|
+
this.logger.debug("Adaptive source selected", {
|
|
17724
|
+
...Number.isFinite(adaptiveDeviceId) ? { tags: { deviceId: adaptiveDeviceId } } : {},
|
|
17725
|
+
meta: {
|
|
17726
|
+
deviceKey,
|
|
17727
|
+
selected: bestId,
|
|
17728
|
+
tier: bestTier,
|
|
17729
|
+
score: Math.round(bestScore * 100) / 100,
|
|
17730
|
+
hints
|
|
17731
|
+
}
|
|
17732
|
+
});
|
|
17138
17733
|
return bestId;
|
|
17139
17734
|
}
|
|
17140
17735
|
async resolveIceServers() {
|
|
@@ -17297,10 +17892,13 @@ var CameraStreamsProvider = class {
|
|
|
17297
17892
|
async pickStream(input) {
|
|
17298
17893
|
const streams = await this.manager.getCameraStreamsForDevice(input.deviceId);
|
|
17299
17894
|
if (streams.length === 0) {
|
|
17300
|
-
this.manager.getLogger?.()?.info("camera-streams: pickStream — no streams for device", {
|
|
17301
|
-
deviceId: input.deviceId,
|
|
17302
|
-
|
|
17303
|
-
|
|
17895
|
+
this.manager.getLogger?.()?.info("camera-streams: pickStream — no streams for device", {
|
|
17896
|
+
tags: { deviceId: input.deviceId },
|
|
17897
|
+
meta: {
|
|
17898
|
+
deviceId: input.deviceId,
|
|
17899
|
+
requirements: input.requirements
|
|
17900
|
+
}
|
|
17901
|
+
});
|
|
17304
17902
|
return null;
|
|
17305
17903
|
}
|
|
17306
17904
|
const requirements = input.requirements;
|
|
@@ -17317,15 +17915,18 @@ var CameraStreamsProvider = class {
|
|
|
17317
17915
|
return true;
|
|
17318
17916
|
});
|
|
17319
17917
|
if (candidates.length === 0) {
|
|
17320
|
-
this.manager.getLogger?.()?.info("camera-streams: pickStream — no matching candidates", {
|
|
17321
|
-
deviceId: input.deviceId,
|
|
17322
|
-
|
|
17323
|
-
|
|
17324
|
-
|
|
17325
|
-
|
|
17326
|
-
|
|
17327
|
-
|
|
17328
|
-
|
|
17918
|
+
this.manager.getLogger?.()?.info("camera-streams: pickStream — no matching candidates", {
|
|
17919
|
+
tags: { deviceId: input.deviceId },
|
|
17920
|
+
meta: {
|
|
17921
|
+
deviceId: input.deviceId,
|
|
17922
|
+
requirements: input.requirements,
|
|
17923
|
+
streams: streams.map((s) => ({
|
|
17924
|
+
camStreamId: s.camStreamId,
|
|
17925
|
+
codec: s.codec,
|
|
17926
|
+
resolution: s.resolution
|
|
17927
|
+
}))
|
|
17928
|
+
}
|
|
17929
|
+
});
|
|
17329
17930
|
return null;
|
|
17330
17931
|
}
|
|
17331
17932
|
const requireSibling = requirements.requireSiblingCodec ?? [];
|
|
@@ -17334,14 +17935,17 @@ var CameraStreamsProvider = class {
|
|
|
17334
17935
|
if (s.camStreamId.startsWith("derived:")) return false;
|
|
17335
17936
|
return codecMatches(s.codec, requireSibling);
|
|
17336
17937
|
})) {
|
|
17337
|
-
this.manager.getLogger?.()?.info("camera-streams: pickStream — no sibling codec, skipping bypass", {
|
|
17338
|
-
deviceId: input.deviceId,
|
|
17339
|
-
|
|
17340
|
-
|
|
17341
|
-
|
|
17342
|
-
|
|
17343
|
-
|
|
17344
|
-
|
|
17938
|
+
this.manager.getLogger?.()?.info("camera-streams: pickStream — no sibling codec, skipping bypass", {
|
|
17939
|
+
tags: { deviceId: input.deviceId },
|
|
17940
|
+
meta: {
|
|
17941
|
+
deviceId: input.deviceId,
|
|
17942
|
+
requireSibling,
|
|
17943
|
+
streams: streams.map((s) => ({
|
|
17944
|
+
camStreamId: s.camStreamId,
|
|
17945
|
+
codec: s.codec
|
|
17946
|
+
}))
|
|
17947
|
+
}
|
|
17948
|
+
});
|
|
17345
17949
|
return null;
|
|
17346
17950
|
}
|
|
17347
17951
|
}
|
|
@@ -17443,6 +18047,34 @@ var WebrtcSessionProvider = class WebrtcSessionProvider {
|
|
|
17443
18047
|
if (camStreamId === void 0) return WEBRTC_FALLBACK_PROFILE;
|
|
17444
18048
|
return withSourceResolution(WEBRTC_FALLBACK_PROFILE, this.manager.getCameraStreamProbedParams(deviceId, camStreamId));
|
|
17445
18049
|
}
|
|
18050
|
+
/**
|
|
18051
|
+
* Ensure a broker is registered for `brokerId` before delegating to the
|
|
18052
|
+
* WebRTC server (Slice 3 Task 3 — cross-node ingest wiring). Defers to
|
|
18053
|
+
* `StreamBrokerManager.ensureBrokerForWebrtcSession`, guarded the same way
|
|
18054
|
+
* `resolveSessionDebug` guards `manager.isStreamingDebug`/`isWebrtcDebug` —
|
|
18055
|
+
* older/test manager doubles that don't implement the hook simply skip it,
|
|
18056
|
+
* leaving today's local-only resolution completely unchanged.
|
|
18057
|
+
*/
|
|
18058
|
+
async ensureBrokerAvailable(deviceId, brokerId, camStreamId) {
|
|
18059
|
+
if (typeof this.manager.ensureBrokerForWebrtcSession !== "function") return;
|
|
18060
|
+
await this.manager.ensureBrokerForWebrtcSession(deviceId, brokerId, camStreamId);
|
|
18061
|
+
}
|
|
18062
|
+
/**
|
|
18063
|
+
* Best-effort cleanup for the remote-encoded adapter `ensureBrokerAvailable`
|
|
18064
|
+
* may have just registered, run when the `BrokerWebrtcServer` delegate that
|
|
18065
|
+
* follows it throws. Without this, a delegate failure AFTER a successful
|
|
18066
|
+
* remote-adapter registration (session-construction/offer error, server
|
|
18067
|
+
* stopping mid-request, …) leaks the adapter forever — its only other
|
|
18068
|
+
* teardown path is the inner broker's `onDemandChange(false)` edge, which
|
|
18069
|
+
* never fires because no session ever reached the `true` (demand-active)
|
|
18070
|
+
* edge. Guarded the same way `ensureBrokerAvailable` guards the manager
|
|
18071
|
+
* hook itself — older/test manager doubles without the method are a no-op,
|
|
18072
|
+
* so the owner==self / local-broker happy path is byte-identical.
|
|
18073
|
+
*/
|
|
18074
|
+
async disposeRemoteBrokerAdapterOnDelegateFailure(brokerId) {
|
|
18075
|
+
if (typeof this.manager.disposeRemoteBrokerAdapterIfPresent !== "function") return;
|
|
18076
|
+
await this.manager.disposeRemoteBrokerAdapterIfPresent(brokerId).catch(() => {});
|
|
18077
|
+
}
|
|
17446
18078
|
async listStreams(input) {
|
|
17447
18079
|
const { deviceId } = input;
|
|
17448
18080
|
const assignedSlots = this.manager.getProfileSlotsForDevice(deviceId).filter((s) => s.sourceCamStreamId !== null);
|
|
@@ -17504,32 +18136,44 @@ var WebrtcSessionProvider = class WebrtcSessionProvider {
|
|
|
17504
18136
|
}
|
|
17505
18137
|
async createSession(input) {
|
|
17506
18138
|
const streamId = this.resolveTargetToStreamId(input.deviceId, input.target);
|
|
17507
|
-
const streamingDebug = this.resolveSessionDebug(input.deviceId);
|
|
17508
18139
|
const camStreamId = this.resolveCamStreamId(input.deviceId, input.target);
|
|
18140
|
+
await this.ensureBrokerAvailable(input.deviceId, streamId, camStreamId);
|
|
18141
|
+
const streamingDebug = this.resolveSessionDebug(input.deviceId);
|
|
17509
18142
|
const transcodeProfile = this.resolveTranscodeProfile(input.deviceId, camStreamId);
|
|
17510
|
-
|
|
17511
|
-
|
|
17512
|
-
|
|
17513
|
-
|
|
17514
|
-
|
|
17515
|
-
|
|
18143
|
+
try {
|
|
18144
|
+
return await this.webrtcServer.createSession(streamId, input.hints, {
|
|
18145
|
+
streamingDebug,
|
|
18146
|
+
relayOnly: input.relayOnly === true,
|
|
18147
|
+
transcodeProfile,
|
|
18148
|
+
...input.consumerAttribution !== void 0 ? { consumerAttribution: input.consumerAttribution } : {}
|
|
18149
|
+
});
|
|
18150
|
+
} catch (err) {
|
|
18151
|
+
await this.disposeRemoteBrokerAdapterOnDelegateFailure(streamId);
|
|
18152
|
+
throw err;
|
|
18153
|
+
}
|
|
17516
18154
|
}
|
|
17517
18155
|
async handleOffer(input) {
|
|
17518
18156
|
const target = input.target ?? { kind: "adaptive" };
|
|
17519
18157
|
const streamId = this.resolveTargetToStreamId(input.deviceId, target);
|
|
17520
|
-
const streamingDebug = this.resolveSessionDebug(input.deviceId);
|
|
17521
18158
|
const camStreamId = this.resolveCamStreamId(input.deviceId, target);
|
|
18159
|
+
await this.ensureBrokerAvailable(input.deviceId, streamId, camStreamId);
|
|
18160
|
+
const streamingDebug = this.resolveSessionDebug(input.deviceId);
|
|
17522
18161
|
const transcodeProfile = this.resolveTranscodeProfile(input.deviceId, camStreamId);
|
|
17523
|
-
|
|
17524
|
-
|
|
17525
|
-
|
|
17526
|
-
|
|
17527
|
-
|
|
17528
|
-
|
|
17529
|
-
|
|
17530
|
-
|
|
17531
|
-
|
|
17532
|
-
|
|
18162
|
+
try {
|
|
18163
|
+
return await this.webrtcServer.handleOffer(streamId, input.sdpOffer, void 0, {
|
|
18164
|
+
streamingDebug,
|
|
18165
|
+
sessionId: input.sessionId,
|
|
18166
|
+
relayOnly: input.relayOnly === true,
|
|
18167
|
+
nonTrickle: input.nonTrickle === true,
|
|
18168
|
+
disableTurn: input.disableTurn === true,
|
|
18169
|
+
disableIpv6: input.disableIpv6 === true,
|
|
18170
|
+
transcodeProfile,
|
|
18171
|
+
...input.consumerAttribution !== void 0 ? { consumerAttribution: input.consumerAttribution } : {}
|
|
18172
|
+
});
|
|
18173
|
+
} catch (err) {
|
|
18174
|
+
await this.disposeRemoteBrokerAdapterOnDelegateFailure(streamId);
|
|
18175
|
+
throw err;
|
|
18176
|
+
}
|
|
17533
18177
|
}
|
|
17534
18178
|
async handleAnswer(input) {
|
|
17535
18179
|
await this.webrtcServer.handleAnswer(input.sessionId, input.sdpAnswer);
|
|
@@ -17753,7 +18397,6 @@ var StreamBrokerAddon = class extends BaseAddon {
|
|
|
17753
18397
|
const localNode = this.ctx.kernel.localNodeId;
|
|
17754
18398
|
const localNodeId = localNode ? localNode.includes("/") ? localNode.split("/")[0] : localNode : void 0;
|
|
17755
18399
|
this.brokerManager.setApiAccess(this.ctx.api, localNodeId, this.ctx.kernel.hwaccel);
|
|
17756
|
-
if (this.capabilities) this.brokerManager.setCapabilitiesAccess(this.capabilities);
|
|
17757
18400
|
await this.loadFfmpegConfig();
|
|
17758
18401
|
const rtspProvider = this.brokerManager.getRtspRestreamProvider();
|
|
17759
18402
|
const brokerManager = this.brokerManager;
|