@apocaliss92/nodelink-js 0.5.0 → 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 +1 -1
- package/dist/index.cjs +161 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +82 -17
- package/dist/index.d.ts +101 -35
- package/dist/index.js +161 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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,
|
|
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
|
@@ -41646,17 +41646,47 @@ var import_node_events15 = require("events");
|
|
|
41646
41646
|
var net6 = __toESM(require("net"), 1);
|
|
41647
41647
|
var crypto3 = __toESM(require("crypto"), 1);
|
|
41648
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
|
+
}
|
|
41649
41675
|
var BaichuanRtspBackchannelServer = class _BaichuanRtspBackchannelServer extends import_node_events15.EventEmitter {
|
|
41650
|
-
api;
|
|
41651
|
-
channel;
|
|
41652
41676
|
listenHost;
|
|
41653
41677
|
listenPort;
|
|
41654
|
-
path;
|
|
41655
41678
|
logger;
|
|
41656
41679
|
authCredentials;
|
|
41657
41680
|
requireAuth;
|
|
41658
41681
|
authRealm;
|
|
41659
|
-
|
|
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;
|
|
41660
41690
|
server = void 0;
|
|
41661
41691
|
/** Active backchannel sessions keyed by their per-client unique id. */
|
|
41662
41692
|
sessionByClient = /* @__PURE__ */ new Map();
|
|
@@ -41664,11 +41694,8 @@ var BaichuanRtspBackchannelServer = class _BaichuanRtspBackchannelServer extends
|
|
|
41664
41694
|
static NONCE_TTL_MS = 5 * 60 * 1e3;
|
|
41665
41695
|
constructor(options) {
|
|
41666
41696
|
super();
|
|
41667
|
-
this.api = options.api;
|
|
41668
|
-
this.channel = options.channel;
|
|
41669
41697
|
this.listenHost = options.listenHost ?? "127.0.0.1";
|
|
41670
41698
|
this.listenPort = options.listenPort ?? 8555;
|
|
41671
|
-
this.path = options.path ?? "/talk";
|
|
41672
41699
|
this.logger = options.logger ?? console;
|
|
41673
41700
|
this.authCredentials = (options.credentials ?? []).map((c) => ({
|
|
41674
41701
|
username: c.username,
|
|
@@ -41677,11 +41704,88 @@ var BaichuanRtspBackchannelServer = class _BaichuanRtspBackchannelServer extends
|
|
|
41677
41704
|
}));
|
|
41678
41705
|
this.requireAuth = options.requireAuth ?? this.authCredentials.length > 0;
|
|
41679
41706
|
this.authRealm = options.authRealm ?? "BaichuanRtspBackchannelServer";
|
|
41680
|
-
|
|
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
|
+
}
|
|
41681
41735
|
}
|
|
41682
41736
|
get listening() {
|
|
41683
41737
|
return this.server !== void 0 && this.server.listening;
|
|
41684
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
|
+
}
|
|
41685
41789
|
async start() {
|
|
41686
41790
|
if (this.server) return;
|
|
41687
41791
|
await new Promise((resolve, reject) => {
|
|
@@ -41694,13 +41798,33 @@ var BaichuanRtspBackchannelServer = class _BaichuanRtspBackchannelServer extends
|
|
|
41694
41798
|
server.listen(this.listenPort, this.listenHost, () => {
|
|
41695
41799
|
server.removeListener("error", onError);
|
|
41696
41800
|
this.server = server;
|
|
41801
|
+
const routeList = Array.from(this.routes.keys()).join(", ") || "(none)";
|
|
41697
41802
|
this.logger.info?.(
|
|
41698
|
-
`[BaichuanRtspBackchannelServer] listening on ${this.listenHost}:${this.listenPort}
|
|
41803
|
+
`[BaichuanRtspBackchannelServer] listening on ${this.listenHost}:${this.listenPort} routes=[${routeList}]`
|
|
41699
41804
|
);
|
|
41700
41805
|
resolve();
|
|
41701
41806
|
});
|
|
41702
41807
|
});
|
|
41703
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
|
+
}
|
|
41704
41828
|
async stop() {
|
|
41705
41829
|
const server = this.server;
|
|
41706
41830
|
this.server = void 0;
|
|
@@ -41724,7 +41848,7 @@ var BaichuanRtspBackchannelServer = class _BaichuanRtspBackchannelServer extends
|
|
|
41724
41848
|
const connectedAt = Date.now();
|
|
41725
41849
|
let buffer = Buffer.alloc(0);
|
|
41726
41850
|
this.logger.info?.(
|
|
41727
|
-
`[BaichuanRtspBackchannelServer] client connected client=${clientId}
|
|
41851
|
+
`[BaichuanRtspBackchannelServer] client connected client=${clientId}`
|
|
41728
41852
|
);
|
|
41729
41853
|
this.emit("client", clientId);
|
|
41730
41854
|
const cleanup = () => {
|
|
@@ -41892,9 +42016,17 @@ CSeq: ${cseq}\r
|
|
|
41892
42016
|
});
|
|
41893
42017
|
return;
|
|
41894
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
|
+
}
|
|
41895
42027
|
const sdp = this.buildSdp();
|
|
41896
42028
|
this.logger.debug?.(
|
|
41897
|
-
`[BaichuanRtspBackchannelServer] DESCRIBE sdp for ${clientId}:
|
|
42029
|
+
`[BaichuanRtspBackchannelServer] DESCRIBE sdp for ${clientId} path=${resolved.path}:
|
|
41898
42030
|
${sdp.trimEnd()}`
|
|
41899
42031
|
);
|
|
41900
42032
|
send(
|
|
@@ -41902,7 +42034,7 @@ ${sdp.trimEnd()}`
|
|
|
41902
42034
|
"OK",
|
|
41903
42035
|
{
|
|
41904
42036
|
"Content-Type": "application/sdp",
|
|
41905
|
-
"Content-Base": `rtsp://${this.listenHost}:${this.listenPort}${
|
|
42037
|
+
"Content-Base": `rtsp://${this.listenHost}:${this.listenPort}${resolved.path}/`
|
|
41906
42038
|
},
|
|
41907
42039
|
sdp
|
|
41908
42040
|
);
|
|
@@ -41916,9 +42048,17 @@ ${sdp.trimEnd()}`
|
|
|
41916
42048
|
send(404, "Not Found");
|
|
41917
42049
|
return;
|
|
41918
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
|
+
}
|
|
41919
42059
|
const transportLine = requestText.match(/Transport:\s*([^\r\n]+)/i)?.[1] ?? "";
|
|
41920
42060
|
this.logger.info?.(
|
|
41921
|
-
`[BaichuanRtspBackchannelServer] SETUP transport="${transportLine}" client=${clientId}`
|
|
42061
|
+
`[BaichuanRtspBackchannelServer] SETUP transport="${transportLine}" client=${clientId} path=${resolved.path}`
|
|
41922
42062
|
);
|
|
41923
42063
|
if (!transportLine.toUpperCase().includes("RTP/AVP/TCP") && !transportLine.toLowerCase().includes("interleaved")) {
|
|
41924
42064
|
this.logger.warn?.(
|
|
@@ -41939,10 +42079,12 @@ ${sdp.trimEnd()}`
|
|
|
41939
42079
|
clientId,
|
|
41940
42080
|
socket,
|
|
41941
42081
|
rtpChannel: interleaved.rtp,
|
|
41942
|
-
rtcpChannel: interleaved.rtcp
|
|
42082
|
+
rtcpChannel: interleaved.rtcp,
|
|
42083
|
+
route: resolved.route,
|
|
42084
|
+
routePath: resolved.path
|
|
41943
42085
|
});
|
|
41944
42086
|
this.logger.info?.(
|
|
41945
|
-
`[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}`
|
|
41946
42088
|
);
|
|
41947
42089
|
send(200, "OK", {
|
|
41948
42090
|
Transport: `RTP/AVP/TCP;unicast;interleaved=${interleaved.rtp}-${interleaved.rtcp};mode=record`,
|
|
@@ -41968,10 +42110,10 @@ ${sdp.trimEnd()}`
|
|
|
41968
42110
|
send(200, "OK", { Session: session.sessionId });
|
|
41969
42111
|
return;
|
|
41970
42112
|
}
|
|
41971
|
-
const apiRef =
|
|
41972
|
-
const channelForCamera =
|
|
42113
|
+
const apiRef = session.route.api;
|
|
42114
|
+
const channelForCamera = session.route.channel;
|
|
41973
42115
|
const loggerRef = this.logger;
|
|
41974
|
-
const deviceIdRef =
|
|
42116
|
+
const deviceIdRef = session.route.deviceId ?? `rtsp-backchannel-${clientId}`;
|
|
41975
42117
|
const handler = new RtspBackchannel({
|
|
41976
42118
|
openTalkSession: () => apiRef.createDedicatedTalkSession(channelForCamera, {
|
|
41977
42119
|
deviceId: deviceIdRef,
|
|
@@ -41981,7 +42123,7 @@ ${sdp.trimEnd()}`
|
|
|
41981
42123
|
});
|
|
41982
42124
|
const recordStart = Date.now();
|
|
41983
42125
|
this.logger.info?.(
|
|
41984
|
-
`[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}"`
|
|
41985
42127
|
);
|
|
41986
42128
|
try {
|
|
41987
42129
|
const talk = await handler.start();
|