@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.
Files changed (46) hide show
  1. package/assets/icon.svg +6 -0
  2. package/dist/audio-analyzer/index.js +16 -16
  3. package/dist/audio-analyzer/index.mjs +1 -1
  4. package/dist/audio-codec-nodeav/index.js +7 -7
  5. package/dist/audio-codec-nodeav/index.mjs +1 -1
  6. package/dist/decoder-nodeav/index.js +12 -12
  7. package/dist/decoder-nodeav/index.mjs +1 -1
  8. package/dist/detection-pipeline/index.js +45 -45
  9. package/dist/detection-pipeline/index.js.map +1 -1
  10. package/dist/detection-pipeline/index.mjs +1 -1
  11. package/dist/index-BhOycEVH.js +13867 -0
  12. package/dist/index-BhOycEVH.js.map +1 -0
  13. package/dist/index-FxfFGsiL.mjs +13868 -0
  14. package/dist/index-FxfFGsiL.mjs.map +1 -0
  15. package/dist/motion-wasm/index.js +8 -8
  16. package/dist/motion-wasm/index.mjs +1 -1
  17. package/dist/pipeline-runner/index.js +76 -77
  18. package/dist/pipeline-runner/index.js.map +1 -1
  19. package/dist/pipeline-runner/index.mjs +50 -51
  20. package/dist/pipeline-runner/index.mjs.map +1 -1
  21. package/dist/stream-broker/@mf-types.zip +0 -0
  22. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-ChoHjdk6.mjs +17 -0
  23. 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
  24. package/dist/stream-broker/_stub.js +1 -1
  25. 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
  26. package/dist/stream-broker/{hostInit-wnZIaWA5.mjs → hostInit-fG6oFw4t.mjs} +6 -6
  27. package/dist/stream-broker/{index-DadYrR5H.mjs → index-BOmtakNy.mjs} +67 -44
  28. package/dist/stream-broker/{index-BQ_NNiX0.mjs → index-Bpv0NSqI.mjs} +5414 -5170
  29. package/dist/stream-broker/{index-Dwc0KrUN.mjs → index-l13fl8lu.mjs} +4249 -4164
  30. package/dist/stream-broker/index.js +521 -110
  31. package/dist/stream-broker/index.js.map +1 -1
  32. package/dist/stream-broker/index.mjs +426 -15
  33. package/dist/stream-broker/index.mjs.map +1 -1
  34. package/dist/stream-broker/remoteEntry.js +1 -1
  35. package/package.json +24 -9
  36. package/python/postprocessors/__pycache__/__init__.cpython-312.pyc +0 -0
  37. package/python/postprocessors/__pycache__/arcface.cpython-312.pyc +0 -0
  38. package/python/postprocessors/__pycache__/ctc.cpython-312.pyc +0 -0
  39. package/python/postprocessors/__pycache__/saliency.cpython-312.pyc +0 -0
  40. package/python/postprocessors/__pycache__/scrfd.cpython-312.pyc +0 -0
  41. package/python/postprocessors/__pycache__/softmax.cpython-312.pyc +0 -0
  42. package/python/postprocessors/__pycache__/yamnet.cpython-312.pyc +0 -0
  43. package/python/postprocessors/__pycache__/yolo.cpython-312.pyc +0 -0
  44. package/python/postprocessors/__pycache__/yolo_seg.cpython-312.pyc +0 -0
  45. package/dist/stream-broker/__mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-ezH__dL2.mjs +0 -17
  46. 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 types = require("@camstack/types");
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(types.errMsg(err));
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: types.errMsg(err) }
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: types.errMsg(err) }
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", types.errMsg(err));
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, types.errMsg(err));
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(index) {
2216
- return AAC_SAMPLE_RATES[index] ?? null;
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" ? types.maskUrlCredentials(src.url ?? "") : "";
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: types.maskUrlCredentials(source.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: types.errMsg(err) } });
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: types.errMsg(err) }
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: types.errMsg(err) }
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: types.errMsg(err) } });
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: types.errMsg(err) }
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: types.errMsg(err) }
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: types.errMsg(err) } });
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: types.errMsg(err) }
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: types.errMsg(err) }
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: types.errMsg(err) }
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: types.maskUrlCredentials(source.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: types.errMsg(err), ...this.sourceMetaForLog() }
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: types.errMsg(err) }
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: types.errMsg(err) }
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: types.errMsg(err), ...this.sourceMetaForLog() }
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: types.errMsg(err) } });
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: types.errMsg(err), ...this.sourceMetaForLog() }
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: types.errMsg(err) }
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: types.errMsg(err) } });
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: types.errMsg(err) } });
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: types.errMsg(err) }
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 types.CAM_PROFILE_ORDER) {
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 types.RingBuffer(FRAME_BUFFER_CAPACITY);
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: types.EventCategory.BatteryOnStatusChanged }, (event) => {
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: types.EventCategory.DeviceOffline }, handlerForReason("offline"));
6086
- bus.subscribe({ category: types.EventCategory.DeviceDisabled }, handlerForReason("disabled"));
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 types.CAM_PROFILE_ORDER) {
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: types.errMsg(err) }
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, types.DeviceFeature.BatteryOperated)) return 0;
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 (!types.CAM_PROFILE_ORDER.includes(profile)) continue;
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 types.CAM_PROFILE_ORDER) {
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 types.CAM_PROFILE_ORDER) {
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 types.CAM_PROFILE_ORDER) {
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 types.CAM_PROFILE_ORDER) {
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(types.DeviceFeature.BatteryOperated) ? { allowStall: true } : {};
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: types.errMsg(err) }
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: types.errMsg(err) }
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 types.CAM_PROFILE_ORDER) {
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(types.EventCategory.StreamBrokerOnCamStreamDemand, deviceId, { deviceId, camStreamId, profile });
7424
+ this.emitCapEvent(index.EventCategory.StreamBrokerOnCamStreamDemand, deviceId, { deviceId, camStreamId, profile });
7091
7425
  }
7092
7426
  emitCamStreamIdle(deviceId, camStreamId) {
7093
- this.emitCapEvent(types.EventCategory.StreamBrokerOnCamStreamIdle, deviceId, { deviceId, camStreamId });
7427
+ this.emitCapEvent(index.EventCategory.StreamBrokerOnCamStreamIdle, deviceId, { deviceId, camStreamId });
7094
7428
  }
7095
7429
  emitRequestStreamSourceRefresh(deviceId, camStreamId, brokerId) {
7096
- this.emitCapEvent(types.EventCategory.StreamBrokerOnRequestStreamSourceRefresh, deviceId, {
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: types.errMsg(err) } });
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 index = (start + i) % this.jitterSize;
7529
- const packet = this.pending[index];
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[index] = void 0;
7870
+ this.pending[index2] = void 0;
7537
7871
  ret.push(packet);
7538
7872
  } else if (sd === 1) {
7539
- this.pending[index] = void 0;
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, index) => {
7858
- if (index !== 0)
8191
+ nalus.forEach((packetized, index2) => {
8192
+ if (index2 !== 0)
7859
8193
  this.extraPackets++;
7860
- const marker = hadMarker && index === nalus.length - 1;
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: types.errMsg(err) } });
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: types.errMsg(err), len: rtpData.length }
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: types.errMsg(err) }
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: types.errMsg(e) }
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: types.errMsg(err) }
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: types.errMsg(err) } });
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: types.errMsg(err) } });
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: types.errMsg(e) }
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: types.errMsg(err) }
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: types.errMsg(err) } });
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 sessionId = crypto.randomUUID();
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: types.errMsg(err) }
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: types.errMsg(err) }
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
- try {
9332
- const offer = await session.createOffer();
9333
- sessionLogger.info("WebRTC session created", {
9334
- meta: { sessionId, brokerId, iceServers: iceServers.length, codec: sessionCodec, useH265Repacketizer }
9335
- });
9336
- return { sessionId, sdpOffer: offer.sdp };
9337
- } catch (err) {
9338
- this.cleanupSession(sessionId);
9339
- await session.close().catch(() => {
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: types.errMsg(err) } });
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 types.BaseAddon {
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 = types.asJsonObject(addonStore["rtspTokens"]);
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 = types.asJsonObject(addonStore["rtspEnabled"]);
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: types.errMsg(err) } });
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: types.errMsg(err) } });
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 = types.asJsonObject(addonStore["deviceOverrides"]);
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 = types.asJsonObject(raw);
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 = types.asJsonObject(o["preBuffer"]);
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 = types.asJsonObject(v);
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: types.errMsg(err) } });
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 = types.asJsonObject(addonStore["profileMap"]);
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 = types.asJsonObject(rawVal);
10194
+ const val = index.asJsonObject(rawVal);
9784
10195
  if (!val) continue;
9785
- const mapObj = types.asJsonObject(val["map"]) ?? {};
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: types.errMsg(err) } });
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: types.EventCategory.PipelineAgentHwaccelChanged },
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: types.errMsg(err) }
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: types.errMsg(err) }
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: types.streamBrokerCapability, provider: this.brokerManager },
10317
+ { capability: index.streamBrokerCapability, provider: this.brokerManager },
9907
10318
  {
9908
- capability: types.cameraStreamsCapability,
10319
+ capability: index.cameraStreamsCapability,
9909
10320
  provider: cameraStreamsProvider,
9910
10321
  kind: "wrapper",
9911
10322
  defaultActive: true
9912
10323
  },
9913
10324
  {
9914
- capability: types.webrtcSessionCapability,
10325
+ capability: index.webrtcSessionCapability,
9915
10326
  provider: webrtcSessionProvider,
9916
10327
  kind: "wrapper",
9917
10328
  defaultActive: true
9918
10329
  },
9919
- { capability: types.addonWidgetsSourceCapability, provider: widgetsProvider }
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(types.createEvent(
9961
- types.EventCategory.StreamBrokerMetricsSnapshot,
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 = types.errMsg(err);
11348
+ const msg = index.errMsg(err);
10938
11349
  this.rejectPending(connectionId, `auth-failed:${msg}`);
10939
11350
  }
10940
11351
  );