@camstack/addon-pipeline 0.1.10 → 0.1.11
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/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-DbMNirr7.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-BuV9ar3i.mjs} +6 -6
- package/dist/stream-broker/{hostInit-wnZIaWA5.mjs → hostInit-CjVI5LuK.mjs} +6 -6
- package/dist/stream-broker/{index-DadYrR5H.mjs → index-BF5Qr03x.mjs} +1 -1
- package/dist/stream-broker/{index-BQ_NNiX0.mjs → index-DUJwOcGq.mjs} +5230 -5150
- package/dist/stream-broker/{index-Dwc0KrUN.mjs → index-DuBCn5us.mjs} +4244 -4164
- package/dist/stream-broker/index.js +425 -14
- package/dist/stream-broker/index.js.map +1 -1
- package/dist/stream-broker/index.mjs +425 -14
- package/dist/stream-broker/index.mjs.map +1 -1
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/package.json +1 -1
- 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
|
@@ -5,8 +5,8 @@ import * as crypto from "node:crypto";
|
|
|
5
5
|
import crypto__default, { randomUUID, createHash, randomBytes } from "node:crypto";
|
|
6
6
|
import { Socket } from "net";
|
|
7
7
|
import { once } from "events";
|
|
8
|
-
import { lookup } from "node:dns/promises";
|
|
9
8
|
import { spawn } from "node:child_process";
|
|
9
|
+
import { lookup } from "node:dns/promises";
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
import * as path from "node:path";
|
|
12
12
|
import * as os from "node:os";
|
|
@@ -5773,6 +5773,305 @@ class RtspRestreamProviderImpl {
|
|
|
5773
5773
|
}
|
|
5774
5774
|
}
|
|
5775
5775
|
}
|
|
5776
|
+
function normaliseCodec(codec) {
|
|
5777
|
+
if (!codec) return null;
|
|
5778
|
+
const c = codec.toLowerCase();
|
|
5779
|
+
if (c === "h264" || c === "avc" || c === "avc1") return "H264";
|
|
5780
|
+
if (c === "h265" || c === "hevc" || c === "hvc1" || c === "hev1") return "H265";
|
|
5781
|
+
return null;
|
|
5782
|
+
}
|
|
5783
|
+
function pipelineKeyFor(deviceId, videoCodec, audioCodec, width, height) {
|
|
5784
|
+
return `${deviceId}|v=${videoCodec}|a=${audioCodec}|${width}x${height}`;
|
|
5785
|
+
}
|
|
5786
|
+
function pickVideoEncoder(target, encoders) {
|
|
5787
|
+
if (!encoders) return target === "H264" ? "libx264" : "libx265";
|
|
5788
|
+
return target === "H264" ? encoders.defaultH264 : encoders.defaultH265;
|
|
5789
|
+
}
|
|
5790
|
+
function pickAudioEncoderArgs(audio) {
|
|
5791
|
+
switch (audio) {
|
|
5792
|
+
case "AAC":
|
|
5793
|
+
return ["-c:a", "aac", "-b:a", "128k", "-ar", "48000", "-ac", "2"];
|
|
5794
|
+
case "Opus":
|
|
5795
|
+
return ["-c:a", "libopus", "-b:a", "64k", "-ar", "48000", "-ac", "2"];
|
|
5796
|
+
case "PCMU":
|
|
5797
|
+
return ["-c:a", "pcm_mulaw", "-ar", "8000", "-ac", "1"];
|
|
5798
|
+
case "none":
|
|
5799
|
+
return ["-an"];
|
|
5800
|
+
default:
|
|
5801
|
+
return ["-c:a", "aac", "-b:a", "128k"];
|
|
5802
|
+
}
|
|
5803
|
+
}
|
|
5804
|
+
class TranscodePipelineManager {
|
|
5805
|
+
entries = /* @__PURE__ */ new Map();
|
|
5806
|
+
logger;
|
|
5807
|
+
api;
|
|
5808
|
+
cameraStreamLookup;
|
|
5809
|
+
rtspEntryLookup;
|
|
5810
|
+
localRtspPort;
|
|
5811
|
+
releaseGraceMs;
|
|
5812
|
+
ffmpegPath;
|
|
5813
|
+
constructor(opts) {
|
|
5814
|
+
this.logger = opts.logger;
|
|
5815
|
+
this.api = opts.api;
|
|
5816
|
+
this.cameraStreamLookup = opts.cameraStreamLookup;
|
|
5817
|
+
this.rtspEntryLookup = opts.rtspEntryLookup;
|
|
5818
|
+
this.localRtspPort = opts.localRtspPort;
|
|
5819
|
+
this.releaseGraceMs = opts.releaseGraceMs ?? 5e3;
|
|
5820
|
+
this.ffmpegPath = opts.ffmpegPath ?? "ffmpeg";
|
|
5821
|
+
}
|
|
5822
|
+
async acquire(input) {
|
|
5823
|
+
const wantedVideo = input.videoCodec;
|
|
5824
|
+
const wantedAudio = input.audioCodec ?? "AAC";
|
|
5825
|
+
const direct = this.findDirectMatch(input.deviceId, wantedVideo, input.maxResolution);
|
|
5826
|
+
if (direct) {
|
|
5827
|
+
const key2 = pipelineKeyFor(
|
|
5828
|
+
input.deviceId,
|
|
5829
|
+
direct.videoCodec,
|
|
5830
|
+
wantedAudio,
|
|
5831
|
+
direct.resolution.width,
|
|
5832
|
+
direct.resolution.height
|
|
5833
|
+
);
|
|
5834
|
+
return this.shareOrCreate(key2, () => ({
|
|
5835
|
+
url: direct.url,
|
|
5836
|
+
videoCodec: direct.videoCodec,
|
|
5837
|
+
audioCodec: "passthrough",
|
|
5838
|
+
resolution: direct.resolution,
|
|
5839
|
+
transcoded: false,
|
|
5840
|
+
encoder: "copy",
|
|
5841
|
+
pipelineKey: key2
|
|
5842
|
+
}), null);
|
|
5843
|
+
}
|
|
5844
|
+
const sourceStream = this.pickTranscodeSource(input.deviceId, input.maxResolution);
|
|
5845
|
+
if (!sourceStream || !sourceStream.url) {
|
|
5846
|
+
throw new Error(
|
|
5847
|
+
`getStreamWithCodec: no source stream available for device ${input.deviceId}`
|
|
5848
|
+
);
|
|
5849
|
+
}
|
|
5850
|
+
const targetCodec = wantedVideo === "auto" ? normaliseCodec(sourceStream.codec) ?? "H264" : wantedVideo;
|
|
5851
|
+
const targetResolution = input.maxResolution ?? sourceStream.resolution ?? { width: 1280, height: 720 };
|
|
5852
|
+
const key = pipelineKeyFor(
|
|
5853
|
+
input.deviceId,
|
|
5854
|
+
targetCodec,
|
|
5855
|
+
wantedAudio,
|
|
5856
|
+
targetResolution.width,
|
|
5857
|
+
targetResolution.height
|
|
5858
|
+
);
|
|
5859
|
+
const existing = this.entries.get(key);
|
|
5860
|
+
if (existing) {
|
|
5861
|
+
return this.shareEntry(existing).source;
|
|
5862
|
+
}
|
|
5863
|
+
const encoders = await this.loadHardwareEncoders();
|
|
5864
|
+
const encoder = pickVideoEncoder(targetCodec, encoders);
|
|
5865
|
+
const dialableUrl = this.allocateLoopbackUrl(input.deviceId, key);
|
|
5866
|
+
const child = this.spawnFfmpeg({
|
|
5867
|
+
sourceUrl: sourceStream.url,
|
|
5868
|
+
outputUrl: dialableUrl,
|
|
5869
|
+
encoder,
|
|
5870
|
+
targetCodec,
|
|
5871
|
+
width: targetResolution.width,
|
|
5872
|
+
height: targetResolution.height,
|
|
5873
|
+
audio: wantedAudio,
|
|
5874
|
+
tag: input.tag ?? `stream-with-codec:${input.deviceId}`
|
|
5875
|
+
});
|
|
5876
|
+
const source = {
|
|
5877
|
+
url: dialableUrl,
|
|
5878
|
+
videoCodec: targetCodec,
|
|
5879
|
+
audioCodec: wantedAudio,
|
|
5880
|
+
resolution: targetResolution,
|
|
5881
|
+
transcoded: true,
|
|
5882
|
+
encoder,
|
|
5883
|
+
pipelineKey: key
|
|
5884
|
+
};
|
|
5885
|
+
const entry = { key, source, child, refcount: 1, releaseTimer: null };
|
|
5886
|
+
this.entries.set(key, entry);
|
|
5887
|
+
this.logger.info("Transcode pipeline started", {
|
|
5888
|
+
meta: { key, encoder, sourceUrl: sourceStream.url, outputUrl: dialableUrl }
|
|
5889
|
+
});
|
|
5890
|
+
if (child) {
|
|
5891
|
+
child.once("exit", (code, signal) => {
|
|
5892
|
+
this.logger.info("Transcode pipeline exited", {
|
|
5893
|
+
meta: { key, code, signal }
|
|
5894
|
+
});
|
|
5895
|
+
const cur = this.entries.get(key);
|
|
5896
|
+
if (cur && cur.child === child) {
|
|
5897
|
+
this.entries.delete(key);
|
|
5898
|
+
}
|
|
5899
|
+
});
|
|
5900
|
+
}
|
|
5901
|
+
return source;
|
|
5902
|
+
}
|
|
5903
|
+
release(pipelineKey) {
|
|
5904
|
+
const entry = this.entries.get(pipelineKey);
|
|
5905
|
+
if (!entry) return { released: false, refcount: 0 };
|
|
5906
|
+
entry.refcount = Math.max(0, entry.refcount - 1);
|
|
5907
|
+
if (entry.refcount > 0) {
|
|
5908
|
+
return { released: true, refcount: entry.refcount };
|
|
5909
|
+
}
|
|
5910
|
+
if (entry.releaseTimer) {
|
|
5911
|
+
clearTimeout(entry.releaseTimer);
|
|
5912
|
+
}
|
|
5913
|
+
entry.releaseTimer = setTimeout(() => {
|
|
5914
|
+
const cur = this.entries.get(pipelineKey);
|
|
5915
|
+
if (!cur || cur.refcount > 0) return;
|
|
5916
|
+
this.shutdownEntry(cur);
|
|
5917
|
+
}, this.releaseGraceMs);
|
|
5918
|
+
return { released: true, refcount: 0 };
|
|
5919
|
+
}
|
|
5920
|
+
shutdownAll() {
|
|
5921
|
+
for (const entry of this.entries.values()) {
|
|
5922
|
+
this.shutdownEntry(entry);
|
|
5923
|
+
}
|
|
5924
|
+
this.entries.clear();
|
|
5925
|
+
}
|
|
5926
|
+
findDirectMatch(deviceId, wanted, maxRes) {
|
|
5927
|
+
if (wanted === "auto") return null;
|
|
5928
|
+
const streams = this.cameraStreamLookup(deviceId);
|
|
5929
|
+
for (const s of streams) {
|
|
5930
|
+
if (!s.url) continue;
|
|
5931
|
+
const codec = normaliseCodec(s.codec);
|
|
5932
|
+
if (codec !== wanted) continue;
|
|
5933
|
+
if (maxRes && s.resolution) {
|
|
5934
|
+
if (s.resolution.width > maxRes.width || s.resolution.height > maxRes.height) continue;
|
|
5935
|
+
}
|
|
5936
|
+
const resolution = s.resolution ?? { width: 1280, height: 720 };
|
|
5937
|
+
return { url: s.url, videoCodec: codec, resolution };
|
|
5938
|
+
}
|
|
5939
|
+
return null;
|
|
5940
|
+
}
|
|
5941
|
+
pickTranscodeSource(deviceId, maxRes) {
|
|
5942
|
+
const streams = this.cameraStreamLookup(deviceId);
|
|
5943
|
+
if (streams.length === 0) return null;
|
|
5944
|
+
if (!maxRes) return streams[0] ?? null;
|
|
5945
|
+
let best = null;
|
|
5946
|
+
let bestArea = -1;
|
|
5947
|
+
for (const s of streams) {
|
|
5948
|
+
if (!s.url) continue;
|
|
5949
|
+
const w = s.resolution?.width ?? 1280;
|
|
5950
|
+
const h = s.resolution?.height ?? 720;
|
|
5951
|
+
if (w > maxRes.width || h > maxRes.height) continue;
|
|
5952
|
+
const area = w * h;
|
|
5953
|
+
if (area > bestArea) {
|
|
5954
|
+
bestArea = area;
|
|
5955
|
+
best = s;
|
|
5956
|
+
}
|
|
5957
|
+
}
|
|
5958
|
+
if (best) return best;
|
|
5959
|
+
let smallest = null;
|
|
5960
|
+
let smallestArea = Number.MAX_SAFE_INTEGER;
|
|
5961
|
+
for (const s of streams) {
|
|
5962
|
+
if (!s.url) continue;
|
|
5963
|
+
const w = s.resolution?.width ?? 1280;
|
|
5964
|
+
const h = s.resolution?.height ?? 720;
|
|
5965
|
+
const area = w * h;
|
|
5966
|
+
if (area < smallestArea) {
|
|
5967
|
+
smallestArea = area;
|
|
5968
|
+
smallest = s;
|
|
5969
|
+
}
|
|
5970
|
+
}
|
|
5971
|
+
return smallest;
|
|
5972
|
+
}
|
|
5973
|
+
allocateLoopbackUrl(deviceId, pipelineKey) {
|
|
5974
|
+
const port = this.localRtspPort();
|
|
5975
|
+
const token = `t-${deviceId}-${randomUUID().slice(0, 8)}`;
|
|
5976
|
+
if (!port) {
|
|
5977
|
+
return `rtsp://0.0.0.0:0/${token}`;
|
|
5978
|
+
}
|
|
5979
|
+
return `rtsp://127.0.0.1:${port}/${token}`;
|
|
5980
|
+
}
|
|
5981
|
+
async loadHardwareEncoders() {
|
|
5982
|
+
if (!this.api) return null;
|
|
5983
|
+
try {
|
|
5984
|
+
const api = this.api;
|
|
5985
|
+
const probe = api["platformProbe"];
|
|
5986
|
+
if (!probe || typeof probe !== "object") return null;
|
|
5987
|
+
const fn = probe["getHardwareEncoders"];
|
|
5988
|
+
if (!fn || typeof fn !== "object") return null;
|
|
5989
|
+
const query = fn["query"];
|
|
5990
|
+
if (typeof query !== "function") return null;
|
|
5991
|
+
const callable = query;
|
|
5992
|
+
return await callable();
|
|
5993
|
+
} catch (err) {
|
|
5994
|
+
this.logger.warn("platformProbe.getHardwareEncoders failed — using software fallback", {
|
|
5995
|
+
meta: { error: errMsg(err) }
|
|
5996
|
+
});
|
|
5997
|
+
return null;
|
|
5998
|
+
}
|
|
5999
|
+
}
|
|
6000
|
+
spawnFfmpeg(opts) {
|
|
6001
|
+
const args = [
|
|
6002
|
+
"-hide_banner",
|
|
6003
|
+
"-loglevel",
|
|
6004
|
+
"warning",
|
|
6005
|
+
"-rtsp_transport",
|
|
6006
|
+
"tcp",
|
|
6007
|
+
"-i",
|
|
6008
|
+
opts.sourceUrl,
|
|
6009
|
+
"-vf",
|
|
6010
|
+
`scale=${opts.width}:${opts.height}`,
|
|
6011
|
+
"-c:v",
|
|
6012
|
+
opts.encoder,
|
|
6013
|
+
...pickAudioEncoderArgs(opts.audio),
|
|
6014
|
+
"-f",
|
|
6015
|
+
"rtsp",
|
|
6016
|
+
"-rtsp_transport",
|
|
6017
|
+
"tcp",
|
|
6018
|
+
opts.outputUrl
|
|
6019
|
+
];
|
|
6020
|
+
try {
|
|
6021
|
+
const child = spawn(this.ffmpegPath, args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
6022
|
+
child.stderr?.setEncoding("utf8");
|
|
6023
|
+
child.stderr?.on("data", (chunk) => {
|
|
6024
|
+
this.logger.debug("ffmpeg", { meta: { tag: opts.tag, line: chunk.trim() } });
|
|
6025
|
+
});
|
|
6026
|
+
child.once("error", (err) => {
|
|
6027
|
+
this.logger.error("ffmpeg spawn error", {
|
|
6028
|
+
meta: { tag: opts.tag, error: errMsg(err) }
|
|
6029
|
+
});
|
|
6030
|
+
});
|
|
6031
|
+
return child;
|
|
6032
|
+
} catch (err) {
|
|
6033
|
+
this.logger.error("Failed to spawn ffmpeg", {
|
|
6034
|
+
meta: { tag: opts.tag, error: errMsg(err) }
|
|
6035
|
+
});
|
|
6036
|
+
return null;
|
|
6037
|
+
}
|
|
6038
|
+
}
|
|
6039
|
+
shareEntry(entry) {
|
|
6040
|
+
entry.refcount += 1;
|
|
6041
|
+
if (entry.releaseTimer) {
|
|
6042
|
+
clearTimeout(entry.releaseTimer);
|
|
6043
|
+
entry.releaseTimer = null;
|
|
6044
|
+
}
|
|
6045
|
+
return entry;
|
|
6046
|
+
}
|
|
6047
|
+
shareOrCreate(key, factory, child) {
|
|
6048
|
+
const existing = this.entries.get(key);
|
|
6049
|
+
if (existing) {
|
|
6050
|
+
this.shareEntry(existing);
|
|
6051
|
+
return existing.source;
|
|
6052
|
+
}
|
|
6053
|
+
const source = factory();
|
|
6054
|
+
const entry = { key, source, child, refcount: 1, releaseTimer: null };
|
|
6055
|
+
this.entries.set(key, entry);
|
|
6056
|
+
return source;
|
|
6057
|
+
}
|
|
6058
|
+
shutdownEntry(entry) {
|
|
6059
|
+
if (entry.releaseTimer) {
|
|
6060
|
+
clearTimeout(entry.releaseTimer);
|
|
6061
|
+
entry.releaseTimer = null;
|
|
6062
|
+
}
|
|
6063
|
+
if (entry.child && !entry.child.killed) {
|
|
6064
|
+
try {
|
|
6065
|
+
entry.child.kill("SIGTERM");
|
|
6066
|
+
} catch (err) {
|
|
6067
|
+
this.logger.warn("ffmpeg kill error", {
|
|
6068
|
+
meta: { key: entry.key, error: errMsg(err) }
|
|
6069
|
+
});
|
|
6070
|
+
}
|
|
6071
|
+
}
|
|
6072
|
+
this.entries.delete(entry.key);
|
|
6073
|
+
}
|
|
6074
|
+
}
|
|
5776
6075
|
const PUSH_KINDS = /* @__PURE__ */ new Set(["push-annexb"]);
|
|
5777
6076
|
function findProfileForStream(map, camStreamId) {
|
|
5778
6077
|
for (const profile of CAM_PROFILE_ORDER) {
|
|
@@ -5839,11 +6138,42 @@ class StreamBrokerManager {
|
|
|
5839
6138
|
api = null;
|
|
5840
6139
|
defaultPreBufferSec = 10;
|
|
5841
6140
|
webrtcServer = null;
|
|
6141
|
+
/**
|
|
6142
|
+
* Transcode-pipeline manager — backs `getStreamWithCodec`. Lazily
|
|
6143
|
+
* created on first acquire, lives until `destroyAll`.
|
|
6144
|
+
*/
|
|
6145
|
+
transcodeManager = null;
|
|
5842
6146
|
constructor(staticDecoders, logger) {
|
|
5843
6147
|
this.staticDecoders = staticDecoders ?? [];
|
|
5844
6148
|
this.logger = logger;
|
|
5845
6149
|
this.rtspProvider = new RtspRestreamProviderImpl(this.rtspServer, logger.child("rtsp-restream"));
|
|
5846
6150
|
}
|
|
6151
|
+
/**
|
|
6152
|
+
* Lazily create the transcode manager wired with closures rather than
|
|
6153
|
+
* direct refs so it only sees the surface it needs.
|
|
6154
|
+
*/
|
|
6155
|
+
getTranscodeManager() {
|
|
6156
|
+
if (!this.transcodeManager) {
|
|
6157
|
+
this.transcodeManager = new TranscodePipelineManager({
|
|
6158
|
+
logger: this.logger.child("transcode"),
|
|
6159
|
+
api: this.api,
|
|
6160
|
+
cameraStreamLookup: (deviceId) => this.getCameraStreamsForDevice(deviceId),
|
|
6161
|
+
rtspEntryLookup: (brokerId) => {
|
|
6162
|
+
const entry = this.rtspProvider.getEntry(brokerId);
|
|
6163
|
+
return entry ? { url: entry.url } : null;
|
|
6164
|
+
},
|
|
6165
|
+
localRtspPort: () => this.rtspProvider.getRtspPort()
|
|
6166
|
+
});
|
|
6167
|
+
}
|
|
6168
|
+
return this.transcodeManager;
|
|
6169
|
+
}
|
|
6170
|
+
// ── Cap methods: getStreamWithCodec / releaseStreamWithCodec ─────────
|
|
6171
|
+
async getStreamWithCodec(input) {
|
|
6172
|
+
return this.getTranscodeManager().acquire(input);
|
|
6173
|
+
}
|
|
6174
|
+
async releaseStreamWithCodec(input) {
|
|
6175
|
+
return this.getTranscodeManager().release(input.pipelineKey);
|
|
6176
|
+
}
|
|
5847
6177
|
// ── Decoder resolution ───────────────────────────────────────────────
|
|
5848
6178
|
//
|
|
5849
6179
|
// Routes every decoder call through `ctx.api.decoder.*` (tRPC). The
|
|
@@ -6293,6 +6623,10 @@ class StreamBrokerManager {
|
|
|
6293
6623
|
clearInterval(this.streamHealthTimer);
|
|
6294
6624
|
this.streamHealthTimer = void 0;
|
|
6295
6625
|
}
|
|
6626
|
+
if (this.transcodeManager) {
|
|
6627
|
+
this.transcodeManager.shutdownAll();
|
|
6628
|
+
this.transcodeManager = null;
|
|
6629
|
+
}
|
|
6296
6630
|
const stopPromises = [...this.brokers.values()].map((broker) => broker.stop());
|
|
6297
6631
|
await Promise.all(stopPromises);
|
|
6298
6632
|
this.brokers.clear();
|
|
@@ -9114,6 +9448,73 @@ class BrokerWebrtcServer {
|
|
|
9114
9448
|
}
|
|
9115
9449
|
// ── Session lifecycle ───────────────────────────────────────────────
|
|
9116
9450
|
async createSession(streamId, hints = {}, opts = {}) {
|
|
9451
|
+
const setup = await this.setupSessionForBroker(streamId, hints, opts);
|
|
9452
|
+
try {
|
|
9453
|
+
const offer = await setup.session.createOffer();
|
|
9454
|
+
setup.sessionLogger.info("WebRTC session created", {
|
|
9455
|
+
meta: {
|
|
9456
|
+
sessionId: setup.sessionId,
|
|
9457
|
+
brokerId: setup.brokerId,
|
|
9458
|
+
iceServers: setup.iceServerCount,
|
|
9459
|
+
codec: setup.sessionCodec,
|
|
9460
|
+
useH265Repacketizer: setup.useH265Repacketizer
|
|
9461
|
+
}
|
|
9462
|
+
});
|
|
9463
|
+
return { sessionId: setup.sessionId, sdpOffer: offer.sdp };
|
|
9464
|
+
} catch (err) {
|
|
9465
|
+
this.cleanupSession(setup.sessionId);
|
|
9466
|
+
await setup.session.close().catch(() => {
|
|
9467
|
+
});
|
|
9468
|
+
throw err;
|
|
9469
|
+
}
|
|
9470
|
+
}
|
|
9471
|
+
/**
|
|
9472
|
+
* WHEP-style client-offer signaling: caller has already constructed
|
|
9473
|
+
* an SDP offer (Alexa / WHEP / mobile clients that do
|
|
9474
|
+
* `createOffer()` locally) and posts it here. We build the same
|
|
9475
|
+
* broker subscription + AdaptiveSession plumbing as `createSession`,
|
|
9476
|
+
* then call `session.handleOffer(clientOffer)` to produce the
|
|
9477
|
+
* server's SDP answer.
|
|
9478
|
+
*
|
|
9479
|
+
* The session lifecycle is identical to `createSession` — broker
|
|
9480
|
+
* unsubscribe and `closeSession(sessionId)` still apply — so the
|
|
9481
|
+
* RTC controller / Alexa handler closes the session the same way.
|
|
9482
|
+
*/
|
|
9483
|
+
async handleOffer(streamId, clientOfferSdp, hints = {}, opts = {}) {
|
|
9484
|
+
const setup = await this.setupSessionForBroker(streamId, hints, opts);
|
|
9485
|
+
try {
|
|
9486
|
+
const answer = await setup.session.handleOffer({ sdp: clientOfferSdp, type: "offer" });
|
|
9487
|
+
setup.sessionLogger.info("WebRTC session created (client-offer)", {
|
|
9488
|
+
meta: {
|
|
9489
|
+
sessionId: setup.sessionId,
|
|
9490
|
+
brokerId: setup.brokerId,
|
|
9491
|
+
iceServers: setup.iceServerCount,
|
|
9492
|
+
codec: setup.sessionCodec,
|
|
9493
|
+
useH265Repacketizer: setup.useH265Repacketizer
|
|
9494
|
+
}
|
|
9495
|
+
});
|
|
9496
|
+
return { sessionId: setup.sessionId, sdpAnswer: answer.sdp };
|
|
9497
|
+
} catch (err) {
|
|
9498
|
+
this.cleanupSession(setup.sessionId);
|
|
9499
|
+
await setup.session.close().catch(() => {
|
|
9500
|
+
});
|
|
9501
|
+
throw err;
|
|
9502
|
+
}
|
|
9503
|
+
}
|
|
9504
|
+
/**
|
|
9505
|
+
* Shared session bring-up: resolve broker → subscribe to encoded
|
|
9506
|
+
* data + RTP → flush pre-buffer → seed H.265 codec info → register
|
|
9507
|
+
* the SessionEntry. Returns the `AdaptiveSession` ready for SDP
|
|
9508
|
+
* negotiation (either `createOffer()` for the server-offer flow or
|
|
9509
|
+
* `handleOffer(clientOffer)` for the WHEP / Alexa flow).
|
|
9510
|
+
*
|
|
9511
|
+
* The returned session has its `onClosed` hook already wired to
|
|
9512
|
+
* cleanup, and the SessionEntry is already in `this.sessions` keyed
|
|
9513
|
+
* by `sessionId` — callers MUST cleanup + close on negotiation
|
|
9514
|
+
* failure (both `createSession` and `handleOffer` do this in their
|
|
9515
|
+
* catch blocks).
|
|
9516
|
+
*/
|
|
9517
|
+
async setupSessionForBroker(streamId, hints, opts) {
|
|
9117
9518
|
if (this.stopped) throw new Error("Server stopped");
|
|
9118
9519
|
const brokerId = this.resolveBrokerId(streamId, hints);
|
|
9119
9520
|
const broker = this.brokers.get(brokerId);
|
|
@@ -9225,7 +9626,11 @@ class BrokerWebrtcServer {
|
|
|
9225
9626
|
});
|
|
9226
9627
|
}
|
|
9227
9628
|
}
|
|
9228
|
-
const
|
|
9629
|
+
const requestedSessionId = opts.sessionId;
|
|
9630
|
+
if (requestedSessionId !== void 0 && this.sessions.has(requestedSessionId)) {
|
|
9631
|
+
throw new Error(`Session id already in use: ${requestedSessionId}`);
|
|
9632
|
+
}
|
|
9633
|
+
const sessionId = requestedSessionId ?? crypto__default.randomUUID();
|
|
9229
9634
|
const iceServers = await this.resolveIceServers();
|
|
9230
9635
|
const session = new AdaptiveSession({
|
|
9231
9636
|
sessionId,
|
|
@@ -9285,18 +9690,15 @@ class BrokerWebrtcServer {
|
|
|
9285
9690
|
const entry = { session, brokerId, unsubBroker, unsubRtp, unsubSdpParams, closeSource };
|
|
9286
9691
|
this.sessions.set(sessionId, entry);
|
|
9287
9692
|
session.onClosed = () => this.cleanupSession(sessionId);
|
|
9288
|
-
|
|
9289
|
-
|
|
9290
|
-
|
|
9291
|
-
|
|
9292
|
-
|
|
9293
|
-
|
|
9294
|
-
|
|
9295
|
-
|
|
9296
|
-
|
|
9297
|
-
});
|
|
9298
|
-
throw err;
|
|
9299
|
-
}
|
|
9693
|
+
return {
|
|
9694
|
+
sessionId,
|
|
9695
|
+
session,
|
|
9696
|
+
sessionLogger,
|
|
9697
|
+
brokerId,
|
|
9698
|
+
sessionCodec,
|
|
9699
|
+
useH265Repacketizer,
|
|
9700
|
+
iceServerCount: iceServers.length
|
|
9701
|
+
};
|
|
9300
9702
|
}
|
|
9301
9703
|
async handleAnswer(sessionId, sdpAnswer) {
|
|
9302
9704
|
const entry = this.sessions.get(sessionId);
|
|
@@ -9577,6 +9979,15 @@ class WebrtcSessionProvider {
|
|
|
9577
9979
|
const streamingDebug = typeof this.manager.isStreamingDebug === "function" ? this.manager.isStreamingDebug(input.deviceId) : false;
|
|
9578
9980
|
return this.webrtcServer.createSession(streamId, input.hints, { streamingDebug });
|
|
9579
9981
|
}
|
|
9982
|
+
async handleOffer(input) {
|
|
9983
|
+
const target = input.target ?? { kind: "adaptive" };
|
|
9984
|
+
const streamId = this.resolveTargetToStreamId(input.deviceId, target);
|
|
9985
|
+
const streamingDebug = typeof this.manager.isStreamingDebug === "function" ? this.manager.isStreamingDebug(input.deviceId) : false;
|
|
9986
|
+
return this.webrtcServer.handleOffer(streamId, input.sdpOffer, void 0, {
|
|
9987
|
+
streamingDebug,
|
|
9988
|
+
sessionId: input.sessionId
|
|
9989
|
+
});
|
|
9990
|
+
}
|
|
9580
9991
|
async handleAnswer(input) {
|
|
9581
9992
|
await this.webrtcServer.handleAnswer(input.sessionId, input.sdpAnswer);
|
|
9582
9993
|
}
|