@apocaliss92/nodelink-js 0.4.6 → 0.4.7
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/dist/{chunk-F2Y5U3YP.js → chunk-GKLOJJ34.js} +92 -17
- package/dist/chunk-GKLOJJ34.js.map +1 -0
- package/dist/cli/rtsp-server.cjs +91 -16
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +1 -1
- package/dist/index.cjs +153 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +45 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +63 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-F2Y5U3YP.js.map +0 -1
package/dist/cli/rtsp-server.js
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -18072,6 +18072,13 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18072
18072
|
* - "replay:XXX" - dedicated per replay session
|
|
18073
18073
|
*/
|
|
18074
18074
|
socketPool = /* @__PURE__ */ new Map();
|
|
18075
|
+
/**
|
|
18076
|
+
* Consecutive stream-start (cmdId=3) timeout counter per socket tag.
|
|
18077
|
+
* When a streaming socket has N consecutive timeouts, the socket is force-closed
|
|
18078
|
+
* so the next attempt creates a fresh connection. Resets on success.
|
|
18079
|
+
*/
|
|
18080
|
+
consecutiveStreamTimeouts = /* @__PURE__ */ new Map();
|
|
18081
|
+
static MAX_CONSECUTIVE_STREAM_TIMEOUTS = 3;
|
|
18075
18082
|
/** BaichuanClientOptions to use when creating new sockets */
|
|
18076
18083
|
clientOptions;
|
|
18077
18084
|
/**
|
|
@@ -18226,14 +18233,20 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18226
18233
|
if (!xml) return;
|
|
18227
18234
|
const channel = frame.header.channelId;
|
|
18228
18235
|
const battery = this.parseBatteryInfoXml(xml, channel);
|
|
18229
|
-
if (battery.batteryPercent
|
|
18230
|
-
|
|
18231
|
-
|
|
18232
|
-
|
|
18233
|
-
|
|
18234
|
-
|
|
18235
|
-
});
|
|
18236
|
+
if (battery.batteryPercent === void 0 && battery.chargeStatus === void 0 && battery.adapterStatus === void 0) {
|
|
18237
|
+
return;
|
|
18238
|
+
}
|
|
18239
|
+
const key = `${battery.batteryPercent ?? ""}|${battery.chargeStatus ?? ""}|${battery.adapterStatus ?? ""}`;
|
|
18240
|
+
if (this.lastBatteryPushKey.get(channel) === key) {
|
|
18241
|
+
return;
|
|
18236
18242
|
}
|
|
18243
|
+
this.lastBatteryPushKey.set(channel, key);
|
|
18244
|
+
this.dispatchSimpleEvent({
|
|
18245
|
+
type: "battery",
|
|
18246
|
+
channel,
|
|
18247
|
+
timestamp: Date.now(),
|
|
18248
|
+
battery
|
|
18249
|
+
});
|
|
18237
18250
|
} catch (e) {
|
|
18238
18251
|
this.logger.debug?.(
|
|
18239
18252
|
"[ReolinkBaichuanApi] Error parsing battery push",
|
|
@@ -18399,6 +18412,14 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18399
18412
|
deviceCapabilitiesCache = /* @__PURE__ */ new Map();
|
|
18400
18413
|
static CAPABILITIES_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
18401
18414
|
// 5 minutes
|
|
18415
|
+
/**
|
|
18416
|
+
* Dedupe key for battery push events (cmd_id 252), per channel.
|
|
18417
|
+
* Cameras emit BatteryInfoList frequently while streaming (every few
|
|
18418
|
+
* seconds). We only forward an event when the meaningful fields change
|
|
18419
|
+
* (percent, chargeStatus, adapterStatus) to avoid flooding SSE/MQTT
|
|
18420
|
+
* consumers and the UI event log.
|
|
18421
|
+
*/
|
|
18422
|
+
lastBatteryPushKey = /* @__PURE__ */ new Map();
|
|
18402
18423
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
18403
18424
|
// SOCKET POOL CONSTANTS
|
|
18404
18425
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -23871,6 +23892,7 @@ ${stderr}`)
|
|
|
23871
23892
|
`${ch}:${profile}:${variant}`,
|
|
23872
23893
|
frame.header.msgNum
|
|
23873
23894
|
);
|
|
23895
|
+
this.resetStreamTimeoutCounter(targetClient);
|
|
23874
23896
|
return;
|
|
23875
23897
|
} catch (error) {
|
|
23876
23898
|
lastError = error;
|
|
@@ -23885,6 +23907,10 @@ ${stderr}`)
|
|
|
23885
23907
|
}
|
|
23886
23908
|
}
|
|
23887
23909
|
}
|
|
23910
|
+
const isTimeout = lastError instanceof Error && lastError.message?.includes("timeout");
|
|
23911
|
+
if (isTimeout) {
|
|
23912
|
+
this.trackStreamTimeout(targetClient);
|
|
23913
|
+
}
|
|
23888
23914
|
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
23889
23915
|
}
|
|
23890
23916
|
/**
|
|
@@ -24344,6 +24370,18 @@ ${stderr}`)
|
|
|
24344
24370
|
notifyD2cDisc() {
|
|
24345
24371
|
const now = Date.now();
|
|
24346
24372
|
this.lastD2cDiscAtMs = now;
|
|
24373
|
+
const streamingTags = Array.from(this.socketPool.keys()).filter(
|
|
24374
|
+
(tag) => tag.startsWith("streaming:")
|
|
24375
|
+
);
|
|
24376
|
+
if (streamingTags.length > 0) {
|
|
24377
|
+
this.logger?.log?.(
|
|
24378
|
+
`[D2C_DISC] Force-closing ${streamingTags.length} streaming socket(s): ${streamingTags.join(", ")}`
|
|
24379
|
+
);
|
|
24380
|
+
for (const tag of streamingTags) {
|
|
24381
|
+
this.forceClosePooledSocket(tag, this.logger).catch(() => {
|
|
24382
|
+
});
|
|
24383
|
+
}
|
|
24384
|
+
}
|
|
24347
24385
|
const immediateCooldownUntil = now + _ReolinkBaichuanApi.D2C_DISC_IMMEDIATE_COOLDOWN_MS;
|
|
24348
24386
|
const existing = this.socketPoolCooldowns.get(this.host);
|
|
24349
24387
|
if (!existing || existing.cooldownUntil < immediateCooldownUntil) {
|
|
@@ -24376,6 +24414,43 @@ ${stderr}`)
|
|
|
24376
24414
|
}
|
|
24377
24415
|
}
|
|
24378
24416
|
}
|
|
24417
|
+
/**
|
|
24418
|
+
* Find the socket pool tag for a given BaichuanClient instance.
|
|
24419
|
+
* Returns undefined if the client is not in the pool (e.g. it's the general socket used directly).
|
|
24420
|
+
*/
|
|
24421
|
+
findSocketTagForClient(client) {
|
|
24422
|
+
for (const [tag, entry] of this.socketPool) {
|
|
24423
|
+
if (entry.client === client) return tag;
|
|
24424
|
+
}
|
|
24425
|
+
return void 0;
|
|
24426
|
+
}
|
|
24427
|
+
/**
|
|
24428
|
+
* Reset the consecutive stream-start timeout counter for a streaming socket.
|
|
24429
|
+
* Called on successful stream start.
|
|
24430
|
+
*/
|
|
24431
|
+
resetStreamTimeoutCounter(client) {
|
|
24432
|
+
const tag = this.findSocketTagForClient(client);
|
|
24433
|
+
if (tag) this.consecutiveStreamTimeouts.delete(tag);
|
|
24434
|
+
}
|
|
24435
|
+
/**
|
|
24436
|
+
* Track a stream-start timeout on a streaming socket.
|
|
24437
|
+
* After MAX_CONSECUTIVE_STREAM_TIMEOUTS consecutive timeouts, force-close the
|
|
24438
|
+
* socket so the next attempt creates a fresh connection.
|
|
24439
|
+
*/
|
|
24440
|
+
trackStreamTimeout(client) {
|
|
24441
|
+
const tag = this.findSocketTagForClient(client);
|
|
24442
|
+
if (!tag || !tag.startsWith("streaming:")) return;
|
|
24443
|
+
const count = (this.consecutiveStreamTimeouts.get(tag) ?? 0) + 1;
|
|
24444
|
+
this.consecutiveStreamTimeouts.set(tag, count);
|
|
24445
|
+
if (count >= _ReolinkBaichuanApi.MAX_CONSECUTIVE_STREAM_TIMEOUTS) {
|
|
24446
|
+
this.logger?.warn?.(
|
|
24447
|
+
`[SocketPool] ${count} consecutive stream timeouts on tag=${tag}, force-closing socket`
|
|
24448
|
+
);
|
|
24449
|
+
this.consecutiveStreamTimeouts.delete(tag);
|
|
24450
|
+
this.forceClosePooledSocket(tag, this.logger).catch(() => {
|
|
24451
|
+
});
|
|
24452
|
+
}
|
|
24453
|
+
}
|
|
24379
24454
|
/**
|
|
24380
24455
|
* Best-effort sleeping inference for battery/BCUDP cameras.
|
|
24381
24456
|
*
|
|
@@ -33640,6 +33715,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33640
33715
|
gracePeriodMs;
|
|
33641
33716
|
prebufferMaxMs;
|
|
33642
33717
|
maxBufferBytes;
|
|
33718
|
+
streamTimeoutMs;
|
|
33643
33719
|
prestartStream;
|
|
33644
33720
|
active = false;
|
|
33645
33721
|
server;
|
|
@@ -33653,6 +33729,11 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33653
33729
|
connectedClients = /* @__PURE__ */ new Set();
|
|
33654
33730
|
clientSockets = /* @__PURE__ */ new Map();
|
|
33655
33731
|
stopGraceTimer;
|
|
33732
|
+
// Stream health monitoring
|
|
33733
|
+
lastFrameAt = 0;
|
|
33734
|
+
streamHealthTimer;
|
|
33735
|
+
totalFramesReceived = 0;
|
|
33736
|
+
totalVideoFramesWritten = 0;
|
|
33656
33737
|
// Prebuffer
|
|
33657
33738
|
prebuffer = [];
|
|
33658
33739
|
constructor(options) {
|
|
@@ -33668,6 +33749,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33668
33749
|
this.gracePeriodMs = options.gracePeriodMs ?? 3e4;
|
|
33669
33750
|
this.prebufferMaxMs = options.prebufferMs ?? 3e3;
|
|
33670
33751
|
this.maxBufferBytes = options.maxBufferBytes ?? 1e8;
|
|
33752
|
+
this.streamTimeoutMs = options.streamTimeoutMs ?? 15e3;
|
|
33671
33753
|
this.prestartStream = options.prestartStream ?? true;
|
|
33672
33754
|
}
|
|
33673
33755
|
// -----------------------------------------------------------------------
|
|
@@ -33706,6 +33788,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33706
33788
|
if (!this.active) return;
|
|
33707
33789
|
this.active = false;
|
|
33708
33790
|
clearTimeout(this.stopGraceTimer);
|
|
33791
|
+
this.stopStreamHealthMonitor();
|
|
33709
33792
|
for (const [id, sock] of this.clientSockets) {
|
|
33710
33793
|
sock.destroy();
|
|
33711
33794
|
this.connectedClients.delete(id);
|
|
@@ -33759,12 +33842,12 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33759
33842
|
`[Go2rtcTcpServer] feedClient error id=${clientId}: ${err}`
|
|
33760
33843
|
);
|
|
33761
33844
|
});
|
|
33762
|
-
const cleanup = () => {
|
|
33763
|
-
this.removeClient(clientId);
|
|
33845
|
+
const cleanup = (reason) => {
|
|
33846
|
+
this.removeClient(clientId, reason);
|
|
33764
33847
|
socket.destroy();
|
|
33765
33848
|
};
|
|
33766
|
-
socket.on("error", cleanup);
|
|
33767
|
-
socket.on("close", cleanup);
|
|
33849
|
+
socket.on("error", (err) => cleanup(`error: ${err.message}`));
|
|
33850
|
+
socket.on("close", (hadError) => cleanup(hadError ? "close (with error)" : "close (clean)"));
|
|
33768
33851
|
}
|
|
33769
33852
|
async feedClient(clientId, socket) {
|
|
33770
33853
|
const fanoutDeadline = Date.now() + 3e4;
|
|
@@ -33825,6 +33908,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33825
33908
|
}
|
|
33826
33909
|
socket.write(annexB);
|
|
33827
33910
|
liveVideoWritten++;
|
|
33911
|
+
this.totalVideoFramesWritten++;
|
|
33828
33912
|
if (Date.now() - lastLogAt > 1e4) {
|
|
33829
33913
|
this.logger.info?.(
|
|
33830
33914
|
`[Go2rtcTcpServer] live stats client=${clientId} received=${liveFrameCount} written=${liveVideoWritten} bufLen=${socket.writableLength}`
|
|
@@ -33951,6 +34035,8 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33951
34035
|
...dedicatedClient ? { client: dedicatedClient } : {}
|
|
33952
34036
|
}),
|
|
33953
34037
|
onFrame: (frame) => {
|
|
34038
|
+
this.lastFrameAt = Date.now();
|
|
34039
|
+
this.totalFramesReceived++;
|
|
33954
34040
|
if (!frame.audio && (frame.videoType === "H264" || frame.videoType === "H265")) {
|
|
33955
34041
|
this.detectedVideoType = frame.videoType;
|
|
33956
34042
|
}
|
|
@@ -33977,6 +34063,12 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33977
34063
|
if (!this.nativeStreamActive) return;
|
|
33978
34064
|
this.nativeStreamActive = false;
|
|
33979
34065
|
this.nativeFanout = null;
|
|
34066
|
+
this.stopStreamHealthMonitor();
|
|
34067
|
+
const silenceMs = this.lastFrameAt > 0 ? Date.now() - this.lastFrameAt : -1;
|
|
34068
|
+
const diagnosis = silenceMs > this.streamTimeoutMs ? "camera stopped sending frames" : silenceMs >= 0 ? "stream source closed" : "no frames were ever received";
|
|
34069
|
+
this.logger.warn?.(
|
|
34070
|
+
`[Go2rtcTcpServer] native stream ended diagnosis="${diagnosis}" lastFrame=${silenceMs >= 0 ? `${(silenceMs / 1e3).toFixed(1)}s ago` : "never"} totalRx=${this.totalFramesReceived} clients=${this.connectedClients.size}`
|
|
34071
|
+
);
|
|
33980
34072
|
if (this.dedicatedSessionRelease) {
|
|
33981
34073
|
this.dedicatedSessionRelease().catch(() => {
|
|
33982
34074
|
});
|
|
@@ -33984,16 +34076,18 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33984
34076
|
}
|
|
33985
34077
|
if (this.active && (this.connectedClients.size > 0 || this.prestartStream)) {
|
|
33986
34078
|
this.logger.info?.(
|
|
33987
|
-
`[Go2rtcTcpServer] native stream
|
|
34079
|
+
`[Go2rtcTcpServer] restarting native stream (clients=${this.connectedClients.size}, prestart=${this.prestartStream})`
|
|
33988
34080
|
);
|
|
33989
34081
|
this.startNativeStream();
|
|
33990
34082
|
}
|
|
33991
34083
|
}
|
|
33992
34084
|
});
|
|
33993
34085
|
this.nativeFanout.start();
|
|
34086
|
+
this.startStreamHealthMonitor();
|
|
33994
34087
|
}
|
|
33995
34088
|
async stopNativeStream() {
|
|
33996
34089
|
this.nativeStreamActive = false;
|
|
34090
|
+
this.stopStreamHealthMonitor();
|
|
33997
34091
|
const fanout = this.nativeFanout;
|
|
33998
34092
|
this.nativeFanout = null;
|
|
33999
34093
|
if (fanout) {
|
|
@@ -34007,14 +34101,50 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
34007
34101
|
}
|
|
34008
34102
|
}
|
|
34009
34103
|
// -----------------------------------------------------------------------
|
|
34104
|
+
// Stream health monitoring
|
|
34105
|
+
// -----------------------------------------------------------------------
|
|
34106
|
+
startStreamHealthMonitor() {
|
|
34107
|
+
this.stopStreamHealthMonitor();
|
|
34108
|
+
if (this.streamTimeoutMs <= 0) return;
|
|
34109
|
+
this.lastFrameAt = Date.now();
|
|
34110
|
+
this.streamHealthTimer = setInterval(() => {
|
|
34111
|
+
if (!this.nativeStreamActive || !this.active) {
|
|
34112
|
+
this.stopStreamHealthMonitor();
|
|
34113
|
+
return;
|
|
34114
|
+
}
|
|
34115
|
+
const silenceMs = Date.now() - this.lastFrameAt;
|
|
34116
|
+
if (silenceMs > this.streamTimeoutMs) {
|
|
34117
|
+
this.logger.warn?.(
|
|
34118
|
+
`[Go2rtcTcpServer] stream inactivity timeout: no frames for ${(silenceMs / 1e3).toFixed(1)}s (threshold=${this.streamTimeoutMs}ms), totalReceived=${this.totalFramesReceived} clients=${this.connectedClients.size} \u2014 forcing stream restart`
|
|
34119
|
+
);
|
|
34120
|
+
this.stopStreamHealthMonitor();
|
|
34121
|
+
const fanout = this.nativeFanout;
|
|
34122
|
+
if (fanout) {
|
|
34123
|
+
this.nativeStreamActive = false;
|
|
34124
|
+
this.nativeFanout = null;
|
|
34125
|
+
fanout.stop().catch(() => {
|
|
34126
|
+
});
|
|
34127
|
+
}
|
|
34128
|
+
}
|
|
34129
|
+
}, Math.min(this.streamTimeoutMs / 2, 5e3));
|
|
34130
|
+
}
|
|
34131
|
+
stopStreamHealthMonitor() {
|
|
34132
|
+
if (this.streamHealthTimer) {
|
|
34133
|
+
clearInterval(this.streamHealthTimer);
|
|
34134
|
+
this.streamHealthTimer = void 0;
|
|
34135
|
+
}
|
|
34136
|
+
}
|
|
34137
|
+
// -----------------------------------------------------------------------
|
|
34010
34138
|
// Client lifecycle
|
|
34011
34139
|
// -----------------------------------------------------------------------
|
|
34012
|
-
removeClient(clientId) {
|
|
34140
|
+
removeClient(clientId, reason) {
|
|
34013
34141
|
if (!this.connectedClients.has(clientId)) return;
|
|
34014
34142
|
this.connectedClients.delete(clientId);
|
|
34015
34143
|
this.clientSockets.delete(clientId);
|
|
34144
|
+
const silenceMs = this.lastFrameAt > 0 ? Date.now() - this.lastFrameAt : -1;
|
|
34145
|
+
const silenceInfo = silenceMs >= 0 ? ` lastFrame=${(silenceMs / 1e3).toFixed(1)}s ago` : "";
|
|
34016
34146
|
this.logger.info?.(
|
|
34017
|
-
`[Go2rtcTcpServer] client disconnected id=${clientId} remaining=${this.connectedClients.size}`
|
|
34147
|
+
`[Go2rtcTcpServer] client disconnected id=${clientId} reason=${reason ?? "unknown"} remaining=${this.connectedClients.size} totalRx=${this.totalFramesReceived} totalTx=${this.totalVideoFramesWritten}${silenceInfo}`
|
|
34018
34148
|
);
|
|
34019
34149
|
this.emit("clientDisconnected", clientId);
|
|
34020
34150
|
if (this.connectedClients.size === 0 && !this.prestartStream) {
|
|
@@ -36349,16 +36479,16 @@ function isTcpFailureThatShouldFallbackToUdp(e) {
|
|
|
36349
36479
|
return message.includes("ECONNREFUSED") || message.includes("ETIMEDOUT") || message.includes("EHOSTUNREACH") || message.includes("ENETUNREACH") || message.includes("socket hang up") || message.includes("TCP connection timeout") || message.includes("Baichuan socket closed") || message.includes("timeout waiting for nonce") || message.includes("expected encryption info") || message.includes("ECONNRESET") || message.includes("EPIPE");
|
|
36350
36480
|
}
|
|
36351
36481
|
async function pingHost(host, timeoutMs = 3e3) {
|
|
36482
|
+
const { exec } = await import("child_process");
|
|
36483
|
+
const platform2 = process.platform;
|
|
36484
|
+
const pingCmd = platform2 === "win32" ? `ping -n 1 -w ${timeoutMs} ${host}` : platform2 === "darwin" ? (
|
|
36485
|
+
// macOS: -W is in milliseconds (Linux: seconds)
|
|
36486
|
+
`ping -c 1 -W ${timeoutMs} ${host}`
|
|
36487
|
+
) : (
|
|
36488
|
+
// Linux/BSD-ish: -W is in seconds on most distros
|
|
36489
|
+
`ping -c 1 -W ${Math.max(1, Math.floor(timeoutMs / 1e3))} ${host}`
|
|
36490
|
+
);
|
|
36352
36491
|
return new Promise((resolve) => {
|
|
36353
|
-
const { exec } = require("child_process");
|
|
36354
|
-
const platform2 = process.platform;
|
|
36355
|
-
const pingCmd = platform2 === "win32" ? `ping -n 1 -w ${timeoutMs} ${host}` : platform2 === "darwin" ? (
|
|
36356
|
-
// macOS: -W is in milliseconds (Linux: seconds)
|
|
36357
|
-
`ping -c 1 -W ${timeoutMs} ${host}`
|
|
36358
|
-
) : (
|
|
36359
|
-
// Linux/BSD-ish: -W is in seconds on most distros
|
|
36360
|
-
`ping -c 1 -W ${Math.max(1, Math.floor(timeoutMs / 1e3))} ${host}`
|
|
36361
|
-
);
|
|
36362
36492
|
exec(pingCmd, (error) => {
|
|
36363
36493
|
resolve(!error);
|
|
36364
36494
|
});
|