@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.
Files changed (26) hide show
  1. package/dist/stream-broker/@mf-types.zip +0 -0
  2. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-ChoHjdk6.mjs +17 -0
  3. 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
  4. package/dist/stream-broker/_stub.js +1 -1
  5. 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
  6. package/dist/stream-broker/{hostInit-wnZIaWA5.mjs → hostInit-CjVI5LuK.mjs} +6 -6
  7. package/dist/stream-broker/{index-DadYrR5H.mjs → index-BF5Qr03x.mjs} +1 -1
  8. package/dist/stream-broker/{index-BQ_NNiX0.mjs → index-DUJwOcGq.mjs} +5230 -5150
  9. package/dist/stream-broker/{index-Dwc0KrUN.mjs → index-DuBCn5us.mjs} +4244 -4164
  10. package/dist/stream-broker/index.js +425 -14
  11. package/dist/stream-broker/index.js.map +1 -1
  12. package/dist/stream-broker/index.mjs +425 -14
  13. package/dist/stream-broker/index.mjs.map +1 -1
  14. package/dist/stream-broker/remoteEntry.js +1 -1
  15. package/package.json +1 -1
  16. package/python/postprocessors/__pycache__/__init__.cpython-312.pyc +0 -0
  17. package/python/postprocessors/__pycache__/arcface.cpython-312.pyc +0 -0
  18. package/python/postprocessors/__pycache__/ctc.cpython-312.pyc +0 -0
  19. package/python/postprocessors/__pycache__/saliency.cpython-312.pyc +0 -0
  20. package/python/postprocessors/__pycache__/scrfd.cpython-312.pyc +0 -0
  21. package/python/postprocessors/__pycache__/softmax.cpython-312.pyc +0 -0
  22. package/python/postprocessors/__pycache__/yamnet.cpython-312.pyc +0 -0
  23. package/python/postprocessors/__pycache__/yolo.cpython-312.pyc +0 -0
  24. package/python/postprocessors/__pycache__/yolo_seg.cpython-312.pyc +0 -0
  25. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-ezH__dL2.mjs +0 -17
  26. 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 sessionId = crypto__default.randomUUID();
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
- try {
9289
- const offer = await session.createOffer();
9290
- sessionLogger.info("WebRTC session created", {
9291
- meta: { sessionId, brokerId, iceServers: iceServers.length, codec: sessionCodec, useH265Repacketizer }
9292
- });
9293
- return { sessionId, sdpOffer: offer.sdp };
9294
- } catch (err) {
9295
- this.cleanupSession(sessionId);
9296
- await session.close().catch(() => {
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
  }