@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.js
CHANGED
|
@@ -3,11 +3,11 @@ import {
|
|
|
3
3
|
BaichuanRtspServer,
|
|
4
4
|
ReolinkBaichuanApi,
|
|
5
5
|
autoDetectDeviceType
|
|
6
|
-
} from "../chunk-
|
|
7
|
-
import "../chunk-
|
|
6
|
+
} from "../chunk-OJ4YR45R.js";
|
|
7
|
+
import "../chunk-27ZKJZFH.js";
|
|
8
8
|
import {
|
|
9
9
|
__require
|
|
10
|
-
} from "../chunk-
|
|
10
|
+
} from "../chunk-4LZABN6I.js";
|
|
11
11
|
|
|
12
12
|
// src/cli/rtsp-server.ts
|
|
13
13
|
function parseArgs() {
|
package/dist/index.cjs
CHANGED
|
@@ -2311,6 +2311,14 @@ var init_BaichuanVideoStream = __esm({
|
|
|
2311
2311
|
logger;
|
|
2312
2312
|
active = false;
|
|
2313
2313
|
videoFrameHandler;
|
|
2314
|
+
// Fired when the underlying BaichuanClient socket closes/errors (e.g. the
|
|
2315
|
+
// keepalive watchdog tears down a half-open socket after a camera reboot).
|
|
2316
|
+
// Without this, the stream keeps believing it is active and only the idle
|
|
2317
|
+
// watchdog would (eventually, and uselessly) try to restart on a dead
|
|
2318
|
+
// client. Surfacing the socket death as a stream "close" lets the consumer
|
|
2319
|
+
// (createNativeStream → fanout onEnd → BaichuanRtspServer) rebuild the
|
|
2320
|
+
// stream on a fresh dedicated session.
|
|
2321
|
+
clientCloseHandler;
|
|
2314
2322
|
expectedStreamTypes;
|
|
2315
2323
|
activeMsgNum;
|
|
2316
2324
|
cmdId;
|
|
@@ -3191,6 +3199,19 @@ var init_BaichuanVideoStream = __esm({
|
|
|
3191
3199
|
}
|
|
3192
3200
|
};
|
|
3193
3201
|
this.client.on("push", this.videoFrameHandler);
|
|
3202
|
+
this.clientCloseHandler = () => {
|
|
3203
|
+
if (!this.active) return;
|
|
3204
|
+
this.logger?.warn?.(
|
|
3205
|
+
`[BaichuanVideoStream] underlying socket closed (channel=${this.channel} profile=${this.profile}) \u2014 ending stream for rebuild`
|
|
3206
|
+
);
|
|
3207
|
+
this.active = false;
|
|
3208
|
+
this.stopWatchdog();
|
|
3209
|
+
if (this.videoFrameHandler)
|
|
3210
|
+
this.client.off("push", this.videoFrameHandler);
|
|
3211
|
+
this.videoFrameHandler = void 0;
|
|
3212
|
+
this.emit("close");
|
|
3213
|
+
};
|
|
3214
|
+
this.client.on("close", this.clientCloseHandler);
|
|
3194
3215
|
this.active = true;
|
|
3195
3216
|
this.startWatchdog();
|
|
3196
3217
|
if (this.api && typeof this.api._registerVideoStreamForDetection === "function") {
|
|
@@ -3261,6 +3282,10 @@ var init_BaichuanVideoStream = __esm({
|
|
|
3261
3282
|
if (this.videoFrameHandler)
|
|
3262
3283
|
this.client.off("push", this.videoFrameHandler);
|
|
3263
3284
|
this.videoFrameHandler = void 0;
|
|
3285
|
+
if (this.clientCloseHandler) {
|
|
3286
|
+
this.client.off("close", this.clientCloseHandler);
|
|
3287
|
+
this.clientCloseHandler = void 0;
|
|
3288
|
+
}
|
|
3264
3289
|
throw err;
|
|
3265
3290
|
}
|
|
3266
3291
|
this.emitSafeError(err);
|
|
@@ -3281,6 +3306,10 @@ var init_BaichuanVideoStream = __esm({
|
|
|
3281
3306
|
this.client.removeListener("push", this.videoFrameHandler);
|
|
3282
3307
|
}
|
|
3283
3308
|
this.videoFrameHandler = void 0;
|
|
3309
|
+
if (this.clientCloseHandler) {
|
|
3310
|
+
this.client.removeListener("close", this.clientCloseHandler);
|
|
3311
|
+
this.clientCloseHandler = void 0;
|
|
3312
|
+
}
|
|
3284
3313
|
this.bcMediaCodec.clear();
|
|
3285
3314
|
this.activeMsgNum = void 0;
|
|
3286
3315
|
if (this.api) {
|
|
@@ -10916,6 +10945,16 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
10916
10945
|
// Public to allow ReolinkBaichuanApi to check subscription status
|
|
10917
10946
|
keepAliveTimer;
|
|
10918
10947
|
keepAlivePingInFlight = false;
|
|
10948
|
+
// Consecutive TCP keepalive (PING) failures with no response. A half-open
|
|
10949
|
+
// socket after a camera reboot is NOT reported as `destroyed`, so
|
|
10950
|
+
// `isSocketConnected()` stays stale-true and recovery never triggers. We
|
|
10951
|
+
// treat repeated unanswered pings as a hard liveness failure and tear the
|
|
10952
|
+
// socket down so the 'close' handler fires (resetting loggedIn/subscribed
|
|
10953
|
+
// and emitting "close"), which lets ensureConnected() rebuild it.
|
|
10954
|
+
consecutiveKeepalivePingFailures = 0;
|
|
10955
|
+
// Number of unanswered keepalive pings tolerated before the TCP socket is
|
|
10956
|
+
// declared dead. 2 misses ≈ up to ~2 keepalive intervals of silence.
|
|
10957
|
+
static KEEPALIVE_MAX_PING_FAILURES = 2;
|
|
10919
10958
|
// Battery/BCUDP cameras may actively terminate sessions (D2C_DISC) when overwhelmed
|
|
10920
10959
|
// or sleeping. Without a cooldown, callers can end up in tight reconnect loops.
|
|
10921
10960
|
udpReconnectCooldownUntilMs = 0;
|
|
@@ -11544,8 +11583,21 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
11544
11583
|
// Keepalive has empty body
|
|
11545
11584
|
internal: true
|
|
11546
11585
|
});
|
|
11586
|
+
this.consecutiveKeepalivePingFailures = 0;
|
|
11547
11587
|
} catch (e) {
|
|
11548
|
-
this.
|
|
11588
|
+
this.consecutiveKeepalivePingFailures++;
|
|
11589
|
+
this.logDebug("keepalive_ping_failed", {
|
|
11590
|
+
error: e,
|
|
11591
|
+
consecutiveFailures: this.consecutiveKeepalivePingFailures
|
|
11592
|
+
});
|
|
11593
|
+
if (this.transport === "tcp" && this.consecutiveKeepalivePingFailures >= _BaichuanClient.KEEPALIVE_MAX_PING_FAILURES && this.tcpSocket && !this.tcpSocket.destroyed) {
|
|
11594
|
+
this.logFixed(
|
|
11595
|
+
"keepalive_dead",
|
|
11596
|
+
`transport=tcp host=${this.opts.host} consecutiveFailures=${this.consecutiveKeepalivePingFailures} \u2014 destroying half-open socket`
|
|
11597
|
+
);
|
|
11598
|
+
this.consecutiveKeepalivePingFailures = 0;
|
|
11599
|
+
this.tcpSocket.destroy();
|
|
11600
|
+
}
|
|
11549
11601
|
}
|
|
11550
11602
|
}
|
|
11551
11603
|
async connect() {
|
|
@@ -11638,6 +11690,7 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
11638
11690
|
this.tcpSocket = sock;
|
|
11639
11691
|
this.transport = "tcp";
|
|
11640
11692
|
this.socketClosed = false;
|
|
11693
|
+
this.consecutiveKeepalivePingFailures = 0;
|
|
11641
11694
|
this.socketSessionId = this.newSocketSessionId("tcp");
|
|
11642
11695
|
try {
|
|
11643
11696
|
sock.setKeepAlive(true, 3e4);
|
|
@@ -15142,6 +15195,20 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events5.E
|
|
|
15142
15195
|
// from the last IDR in the prebuffer immediately.
|
|
15143
15196
|
PREBUFFER_MAX_MS = 3e3;
|
|
15144
15197
|
prebuffer = [];
|
|
15198
|
+
/**
|
|
15199
|
+
* Hard cap on the userspace TCP send buffer for a single RTSP client. A
|
|
15200
|
+
* healthy consumer drains near-instantly; if this much data backs up the
|
|
15201
|
+
* client is dead or far too slow to keep up with the live stream.
|
|
15202
|
+
*/
|
|
15203
|
+
static MAX_CLIENT_BUFFERED_BYTES = 8 * 1024 * 1024;
|
|
15204
|
+
/**
|
|
15205
|
+
* Pure backpressure decision: should the client socket be disconnected
|
|
15206
|
+
* because its send buffer has grown past the hard cap? Extracted so the
|
|
15207
|
+
* decision can be unit-tested without a live socket.
|
|
15208
|
+
*/
|
|
15209
|
+
static shouldDisconnectForBackpressure(bufferedBytes) {
|
|
15210
|
+
return bufferedBytes > _BaichuanRtspServer.MAX_CLIENT_BUFFERED_BYTES;
|
|
15211
|
+
}
|
|
15145
15212
|
static isAdtsAacFrame(b) {
|
|
15146
15213
|
return b.length >= 2 && b[0] === 255 && (b[1] & 240) === 240;
|
|
15147
15214
|
}
|
|
@@ -16118,14 +16185,36 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events5.E
|
|
|
16118
16185
|
const rtspDebugLog = (message) => this.rtspDebugLog(message);
|
|
16119
16186
|
const sendInterleaved = (channel, msg) => {
|
|
16120
16187
|
if (!rtspSocket || rtspSocket.destroyed || !rtspSocket.writable)
|
|
16121
|
-
return
|
|
16188
|
+
return true;
|
|
16189
|
+
if (_BaichuanRtspServer.shouldDisconnectForBackpressure(
|
|
16190
|
+
rtspSocket.writableLength
|
|
16191
|
+
)) {
|
|
16192
|
+
this.logger.warn(
|
|
16193
|
+
`[rebroadcast] backpressure: ${Math.round(rtspSocket.writableLength / 1024)}KB buffered for client=${clientId} \u2014 disconnecting`
|
|
16194
|
+
);
|
|
16195
|
+
rtspSocket.destroy();
|
|
16196
|
+
return true;
|
|
16197
|
+
}
|
|
16122
16198
|
try {
|
|
16123
16199
|
return rtspSocket.write(frameRtpOverTcp(channel, msg));
|
|
16124
16200
|
} catch (error) {
|
|
16125
16201
|
if (error && typeof error === "object" && "code" in error && error.code === "EPIPE")
|
|
16126
|
-
return
|
|
16202
|
+
return true;
|
|
16127
16203
|
}
|
|
16128
|
-
return
|
|
16204
|
+
return true;
|
|
16205
|
+
};
|
|
16206
|
+
const awaitClientDrain = async () => {
|
|
16207
|
+
if (!rtspSocket || rtspSocket.destroyed) return;
|
|
16208
|
+
if (!rtspSocket.writableNeedDrain) return;
|
|
16209
|
+
await new Promise((resolve) => {
|
|
16210
|
+
const done = () => {
|
|
16211
|
+
rtspSocket.removeListener("drain", done);
|
|
16212
|
+
rtspSocket.removeListener("close", done);
|
|
16213
|
+
resolve();
|
|
16214
|
+
};
|
|
16215
|
+
rtspSocket.once("drain", done);
|
|
16216
|
+
rtspSocket.once("close", done);
|
|
16217
|
+
});
|
|
16129
16218
|
};
|
|
16130
16219
|
const getVideoChannel = () => resources?.track0RtpChannel ?? 0;
|
|
16131
16220
|
const getAudioChannel = () => resources?.track1RtpChannel ?? 2;
|
|
@@ -16704,6 +16793,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events5.E
|
|
|
16704
16793
|
resources.framesSent = (resources.framesSent ?? 0) + 1;
|
|
16705
16794
|
}
|
|
16706
16795
|
sendVideoAccessUnit(videoType, normalizedVideoData, true);
|
|
16796
|
+
await awaitClientDrain();
|
|
16707
16797
|
} else {
|
|
16708
16798
|
try {
|
|
16709
16799
|
if (stdin && !stdin.destroyed && !stdin.writableEnded && !stdin.writableFinished) {
|
|
@@ -23189,9 +23279,45 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
23189
23279
|
const generalEntry = this.socketPool.get("general");
|
|
23190
23280
|
if (!generalEntry) return;
|
|
23191
23281
|
if (!generalEntry.client.isSocketConnected?.() || !generalEntry.client.loggedIn) {
|
|
23192
|
-
this.
|
|
23193
|
-
|
|
23282
|
+
if (!this.eventWatchdogSilenceResubscribeEnabled) {
|
|
23283
|
+
this.logger.debug?.(
|
|
23284
|
+
`[ReolinkBaichuanApi] event watchdog tick: skipping (connection not alive, battery/UDP \u2014 not forcing reconnect: connected=${generalEntry.client.isSocketConnected?.()} loggedIn=${generalEntry.client.loggedIn})`
|
|
23285
|
+
);
|
|
23286
|
+
return;
|
|
23287
|
+
}
|
|
23288
|
+
const now2 = Date.now();
|
|
23289
|
+
const backoffMs = Math.min(
|
|
23290
|
+
3e4 * Math.pow(2, this.simpleEventWatchdogRecoveryAttempts),
|
|
23291
|
+
this.simpleEventWatchdogSilenceThresholdMs
|
|
23194
23292
|
);
|
|
23293
|
+
if (now2 - this.simpleEventWatchdogLastRecoveryAt < backoffMs) {
|
|
23294
|
+
this.logger.debug?.(
|
|
23295
|
+
`[ReolinkBaichuanApi] event watchdog tick: connection down, reconnect backoff active (connected=${generalEntry.client.isSocketConnected?.()} loggedIn=${generalEntry.client.loggedIn})`
|
|
23296
|
+
);
|
|
23297
|
+
return;
|
|
23298
|
+
}
|
|
23299
|
+
this.simpleEventWatchdogRecoveryAttempts++;
|
|
23300
|
+
this.simpleEventWatchdogLastRecoveryAt = now2;
|
|
23301
|
+
(this.logger.info ?? this.logger.log).call(
|
|
23302
|
+
this.logger,
|
|
23303
|
+
`[ReolinkBaichuanApi] event watchdog: control socket down, forcing full reconnect (attempt #${this.simpleEventWatchdogRecoveryAttempts})`,
|
|
23304
|
+
{ host: this.host }
|
|
23305
|
+
);
|
|
23306
|
+
try {
|
|
23307
|
+
await this.ensureConnected();
|
|
23308
|
+
this.simpleEventLastReceivedAt = Date.now();
|
|
23309
|
+
this.simpleEventWatchdogRecoveryAttempts = 0;
|
|
23310
|
+
(this.logger.info ?? this.logger.log).call(
|
|
23311
|
+
this.logger,
|
|
23312
|
+
`[ReolinkBaichuanApi] event watchdog: full reconnect successful`
|
|
23313
|
+
);
|
|
23314
|
+
} catch (e) {
|
|
23315
|
+
(this.logger.debug ?? this.logger.log).call(
|
|
23316
|
+
this.logger,
|
|
23317
|
+
`[ReolinkBaichuanApi] event watchdog: full reconnect attempt #${this.simpleEventWatchdogRecoveryAttempts} failed`,
|
|
23318
|
+
formatErrorForLog(e)
|
|
23319
|
+
);
|
|
23320
|
+
}
|
|
23195
23321
|
return;
|
|
23196
23322
|
}
|
|
23197
23323
|
const now = Date.now();
|