@apocaliss92/nodelink-js 0.6.7 → 0.6.8-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/dist/BaichuanVideoStream-6Y2JSBLN.js +7 -0
- package/dist/{DiagnosticsTools-QJ3CRYGA.js → DiagnosticsTools-G3S6L3FX.js} +3 -3
- package/dist/{chunk-IQVVVSXO.js → chunk-27ZKJZFH.js} +2 -2
- package/dist/{chunk-MZUSWKF3.js → chunk-4LZABN6I.js} +30 -1
- package/dist/chunk-4LZABN6I.js.map +1 -0
- package/dist/{chunk-T22QCNBR.js → chunk-OJ4YR45R.js} +109 -12
- package/dist/chunk-OJ4YR45R.js.map +1 -0
- package/dist/cli/rtsp-server.cjs +132 -6
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +3 -3
- package/dist/index.cjs +132 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +3 -3
- package/package.json +1 -1
- package/dist/BaichuanVideoStream-OCLOM452.js +0 -7
- package/dist/chunk-MZUSWKF3.js.map +0 -1
- package/dist/chunk-T22QCNBR.js.map +0 -1
- /package/dist/{BaichuanVideoStream-OCLOM452.js.map → BaichuanVideoStream-6Y2JSBLN.js.map} +0 -0
- /package/dist/{DiagnosticsTools-QJ3CRYGA.js.map → DiagnosticsTools-G3S6L3FX.js.map} +0 -0
- /package/dist/{chunk-IQVVVSXO.js.map → chunk-27ZKJZFH.js.map} +0 -0
package/dist/cli/rtsp-server.cjs
CHANGED
|
@@ -1572,6 +1572,14 @@ var init_BaichuanVideoStream = __esm({
|
|
|
1572
1572
|
logger;
|
|
1573
1573
|
active = false;
|
|
1574
1574
|
videoFrameHandler;
|
|
1575
|
+
// Fired when the underlying BaichuanClient socket closes/errors (e.g. the
|
|
1576
|
+
// keepalive watchdog tears down a half-open socket after a camera reboot).
|
|
1577
|
+
// Without this, the stream keeps believing it is active and only the idle
|
|
1578
|
+
// watchdog would (eventually, and uselessly) try to restart on a dead
|
|
1579
|
+
// client. Surfacing the socket death as a stream "close" lets the consumer
|
|
1580
|
+
// (createNativeStream → fanout onEnd → BaichuanRtspServer) rebuild the
|
|
1581
|
+
// stream on a fresh dedicated session.
|
|
1582
|
+
clientCloseHandler;
|
|
1575
1583
|
expectedStreamTypes;
|
|
1576
1584
|
activeMsgNum;
|
|
1577
1585
|
cmdId;
|
|
@@ -2452,6 +2460,19 @@ var init_BaichuanVideoStream = __esm({
|
|
|
2452
2460
|
}
|
|
2453
2461
|
};
|
|
2454
2462
|
this.client.on("push", this.videoFrameHandler);
|
|
2463
|
+
this.clientCloseHandler = () => {
|
|
2464
|
+
if (!this.active) return;
|
|
2465
|
+
this.logger?.warn?.(
|
|
2466
|
+
`[BaichuanVideoStream] underlying socket closed (channel=${this.channel} profile=${this.profile}) \u2014 ending stream for rebuild`
|
|
2467
|
+
);
|
|
2468
|
+
this.active = false;
|
|
2469
|
+
this.stopWatchdog();
|
|
2470
|
+
if (this.videoFrameHandler)
|
|
2471
|
+
this.client.off("push", this.videoFrameHandler);
|
|
2472
|
+
this.videoFrameHandler = void 0;
|
|
2473
|
+
this.emit("close");
|
|
2474
|
+
};
|
|
2475
|
+
this.client.on("close", this.clientCloseHandler);
|
|
2455
2476
|
this.active = true;
|
|
2456
2477
|
this.startWatchdog();
|
|
2457
2478
|
if (this.api && typeof this.api._registerVideoStreamForDetection === "function") {
|
|
@@ -2522,6 +2543,10 @@ var init_BaichuanVideoStream = __esm({
|
|
|
2522
2543
|
if (this.videoFrameHandler)
|
|
2523
2544
|
this.client.off("push", this.videoFrameHandler);
|
|
2524
2545
|
this.videoFrameHandler = void 0;
|
|
2546
|
+
if (this.clientCloseHandler) {
|
|
2547
|
+
this.client.off("close", this.clientCloseHandler);
|
|
2548
|
+
this.clientCloseHandler = void 0;
|
|
2549
|
+
}
|
|
2525
2550
|
throw err;
|
|
2526
2551
|
}
|
|
2527
2552
|
this.emitSafeError(err);
|
|
@@ -2542,6 +2567,10 @@ var init_BaichuanVideoStream = __esm({
|
|
|
2542
2567
|
this.client.removeListener("push", this.videoFrameHandler);
|
|
2543
2568
|
}
|
|
2544
2569
|
this.videoFrameHandler = void 0;
|
|
2570
|
+
if (this.clientCloseHandler) {
|
|
2571
|
+
this.client.removeListener("close", this.clientCloseHandler);
|
|
2572
|
+
this.clientCloseHandler = void 0;
|
|
2573
|
+
}
|
|
2545
2574
|
this.bcMediaCodec.clear();
|
|
2546
2575
|
this.activeMsgNum = void 0;
|
|
2547
2576
|
if (this.api) {
|
|
@@ -9081,6 +9110,20 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events3.E
|
|
|
9081
9110
|
// from the last IDR in the prebuffer immediately.
|
|
9082
9111
|
PREBUFFER_MAX_MS = 3e3;
|
|
9083
9112
|
prebuffer = [];
|
|
9113
|
+
/**
|
|
9114
|
+
* Hard cap on the userspace TCP send buffer for a single RTSP client. A
|
|
9115
|
+
* healthy consumer drains near-instantly; if this much data backs up the
|
|
9116
|
+
* client is dead or far too slow to keep up with the live stream.
|
|
9117
|
+
*/
|
|
9118
|
+
static MAX_CLIENT_BUFFERED_BYTES = 8 * 1024 * 1024;
|
|
9119
|
+
/**
|
|
9120
|
+
* Pure backpressure decision: should the client socket be disconnected
|
|
9121
|
+
* because its send buffer has grown past the hard cap? Extracted so the
|
|
9122
|
+
* decision can be unit-tested without a live socket.
|
|
9123
|
+
*/
|
|
9124
|
+
static shouldDisconnectForBackpressure(bufferedBytes) {
|
|
9125
|
+
return bufferedBytes > _BaichuanRtspServer.MAX_CLIENT_BUFFERED_BYTES;
|
|
9126
|
+
}
|
|
9084
9127
|
static isAdtsAacFrame(b) {
|
|
9085
9128
|
return b.length >= 2 && b[0] === 255 && (b[1] & 240) === 240;
|
|
9086
9129
|
}
|
|
@@ -10057,14 +10100,36 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events3.E
|
|
|
10057
10100
|
const rtspDebugLog = (message) => this.rtspDebugLog(message);
|
|
10058
10101
|
const sendInterleaved = (channel, msg) => {
|
|
10059
10102
|
if (!rtspSocket || rtspSocket.destroyed || !rtspSocket.writable)
|
|
10060
|
-
return
|
|
10103
|
+
return true;
|
|
10104
|
+
if (_BaichuanRtspServer.shouldDisconnectForBackpressure(
|
|
10105
|
+
rtspSocket.writableLength
|
|
10106
|
+
)) {
|
|
10107
|
+
this.logger.warn(
|
|
10108
|
+
`[rebroadcast] backpressure: ${Math.round(rtspSocket.writableLength / 1024)}KB buffered for client=${clientId} \u2014 disconnecting`
|
|
10109
|
+
);
|
|
10110
|
+
rtspSocket.destroy();
|
|
10111
|
+
return true;
|
|
10112
|
+
}
|
|
10061
10113
|
try {
|
|
10062
10114
|
return rtspSocket.write(frameRtpOverTcp(channel, msg));
|
|
10063
10115
|
} catch (error) {
|
|
10064
10116
|
if (error && typeof error === "object" && "code" in error && error.code === "EPIPE")
|
|
10065
|
-
return
|
|
10117
|
+
return true;
|
|
10066
10118
|
}
|
|
10067
|
-
return
|
|
10119
|
+
return true;
|
|
10120
|
+
};
|
|
10121
|
+
const awaitClientDrain = async () => {
|
|
10122
|
+
if (!rtspSocket || rtspSocket.destroyed) return;
|
|
10123
|
+
if (!rtspSocket.writableNeedDrain) return;
|
|
10124
|
+
await new Promise((resolve) => {
|
|
10125
|
+
const done = () => {
|
|
10126
|
+
rtspSocket.removeListener("drain", done);
|
|
10127
|
+
rtspSocket.removeListener("close", done);
|
|
10128
|
+
resolve();
|
|
10129
|
+
};
|
|
10130
|
+
rtspSocket.once("drain", done);
|
|
10131
|
+
rtspSocket.once("close", done);
|
|
10132
|
+
});
|
|
10068
10133
|
};
|
|
10069
10134
|
const getVideoChannel = () => resources?.track0RtpChannel ?? 0;
|
|
10070
10135
|
const getAudioChannel = () => resources?.track1RtpChannel ?? 2;
|
|
@@ -10643,6 +10708,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events3.E
|
|
|
10643
10708
|
resources.framesSent = (resources.framesSent ?? 0) + 1;
|
|
10644
10709
|
}
|
|
10645
10710
|
sendVideoAccessUnit(videoType, normalizedVideoData, true);
|
|
10711
|
+
await awaitClientDrain();
|
|
10646
10712
|
} else {
|
|
10647
10713
|
try {
|
|
10648
10714
|
if (stdin && !stdin.destroyed && !stdin.writableEnded && !stdin.writableFinished) {
|
|
@@ -13763,6 +13829,16 @@ var BaichuanClient = class _BaichuanClient extends import_node_events5.EventEmit
|
|
|
13763
13829
|
// Public to allow ReolinkBaichuanApi to check subscription status
|
|
13764
13830
|
keepAliveTimer;
|
|
13765
13831
|
keepAlivePingInFlight = false;
|
|
13832
|
+
// Consecutive TCP keepalive (PING) failures with no response. A half-open
|
|
13833
|
+
// socket after a camera reboot is NOT reported as `destroyed`, so
|
|
13834
|
+
// `isSocketConnected()` stays stale-true and recovery never triggers. We
|
|
13835
|
+
// treat repeated unanswered pings as a hard liveness failure and tear the
|
|
13836
|
+
// socket down so the 'close' handler fires (resetting loggedIn/subscribed
|
|
13837
|
+
// and emitting "close"), which lets ensureConnected() rebuild it.
|
|
13838
|
+
consecutiveKeepalivePingFailures = 0;
|
|
13839
|
+
// Number of unanswered keepalive pings tolerated before the TCP socket is
|
|
13840
|
+
// declared dead. 2 misses ≈ up to ~2 keepalive intervals of silence.
|
|
13841
|
+
static KEEPALIVE_MAX_PING_FAILURES = 2;
|
|
13766
13842
|
// Battery/BCUDP cameras may actively terminate sessions (D2C_DISC) when overwhelmed
|
|
13767
13843
|
// or sleeping. Without a cooldown, callers can end up in tight reconnect loops.
|
|
13768
13844
|
udpReconnectCooldownUntilMs = 0;
|
|
@@ -14391,8 +14467,21 @@ var BaichuanClient = class _BaichuanClient extends import_node_events5.EventEmit
|
|
|
14391
14467
|
// Keepalive has empty body
|
|
14392
14468
|
internal: true
|
|
14393
14469
|
});
|
|
14470
|
+
this.consecutiveKeepalivePingFailures = 0;
|
|
14394
14471
|
} catch (e) {
|
|
14395
|
-
this.
|
|
14472
|
+
this.consecutiveKeepalivePingFailures++;
|
|
14473
|
+
this.logDebug("keepalive_ping_failed", {
|
|
14474
|
+
error: e,
|
|
14475
|
+
consecutiveFailures: this.consecutiveKeepalivePingFailures
|
|
14476
|
+
});
|
|
14477
|
+
if (this.transport === "tcp" && this.consecutiveKeepalivePingFailures >= _BaichuanClient.KEEPALIVE_MAX_PING_FAILURES && this.tcpSocket && !this.tcpSocket.destroyed) {
|
|
14478
|
+
this.logFixed(
|
|
14479
|
+
"keepalive_dead",
|
|
14480
|
+
`transport=tcp host=${this.opts.host} consecutiveFailures=${this.consecutiveKeepalivePingFailures} \u2014 destroying half-open socket`
|
|
14481
|
+
);
|
|
14482
|
+
this.consecutiveKeepalivePingFailures = 0;
|
|
14483
|
+
this.tcpSocket.destroy();
|
|
14484
|
+
}
|
|
14396
14485
|
}
|
|
14397
14486
|
}
|
|
14398
14487
|
async connect() {
|
|
@@ -14485,6 +14574,7 @@ var BaichuanClient = class _BaichuanClient extends import_node_events5.EventEmit
|
|
|
14485
14574
|
this.tcpSocket = sock;
|
|
14486
14575
|
this.transport = "tcp";
|
|
14487
14576
|
this.socketClosed = false;
|
|
14577
|
+
this.consecutiveKeepalivePingFailures = 0;
|
|
14488
14578
|
this.socketSessionId = this.newSocketSessionId("tcp");
|
|
14489
14579
|
try {
|
|
14490
14580
|
sock.setKeepAlive(true, 3e4);
|
|
@@ -22486,9 +22576,45 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
22486
22576
|
const generalEntry = this.socketPool.get("general");
|
|
22487
22577
|
if (!generalEntry) return;
|
|
22488
22578
|
if (!generalEntry.client.isSocketConnected?.() || !generalEntry.client.loggedIn) {
|
|
22489
|
-
this.
|
|
22490
|
-
|
|
22579
|
+
if (!this.eventWatchdogSilenceResubscribeEnabled) {
|
|
22580
|
+
this.logger.debug?.(
|
|
22581
|
+
`[ReolinkBaichuanApi] event watchdog tick: skipping (connection not alive, battery/UDP \u2014 not forcing reconnect: connected=${generalEntry.client.isSocketConnected?.()} loggedIn=${generalEntry.client.loggedIn})`
|
|
22582
|
+
);
|
|
22583
|
+
return;
|
|
22584
|
+
}
|
|
22585
|
+
const now2 = Date.now();
|
|
22586
|
+
const backoffMs = Math.min(
|
|
22587
|
+
3e4 * Math.pow(2, this.simpleEventWatchdogRecoveryAttempts),
|
|
22588
|
+
this.simpleEventWatchdogSilenceThresholdMs
|
|
22491
22589
|
);
|
|
22590
|
+
if (now2 - this.simpleEventWatchdogLastRecoveryAt < backoffMs) {
|
|
22591
|
+
this.logger.debug?.(
|
|
22592
|
+
`[ReolinkBaichuanApi] event watchdog tick: connection down, reconnect backoff active (connected=${generalEntry.client.isSocketConnected?.()} loggedIn=${generalEntry.client.loggedIn})`
|
|
22593
|
+
);
|
|
22594
|
+
return;
|
|
22595
|
+
}
|
|
22596
|
+
this.simpleEventWatchdogRecoveryAttempts++;
|
|
22597
|
+
this.simpleEventWatchdogLastRecoveryAt = now2;
|
|
22598
|
+
(this.logger.info ?? this.logger.log).call(
|
|
22599
|
+
this.logger,
|
|
22600
|
+
`[ReolinkBaichuanApi] event watchdog: control socket down, forcing full reconnect (attempt #${this.simpleEventWatchdogRecoveryAttempts})`,
|
|
22601
|
+
{ host: this.host }
|
|
22602
|
+
);
|
|
22603
|
+
try {
|
|
22604
|
+
await this.ensureConnected();
|
|
22605
|
+
this.simpleEventLastReceivedAt = Date.now();
|
|
22606
|
+
this.simpleEventWatchdogRecoveryAttempts = 0;
|
|
22607
|
+
(this.logger.info ?? this.logger.log).call(
|
|
22608
|
+
this.logger,
|
|
22609
|
+
`[ReolinkBaichuanApi] event watchdog: full reconnect successful`
|
|
22610
|
+
);
|
|
22611
|
+
} catch (e) {
|
|
22612
|
+
(this.logger.debug ?? this.logger.log).call(
|
|
22613
|
+
this.logger,
|
|
22614
|
+
`[ReolinkBaichuanApi] event watchdog: full reconnect attempt #${this.simpleEventWatchdogRecoveryAttempts} failed`,
|
|
22615
|
+
formatErrorForLog(e)
|
|
22616
|
+
);
|
|
22617
|
+
}
|
|
22492
22618
|
return;
|
|
22493
22619
|
}
|
|
22494
22620
|
const now = Date.now();
|