@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 +1 -1
- package/dist/index.cjs +165 -22
- 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 +163 -20
- 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
|
@@ -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
|
|
41362
|
+
return mulaw.decode(bytes);
|
|
41362
41363
|
}
|
|
41363
41364
|
function alawToPcm16(bytes) {
|
|
41364
41365
|
if (bytes.length === 0) return new Int16Array(0);
|
|
41365
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
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}
|
|
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}${
|
|
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 =
|
|
41971
|
-
const channelForCamera =
|
|
42113
|
+
const apiRef = session.route.api;
|
|
42114
|
+
const channelForCamera = session.route.channel;
|
|
41972
42115
|
const loggerRef = this.logger;
|
|
41973
|
-
const deviceIdRef =
|
|
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();
|