@apocaliss92/nodelink-js 0.4.36 → 0.5.1-beta.0

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/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  ### [Manager UI (Web Dashboard)](./app/README.md)
10
10
 
11
- A complete web-based management interface for camera configuration and live streaming — no code required. Docker deployment, go2rtc restreamer, real-time events, MQTT, Home Assistant integration.
11
+ A complete web-based management interface for camera configuration and live streaming — no code required. Docker deployment, native RTSP + WebRTC, real-time events, MQTT, Home Assistant integration.
12
12
 
13
13
  ### [Library (`@apocaliss92/nodelink-js`)](./documentation/baichuan-api/README.md)
14
14
 
package/dist/index.cjs CHANGED
@@ -41355,14 +41355,15 @@ function encodeImaAdpcm(pcm, blockSizeBytes) {
41355
41355
  }
41356
41356
 
41357
41357
  // src/reolink/baichuan/utils/audioMulaw.ts
41358
- var import_alawmulaw = require("alawmulaw");
41358
+ var import_alawmulaw = __toESM(require("alawmulaw"), 1);
41359
+ var { mulaw, alaw } = import_alawmulaw.default;
41359
41360
  function mulawToPcm16(bytes) {
41360
41361
  if (bytes.length === 0) return new Int16Array(0);
41361
- return import_alawmulaw.mulaw.decode(bytes);
41362
+ return mulaw.decode(bytes);
41362
41363
  }
41363
41364
  function alawToPcm16(bytes) {
41364
41365
  if (bytes.length === 0) return new Int16Array(0);
41365
- return import_alawmulaw.alaw.decode(bytes);
41366
+ return alaw.decode(bytes);
41366
41367
  }
41367
41368
 
41368
41369
  // src/reolink/baichuan/utils/audioResample.ts
@@ -41645,17 +41646,47 @@ var import_node_events15 = require("events");
41645
41646
  var net6 = __toESM(require("net"), 1);
41646
41647
  var crypto3 = __toESM(require("crypto"), 1);
41647
41648
  var md5Hex = (s) => crypto3.createHash("md5").update(s).digest("hex");
41649
+ function normalizePath(path7) {
41650
+ if (!path7) return "/";
41651
+ let p = path7;
41652
+ if (!p.startsWith("/")) p = "/" + p;
41653
+ while (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
41654
+ return p;
41655
+ }
41656
+ function extractBasePath(url) {
41657
+ if (!url || url === "*") return null;
41658
+ let pathPart;
41659
+ if (url.startsWith("rtsp://") || url.startsWith("rtsps://")) {
41660
+ try {
41661
+ const u = new URL(url.replace(/^rtsps?:/, "http:"));
41662
+ pathPart = u.pathname || "/";
41663
+ } catch {
41664
+ return null;
41665
+ }
41666
+ } else {
41667
+ const q = url.indexOf("?");
41668
+ pathPart = q >= 0 ? url.slice(0, q) : url;
41669
+ }
41670
+ if (pathPart.endsWith("/audiobackchannel")) {
41671
+ pathPart = pathPart.slice(0, -"/audiobackchannel".length);
41672
+ }
41673
+ return normalizePath(pathPart);
41674
+ }
41648
41675
  var BaichuanRtspBackchannelServer = class _BaichuanRtspBackchannelServer extends import_node_events15.EventEmitter {
41649
- api;
41650
- channel;
41651
41676
  listenHost;
41652
41677
  listenPort;
41653
- path;
41654
41678
  logger;
41655
41679
  authCredentials;
41656
41680
  requireAuth;
41657
41681
  authRealm;
41658
- deviceId;
41682
+ /**
41683
+ * Path → route table. Single-camera mode stores one entry; multi-tenant
41684
+ * mode can have many. The internal `singleCameraFallback` flag (below)
41685
+ * controls whether an unmatched request falls through to the sole
41686
+ * registered route (legacy behavior) or returns 404 (strict).
41687
+ */
41688
+ routes = /* @__PURE__ */ new Map();
41689
+ singleCameraFallback;
41659
41690
  server = void 0;
41660
41691
  /** Active backchannel sessions keyed by their per-client unique id. */
41661
41692
  sessionByClient = /* @__PURE__ */ new Map();
@@ -41663,11 +41694,8 @@ var BaichuanRtspBackchannelServer = class _BaichuanRtspBackchannelServer extends
41663
41694
  static NONCE_TTL_MS = 5 * 60 * 1e3;
41664
41695
  constructor(options) {
41665
41696
  super();
41666
- this.api = options.api;
41667
- this.channel = options.channel;
41668
41697
  this.listenHost = options.listenHost ?? "127.0.0.1";
41669
41698
  this.listenPort = options.listenPort ?? 8555;
41670
- this.path = options.path ?? "/talk";
41671
41699
  this.logger = options.logger ?? console;
41672
41700
  this.authCredentials = (options.credentials ?? []).map((c) => ({
41673
41701
  username: c.username,
@@ -41676,11 +41704,88 @@ var BaichuanRtspBackchannelServer = class _BaichuanRtspBackchannelServer extends
41676
41704
  }));
41677
41705
  this.requireAuth = options.requireAuth ?? this.authCredentials.length > 0;
41678
41706
  this.authRealm = options.authRealm ?? "BaichuanRtspBackchannelServer";
41679
- this.deviceId = options.deviceId;
41707
+ const singleCamera = options.api !== void 0 && options.channel !== void 0;
41708
+ const hasRoutes = options.routes !== void 0 && Object.keys(options.routes).length > 0;
41709
+ if (singleCamera && hasRoutes) {
41710
+ throw new Error(
41711
+ "BaichuanRtspBackchannelServer: pass either { api, channel } (single camera) or { routes } (multi tenant), not both"
41712
+ );
41713
+ }
41714
+ if (!singleCamera && options.routes === void 0) {
41715
+ throw new Error(
41716
+ "BaichuanRtspBackchannelServer: provide { api, channel } or { routes } (or both via constructor; addRoute() works too)"
41717
+ );
41718
+ }
41719
+ if (singleCamera) {
41720
+ const path7 = normalizePath(options.path ?? "/talk");
41721
+ this.routes.set(path7, {
41722
+ api: options.api,
41723
+ channel: options.channel,
41724
+ ...options.deviceId !== void 0 ? { deviceId: options.deviceId } : {}
41725
+ });
41726
+ this.singleCameraFallback = true;
41727
+ } else {
41728
+ this.singleCameraFallback = false;
41729
+ if (options.routes) {
41730
+ for (const [path7, route] of Object.entries(options.routes)) {
41731
+ this.routes.set(normalizePath(path7), { ...route });
41732
+ }
41733
+ }
41734
+ }
41680
41735
  }
41681
41736
  get listening() {
41682
41737
  return this.server !== void 0 && this.server.listening;
41683
41738
  }
41739
+ /**
41740
+ * Register (or replace) a camera route at runtime. The path is normalized
41741
+ * (leading slash, no trailing slash). The manager uses this to wire a
41742
+ * single shared port to N cameras as they come online.
41743
+ */
41744
+ addRoute(path7, route) {
41745
+ const normalized = normalizePath(path7);
41746
+ this.routes.set(normalized, { ...route });
41747
+ this.logger.info?.(
41748
+ `[BaichuanRtspBackchannelServer] route registered path=${normalized} channel=${route.channel}${route.deviceId ? ` deviceId="${route.deviceId}"` : ""} totalRoutes=${this.routes.size}`
41749
+ );
41750
+ }
41751
+ /** Remove a route. Returns true if a route was actually removed. */
41752
+ removeRoute(path7) {
41753
+ const normalized = normalizePath(path7);
41754
+ const had = this.routes.delete(normalized);
41755
+ if (had) {
41756
+ this.logger.info?.(
41757
+ `[BaichuanRtspBackchannelServer] route unregistered path=${normalized} totalRoutes=${this.routes.size}`
41758
+ );
41759
+ }
41760
+ return had;
41761
+ }
41762
+ /** Cheap presence check. */
41763
+ hasRoute(path7) {
41764
+ return this.routes.has(normalizePath(path7));
41765
+ }
41766
+ /** Snapshot of currently registered route paths. */
41767
+ listRoutes() {
41768
+ return Array.from(this.routes.keys());
41769
+ }
41770
+ /**
41771
+ * Resolve the {@link BackchannelRoute} for an incoming RTSP request URL.
41772
+ * In single-camera mode this falls back to the only registered route to
41773
+ * preserve permissive legacy behavior; in multi-tenant mode unmatched
41774
+ * paths return undefined so the caller can reply 404.
41775
+ */
41776
+ resolveRouteForRequest(url) {
41777
+ const base = extractBasePath(url);
41778
+ if (base !== null) {
41779
+ const exact = this.routes.get(base);
41780
+ if (exact) return { route: exact, path: base };
41781
+ }
41782
+ if (this.singleCameraFallback && this.routes.size === 1) {
41783
+ const path7 = this.routes.keys().next().value;
41784
+ const route = this.routes.get(path7);
41785
+ return { route, path: path7 };
41786
+ }
41787
+ return void 0;
41788
+ }
41684
41789
  async start() {
41685
41790
  if (this.server) return;
41686
41791
  await new Promise((resolve, reject) => {
@@ -41693,13 +41798,33 @@ var BaichuanRtspBackchannelServer = class _BaichuanRtspBackchannelServer extends
41693
41798
  server.listen(this.listenPort, this.listenHost, () => {
41694
41799
  server.removeListener("error", onError);
41695
41800
  this.server = server;
41801
+ const routeList = Array.from(this.routes.keys()).join(", ") || "(none)";
41696
41802
  this.logger.info?.(
41697
- `[BaichuanRtspBackchannelServer] listening on ${this.listenHost}:${this.listenPort} path=${this.path}`
41803
+ `[BaichuanRtspBackchannelServer] listening on ${this.listenHost}:${this.listenPort} routes=[${routeList}]`
41698
41804
  );
41699
41805
  resolve();
41700
41806
  });
41701
41807
  });
41702
41808
  }
41809
+ /**
41810
+ * Inject an already-accepted client socket from a multiplexer (e.g.
41811
+ * `LocalRtspMux`) that owns the listening port. The mux reads the first
41812
+ * RTSP request line to dispatch on path, then hands the socket over;
41813
+ * any bytes already consumed during routing are replayed via
41814
+ * `socket.unshift()` so the RTSP parser in `handleConnection` sees the
41815
+ * full original request.
41816
+ *
41817
+ * This lets the same backchannel server share a TCP port with the
41818
+ * per-profile video servers when the manager runs in local restreamer
41819
+ * mode. The server doesn't need to be `start()`ed in that mode — `stop()`
41820
+ * still tears down any in-flight talk sessions correctly.
41821
+ */
41822
+ injectSocket(socket, preReadData) {
41823
+ if (preReadData && preReadData.length > 0) {
41824
+ socket.unshift(preReadData);
41825
+ }
41826
+ this.handleConnection(socket);
41827
+ }
41703
41828
  async stop() {
41704
41829
  const server = this.server;
41705
41830
  this.server = void 0;
@@ -41723,7 +41848,7 @@ var BaichuanRtspBackchannelServer = class _BaichuanRtspBackchannelServer extends
41723
41848
  const connectedAt = Date.now();
41724
41849
  let buffer = Buffer.alloc(0);
41725
41850
  this.logger.info?.(
41726
- `[BaichuanRtspBackchannelServer] client connected client=${clientId} path=${this.path}`
41851
+ `[BaichuanRtspBackchannelServer] client connected client=${clientId}`
41727
41852
  );
41728
41853
  this.emit("client", clientId);
41729
41854
  const cleanup = () => {
@@ -41891,9 +42016,17 @@ CSeq: ${cseq}\r
41891
42016
  });
41892
42017
  return;
41893
42018
  case "DESCRIBE": {
42019
+ const resolved = this.resolveRouteForRequest(url);
42020
+ if (!resolved) {
42021
+ this.logger.warn?.(
42022
+ `[BaichuanRtspBackchannelServer] DESCRIBE no route matches url="${url}" client=${clientId} known=[${Array.from(this.routes.keys()).join(", ")}]`
42023
+ );
42024
+ send(404, "Not Found");
42025
+ return;
42026
+ }
41894
42027
  const sdp = this.buildSdp();
41895
42028
  this.logger.debug?.(
41896
- `[BaichuanRtspBackchannelServer] DESCRIBE sdp for ${clientId}:
42029
+ `[BaichuanRtspBackchannelServer] DESCRIBE sdp for ${clientId} path=${resolved.path}:
41897
42030
  ${sdp.trimEnd()}`
41898
42031
  );
41899
42032
  send(
@@ -41901,7 +42034,7 @@ ${sdp.trimEnd()}`
41901
42034
  "OK",
41902
42035
  {
41903
42036
  "Content-Type": "application/sdp",
41904
- "Content-Base": `rtsp://${this.listenHost}:${this.listenPort}${this.path}/`
42037
+ "Content-Base": `rtsp://${this.listenHost}:${this.listenPort}${resolved.path}/`
41905
42038
  },
41906
42039
  sdp
41907
42040
  );
@@ -41915,9 +42048,17 @@ ${sdp.trimEnd()}`
41915
42048
  send(404, "Not Found");
41916
42049
  return;
41917
42050
  }
42051
+ const resolved = this.resolveRouteForRequest(url);
42052
+ if (!resolved) {
42053
+ this.logger.warn?.(
42054
+ `[BaichuanRtspBackchannelServer] SETUP no route matches url="${url}" client=${clientId} known=[${Array.from(this.routes.keys()).join(", ")}]`
42055
+ );
42056
+ send(404, "Not Found");
42057
+ return;
42058
+ }
41918
42059
  const transportLine = requestText.match(/Transport:\s*([^\r\n]+)/i)?.[1] ?? "";
41919
42060
  this.logger.info?.(
41920
- `[BaichuanRtspBackchannelServer] SETUP transport="${transportLine}" client=${clientId}`
42061
+ `[BaichuanRtspBackchannelServer] SETUP transport="${transportLine}" client=${clientId} path=${resolved.path}`
41921
42062
  );
41922
42063
  if (!transportLine.toUpperCase().includes("RTP/AVP/TCP") && !transportLine.toLowerCase().includes("interleaved")) {
41923
42064
  this.logger.warn?.(
@@ -41938,10 +42079,12 @@ ${sdp.trimEnd()}`
41938
42079
  clientId,
41939
42080
  socket,
41940
42081
  rtpChannel: interleaved.rtp,
41941
- rtcpChannel: interleaved.rtcp
42082
+ rtcpChannel: interleaved.rtcp,
42083
+ route: resolved.route,
42084
+ routePath: resolved.path
41942
42085
  });
41943
42086
  this.logger.info?.(
41944
- `[BaichuanRtspBackchannelServer] SETUP ok client=${clientId} session=${sessionId} interleaved=${interleaved.rtp}-${interleaved.rtcp}`
42087
+ `[BaichuanRtspBackchannelServer] SETUP ok client=${clientId} session=${sessionId} interleaved=${interleaved.rtp}-${interleaved.rtcp} path=${resolved.path}`
41945
42088
  );
41946
42089
  send(200, "OK", {
41947
42090
  Transport: `RTP/AVP/TCP;unicast;interleaved=${interleaved.rtp}-${interleaved.rtcp};mode=record`,
@@ -41967,10 +42110,10 @@ ${sdp.trimEnd()}`
41967
42110
  send(200, "OK", { Session: session.sessionId });
41968
42111
  return;
41969
42112
  }
41970
- const apiRef = this.api;
41971
- const channelForCamera = this.channel;
42113
+ const apiRef = session.route.api;
42114
+ const channelForCamera = session.route.channel;
41972
42115
  const loggerRef = this.logger;
41973
- const deviceIdRef = this.deviceId ?? `rtsp-backchannel-${clientId}`;
42116
+ const deviceIdRef = session.route.deviceId ?? `rtsp-backchannel-${clientId}`;
41974
42117
  const handler = new RtspBackchannel({
41975
42118
  openTalkSession: () => apiRef.createDedicatedTalkSession(channelForCamera, {
41976
42119
  deviceId: deviceIdRef,
@@ -41980,7 +42123,7 @@ ${sdp.trimEnd()}`
41980
42123
  });
41981
42124
  const recordStart = Date.now();
41982
42125
  this.logger.info?.(
41983
- `[BaichuanRtspBackchannelServer] RECORD opening TalkSession client=${clientId} session=${session.sessionId} channel=${channelForCamera} deviceId="${deviceIdRef}"`
42126
+ `[BaichuanRtspBackchannelServer] RECORD opening TalkSession client=${clientId} session=${session.sessionId} path=${session.routePath} channel=${channelForCamera} deviceId="${deviceIdRef}"`
41984
42127
  );
41985
42128
  try {
41986
42129
  const talk = await handler.start();