@camstack/addon-pipeline 0.1.10 → 0.1.12
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/assets/icon.svg +6 -0
- package/dist/audio-analyzer/index.js +16 -16
- package/dist/audio-analyzer/index.mjs +1 -1
- package/dist/audio-codec-nodeav/index.js +7 -7
- package/dist/audio-codec-nodeav/index.mjs +1 -1
- package/dist/decoder-nodeav/index.js +12 -12
- package/dist/decoder-nodeav/index.mjs +1 -1
- package/dist/detection-pipeline/index.js +45 -45
- package/dist/detection-pipeline/index.js.map +1 -1
- package/dist/detection-pipeline/index.mjs +1 -1
- package/dist/index-BhOycEVH.js +13867 -0
- package/dist/index-BhOycEVH.js.map +1 -0
- package/dist/index-FxfFGsiL.mjs +13868 -0
- package/dist/index-FxfFGsiL.mjs.map +1 -0
- package/dist/motion-wasm/index.js +8 -8
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +76 -77
- package/dist/pipeline-runner/index.js.map +1 -1
- package/dist/pipeline-runner/index.mjs +50 -51
- package/dist/pipeline-runner/index.mjs.map +1 -1
- package/dist/stream-broker/@mf-types.zip +0 -0
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-ChoHjdk6.mjs +17 -0
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-BvJPhiY_.mjs +20 -0
- package/dist/stream-broker/_stub.js +1 -1
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BTj1RQPs.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Czwg8GUO.mjs} +6 -6
- package/dist/stream-broker/{hostInit-wnZIaWA5.mjs → hostInit-fG6oFw4t.mjs} +6 -6
- package/dist/stream-broker/{index-DadYrR5H.mjs → index-BOmtakNy.mjs} +67 -44
- package/dist/stream-broker/{index-BQ_NNiX0.mjs → index-Bpv0NSqI.mjs} +5414 -5170
- package/dist/stream-broker/{index-Dwc0KrUN.mjs → index-l13fl8lu.mjs} +4249 -4164
- package/dist/stream-broker/index.js +521 -110
- package/dist/stream-broker/index.js.map +1 -1
- package/dist/stream-broker/index.mjs +426 -15
- package/dist/stream-broker/index.mjs.map +1 -1
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/package.json +24 -9
- package/python/postprocessors/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/arcface.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/ctc.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/saliency.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/scrfd.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/softmax.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/yamnet.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo.cpython-312.pyc +0 -0
- package/python/postprocessors/__pycache__/yolo_seg.cpython-312.pyc +0 -0
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-ezH__dL2.mjs +0 -17
- package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-ocGWYEqu.mjs +0 -20
|
@@ -22,13 +22,13 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
22
22
|
mod
|
|
23
23
|
));
|
|
24
24
|
Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
|
|
25
|
-
const
|
|
25
|
+
const index = require("../index-BhOycEVH.js");
|
|
26
26
|
const net = require("node:net");
|
|
27
27
|
const crypto = require("node:crypto");
|
|
28
28
|
const net$1 = require("net");
|
|
29
29
|
const events = require("events");
|
|
30
|
-
const promises = require("node:dns/promises");
|
|
31
30
|
const node_child_process = require("node:child_process");
|
|
31
|
+
const promises = require("node:dns/promises");
|
|
32
32
|
const fs = require("node:fs");
|
|
33
33
|
const path = require("node:path");
|
|
34
34
|
const os = require("node:os");
|
|
@@ -201,7 +201,7 @@ const TARGET_CHUNK_MS = 500;
|
|
|
201
201
|
const F32_BYTES = 4;
|
|
202
202
|
const SESSION_NOT_FOUND_RE = /decode session\b.*\bnot found\b/;
|
|
203
203
|
function isSessionGone(err) {
|
|
204
|
-
return SESSION_NOT_FOUND_RE.test(
|
|
204
|
+
return SESSION_NOT_FOUND_RE.test(index.errMsg(err));
|
|
205
205
|
}
|
|
206
206
|
class AudioCodecSession {
|
|
207
207
|
constructor(api, cfg, emit, logger) {
|
|
@@ -303,7 +303,7 @@ class AudioCodecSession {
|
|
|
303
303
|
await this.api.closeSession({ sessionId: sid, ...nodeId ? { nodeId } : {} });
|
|
304
304
|
} catch (err) {
|
|
305
305
|
this.logger?.warn("audio-codec: closeSession failed", {
|
|
306
|
-
meta: { error:
|
|
306
|
+
meta: { error: index.errMsg(err) }
|
|
307
307
|
});
|
|
308
308
|
}
|
|
309
309
|
this.sessionId = null;
|
|
@@ -339,7 +339,7 @@ class AudioCodecSession {
|
|
|
339
339
|
return res.sessionId;
|
|
340
340
|
} catch (err) {
|
|
341
341
|
this.logger?.error("audio-codec: createDecodeSession failed", {
|
|
342
|
-
meta: { codec: this.cfg.codec, error:
|
|
342
|
+
meta: { codec: this.cfg.codec, error: index.errMsg(err) }
|
|
343
343
|
});
|
|
344
344
|
return null;
|
|
345
345
|
} finally {
|
|
@@ -373,7 +373,7 @@ class AudioCodecSession {
|
|
|
373
373
|
this.handleSessionGone(sid);
|
|
374
374
|
return;
|
|
375
375
|
}
|
|
376
|
-
this.warnThrottled("audio-codec: pullPcm failed",
|
|
376
|
+
this.warnThrottled("audio-codec: pullPcm failed", index.errMsg(err));
|
|
377
377
|
}
|
|
378
378
|
}
|
|
379
379
|
// ── Sample accumulator ──────────────────────────────────────────────────
|
|
@@ -450,7 +450,7 @@ class AudioCodecSession {
|
|
|
450
450
|
this.handleSessionGone(sid);
|
|
451
451
|
return;
|
|
452
452
|
}
|
|
453
|
-
this.warnThrottled(label,
|
|
453
|
+
this.warnThrottled(label, index.errMsg(err));
|
|
454
454
|
}
|
|
455
455
|
/**
|
|
456
456
|
* Logs at most once per WARN_THROTTLE_MS per (label,detail) pair and
|
|
@@ -2212,8 +2212,8 @@ const AAC_SAMPLE_RATES = [
|
|
|
2212
2212
|
8e3,
|
|
2213
2213
|
7350
|
|
2214
2214
|
];
|
|
2215
|
-
function aacSamplingIndexToHz(
|
|
2216
|
-
return AAC_SAMPLE_RATES[
|
|
2215
|
+
function aacSamplingIndexToHz(index2) {
|
|
2216
|
+
return AAC_SAMPLE_RATES[index2] ?? null;
|
|
2217
2217
|
}
|
|
2218
2218
|
class StreamEndError extends Error {
|
|
2219
2219
|
constructor(where) {
|
|
@@ -3430,7 +3430,7 @@ class StreamBroker {
|
|
|
3430
3430
|
*/
|
|
3431
3431
|
sourceMetaForLog() {
|
|
3432
3432
|
const src = this.source;
|
|
3433
|
-
const url = src && src.type !== "placeholder" && src.type !== "push" && src.type !== "push-rtp" ?
|
|
3433
|
+
const url = src && src.type !== "placeholder" && src.type !== "push" && src.type !== "push-rtp" ? index.maskUrlCredentials(src.url ?? "") : "";
|
|
3434
3434
|
return {
|
|
3435
3435
|
brokerId: this.deviceId,
|
|
3436
3436
|
sourceType: src?.type ?? "unknown",
|
|
@@ -3524,7 +3524,7 @@ class StreamBroker {
|
|
|
3524
3524
|
this.logger?.info("Broker starting", {
|
|
3525
3525
|
meta: {
|
|
3526
3526
|
sourceType: source.type,
|
|
3527
|
-
...source.type !== "placeholder" ? { url:
|
|
3527
|
+
...source.type !== "placeholder" ? { url: index.maskUrlCredentials(source.url) } : {},
|
|
3528
3528
|
...this.detectedCodec ? { codec: this.detectedCodec } : {}
|
|
3529
3529
|
}
|
|
3530
3530
|
});
|
|
@@ -3671,7 +3671,7 @@ class StreamBroker {
|
|
|
3671
3671
|
});
|
|
3672
3672
|
this.decoderOutputFormat = needed;
|
|
3673
3673
|
this.decoderProxy.updateConfig({ outputFormat: needed }).catch((err) => {
|
|
3674
|
-
this.logger?.warn("decoder format upgrade failed", { meta: { error:
|
|
3674
|
+
this.logger?.warn("decoder format upgrade failed", { meta: { error: index.errMsg(err) } });
|
|
3675
3675
|
});
|
|
3676
3676
|
}
|
|
3677
3677
|
/**
|
|
@@ -3723,7 +3723,7 @@ class StreamBroker {
|
|
|
3723
3723
|
}).catch((err) => {
|
|
3724
3724
|
subscriber.framesDropped++;
|
|
3725
3725
|
this.logger?.warn("frame conversion failed", {
|
|
3726
|
-
meta: { tag: subscriber.tag, target: subscriber.format, error:
|
|
3726
|
+
meta: { tag: subscriber.tag, target: subscriber.format, error: index.errMsg(err) }
|
|
3727
3727
|
});
|
|
3728
3728
|
});
|
|
3729
3729
|
return;
|
|
@@ -3737,7 +3737,7 @@ class StreamBroker {
|
|
|
3737
3737
|
}).catch((err) => {
|
|
3738
3738
|
subscriber.framesDropped++;
|
|
3739
3739
|
this.logger?.warn("frame conversion failed", {
|
|
3740
|
-
meta: { tag: subscriber.tag, target: subscriber.format, error:
|
|
3740
|
+
meta: { tag: subscriber.tag, target: subscriber.format, error: index.errMsg(err) }
|
|
3741
3741
|
});
|
|
3742
3742
|
});
|
|
3743
3743
|
}
|
|
@@ -3793,7 +3793,7 @@ class StreamBroker {
|
|
|
3793
3793
|
this._suspended = false;
|
|
3794
3794
|
this.logger?.info("demand detected — resuming stream");
|
|
3795
3795
|
this.start(fresh).catch((err) => {
|
|
3796
|
-
this.logger?.error("resume failed", { meta: { error:
|
|
3796
|
+
this.logger?.error("resume failed", { meta: { error: index.errMsg(err) } });
|
|
3797
3797
|
this._suspended = true;
|
|
3798
3798
|
this._status = "idle";
|
|
3799
3799
|
});
|
|
@@ -3854,7 +3854,7 @@ class StreamBroker {
|
|
|
3854
3854
|
this.audioCodecSession = null;
|
|
3855
3855
|
void session.close().catch((err) => {
|
|
3856
3856
|
this.logger?.warn("audio-codec: push session close failed", {
|
|
3857
|
-
meta: { error:
|
|
3857
|
+
meta: { error: index.errMsg(err) }
|
|
3858
3858
|
});
|
|
3859
3859
|
});
|
|
3860
3860
|
}
|
|
@@ -3893,7 +3893,7 @@ class StreamBroker {
|
|
|
3893
3893
|
this.audioCodecSession = null;
|
|
3894
3894
|
void prior.close().catch((err) => {
|
|
3895
3895
|
this.logger?.debug("audio-codec: prior session close failed", {
|
|
3896
|
-
meta: { error:
|
|
3896
|
+
meta: { error: index.errMsg(err) }
|
|
3897
3897
|
});
|
|
3898
3898
|
});
|
|
3899
3899
|
}
|
|
@@ -3989,7 +3989,7 @@ class StreamBroker {
|
|
|
3989
3989
|
});
|
|
3990
3990
|
}
|
|
3991
3991
|
this.decoderProxy.pushPacket(packet).catch((err) => {
|
|
3992
|
-
this.logger?.warn("decoder push error", { meta: { error:
|
|
3992
|
+
this.logger?.warn("decoder push error", { meta: { error: index.errMsg(err) } });
|
|
3993
3993
|
});
|
|
3994
3994
|
}
|
|
3995
3995
|
}
|
|
@@ -4073,7 +4073,7 @@ class StreamBroker {
|
|
|
4073
4073
|
callback(this.sdpParameterSets);
|
|
4074
4074
|
} catch (err) {
|
|
4075
4075
|
this.logger?.warn("sdp param-set subscriber threw on initial delivery", {
|
|
4076
|
-
meta: { error:
|
|
4076
|
+
meta: { error: index.errMsg(err) }
|
|
4077
4077
|
});
|
|
4078
4078
|
}
|
|
4079
4079
|
}
|
|
@@ -4090,7 +4090,7 @@ class StreamBroker {
|
|
|
4090
4090
|
cb(ps);
|
|
4091
4091
|
} catch (err) {
|
|
4092
4092
|
this.logger?.warn("sdp param-set subscriber threw", {
|
|
4093
|
-
meta: { error:
|
|
4093
|
+
meta: { error: index.errMsg(err) }
|
|
4094
4094
|
});
|
|
4095
4095
|
}
|
|
4096
4096
|
}
|
|
@@ -4154,7 +4154,7 @@ class StreamBroker {
|
|
|
4154
4154
|
cb(rtpData);
|
|
4155
4155
|
} catch (err) {
|
|
4156
4156
|
this.logger?.warn("rtp video subscriber threw", {
|
|
4157
|
-
meta: { error:
|
|
4157
|
+
meta: { error: index.errMsg(err) }
|
|
4158
4158
|
});
|
|
4159
4159
|
}
|
|
4160
4160
|
}
|
|
@@ -4362,7 +4362,7 @@ class StreamBroker {
|
|
|
4362
4362
|
* backoff used by the RTSP path.
|
|
4363
4363
|
*/
|
|
4364
4364
|
startRtmpReader(source) {
|
|
4365
|
-
this.logger?.info("starting RTMP reader", { meta: { url:
|
|
4365
|
+
this.logger?.info("starting RTMP reader", { meta: { url: index.maskUrlCredentials(source.url) } });
|
|
4366
4366
|
this.destroyRtmpReader();
|
|
4367
4367
|
const reader = new RtmpReader(source.url, this.logger?.child("rtmp"), {
|
|
4368
4368
|
onVideoCodec: (codec) => {
|
|
@@ -4398,7 +4398,7 @@ class StreamBroker {
|
|
|
4398
4398
|
reader.connect().catch((err) => {
|
|
4399
4399
|
if (this.manualStop || this._suspended) return;
|
|
4400
4400
|
this.logger?.warn("rtmp connect failed", {
|
|
4401
|
-
meta: { error:
|
|
4401
|
+
meta: { error: index.errMsg(err), ...this.sourceMetaForLog() }
|
|
4402
4402
|
});
|
|
4403
4403
|
this.destroyRtmpReader();
|
|
4404
4404
|
this._status = "error";
|
|
@@ -4489,7 +4489,7 @@ class StreamBroker {
|
|
|
4489
4489
|
cb(rtpData);
|
|
4490
4490
|
} catch (err) {
|
|
4491
4491
|
this.logger?.warn("rtp video subscriber threw", {
|
|
4492
|
-
meta: { error:
|
|
4492
|
+
meta: { error: index.errMsg(err) }
|
|
4493
4493
|
});
|
|
4494
4494
|
}
|
|
4495
4495
|
}
|
|
@@ -4535,7 +4535,7 @@ class StreamBroker {
|
|
|
4535
4535
|
this.audioCodecSession = null;
|
|
4536
4536
|
void prior.close().catch((err) => {
|
|
4537
4537
|
this.logger?.debug("audio-codec: prior RTSP session close failed", {
|
|
4538
|
-
meta: { error:
|
|
4538
|
+
meta: { error: index.errMsg(err) }
|
|
4539
4539
|
});
|
|
4540
4540
|
});
|
|
4541
4541
|
}
|
|
@@ -4643,7 +4643,7 @@ class StreamBroker {
|
|
|
4643
4643
|
client.connect().catch((err) => {
|
|
4644
4644
|
if (this.manualStop || this._suspended) return;
|
|
4645
4645
|
this.logger?.warn("native RTSP connect failed", {
|
|
4646
|
-
meta: { error:
|
|
4646
|
+
meta: { error: index.errMsg(err), ...this.sourceMetaForLog() }
|
|
4647
4647
|
});
|
|
4648
4648
|
this.destroyNativeClient();
|
|
4649
4649
|
this._status = "error";
|
|
@@ -4967,7 +4967,7 @@ class StreamBroker {
|
|
|
4967
4967
|
const codec = this.detectedCodec ?? this.source.videoCodec;
|
|
4968
4968
|
if (codec) {
|
|
4969
4969
|
this.createSharedDecoderSession(this.pendingDecodeOptions).catch((err) => {
|
|
4970
|
-
this.logger?.warn("decoder session rebuild after rotation failed", { meta: { error:
|
|
4970
|
+
this.logger?.warn("decoder session rebuild after rotation failed", { meta: { error: index.errMsg(err) } });
|
|
4971
4971
|
});
|
|
4972
4972
|
}
|
|
4973
4973
|
}
|
|
@@ -5066,7 +5066,7 @@ class StreamBroker {
|
|
|
5066
5066
|
reader.connect().catch((err) => {
|
|
5067
5067
|
if (this.manualStop || this._suspended) return;
|
|
5068
5068
|
this.logger?.warn("rfc4571 connect failed", {
|
|
5069
|
-
meta: { error:
|
|
5069
|
+
meta: { error: index.errMsg(err), ...this.sourceMetaForLog() }
|
|
5070
5070
|
});
|
|
5071
5071
|
this.destroyRfc4571Reader();
|
|
5072
5072
|
this._status = "error";
|
|
@@ -5097,7 +5097,7 @@ class StreamBroker {
|
|
|
5097
5097
|
this.audioCodecSession = null;
|
|
5098
5098
|
void session.close().catch((err) => {
|
|
5099
5099
|
this.logger?.warn("audio-codec: session close failed", {
|
|
5100
|
-
meta: { error:
|
|
5100
|
+
meta: { error: index.errMsg(err) }
|
|
5101
5101
|
});
|
|
5102
5102
|
});
|
|
5103
5103
|
}
|
|
@@ -5151,7 +5151,7 @@ class StreamBroker {
|
|
|
5151
5151
|
this.fanoutDecodedFrame(frame);
|
|
5152
5152
|
};
|
|
5153
5153
|
proxy.startPolling(onFrame).catch((err) => {
|
|
5154
|
-
this.logger?.warn("decoder polling error", { meta: { error:
|
|
5154
|
+
this.logger?.warn("decoder polling error", { meta: { error: index.errMsg(err) } });
|
|
5155
5155
|
});
|
|
5156
5156
|
const info = await this.decoderApi.getInfo();
|
|
5157
5157
|
if (info.isPullMode) {
|
|
@@ -5163,7 +5163,7 @@ class StreamBroker {
|
|
|
5163
5163
|
meta: { localUrl, codec }
|
|
5164
5164
|
});
|
|
5165
5165
|
proxy.openStream(inputUrl).catch((err) => {
|
|
5166
|
-
this.logger?.error("Pull mode decoder failed", { meta: { error:
|
|
5166
|
+
this.logger?.error("Pull mode decoder failed", { meta: { error: index.errMsg(err) } });
|
|
5167
5167
|
});
|
|
5168
5168
|
};
|
|
5169
5169
|
if (this.firstKeyframeReceived) {
|
|
@@ -5280,7 +5280,7 @@ class StreamBroker {
|
|
|
5280
5280
|
extraData = hexToBytes(configHex);
|
|
5281
5281
|
} catch (err) {
|
|
5282
5282
|
this.logger?.warn("audio-codec: AudioSpecificConfig parse failed", {
|
|
5283
|
-
meta: { configHex, error:
|
|
5283
|
+
meta: { configHex, error: index.errMsg(err) }
|
|
5284
5284
|
});
|
|
5285
5285
|
}
|
|
5286
5286
|
}
|
|
@@ -5816,9 +5816,308 @@ class RtspRestreamProviderImpl {
|
|
|
5816
5816
|
}
|
|
5817
5817
|
}
|
|
5818
5818
|
}
|
|
5819
|
+
function normaliseCodec(codec) {
|
|
5820
|
+
if (!codec) return null;
|
|
5821
|
+
const c = codec.toLowerCase();
|
|
5822
|
+
if (c === "h264" || c === "avc" || c === "avc1") return "H264";
|
|
5823
|
+
if (c === "h265" || c === "hevc" || c === "hvc1" || c === "hev1") return "H265";
|
|
5824
|
+
return null;
|
|
5825
|
+
}
|
|
5826
|
+
function pipelineKeyFor(deviceId, videoCodec, audioCodec, width, height) {
|
|
5827
|
+
return `${deviceId}|v=${videoCodec}|a=${audioCodec}|${width}x${height}`;
|
|
5828
|
+
}
|
|
5829
|
+
function pickVideoEncoder(target, encoders) {
|
|
5830
|
+
if (!encoders) return target === "H264" ? "libx264" : "libx265";
|
|
5831
|
+
return target === "H264" ? encoders.defaultH264 : encoders.defaultH265;
|
|
5832
|
+
}
|
|
5833
|
+
function pickAudioEncoderArgs(audio) {
|
|
5834
|
+
switch (audio) {
|
|
5835
|
+
case "AAC":
|
|
5836
|
+
return ["-c:a", "aac", "-b:a", "128k", "-ar", "48000", "-ac", "2"];
|
|
5837
|
+
case "Opus":
|
|
5838
|
+
return ["-c:a", "libopus", "-b:a", "64k", "-ar", "48000", "-ac", "2"];
|
|
5839
|
+
case "PCMU":
|
|
5840
|
+
return ["-c:a", "pcm_mulaw", "-ar", "8000", "-ac", "1"];
|
|
5841
|
+
case "none":
|
|
5842
|
+
return ["-an"];
|
|
5843
|
+
default:
|
|
5844
|
+
return ["-c:a", "aac", "-b:a", "128k"];
|
|
5845
|
+
}
|
|
5846
|
+
}
|
|
5847
|
+
class TranscodePipelineManager {
|
|
5848
|
+
entries = /* @__PURE__ */ new Map();
|
|
5849
|
+
logger;
|
|
5850
|
+
api;
|
|
5851
|
+
cameraStreamLookup;
|
|
5852
|
+
rtspEntryLookup;
|
|
5853
|
+
localRtspPort;
|
|
5854
|
+
releaseGraceMs;
|
|
5855
|
+
ffmpegPath;
|
|
5856
|
+
constructor(opts) {
|
|
5857
|
+
this.logger = opts.logger;
|
|
5858
|
+
this.api = opts.api;
|
|
5859
|
+
this.cameraStreamLookup = opts.cameraStreamLookup;
|
|
5860
|
+
this.rtspEntryLookup = opts.rtspEntryLookup;
|
|
5861
|
+
this.localRtspPort = opts.localRtspPort;
|
|
5862
|
+
this.releaseGraceMs = opts.releaseGraceMs ?? 5e3;
|
|
5863
|
+
this.ffmpegPath = opts.ffmpegPath ?? "ffmpeg";
|
|
5864
|
+
}
|
|
5865
|
+
async acquire(input) {
|
|
5866
|
+
const wantedVideo = input.videoCodec;
|
|
5867
|
+
const wantedAudio = input.audioCodec ?? "AAC";
|
|
5868
|
+
const direct = this.findDirectMatch(input.deviceId, wantedVideo, input.maxResolution);
|
|
5869
|
+
if (direct) {
|
|
5870
|
+
const key2 = pipelineKeyFor(
|
|
5871
|
+
input.deviceId,
|
|
5872
|
+
direct.videoCodec,
|
|
5873
|
+
wantedAudio,
|
|
5874
|
+
direct.resolution.width,
|
|
5875
|
+
direct.resolution.height
|
|
5876
|
+
);
|
|
5877
|
+
return this.shareOrCreate(key2, () => ({
|
|
5878
|
+
url: direct.url,
|
|
5879
|
+
videoCodec: direct.videoCodec,
|
|
5880
|
+
audioCodec: "passthrough",
|
|
5881
|
+
resolution: direct.resolution,
|
|
5882
|
+
transcoded: false,
|
|
5883
|
+
encoder: "copy",
|
|
5884
|
+
pipelineKey: key2
|
|
5885
|
+
}), null);
|
|
5886
|
+
}
|
|
5887
|
+
const sourceStream = this.pickTranscodeSource(input.deviceId, input.maxResolution);
|
|
5888
|
+
if (!sourceStream || !sourceStream.url) {
|
|
5889
|
+
throw new Error(
|
|
5890
|
+
`getStreamWithCodec: no source stream available for device ${input.deviceId}`
|
|
5891
|
+
);
|
|
5892
|
+
}
|
|
5893
|
+
const targetCodec = wantedVideo === "auto" ? normaliseCodec(sourceStream.codec) ?? "H264" : wantedVideo;
|
|
5894
|
+
const targetResolution = input.maxResolution ?? sourceStream.resolution ?? { width: 1280, height: 720 };
|
|
5895
|
+
const key = pipelineKeyFor(
|
|
5896
|
+
input.deviceId,
|
|
5897
|
+
targetCodec,
|
|
5898
|
+
wantedAudio,
|
|
5899
|
+
targetResolution.width,
|
|
5900
|
+
targetResolution.height
|
|
5901
|
+
);
|
|
5902
|
+
const existing = this.entries.get(key);
|
|
5903
|
+
if (existing) {
|
|
5904
|
+
return this.shareEntry(existing).source;
|
|
5905
|
+
}
|
|
5906
|
+
const encoders = await this.loadHardwareEncoders();
|
|
5907
|
+
const encoder = pickVideoEncoder(targetCodec, encoders);
|
|
5908
|
+
const dialableUrl = this.allocateLoopbackUrl(input.deviceId, key);
|
|
5909
|
+
const child = this.spawnFfmpeg({
|
|
5910
|
+
sourceUrl: sourceStream.url,
|
|
5911
|
+
outputUrl: dialableUrl,
|
|
5912
|
+
encoder,
|
|
5913
|
+
targetCodec,
|
|
5914
|
+
width: targetResolution.width,
|
|
5915
|
+
height: targetResolution.height,
|
|
5916
|
+
audio: wantedAudio,
|
|
5917
|
+
tag: input.tag ?? `stream-with-codec:${input.deviceId}`
|
|
5918
|
+
});
|
|
5919
|
+
const source = {
|
|
5920
|
+
url: dialableUrl,
|
|
5921
|
+
videoCodec: targetCodec,
|
|
5922
|
+
audioCodec: wantedAudio,
|
|
5923
|
+
resolution: targetResolution,
|
|
5924
|
+
transcoded: true,
|
|
5925
|
+
encoder,
|
|
5926
|
+
pipelineKey: key
|
|
5927
|
+
};
|
|
5928
|
+
const entry = { key, source, child, refcount: 1, releaseTimer: null };
|
|
5929
|
+
this.entries.set(key, entry);
|
|
5930
|
+
this.logger.info("Transcode pipeline started", {
|
|
5931
|
+
meta: { key, encoder, sourceUrl: sourceStream.url, outputUrl: dialableUrl }
|
|
5932
|
+
});
|
|
5933
|
+
if (child) {
|
|
5934
|
+
child.once("exit", (code, signal) => {
|
|
5935
|
+
this.logger.info("Transcode pipeline exited", {
|
|
5936
|
+
meta: { key, code, signal }
|
|
5937
|
+
});
|
|
5938
|
+
const cur = this.entries.get(key);
|
|
5939
|
+
if (cur && cur.child === child) {
|
|
5940
|
+
this.entries.delete(key);
|
|
5941
|
+
}
|
|
5942
|
+
});
|
|
5943
|
+
}
|
|
5944
|
+
return source;
|
|
5945
|
+
}
|
|
5946
|
+
release(pipelineKey) {
|
|
5947
|
+
const entry = this.entries.get(pipelineKey);
|
|
5948
|
+
if (!entry) return { released: false, refcount: 0 };
|
|
5949
|
+
entry.refcount = Math.max(0, entry.refcount - 1);
|
|
5950
|
+
if (entry.refcount > 0) {
|
|
5951
|
+
return { released: true, refcount: entry.refcount };
|
|
5952
|
+
}
|
|
5953
|
+
if (entry.releaseTimer) {
|
|
5954
|
+
clearTimeout(entry.releaseTimer);
|
|
5955
|
+
}
|
|
5956
|
+
entry.releaseTimer = setTimeout(() => {
|
|
5957
|
+
const cur = this.entries.get(pipelineKey);
|
|
5958
|
+
if (!cur || cur.refcount > 0) return;
|
|
5959
|
+
this.shutdownEntry(cur);
|
|
5960
|
+
}, this.releaseGraceMs);
|
|
5961
|
+
return { released: true, refcount: 0 };
|
|
5962
|
+
}
|
|
5963
|
+
shutdownAll() {
|
|
5964
|
+
for (const entry of this.entries.values()) {
|
|
5965
|
+
this.shutdownEntry(entry);
|
|
5966
|
+
}
|
|
5967
|
+
this.entries.clear();
|
|
5968
|
+
}
|
|
5969
|
+
findDirectMatch(deviceId, wanted, maxRes) {
|
|
5970
|
+
if (wanted === "auto") return null;
|
|
5971
|
+
const streams = this.cameraStreamLookup(deviceId);
|
|
5972
|
+
for (const s of streams) {
|
|
5973
|
+
if (!s.url) continue;
|
|
5974
|
+
const codec = normaliseCodec(s.codec);
|
|
5975
|
+
if (codec !== wanted) continue;
|
|
5976
|
+
if (maxRes && s.resolution) {
|
|
5977
|
+
if (s.resolution.width > maxRes.width || s.resolution.height > maxRes.height) continue;
|
|
5978
|
+
}
|
|
5979
|
+
const resolution = s.resolution ?? { width: 1280, height: 720 };
|
|
5980
|
+
return { url: s.url, videoCodec: codec, resolution };
|
|
5981
|
+
}
|
|
5982
|
+
return null;
|
|
5983
|
+
}
|
|
5984
|
+
pickTranscodeSource(deviceId, maxRes) {
|
|
5985
|
+
const streams = this.cameraStreamLookup(deviceId);
|
|
5986
|
+
if (streams.length === 0) return null;
|
|
5987
|
+
if (!maxRes) return streams[0] ?? null;
|
|
5988
|
+
let best = null;
|
|
5989
|
+
let bestArea = -1;
|
|
5990
|
+
for (const s of streams) {
|
|
5991
|
+
if (!s.url) continue;
|
|
5992
|
+
const w = s.resolution?.width ?? 1280;
|
|
5993
|
+
const h = s.resolution?.height ?? 720;
|
|
5994
|
+
if (w > maxRes.width || h > maxRes.height) continue;
|
|
5995
|
+
const area = w * h;
|
|
5996
|
+
if (area > bestArea) {
|
|
5997
|
+
bestArea = area;
|
|
5998
|
+
best = s;
|
|
5999
|
+
}
|
|
6000
|
+
}
|
|
6001
|
+
if (best) return best;
|
|
6002
|
+
let smallest = null;
|
|
6003
|
+
let smallestArea = Number.MAX_SAFE_INTEGER;
|
|
6004
|
+
for (const s of streams) {
|
|
6005
|
+
if (!s.url) continue;
|
|
6006
|
+
const w = s.resolution?.width ?? 1280;
|
|
6007
|
+
const h = s.resolution?.height ?? 720;
|
|
6008
|
+
const area = w * h;
|
|
6009
|
+
if (area < smallestArea) {
|
|
6010
|
+
smallestArea = area;
|
|
6011
|
+
smallest = s;
|
|
6012
|
+
}
|
|
6013
|
+
}
|
|
6014
|
+
return smallest;
|
|
6015
|
+
}
|
|
6016
|
+
allocateLoopbackUrl(deviceId, pipelineKey) {
|
|
6017
|
+
const port = this.localRtspPort();
|
|
6018
|
+
const token = `t-${deviceId}-${crypto.randomUUID().slice(0, 8)}`;
|
|
6019
|
+
if (!port) {
|
|
6020
|
+
return `rtsp://0.0.0.0:0/${token}`;
|
|
6021
|
+
}
|
|
6022
|
+
return `rtsp://127.0.0.1:${port}/${token}`;
|
|
6023
|
+
}
|
|
6024
|
+
async loadHardwareEncoders() {
|
|
6025
|
+
if (!this.api) return null;
|
|
6026
|
+
try {
|
|
6027
|
+
const api = this.api;
|
|
6028
|
+
const probe = api["platformProbe"];
|
|
6029
|
+
if (!probe || typeof probe !== "object") return null;
|
|
6030
|
+
const fn = probe["getHardwareEncoders"];
|
|
6031
|
+
if (!fn || typeof fn !== "object") return null;
|
|
6032
|
+
const query = fn["query"];
|
|
6033
|
+
if (typeof query !== "function") return null;
|
|
6034
|
+
const callable = query;
|
|
6035
|
+
return await callable();
|
|
6036
|
+
} catch (err) {
|
|
6037
|
+
this.logger.warn("platformProbe.getHardwareEncoders failed — using software fallback", {
|
|
6038
|
+
meta: { error: index.errMsg(err) }
|
|
6039
|
+
});
|
|
6040
|
+
return null;
|
|
6041
|
+
}
|
|
6042
|
+
}
|
|
6043
|
+
spawnFfmpeg(opts) {
|
|
6044
|
+
const args = [
|
|
6045
|
+
"-hide_banner",
|
|
6046
|
+
"-loglevel",
|
|
6047
|
+
"warning",
|
|
6048
|
+
"-rtsp_transport",
|
|
6049
|
+
"tcp",
|
|
6050
|
+
"-i",
|
|
6051
|
+
opts.sourceUrl,
|
|
6052
|
+
"-vf",
|
|
6053
|
+
`scale=${opts.width}:${opts.height}`,
|
|
6054
|
+
"-c:v",
|
|
6055
|
+
opts.encoder,
|
|
6056
|
+
...pickAudioEncoderArgs(opts.audio),
|
|
6057
|
+
"-f",
|
|
6058
|
+
"rtsp",
|
|
6059
|
+
"-rtsp_transport",
|
|
6060
|
+
"tcp",
|
|
6061
|
+
opts.outputUrl
|
|
6062
|
+
];
|
|
6063
|
+
try {
|
|
6064
|
+
const child = node_child_process.spawn(this.ffmpegPath, args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
6065
|
+
child.stderr?.setEncoding("utf8");
|
|
6066
|
+
child.stderr?.on("data", (chunk) => {
|
|
6067
|
+
this.logger.debug("ffmpeg", { meta: { tag: opts.tag, line: chunk.trim() } });
|
|
6068
|
+
});
|
|
6069
|
+
child.once("error", (err) => {
|
|
6070
|
+
this.logger.error("ffmpeg spawn error", {
|
|
6071
|
+
meta: { tag: opts.tag, error: index.errMsg(err) }
|
|
6072
|
+
});
|
|
6073
|
+
});
|
|
6074
|
+
return child;
|
|
6075
|
+
} catch (err) {
|
|
6076
|
+
this.logger.error("Failed to spawn ffmpeg", {
|
|
6077
|
+
meta: { tag: opts.tag, error: index.errMsg(err) }
|
|
6078
|
+
});
|
|
6079
|
+
return null;
|
|
6080
|
+
}
|
|
6081
|
+
}
|
|
6082
|
+
shareEntry(entry) {
|
|
6083
|
+
entry.refcount += 1;
|
|
6084
|
+
if (entry.releaseTimer) {
|
|
6085
|
+
clearTimeout(entry.releaseTimer);
|
|
6086
|
+
entry.releaseTimer = null;
|
|
6087
|
+
}
|
|
6088
|
+
return entry;
|
|
6089
|
+
}
|
|
6090
|
+
shareOrCreate(key, factory, child) {
|
|
6091
|
+
const existing = this.entries.get(key);
|
|
6092
|
+
if (existing) {
|
|
6093
|
+
this.shareEntry(existing);
|
|
6094
|
+
return existing.source;
|
|
6095
|
+
}
|
|
6096
|
+
const source = factory();
|
|
6097
|
+
const entry = { key, source, child, refcount: 1, releaseTimer: null };
|
|
6098
|
+
this.entries.set(key, entry);
|
|
6099
|
+
return source;
|
|
6100
|
+
}
|
|
6101
|
+
shutdownEntry(entry) {
|
|
6102
|
+
if (entry.releaseTimer) {
|
|
6103
|
+
clearTimeout(entry.releaseTimer);
|
|
6104
|
+
entry.releaseTimer = null;
|
|
6105
|
+
}
|
|
6106
|
+
if (entry.child && !entry.child.killed) {
|
|
6107
|
+
try {
|
|
6108
|
+
entry.child.kill("SIGTERM");
|
|
6109
|
+
} catch (err) {
|
|
6110
|
+
this.logger.warn("ffmpeg kill error", {
|
|
6111
|
+
meta: { key: entry.key, error: index.errMsg(err) }
|
|
6112
|
+
});
|
|
6113
|
+
}
|
|
6114
|
+
}
|
|
6115
|
+
this.entries.delete(entry.key);
|
|
6116
|
+
}
|
|
6117
|
+
}
|
|
5819
6118
|
const PUSH_KINDS = /* @__PURE__ */ new Set(["push-annexb"]);
|
|
5820
6119
|
function findProfileForStream(map, camStreamId) {
|
|
5821
|
-
for (const profile of
|
|
6120
|
+
for (const profile of index.CAM_PROFILE_ORDER) {
|
|
5822
6121
|
if (map[profile] === camStreamId) return profile;
|
|
5823
6122
|
}
|
|
5824
6123
|
return null;
|
|
@@ -5882,11 +6181,42 @@ class StreamBrokerManager {
|
|
|
5882
6181
|
api = null;
|
|
5883
6182
|
defaultPreBufferSec = 10;
|
|
5884
6183
|
webrtcServer = null;
|
|
6184
|
+
/**
|
|
6185
|
+
* Transcode-pipeline manager — backs `getStreamWithCodec`. Lazily
|
|
6186
|
+
* created on first acquire, lives until `destroyAll`.
|
|
6187
|
+
*/
|
|
6188
|
+
transcodeManager = null;
|
|
5885
6189
|
constructor(staticDecoders, logger) {
|
|
5886
6190
|
this.staticDecoders = staticDecoders ?? [];
|
|
5887
6191
|
this.logger = logger;
|
|
5888
6192
|
this.rtspProvider = new RtspRestreamProviderImpl(this.rtspServer, logger.child("rtsp-restream"));
|
|
5889
6193
|
}
|
|
6194
|
+
/**
|
|
6195
|
+
* Lazily create the transcode manager wired with closures rather than
|
|
6196
|
+
* direct refs so it only sees the surface it needs.
|
|
6197
|
+
*/
|
|
6198
|
+
getTranscodeManager() {
|
|
6199
|
+
if (!this.transcodeManager) {
|
|
6200
|
+
this.transcodeManager = new TranscodePipelineManager({
|
|
6201
|
+
logger: this.logger.child("transcode"),
|
|
6202
|
+
api: this.api,
|
|
6203
|
+
cameraStreamLookup: (deviceId) => this.getCameraStreamsForDevice(deviceId),
|
|
6204
|
+
rtspEntryLookup: (brokerId) => {
|
|
6205
|
+
const entry = this.rtspProvider.getEntry(brokerId);
|
|
6206
|
+
return entry ? { url: entry.url } : null;
|
|
6207
|
+
},
|
|
6208
|
+
localRtspPort: () => this.rtspProvider.getRtspPort()
|
|
6209
|
+
});
|
|
6210
|
+
}
|
|
6211
|
+
return this.transcodeManager;
|
|
6212
|
+
}
|
|
6213
|
+
// ── Cap methods: getStreamWithCodec / releaseStreamWithCodec ─────────
|
|
6214
|
+
async getStreamWithCodec(input) {
|
|
6215
|
+
return this.getTranscodeManager().acquire(input);
|
|
6216
|
+
}
|
|
6217
|
+
async releaseStreamWithCodec(input) {
|
|
6218
|
+
return this.getTranscodeManager().release(input.pipelineKey);
|
|
6219
|
+
}
|
|
5890
6220
|
// ── Decoder resolution ───────────────────────────────────────────────
|
|
5891
6221
|
//
|
|
5892
6222
|
// Routes every decoder call through `ctx.api.decoder.*` (tRPC). The
|
|
@@ -5988,7 +6318,7 @@ class StreamBrokerManager {
|
|
|
5988
6318
|
const session = await provider.createSession(config);
|
|
5989
6319
|
const sessionId = `dec-${++nextId}`;
|
|
5990
6320
|
sessions.set(sessionId, session);
|
|
5991
|
-
const buffer = new
|
|
6321
|
+
const buffer = new index.RingBuffer(FRAME_BUFFER_CAPACITY);
|
|
5992
6322
|
frameBuffers.set(sessionId, buffer);
|
|
5993
6323
|
const unsub = session.onFrame((frame) => {
|
|
5994
6324
|
buffer.push(frame);
|
|
@@ -6069,7 +6399,7 @@ class StreamBrokerManager {
|
|
|
6069
6399
|
* client-side and we get a typed payload.
|
|
6070
6400
|
*/
|
|
6071
6401
|
subscribePlaceholderStateSources(bus) {
|
|
6072
|
-
bus.subscribe({ category:
|
|
6402
|
+
bus.subscribe({ category: index.EventCategory.BatteryOnStatusChanged }, (event) => {
|
|
6073
6403
|
const data = event.data;
|
|
6074
6404
|
const deviceId = typeof data.deviceId === "number" ? data.deviceId : null;
|
|
6075
6405
|
if (deviceId === null) return;
|
|
@@ -6082,8 +6412,8 @@ class StreamBrokerManager {
|
|
|
6082
6412
|
if (deviceId === null) return;
|
|
6083
6413
|
this.applyPlaceholderReasonToDevice(deviceId, reason);
|
|
6084
6414
|
};
|
|
6085
|
-
bus.subscribe({ category:
|
|
6086
|
-
bus.subscribe({ category:
|
|
6415
|
+
bus.subscribe({ category: index.EventCategory.DeviceOffline }, handlerForReason("offline"));
|
|
6416
|
+
bus.subscribe({ category: index.EventCategory.DeviceDisabled }, handlerForReason("disabled"));
|
|
6087
6417
|
}
|
|
6088
6418
|
setWebrtcServer(server) {
|
|
6089
6419
|
this.webrtcServer = server;
|
|
@@ -6180,7 +6510,7 @@ class StreamBrokerManager {
|
|
|
6180
6510
|
if (!streamMap?.has(camStreamId)) return { success: true };
|
|
6181
6511
|
const current = this.assignments.get(deviceId) ?? { map: {}, auto: true };
|
|
6182
6512
|
let newMap = { ...current.map };
|
|
6183
|
-
for (const profile of
|
|
6513
|
+
for (const profile of index.CAM_PROFILE_ORDER) {
|
|
6184
6514
|
if (newMap[profile] === camStreamId) delete newMap[profile];
|
|
6185
6515
|
}
|
|
6186
6516
|
if (current.auto) {
|
|
@@ -6321,7 +6651,7 @@ class StreamBrokerManager {
|
|
|
6321
6651
|
rotated++;
|
|
6322
6652
|
} catch (err) {
|
|
6323
6653
|
this.logger.warn("decoder rotation failed", {
|
|
6324
|
-
meta: { brokerId: broker.deviceId, agentNodeId, reason, error:
|
|
6654
|
+
meta: { brokerId: broker.deviceId, agentNodeId, reason, error: index.errMsg(err) }
|
|
6325
6655
|
});
|
|
6326
6656
|
}
|
|
6327
6657
|
}
|
|
@@ -6336,6 +6666,10 @@ class StreamBrokerManager {
|
|
|
6336
6666
|
clearInterval(this.streamHealthTimer);
|
|
6337
6667
|
this.streamHealthTimer = void 0;
|
|
6338
6668
|
}
|
|
6669
|
+
if (this.transcodeManager) {
|
|
6670
|
+
this.transcodeManager.shutdownAll();
|
|
6671
|
+
this.transcodeManager = null;
|
|
6672
|
+
}
|
|
6339
6673
|
const stopPromises = [...this.brokers.values()].map((broker) => broker.stop());
|
|
6340
6674
|
await Promise.all(stopPromises);
|
|
6341
6675
|
this.brokers.clear();
|
|
@@ -6513,7 +6847,7 @@ class StreamBrokerManager {
|
|
|
6513
6847
|
return enabled ? perStream.seconds : 0;
|
|
6514
6848
|
}
|
|
6515
6849
|
if (override?.preBufferSecOverride !== void 0) return override.preBufferSecOverride;
|
|
6516
|
-
if (this.deviceHasFeature(deviceId,
|
|
6850
|
+
if (this.deviceHasFeature(deviceId, index.DeviceFeature.BatteryOperated)) return 0;
|
|
6517
6851
|
return addonDefault;
|
|
6518
6852
|
}
|
|
6519
6853
|
/**
|
|
@@ -6688,7 +7022,7 @@ class StreamBrokerManager {
|
|
|
6688
7022
|
for (const [fieldKey, value] of Object.entries(input.patch)) {
|
|
6689
7023
|
if (fieldKey.startsWith("streamProfile:")) {
|
|
6690
7024
|
const profile = fieldKey.slice("streamProfile:".length);
|
|
6691
|
-
if (!
|
|
7025
|
+
if (!index.CAM_PROFILE_ORDER.includes(profile)) continue;
|
|
6692
7026
|
const newCamId = typeof value === "string" && value !== "" ? value : null;
|
|
6693
7027
|
if (newCamId === null) {
|
|
6694
7028
|
await this.unassignProfile({ deviceId, profile });
|
|
@@ -6829,7 +7163,7 @@ class StreamBrokerManager {
|
|
|
6829
7163
|
}
|
|
6830
7164
|
wouldChangeAutoMap(current, streams) {
|
|
6831
7165
|
const computed = this.computeInitialAssignment(streams);
|
|
6832
|
-
for (const p of
|
|
7166
|
+
for (const p of index.CAM_PROFILE_ORDER) {
|
|
6833
7167
|
if (current[p] !== computed[p]) return true;
|
|
6834
7168
|
}
|
|
6835
7169
|
return false;
|
|
@@ -6848,7 +7182,7 @@ class StreamBrokerManager {
|
|
|
6848
7182
|
const oldPushConsumers = this.countPushConsumers(current.map, deviceId);
|
|
6849
7183
|
const newPushConsumers = this.countPushConsumers(newMap, deviceId);
|
|
6850
7184
|
let anyChange = false;
|
|
6851
|
-
for (const profile of
|
|
7185
|
+
for (const profile of index.CAM_PROFILE_ORDER) {
|
|
6852
7186
|
if (current.map[profile] !== newMap[profile]) {
|
|
6853
7187
|
anyChange = true;
|
|
6854
7188
|
break;
|
|
@@ -6863,7 +7197,7 @@ class StreamBrokerManager {
|
|
|
6863
7197
|
for (const [camId, nextCount] of newPushConsumers) {
|
|
6864
7198
|
if ((oldPushConsumers.get(camId) ?? 0) === 0 && nextCount > 0) {
|
|
6865
7199
|
let triggerProfile = "mid";
|
|
6866
|
-
for (const p of
|
|
7200
|
+
for (const p of index.CAM_PROFILE_ORDER) {
|
|
6867
7201
|
if (newMap[p] === camId) {
|
|
6868
7202
|
triggerProfile = p;
|
|
6869
7203
|
break;
|
|
@@ -6882,7 +7216,7 @@ class StreamBrokerManager {
|
|
|
6882
7216
|
countPushConsumers(map, deviceId) {
|
|
6883
7217
|
const out = /* @__PURE__ */ new Map();
|
|
6884
7218
|
const streamMap = this.cameraStreams.get(deviceId);
|
|
6885
|
-
for (const profile of
|
|
7219
|
+
for (const profile of index.CAM_PROFILE_ORDER) {
|
|
6886
7220
|
const camId = map[profile];
|
|
6887
7221
|
if (!camId) continue;
|
|
6888
7222
|
const cam = streamMap?.get(camId);
|
|
@@ -6897,7 +7231,7 @@ class StreamBrokerManager {
|
|
|
6897
7231
|
buildSourceFromCamStream(cam) {
|
|
6898
7232
|
const label = cam.label ?? cam.camStreamId;
|
|
6899
7233
|
const codec = cam.codec !== void 0 ? { videoCodec: cam.codec } : {};
|
|
6900
|
-
const stall = cam.deviceFeatures.includes(
|
|
7234
|
+
const stall = cam.deviceFeatures.includes(index.DeviceFeature.BatteryOperated) ? { allowStall: true } : {};
|
|
6901
7235
|
const passthroughMeta = cam.metadata ?? {};
|
|
6902
7236
|
switch (cam.kind) {
|
|
6903
7237
|
case "push-annexb":
|
|
@@ -7021,7 +7355,7 @@ class StreamBrokerManager {
|
|
|
7021
7355
|
]);
|
|
7022
7356
|
} catch (err) {
|
|
7023
7357
|
log.error("Failed to register with restreamer", {
|
|
7024
|
-
meta: { restreamerId: restreamer.id, error:
|
|
7358
|
+
meta: { restreamerId: restreamer.id, error: index.errMsg(err) }
|
|
7025
7359
|
});
|
|
7026
7360
|
}
|
|
7027
7361
|
}
|
|
@@ -7036,7 +7370,7 @@ class StreamBrokerManager {
|
|
|
7036
7370
|
await restreamer.unregisterDevice(deviceId);
|
|
7037
7371
|
} catch (err) {
|
|
7038
7372
|
log.error("Failed to unregister from restreamer", {
|
|
7039
|
-
meta: { restreamerId: restreamer.id, error:
|
|
7373
|
+
meta: { restreamerId: restreamer.id, error: index.errMsg(err) }
|
|
7040
7374
|
});
|
|
7041
7375
|
}
|
|
7042
7376
|
}
|
|
@@ -7052,7 +7386,7 @@ class StreamBrokerManager {
|
|
|
7052
7386
|
const assignment = this.assignments.get(deviceId) ?? { map: {} };
|
|
7053
7387
|
const streamMap = this.cameraStreams.get(deviceId);
|
|
7054
7388
|
const slots = [];
|
|
7055
|
-
for (const profile of
|
|
7389
|
+
for (const profile of index.CAM_PROFILE_ORDER) {
|
|
7056
7390
|
const camId = assignment.map[profile] ?? null;
|
|
7057
7391
|
const brokerId = camId ? brokerIdFor(deviceId, camId) : `${deviceId}/${profile}`;
|
|
7058
7392
|
const broker = camId ? this.brokers.get(brokerId) : void 0;
|
|
@@ -7087,13 +7421,13 @@ class StreamBrokerManager {
|
|
|
7087
7421
|
});
|
|
7088
7422
|
}
|
|
7089
7423
|
emitCamStreamDemand(deviceId, camStreamId, profile) {
|
|
7090
|
-
this.emitCapEvent(
|
|
7424
|
+
this.emitCapEvent(index.EventCategory.StreamBrokerOnCamStreamDemand, deviceId, { deviceId, camStreamId, profile });
|
|
7091
7425
|
}
|
|
7092
7426
|
emitCamStreamIdle(deviceId, camStreamId) {
|
|
7093
|
-
this.emitCapEvent(
|
|
7427
|
+
this.emitCapEvent(index.EventCategory.StreamBrokerOnCamStreamIdle, deviceId, { deviceId, camStreamId });
|
|
7094
7428
|
}
|
|
7095
7429
|
emitRequestStreamSourceRefresh(deviceId, camStreamId, brokerId) {
|
|
7096
|
-
this.emitCapEvent(
|
|
7430
|
+
this.emitCapEvent(index.EventCategory.StreamBrokerOnRequestStreamSourceRefresh, deviceId, {
|
|
7097
7431
|
deviceId,
|
|
7098
7432
|
camStreamId,
|
|
7099
7433
|
brokerId
|
|
@@ -7480,7 +7814,7 @@ async function resolveMdnsCandidatesInSdp(sdp, logger, sessionTag) {
|
|
|
7480
7814
|
logger.info("mDNS resolve succeeded", { meta: { sessionTag, host, address } });
|
|
7481
7815
|
return [host, address];
|
|
7482
7816
|
} catch (err) {
|
|
7483
|
-
logger.warn("mDNS resolve failed", { meta: { sessionTag, host, error:
|
|
7817
|
+
logger.warn("mDNS resolve failed", { meta: { sessionTag, host, error: index.errMsg(err) } });
|
|
7484
7818
|
return [host, null];
|
|
7485
7819
|
}
|
|
7486
7820
|
})
|
|
@@ -7525,18 +7859,18 @@ class JitterBuffer {
|
|
|
7525
7859
|
return ret;
|
|
7526
7860
|
const start = nextSequenceNumber(afterSequenceNumber);
|
|
7527
7861
|
for (let i = 0; i < this.jitterSize; i++) {
|
|
7528
|
-
const
|
|
7529
|
-
const packet = this.pending[
|
|
7862
|
+
const index2 = (start + i) % this.jitterSize;
|
|
7863
|
+
const packet = this.pending[index2];
|
|
7530
7864
|
if (!packet)
|
|
7531
7865
|
continue;
|
|
7532
7866
|
const { sequenceNumber } = packet.header;
|
|
7533
7867
|
const sd = sequenceNumberDistance(this.lastSequenceNumber ?? sequenceNumber, sequenceNumber);
|
|
7534
7868
|
if (sd <= 0) {
|
|
7535
7869
|
this.console.log("jitter buffer purged packet:", sequenceNumber);
|
|
7536
|
-
this.pending[
|
|
7870
|
+
this.pending[index2] = void 0;
|
|
7537
7871
|
ret.push(packet);
|
|
7538
7872
|
} else if (sd === 1) {
|
|
7539
|
-
this.pending[
|
|
7873
|
+
this.pending[index2] = void 0;
|
|
7540
7874
|
this.lastSequenceNumber = sequenceNumber;
|
|
7541
7875
|
ret.push(packet);
|
|
7542
7876
|
} else ;
|
|
@@ -7854,10 +8188,10 @@ class H265Repacketizer {
|
|
|
7854
8188
|
this.pendingFU = void 0;
|
|
7855
8189
|
}
|
|
7856
8190
|
createRtpPackets(packet, nalus, ret, hadMarker = packet.header.marker) {
|
|
7857
|
-
nalus.forEach((packetized,
|
|
7858
|
-
if (
|
|
8191
|
+
nalus.forEach((packetized, index2) => {
|
|
8192
|
+
if (index2 !== 0)
|
|
7859
8193
|
this.extraPackets++;
|
|
7860
|
-
const marker = hadMarker &&
|
|
8194
|
+
const marker = hadMarker && index2 === nalus.length - 1;
|
|
7861
8195
|
ret.push(this.createPacket(packet, packetized, marker));
|
|
7862
8196
|
});
|
|
7863
8197
|
}
|
|
@@ -8249,7 +8583,7 @@ class AdaptiveSession {
|
|
|
8249
8583
|
const payload = pkt.payload;
|
|
8250
8584
|
if (payload?.length > 0) void cb(payload, "Opus");
|
|
8251
8585
|
} catch (err) {
|
|
8252
|
-
this.logger.error("Intercom error", { meta: { phase: "session", sessionId: this.sessionId, error:
|
|
8586
|
+
this.logger.error("Intercom error", { meta: { phase: "session", sessionId: this.sessionId, error: index.errMsg(err) } });
|
|
8253
8587
|
}
|
|
8254
8588
|
});
|
|
8255
8589
|
});
|
|
@@ -8510,7 +8844,7 @@ class AdaptiveSession {
|
|
|
8510
8844
|
srcPkt = werift.RtpPacket.deSerialize(rtpData);
|
|
8511
8845
|
} catch (err) {
|
|
8512
8846
|
this.logger.warn("H265 RTP deserialize failed", {
|
|
8513
|
-
meta: { phase: "session", sessionId: this.sessionId, error:
|
|
8847
|
+
meta: { phase: "session", sessionId: this.sessionId, error: index.errMsg(err), len: rtpData.length }
|
|
8514
8848
|
});
|
|
8515
8849
|
return;
|
|
8516
8850
|
}
|
|
@@ -8524,7 +8858,7 @@ class AdaptiveSession {
|
|
|
8524
8858
|
outPkts = rep.repacketize(srcPkt);
|
|
8525
8859
|
} catch (err) {
|
|
8526
8860
|
this.logger.warn("H265 repacketize failed", {
|
|
8527
|
-
meta: { phase: "session", sessionId: this.sessionId, error:
|
|
8861
|
+
meta: { phase: "session", sessionId: this.sessionId, error: index.errMsg(err) }
|
|
8528
8862
|
});
|
|
8529
8863
|
return;
|
|
8530
8864
|
}
|
|
@@ -8539,7 +8873,7 @@ class AdaptiveSession {
|
|
|
8539
8873
|
);
|
|
8540
8874
|
} catch (e) {
|
|
8541
8875
|
this.logger.warn("replaceRTP failed (non-fatal)", {
|
|
8542
|
-
meta: { phase: "session", sessionId: this.sessionId, error:
|
|
8876
|
+
meta: { phase: "session", sessionId: this.sessionId, error: index.errMsg(e) }
|
|
8543
8877
|
});
|
|
8544
8878
|
}
|
|
8545
8879
|
}
|
|
@@ -8554,7 +8888,7 @@ class AdaptiveSession {
|
|
|
8554
8888
|
} catch (err) {
|
|
8555
8889
|
if (this.rtpPacketsSent <= 10) {
|
|
8556
8890
|
this.logger.error("sendRtp (h265 forward) error", {
|
|
8557
|
-
meta: { phase: "session", sessionId: this.sessionId, error:
|
|
8891
|
+
meta: { phase: "session", sessionId: this.sessionId, error: index.errMsg(err) }
|
|
8558
8892
|
});
|
|
8559
8893
|
}
|
|
8560
8894
|
}
|
|
@@ -8781,7 +9115,7 @@ class AdaptiveSession {
|
|
|
8781
9115
|
}
|
|
8782
9116
|
} catch (err) {
|
|
8783
9117
|
if (!signal.aborted && !this.closed) {
|
|
8784
|
-
this.logger.error("Feed error", { meta: { phase: "session", sessionId: this.sessionId, error:
|
|
9118
|
+
this.logger.error("Feed error", { meta: { phase: "session", sessionId: this.sessionId, error: index.errMsg(err) } });
|
|
8785
9119
|
}
|
|
8786
9120
|
} finally {
|
|
8787
9121
|
if (!this.closed) {
|
|
@@ -8865,7 +9199,7 @@ class AdaptiveSession {
|
|
|
8865
9199
|
}
|
|
8866
9200
|
} catch (err) {
|
|
8867
9201
|
if (!signal.aborted && !this.closed) {
|
|
8868
|
-
this.logger.error("Transcode feed error", { meta: { sessionId: this.sessionId, error:
|
|
9202
|
+
this.logger.error("Transcode feed error", { meta: { sessionId: this.sessionId, error: index.errMsg(err) } });
|
|
8869
9203
|
}
|
|
8870
9204
|
} finally {
|
|
8871
9205
|
if (!ff.stdin.destroyed) ff.stdin.end();
|
|
@@ -8908,7 +9242,7 @@ class AdaptiveSession {
|
|
|
8908
9242
|
this.videoSender.replaceRTP(header, true);
|
|
8909
9243
|
} catch (e) {
|
|
8910
9244
|
this.logger.warn("replaceRTP failed (non-fatal)", {
|
|
8911
|
-
meta: { phase: "session", sessionId: this.sessionId, error:
|
|
9245
|
+
meta: { phase: "session", sessionId: this.sessionId, error: index.errMsg(e) }
|
|
8912
9246
|
});
|
|
8913
9247
|
}
|
|
8914
9248
|
}
|
|
@@ -8939,7 +9273,7 @@ class AdaptiveSession {
|
|
|
8939
9273
|
} catch (err) {
|
|
8940
9274
|
if (this.rtpPacketsSent <= 10) {
|
|
8941
9275
|
this.logger.error("sendRtp error", {
|
|
8942
|
-
meta: { phase: "session", sessionId: this.sessionId, packetIndex: this.rtpPacketsSent, error:
|
|
9276
|
+
meta: { phase: "session", sessionId: this.sessionId, packetIndex: this.rtpPacketsSent, error: index.errMsg(err) }
|
|
8943
9277
|
});
|
|
8944
9278
|
}
|
|
8945
9279
|
}
|
|
@@ -9013,7 +9347,7 @@ class AdaptiveSession {
|
|
|
9013
9347
|
const pkt = new werift.RtpPacket(header, data);
|
|
9014
9348
|
this.audioSender.sendRtp(pkt);
|
|
9015
9349
|
} catch (err) {
|
|
9016
|
-
this.logger.debug("Audio write error", { meta: { phase: "session", sessionId: this.sessionId, error:
|
|
9350
|
+
this.logger.debug("Audio write error", { meta: { phase: "session", sessionId: this.sessionId, error: index.errMsg(err) } });
|
|
9017
9351
|
}
|
|
9018
9352
|
}
|
|
9019
9353
|
// -----------------------------------------------------------------------
|
|
@@ -9157,6 +9491,73 @@ class BrokerWebrtcServer {
|
|
|
9157
9491
|
}
|
|
9158
9492
|
// ── Session lifecycle ───────────────────────────────────────────────
|
|
9159
9493
|
async createSession(streamId, hints = {}, opts = {}) {
|
|
9494
|
+
const setup = await this.setupSessionForBroker(streamId, hints, opts);
|
|
9495
|
+
try {
|
|
9496
|
+
const offer = await setup.session.createOffer();
|
|
9497
|
+
setup.sessionLogger.info("WebRTC session created", {
|
|
9498
|
+
meta: {
|
|
9499
|
+
sessionId: setup.sessionId,
|
|
9500
|
+
brokerId: setup.brokerId,
|
|
9501
|
+
iceServers: setup.iceServerCount,
|
|
9502
|
+
codec: setup.sessionCodec,
|
|
9503
|
+
useH265Repacketizer: setup.useH265Repacketizer
|
|
9504
|
+
}
|
|
9505
|
+
});
|
|
9506
|
+
return { sessionId: setup.sessionId, sdpOffer: offer.sdp };
|
|
9507
|
+
} catch (err) {
|
|
9508
|
+
this.cleanupSession(setup.sessionId);
|
|
9509
|
+
await setup.session.close().catch(() => {
|
|
9510
|
+
});
|
|
9511
|
+
throw err;
|
|
9512
|
+
}
|
|
9513
|
+
}
|
|
9514
|
+
/**
|
|
9515
|
+
* WHEP-style client-offer signaling: caller has already constructed
|
|
9516
|
+
* an SDP offer (Alexa / WHEP / mobile clients that do
|
|
9517
|
+
* `createOffer()` locally) and posts it here. We build the same
|
|
9518
|
+
* broker subscription + AdaptiveSession plumbing as `createSession`,
|
|
9519
|
+
* then call `session.handleOffer(clientOffer)` to produce the
|
|
9520
|
+
* server's SDP answer.
|
|
9521
|
+
*
|
|
9522
|
+
* The session lifecycle is identical to `createSession` — broker
|
|
9523
|
+
* unsubscribe and `closeSession(sessionId)` still apply — so the
|
|
9524
|
+
* RTC controller / Alexa handler closes the session the same way.
|
|
9525
|
+
*/
|
|
9526
|
+
async handleOffer(streamId, clientOfferSdp, hints = {}, opts = {}) {
|
|
9527
|
+
const setup = await this.setupSessionForBroker(streamId, hints, opts);
|
|
9528
|
+
try {
|
|
9529
|
+
const answer = await setup.session.handleOffer({ sdp: clientOfferSdp, type: "offer" });
|
|
9530
|
+
setup.sessionLogger.info("WebRTC session created (client-offer)", {
|
|
9531
|
+
meta: {
|
|
9532
|
+
sessionId: setup.sessionId,
|
|
9533
|
+
brokerId: setup.brokerId,
|
|
9534
|
+
iceServers: setup.iceServerCount,
|
|
9535
|
+
codec: setup.sessionCodec,
|
|
9536
|
+
useH265Repacketizer: setup.useH265Repacketizer
|
|
9537
|
+
}
|
|
9538
|
+
});
|
|
9539
|
+
return { sessionId: setup.sessionId, sdpAnswer: answer.sdp };
|
|
9540
|
+
} catch (err) {
|
|
9541
|
+
this.cleanupSession(setup.sessionId);
|
|
9542
|
+
await setup.session.close().catch(() => {
|
|
9543
|
+
});
|
|
9544
|
+
throw err;
|
|
9545
|
+
}
|
|
9546
|
+
}
|
|
9547
|
+
/**
|
|
9548
|
+
* Shared session bring-up: resolve broker → subscribe to encoded
|
|
9549
|
+
* data + RTP → flush pre-buffer → seed H.265 codec info → register
|
|
9550
|
+
* the SessionEntry. Returns the `AdaptiveSession` ready for SDP
|
|
9551
|
+
* negotiation (either `createOffer()` for the server-offer flow or
|
|
9552
|
+
* `handleOffer(clientOffer)` for the WHEP / Alexa flow).
|
|
9553
|
+
*
|
|
9554
|
+
* The returned session has its `onClosed` hook already wired to
|
|
9555
|
+
* cleanup, and the SessionEntry is already in `this.sessions` keyed
|
|
9556
|
+
* by `sessionId` — callers MUST cleanup + close on negotiation
|
|
9557
|
+
* failure (both `createSession` and `handleOffer` do this in their
|
|
9558
|
+
* catch blocks).
|
|
9559
|
+
*/
|
|
9560
|
+
async setupSessionForBroker(streamId, hints, opts) {
|
|
9160
9561
|
if (this.stopped) throw new Error("Server stopped");
|
|
9161
9562
|
const brokerId = this.resolveBrokerId(streamId, hints);
|
|
9162
9563
|
const broker = this.brokers.get(brokerId);
|
|
@@ -9268,7 +9669,11 @@ class BrokerWebrtcServer {
|
|
|
9268
9669
|
});
|
|
9269
9670
|
}
|
|
9270
9671
|
}
|
|
9271
|
-
const
|
|
9672
|
+
const requestedSessionId = opts.sessionId;
|
|
9673
|
+
if (requestedSessionId !== void 0 && this.sessions.has(requestedSessionId)) {
|
|
9674
|
+
throw new Error(`Session id already in use: ${requestedSessionId}`);
|
|
9675
|
+
}
|
|
9676
|
+
const sessionId = requestedSessionId ?? crypto.randomUUID();
|
|
9272
9677
|
const iceServers = await this.resolveIceServers();
|
|
9273
9678
|
const session = new AdaptiveSession({
|
|
9274
9679
|
sessionId,
|
|
@@ -9303,7 +9708,7 @@ class BrokerWebrtcServer {
|
|
|
9303
9708
|
});
|
|
9304
9709
|
} catch (err) {
|
|
9305
9710
|
sessionLogger.warn("seedH265CodecInfoFromSdp threw", {
|
|
9306
|
-
meta: { sessionId, error:
|
|
9711
|
+
meta: { sessionId, error: index.errMsg(err) }
|
|
9307
9712
|
});
|
|
9308
9713
|
}
|
|
9309
9714
|
});
|
|
@@ -9320,7 +9725,7 @@ class BrokerWebrtcServer {
|
|
|
9320
9725
|
session.forwardSourceRtpVideo(rtpData);
|
|
9321
9726
|
} catch (err) {
|
|
9322
9727
|
sessionLogger.warn("forwardSourceRtpVideo threw", {
|
|
9323
|
-
meta: { sessionId, error:
|
|
9728
|
+
meta: { sessionId, error: index.errMsg(err) }
|
|
9324
9729
|
});
|
|
9325
9730
|
}
|
|
9326
9731
|
});
|
|
@@ -9328,18 +9733,15 @@ class BrokerWebrtcServer {
|
|
|
9328
9733
|
const entry = { session, brokerId, unsubBroker, unsubRtp, unsubSdpParams, closeSource };
|
|
9329
9734
|
this.sessions.set(sessionId, entry);
|
|
9330
9735
|
session.onClosed = () => this.cleanupSession(sessionId);
|
|
9331
|
-
|
|
9332
|
-
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
|
|
9340
|
-
});
|
|
9341
|
-
throw err;
|
|
9342
|
-
}
|
|
9736
|
+
return {
|
|
9737
|
+
sessionId,
|
|
9738
|
+
session,
|
|
9739
|
+
sessionLogger,
|
|
9740
|
+
brokerId,
|
|
9741
|
+
sessionCodec,
|
|
9742
|
+
useH265Repacketizer,
|
|
9743
|
+
iceServerCount: iceServers.length
|
|
9744
|
+
};
|
|
9343
9745
|
}
|
|
9344
9746
|
async handleAnswer(sessionId, sdpAnswer) {
|
|
9345
9747
|
const entry = this.sessions.get(sessionId);
|
|
@@ -9442,7 +9844,7 @@ class BrokerWebrtcServer {
|
|
|
9442
9844
|
try {
|
|
9443
9845
|
return await this.getIceServersFn();
|
|
9444
9846
|
} catch (err) {
|
|
9445
|
-
this.logger.warn("getIceServers failed", { meta: { error:
|
|
9847
|
+
this.logger.warn("getIceServers failed", { meta: { error: index.errMsg(err) } });
|
|
9446
9848
|
}
|
|
9447
9849
|
}
|
|
9448
9850
|
return this.staticIceServers ?? [];
|
|
@@ -9620,6 +10022,15 @@ class WebrtcSessionProvider {
|
|
|
9620
10022
|
const streamingDebug = typeof this.manager.isStreamingDebug === "function" ? this.manager.isStreamingDebug(input.deviceId) : false;
|
|
9621
10023
|
return this.webrtcServer.createSession(streamId, input.hints, { streamingDebug });
|
|
9622
10024
|
}
|
|
10025
|
+
async handleOffer(input) {
|
|
10026
|
+
const target = input.target ?? { kind: "adaptive" };
|
|
10027
|
+
const streamId = this.resolveTargetToStreamId(input.deviceId, target);
|
|
10028
|
+
const streamingDebug = typeof this.manager.isStreamingDebug === "function" ? this.manager.isStreamingDebug(input.deviceId) : false;
|
|
10029
|
+
return this.webrtcServer.handleOffer(streamId, input.sdpOffer, void 0, {
|
|
10030
|
+
streamingDebug,
|
|
10031
|
+
sessionId: input.sessionId
|
|
10032
|
+
});
|
|
10033
|
+
}
|
|
9623
10034
|
async handleAnswer(input) {
|
|
9624
10035
|
await this.webrtcServer.handleAnswer(input.sessionId, input.sdpAnswer);
|
|
9625
10036
|
}
|
|
@@ -9649,7 +10060,7 @@ function stableSnapshotKey(payload, dropKeys) {
|
|
|
9649
10060
|
return JSON.stringify(strip(payload));
|
|
9650
10061
|
}
|
|
9651
10062
|
const BROKER_METRICS_HEARTBEAT_MS = 3e4;
|
|
9652
|
-
class StreamBrokerAddon extends
|
|
10063
|
+
class StreamBrokerAddon extends index.BaseAddon {
|
|
9653
10064
|
brokerManager = null;
|
|
9654
10065
|
metricsSnapshotTimer = null;
|
|
9655
10066
|
/**
|
|
@@ -9686,7 +10097,7 @@ class StreamBrokerAddon extends types.BaseAddon {
|
|
|
9686
10097
|
const rtspProvider = this.brokerManager.getRtspRestreamProvider();
|
|
9687
10098
|
try {
|
|
9688
10099
|
const addonStore = await this.ctx.settings?.readAddonStore() ?? {};
|
|
9689
|
-
const tokenData =
|
|
10100
|
+
const tokenData = index.asJsonObject(addonStore["rtspTokens"]);
|
|
9690
10101
|
if (tokenData) {
|
|
9691
10102
|
const tokens = /* @__PURE__ */ new Map();
|
|
9692
10103
|
for (const [k, v] of Object.entries(tokenData)) {
|
|
@@ -9695,7 +10106,7 @@ class StreamBrokerAddon extends types.BaseAddon {
|
|
|
9695
10106
|
this.brokerManager.loadPersistedTokens(tokens);
|
|
9696
10107
|
this.ctx.logger.info("Loaded persisted RTSP tokens", { meta: { tokenCount: tokens.size } });
|
|
9697
10108
|
}
|
|
9698
|
-
const enabledData =
|
|
10109
|
+
const enabledData = index.asJsonObject(addonStore["rtspEnabled"]);
|
|
9699
10110
|
if (enabledData) {
|
|
9700
10111
|
const states = /* @__PURE__ */ new Map();
|
|
9701
10112
|
for (const [k, v] of Object.entries(enabledData)) {
|
|
@@ -9712,7 +10123,7 @@ class StreamBrokerAddon extends types.BaseAddon {
|
|
|
9712
10123
|
obj[k] = v;
|
|
9713
10124
|
}
|
|
9714
10125
|
this.ctx.settings?.writeAddonStore({ rtspTokens: obj }).catch((err) => {
|
|
9715
|
-
this.ctx.logger.warn("Failed to persist RTSP tokens", { meta: { error:
|
|
10126
|
+
this.ctx.logger.warn("Failed to persist RTSP tokens", { meta: { error: index.errMsg(err) } });
|
|
9716
10127
|
});
|
|
9717
10128
|
});
|
|
9718
10129
|
rtspProvider.setEnabledPersister((states) => {
|
|
@@ -9721,18 +10132,18 @@ class StreamBrokerAddon extends types.BaseAddon {
|
|
|
9721
10132
|
obj[k] = v;
|
|
9722
10133
|
}
|
|
9723
10134
|
this.ctx.settings?.writeAddonStore({ rtspEnabled: obj }).catch((err) => {
|
|
9724
|
-
this.ctx.logger.warn("Failed to persist RTSP enabled states", { meta: { error:
|
|
10135
|
+
this.ctx.logger.warn("Failed to persist RTSP enabled states", { meta: { error: index.errMsg(err) } });
|
|
9725
10136
|
});
|
|
9726
10137
|
});
|
|
9727
10138
|
try {
|
|
9728
10139
|
const addonStore = await this.ctx.settings?.readAddonStore() ?? {};
|
|
9729
|
-
const overrideData =
|
|
10140
|
+
const overrideData = index.asJsonObject(addonStore["deviceOverrides"]);
|
|
9730
10141
|
if (overrideData) {
|
|
9731
10142
|
const overrides = /* @__PURE__ */ new Map();
|
|
9732
10143
|
for (const [rawKey, raw] of Object.entries(overrideData)) {
|
|
9733
10144
|
const numericKey = Number(rawKey);
|
|
9734
10145
|
if (!Number.isFinite(numericKey) || !Number.isInteger(numericKey)) continue;
|
|
9735
|
-
const o =
|
|
10146
|
+
const o = index.asJsonObject(raw);
|
|
9736
10147
|
if (!o) continue;
|
|
9737
10148
|
const entry = {};
|
|
9738
10149
|
if (typeof o["preBufferSecOverride"] === "number") {
|
|
@@ -9741,11 +10152,11 @@ class StreamBrokerAddon extends types.BaseAddon {
|
|
|
9741
10152
|
if (typeof o["streamingDebug"] === "boolean") {
|
|
9742
10153
|
entry["streamingDebug"] = o["streamingDebug"];
|
|
9743
10154
|
}
|
|
9744
|
-
const pbRaw =
|
|
10155
|
+
const pbRaw = index.asJsonObject(o["preBuffer"]);
|
|
9745
10156
|
if (pbRaw) {
|
|
9746
10157
|
const preBuffer = {};
|
|
9747
10158
|
for (const [profile, v] of Object.entries(pbRaw)) {
|
|
9748
|
-
const sv =
|
|
10159
|
+
const sv = index.asJsonObject(v);
|
|
9749
10160
|
if (sv && typeof sv["seconds"] === "number") {
|
|
9750
10161
|
preBuffer[profile] = { enabled: sv["enabled"] !== false, seconds: sv["seconds"] };
|
|
9751
10162
|
}
|
|
@@ -9769,20 +10180,20 @@ class StreamBrokerAddon extends types.BaseAddon {
|
|
|
9769
10180
|
obj[`${deviceId}`] = entry;
|
|
9770
10181
|
}
|
|
9771
10182
|
this.ctx.settings?.writeAddonStore({ deviceOverrides: obj }).catch((err) => {
|
|
9772
|
-
this.ctx.logger.warn("Failed to persist device overrides", { meta: { error:
|
|
10183
|
+
this.ctx.logger.warn("Failed to persist device overrides", { meta: { error: index.errMsg(err) } });
|
|
9773
10184
|
});
|
|
9774
10185
|
});
|
|
9775
10186
|
try {
|
|
9776
10187
|
const addonStore = await this.ctx.settings?.readAddonStore() ?? {};
|
|
9777
|
-
const raw =
|
|
10188
|
+
const raw = index.asJsonObject(addonStore["profileMap"]);
|
|
9778
10189
|
if (raw) {
|
|
9779
10190
|
const loaded = /* @__PURE__ */ new Map();
|
|
9780
10191
|
for (const [rawKey, rawVal] of Object.entries(raw)) {
|
|
9781
10192
|
const deviceId = Number(rawKey);
|
|
9782
10193
|
if (!Number.isFinite(deviceId) || !Number.isInteger(deviceId)) continue;
|
|
9783
|
-
const val =
|
|
10194
|
+
const val = index.asJsonObject(rawVal);
|
|
9784
10195
|
if (!val) continue;
|
|
9785
|
-
const mapObj =
|
|
10196
|
+
const mapObj = index.asJsonObject(val["map"]) ?? {};
|
|
9786
10197
|
const map = {};
|
|
9787
10198
|
for (const profile of PROFILE_KEYS) {
|
|
9788
10199
|
const v = mapObj[profile];
|
|
@@ -9802,13 +10213,13 @@ class StreamBrokerAddon extends types.BaseAddon {
|
|
|
9802
10213
|
obj[`${deviceId}`] = { map: { ...assignment.map }, auto: assignment.auto };
|
|
9803
10214
|
}
|
|
9804
10215
|
this.ctx.settings?.writeAddonStore({ profileMap: obj }).catch((err) => {
|
|
9805
|
-
this.ctx.logger.warn("Failed to persist profile map", { meta: { error:
|
|
10216
|
+
this.ctx.logger.warn("Failed to persist profile map", { meta: { error: index.errMsg(err) } });
|
|
9806
10217
|
});
|
|
9807
10218
|
};
|
|
9808
10219
|
this.brokerManager.setProfileMapPersister(profileMapPersister);
|
|
9809
10220
|
await this.brokerManager.startRtspServer(this.config.rtspPort);
|
|
9810
10221
|
this.subscribe(
|
|
9811
|
-
{ category:
|
|
10222
|
+
{ category: index.EventCategory.PipelineAgentHwaccelChanged },
|
|
9812
10223
|
(event) => {
|
|
9813
10224
|
const { agentNodeId, reason } = event.data;
|
|
9814
10225
|
if (typeof agentNodeId !== "string" || agentNodeId.length === 0) return;
|
|
@@ -9825,7 +10236,7 @@ class StreamBrokerAddon extends types.BaseAddon {
|
|
|
9825
10236
|
}).catch((err) => {
|
|
9826
10237
|
this.ctx.logger.warn("hwaccel-driven decoder rotation failed", {
|
|
9827
10238
|
tags: { nodeId: agentNodeId },
|
|
9828
|
-
meta: { error:
|
|
10239
|
+
meta: { error: index.errMsg(err) }
|
|
9829
10240
|
});
|
|
9830
10241
|
});
|
|
9831
10242
|
}
|
|
@@ -9846,7 +10257,7 @@ class StreamBrokerAddon extends types.BaseAddon {
|
|
|
9846
10257
|
}).catch((err) => {
|
|
9847
10258
|
this.ctx.logger.warn("decoder-down rotation failed", {
|
|
9848
10259
|
tags: { nodeId },
|
|
9849
|
-
meta: { error:
|
|
10260
|
+
meta: { error: index.errMsg(err) }
|
|
9850
10261
|
});
|
|
9851
10262
|
});
|
|
9852
10263
|
}
|
|
@@ -9903,20 +10314,20 @@ class StreamBrokerAddon extends types.BaseAddon {
|
|
|
9903
10314
|
};
|
|
9904
10315
|
this.ctx.logger.info("Stream broker manager initialized");
|
|
9905
10316
|
const registrations = [
|
|
9906
|
-
{ capability:
|
|
10317
|
+
{ capability: index.streamBrokerCapability, provider: this.brokerManager },
|
|
9907
10318
|
{
|
|
9908
|
-
capability:
|
|
10319
|
+
capability: index.cameraStreamsCapability,
|
|
9909
10320
|
provider: cameraStreamsProvider,
|
|
9910
10321
|
kind: "wrapper",
|
|
9911
10322
|
defaultActive: true
|
|
9912
10323
|
},
|
|
9913
10324
|
{
|
|
9914
|
-
capability:
|
|
10325
|
+
capability: index.webrtcSessionCapability,
|
|
9915
10326
|
provider: webrtcSessionProvider,
|
|
9916
10327
|
kind: "wrapper",
|
|
9917
10328
|
defaultActive: true
|
|
9918
10329
|
},
|
|
9919
|
-
{ capability:
|
|
10330
|
+
{ capability: index.addonWidgetsSourceCapability, provider: widgetsProvider }
|
|
9920
10331
|
];
|
|
9921
10332
|
return registrations;
|
|
9922
10333
|
}
|
|
@@ -9957,8 +10368,8 @@ class StreamBrokerAddon extends types.BaseAddon {
|
|
|
9957
10368
|
const heartbeatDue = !prev || timestamp - prev.emittedAt >= BROKER_METRICS_HEARTBEAT_MS;
|
|
9958
10369
|
if (prev && prev.json === json && !heartbeatDue) continue;
|
|
9959
10370
|
this.lastEmittedBrokerSnapshot.set(brokerId, { json, emittedAt: timestamp });
|
|
9960
|
-
eventBus.emit(
|
|
9961
|
-
|
|
10371
|
+
eventBus.emit(index.createEvent(
|
|
10372
|
+
index.EventCategory.StreamBrokerMetricsSnapshot,
|
|
9962
10373
|
{ type: "device", id: deviceId, nodeId },
|
|
9963
10374
|
{ brokerId, deviceId, profile, nodeId, stats, timestamp }
|
|
9964
10375
|
));
|
|
@@ -10934,7 +11345,7 @@ class FrameServerImpl {
|
|
|
10934
11345
|
for (const h of this.connectHandlers) h(connectionId);
|
|
10935
11346
|
},
|
|
10936
11347
|
(err) => {
|
|
10937
|
-
const msg =
|
|
11348
|
+
const msg = index.errMsg(err);
|
|
10938
11349
|
this.rejectPending(connectionId, `auth-failed:${msg}`);
|
|
10939
11350
|
}
|
|
10940
11351
|
);
|