@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
|
@@ -27,8 +27,8 @@ 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");
|
|
@@ -5816,6 +5816,305 @@ 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: types.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: types.errMsg(err) }
|
|
6072
|
+
});
|
|
6073
|
+
});
|
|
6074
|
+
return child;
|
|
6075
|
+
} catch (err) {
|
|
6076
|
+
this.logger.error("Failed to spawn ffmpeg", {
|
|
6077
|
+
meta: { tag: opts.tag, error: types.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: types.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
6120
|
for (const profile of types.CAM_PROFILE_ORDER) {
|
|
@@ -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
|
|
@@ -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();
|
|
@@ -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,
|
|
@@ -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);
|
|
@@ -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
|
}
|