@apocaliss92/nodelink-js 0.4.5 → 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/{DiagnosticsTools-55PR4WFD.js → DiagnosticsTools-UMN4C7SY.js} +2 -2
- package/dist/{chunk-WDFKIHM5.js → chunk-GKLOJJ34.js} +177 -29
- package/dist/chunk-GKLOJJ34.js.map +1 -0
- package/dist/{chunk-DEOMUWBN.js → chunk-TR3V5FTO.js} +15 -1
- package/dist/chunk-TR3V5FTO.js.map +1 -0
- package/dist/cli/rtsp-server.cjs +187 -25
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +2 -2
- package/dist/index.cjs +278 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +67 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +93 -25
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-DEOMUWBN.js.map +0 -1
- package/dist/chunk-WDFKIHM5.js.map +0 -1
- /package/dist/{DiagnosticsTools-55PR4WFD.js.map → DiagnosticsTools-UMN4C7SY.js.map} +0 -0
package/dist/cli/rtsp-server.js
CHANGED
|
@@ -3,10 +3,10 @@ import {
|
|
|
3
3
|
BaichuanRtspServer,
|
|
4
4
|
ReolinkBaichuanApi,
|
|
5
5
|
autoDetectDeviceType
|
|
6
|
-
} from "../chunk-
|
|
6
|
+
} from "../chunk-GKLOJJ34.js";
|
|
7
7
|
import {
|
|
8
8
|
__require
|
|
9
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-TR3V5FTO.js";
|
|
10
10
|
|
|
11
11
|
// src/cli/rtsp-server.ts
|
|
12
12
|
function parseArgs() {
|
package/dist/index.cjs
CHANGED
|
@@ -2178,6 +2178,19 @@ var init_BaichuanVideoStream = __esm({
|
|
|
2178
2178
|
// Stateful AES decryptor for fragmented BcMedia packets (full_aes mode)
|
|
2179
2179
|
// In CFB mode, continuation frames must use the cipher state from previous frames.
|
|
2180
2180
|
aesStreamDecryptor = null;
|
|
2181
|
+
/**
|
|
2182
|
+
* Pending startup error stashed when emitSafeError is called before any
|
|
2183
|
+
* "error" listener is registered (e.g. camera returns 400 during start()).
|
|
2184
|
+
* The rfc4571-server's waitForKeyframe can consume this immediately instead
|
|
2185
|
+
* of waiting for the full keyframe timeout.
|
|
2186
|
+
*/
|
|
2187
|
+
_pendingStartupError;
|
|
2188
|
+
/** Consume and clear any pending startup error. */
|
|
2189
|
+
consumePendingStartupError() {
|
|
2190
|
+
const err = this._pendingStartupError;
|
|
2191
|
+
this._pendingStartupError = void 0;
|
|
2192
|
+
return err;
|
|
2193
|
+
}
|
|
2181
2194
|
emitSafeError(err) {
|
|
2182
2195
|
if (!this.active) {
|
|
2183
2196
|
this.logger?.warn?.(
|
|
@@ -2189,6 +2202,7 @@ var init_BaichuanVideoStream = __esm({
|
|
|
2189
2202
|
this.logger?.warn?.(
|
|
2190
2203
|
`[BaichuanVideoStream] Unhandled stream error: ${err.message}`
|
|
2191
2204
|
);
|
|
2205
|
+
this._pendingStartupError = err;
|
|
2192
2206
|
return;
|
|
2193
2207
|
}
|
|
2194
2208
|
this.emit("error", err);
|
|
@@ -8527,6 +8541,9 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
|
|
|
8527
8541
|
resendTimer;
|
|
8528
8542
|
hbTimer;
|
|
8529
8543
|
discoveryTid;
|
|
8544
|
+
// Track discovery-phase timers so close() can cancel them even if
|
|
8545
|
+
// discovery is still in progress (prevents ERR_SOCKET_DGRAM_NOT_RUNNING).
|
|
8546
|
+
discoveryTimers = [];
|
|
8530
8547
|
acceptSent = false;
|
|
8531
8548
|
lastAcceptAtMs;
|
|
8532
8549
|
ackScheduled = false;
|
|
@@ -8575,9 +8592,31 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
|
|
|
8575
8592
|
});
|
|
8576
8593
|
sock.on("error", (e) => this.emit("error", e));
|
|
8577
8594
|
sock.on("close", () => this.emit("close"));
|
|
8578
|
-
|
|
8579
|
-
|
|
8580
|
-
|
|
8595
|
+
const portRange = Array.from({ length: 500 }, (_, i) => 53500 + i);
|
|
8596
|
+
for (let i = portRange.length - 1; i > 0; i--) {
|
|
8597
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
8598
|
+
[portRange[i], portRange[j]] = [portRange[j], portRange[i]];
|
|
8599
|
+
}
|
|
8600
|
+
let bound = false;
|
|
8601
|
+
for (const port of portRange) {
|
|
8602
|
+
try {
|
|
8603
|
+
await new Promise((resolve, reject) => {
|
|
8604
|
+
sock.once("error", reject);
|
|
8605
|
+
sock.bind(port, "0.0.0.0", () => {
|
|
8606
|
+
sock.removeListener("error", reject);
|
|
8607
|
+
resolve();
|
|
8608
|
+
});
|
|
8609
|
+
});
|
|
8610
|
+
bound = true;
|
|
8611
|
+
break;
|
|
8612
|
+
} catch {
|
|
8613
|
+
}
|
|
8614
|
+
}
|
|
8615
|
+
if (!bound) {
|
|
8616
|
+
await new Promise(
|
|
8617
|
+
(resolve) => sock.bind(0, "0.0.0.0", () => resolve())
|
|
8618
|
+
);
|
|
8619
|
+
}
|
|
8581
8620
|
if (this.opts.mode === "direct") {
|
|
8582
8621
|
this.remote = { host: this.opts.host, port: this.opts.port };
|
|
8583
8622
|
this.clientId = this.opts.clientId;
|
|
@@ -8997,7 +9036,24 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
|
|
|
8997
9036
|
BCUDP_DISCOVERY_PORT_LOCAL_ANY,
|
|
8998
9037
|
BCUDP_DISCOVERY_PORT_LOCAL_UID
|
|
8999
9038
|
];
|
|
9000
|
-
const
|
|
9039
|
+
const broadcastHosts = ["255.255.255.255"];
|
|
9040
|
+
const ifaces = (0, import_node_os.networkInterfaces)();
|
|
9041
|
+
for (const name of Object.keys(ifaces)) {
|
|
9042
|
+
const entries = ifaces[name];
|
|
9043
|
+
if (!entries) continue;
|
|
9044
|
+
for (const addr2 of entries) {
|
|
9045
|
+
if (addr2.family === "IPv4" && !addr2.internal && addr2.cidr) {
|
|
9046
|
+
const ipParts = addr2.address.split(".").map(Number);
|
|
9047
|
+
const maskParts = addr2.netmask.split(".").map(Number);
|
|
9048
|
+
if (ipParts.length === 4 && maskParts.length === 4) {
|
|
9049
|
+
const bcast = ipParts.map((octet, i) => octet | ~maskParts[i] & 255).join(".");
|
|
9050
|
+
if (!broadcastHosts.includes(bcast)) {
|
|
9051
|
+
broadcastHosts.push(bcast);
|
|
9052
|
+
}
|
|
9053
|
+
}
|
|
9054
|
+
}
|
|
9055
|
+
}
|
|
9056
|
+
}
|
|
9001
9057
|
const directHost = (this.opts.directHost ?? "").trim();
|
|
9002
9058
|
const localMode = opts?.localMode ?? "local-broadcast";
|
|
9003
9059
|
const directFirstWindowMs = localMode === "local-direct" && directHost ? 3e3 : 0;
|
|
@@ -9024,6 +9080,7 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
|
|
|
9024
9080
|
)
|
|
9025
9081
|
);
|
|
9026
9082
|
}, discoveryTimeout);
|
|
9083
|
+
this.discoveryTimers.push(timeout);
|
|
9027
9084
|
let retryTimer;
|
|
9028
9085
|
let retryCount = 0;
|
|
9029
9086
|
let discoveredSid;
|
|
@@ -9200,11 +9257,11 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
|
|
|
9200
9257
|
if (directHost) {
|
|
9201
9258
|
if (directFirstWindowMs > 0 && elapsedMs < directFirstWindowMs)
|
|
9202
9259
|
return [directHost];
|
|
9203
|
-
return [directHost,
|
|
9260
|
+
return [directHost, ...broadcastHosts];
|
|
9204
9261
|
}
|
|
9205
|
-
return
|
|
9262
|
+
return broadcastHosts;
|
|
9206
9263
|
}
|
|
9207
|
-
return
|
|
9264
|
+
return broadcastHosts;
|
|
9208
9265
|
})();
|
|
9209
9266
|
for (const host of Array.from(new Set(hosts))) {
|
|
9210
9267
|
for (const port of ports) {
|
|
@@ -9212,8 +9269,7 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
|
|
|
9212
9269
|
sock.send(packet, port, host);
|
|
9213
9270
|
retryCount++;
|
|
9214
9271
|
this.emit("debug", "discovery_send", { retryCount, host, port });
|
|
9215
|
-
} catch
|
|
9216
|
-
this.emit("error", e instanceof Error ? e : new Error(String(e)));
|
|
9272
|
+
} catch {
|
|
9217
9273
|
}
|
|
9218
9274
|
}
|
|
9219
9275
|
}
|
|
@@ -9222,6 +9278,7 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
|
|
|
9222
9278
|
retryTimer = (0, import_node_timers.setInterval)(() => {
|
|
9223
9279
|
sendDiscovery();
|
|
9224
9280
|
}, retryInterval);
|
|
9281
|
+
this.discoveryTimers.push(retryTimer);
|
|
9225
9282
|
});
|
|
9226
9283
|
this.clientId = reply.cid;
|
|
9227
9284
|
this.cameraId = reply.did;
|
|
@@ -9538,6 +9595,10 @@ var BcUdpStream = class extends import_node_events.EventEmitter {
|
|
|
9538
9595
|
this.ackTimer = void 0;
|
|
9539
9596
|
this.resendTimer = void 0;
|
|
9540
9597
|
this.hbTimer = void 0;
|
|
9598
|
+
for (const t of this.discoveryTimers) {
|
|
9599
|
+
clearInterval(t);
|
|
9600
|
+
}
|
|
9601
|
+
this.discoveryTimers = [];
|
|
9541
9602
|
const s = this.sock;
|
|
9542
9603
|
this.sock = void 0;
|
|
9543
9604
|
if (!s) return;
|
|
@@ -18011,6 +18072,13 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18011
18072
|
* - "replay:XXX" - dedicated per replay session
|
|
18012
18073
|
*/
|
|
18013
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;
|
|
18014
18082
|
/** BaichuanClientOptions to use when creating new sockets */
|
|
18015
18083
|
clientOptions;
|
|
18016
18084
|
/**
|
|
@@ -18165,14 +18233,20 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18165
18233
|
if (!xml) return;
|
|
18166
18234
|
const channel = frame.header.channelId;
|
|
18167
18235
|
const battery = this.parseBatteryInfoXml(xml, channel);
|
|
18168
|
-
if (battery.batteryPercent
|
|
18169
|
-
|
|
18170
|
-
type: "battery",
|
|
18171
|
-
channel,
|
|
18172
|
-
timestamp: Date.now(),
|
|
18173
|
-
battery
|
|
18174
|
-
});
|
|
18236
|
+
if (battery.batteryPercent === void 0 && battery.chargeStatus === void 0 && battery.adapterStatus === void 0) {
|
|
18237
|
+
return;
|
|
18175
18238
|
}
|
|
18239
|
+
const key = `${battery.batteryPercent ?? ""}|${battery.chargeStatus ?? ""}|${battery.adapterStatus ?? ""}`;
|
|
18240
|
+
if (this.lastBatteryPushKey.get(channel) === key) {
|
|
18241
|
+
return;
|
|
18242
|
+
}
|
|
18243
|
+
this.lastBatteryPushKey.set(channel, key);
|
|
18244
|
+
this.dispatchSimpleEvent({
|
|
18245
|
+
type: "battery",
|
|
18246
|
+
channel,
|
|
18247
|
+
timestamp: Date.now(),
|
|
18248
|
+
battery
|
|
18249
|
+
});
|
|
18176
18250
|
} catch (e) {
|
|
18177
18251
|
this.logger.debug?.(
|
|
18178
18252
|
"[ReolinkBaichuanApi] Error parsing battery push",
|
|
@@ -18338,6 +18412,14 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18338
18412
|
deviceCapabilitiesCache = /* @__PURE__ */ new Map();
|
|
18339
18413
|
static CAPABILITIES_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
18340
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();
|
|
18341
18423
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
18342
18424
|
// SOCKET POOL CONSTANTS
|
|
18343
18425
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -18654,6 +18736,20 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18654
18736
|
const prefix = basename.substring(0, 10).toUpperCase();
|
|
18655
18737
|
return prefix.includes("S") ? "subStream" : "mainStream";
|
|
18656
18738
|
}
|
|
18739
|
+
/**
|
|
18740
|
+
* Stream profiles that the device explicitly rejected (response_code 400).
|
|
18741
|
+
* Keyed by `"ch:profile"` (e.g. `"0:ext"`). Once a profile is in this set
|
|
18742
|
+
* it is excluded from `buildVideoStreamOptions()` results and no further
|
|
18743
|
+
* start attempts are made until the API instance is recreated.
|
|
18744
|
+
*/
|
|
18745
|
+
_rejectedStreamProfiles = /* @__PURE__ */ new Set();
|
|
18746
|
+
/**
|
|
18747
|
+
* Check whether a stream profile was rejected by the device at runtime
|
|
18748
|
+
* (e.g. ext returned response_code 400).
|
|
18749
|
+
*/
|
|
18750
|
+
isStreamProfileRejected(channel, profile) {
|
|
18751
|
+
return this._rejectedStreamProfiles.has(`${channel}:${profile}`);
|
|
18752
|
+
}
|
|
18657
18753
|
/**
|
|
18658
18754
|
* Cache for buildVideoStreamOptions.
|
|
18659
18755
|
*
|
|
@@ -23778,6 +23874,16 @@ ${stderr}`)
|
|
|
23778
23874
|
}
|
|
23779
23875
|
if (!frame) frame = await targetClient.sendFrame(baseParams);
|
|
23780
23876
|
if (frame.header.responseCode !== 200) {
|
|
23877
|
+
if (frame.header.responseCode === 400) {
|
|
23878
|
+
const rejKey = `${ch}:${profile}`;
|
|
23879
|
+
if (!this._rejectedStreamProfiles.has(rejKey)) {
|
|
23880
|
+
this._rejectedStreamProfiles.add(rejKey);
|
|
23881
|
+
this.videoStreamOptionsCache.clear();
|
|
23882
|
+
this.logger?.warn?.(
|
|
23883
|
+
`[ReolinkBaichuanApi] Stream profile rejected by device: channel=${ch} profile=${profile} (response_code 400). This profile will be excluded from available streams. The camera may not support this stream profile with the current firmware.`
|
|
23884
|
+
);
|
|
23885
|
+
}
|
|
23886
|
+
}
|
|
23781
23887
|
throw new Error(
|
|
23782
23888
|
`Video stream request rejected (response_code ${frame.header.responseCode}). Expected response_code 200, camera returned ${frame.header.responseCode}`
|
|
23783
23889
|
);
|
|
@@ -23786,6 +23892,7 @@ ${stderr}`)
|
|
|
23786
23892
|
`${ch}:${profile}:${variant}`,
|
|
23787
23893
|
frame.header.msgNum
|
|
23788
23894
|
);
|
|
23895
|
+
this.resetStreamTimeoutCounter(targetClient);
|
|
23789
23896
|
return;
|
|
23790
23897
|
} catch (error) {
|
|
23791
23898
|
lastError = error;
|
|
@@ -23800,6 +23907,10 @@ ${stderr}`)
|
|
|
23800
23907
|
}
|
|
23801
23908
|
}
|
|
23802
23909
|
}
|
|
23910
|
+
const isTimeout = lastError instanceof Error && lastError.message?.includes("timeout");
|
|
23911
|
+
if (isTimeout) {
|
|
23912
|
+
this.trackStreamTimeout(targetClient);
|
|
23913
|
+
}
|
|
23803
23914
|
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
23804
23915
|
}
|
|
23805
23916
|
/**
|
|
@@ -24259,6 +24370,18 @@ ${stderr}`)
|
|
|
24259
24370
|
notifyD2cDisc() {
|
|
24260
24371
|
const now = Date.now();
|
|
24261
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
|
+
}
|
|
24262
24385
|
const immediateCooldownUntil = now + _ReolinkBaichuanApi.D2C_DISC_IMMEDIATE_COOLDOWN_MS;
|
|
24263
24386
|
const existing = this.socketPoolCooldowns.get(this.host);
|
|
24264
24387
|
if (!existing || existing.cooldownUntil < immediateCooldownUntil) {
|
|
@@ -24291,6 +24414,43 @@ ${stderr}`)
|
|
|
24291
24414
|
}
|
|
24292
24415
|
}
|
|
24293
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
|
+
}
|
|
24294
24454
|
/**
|
|
24295
24455
|
* Best-effort sleeping inference for battery/BCUDP cameras.
|
|
24296
24456
|
*
|
|
@@ -25799,6 +25959,8 @@ ${xml}`
|
|
|
25799
25959
|
for (const metadata of params.metadatas) {
|
|
25800
25960
|
const profile = metadata.profile;
|
|
25801
25961
|
if (isMultiFocal && profile === "ext") continue;
|
|
25962
|
+
if (this._rejectedStreamProfiles.has(`${params.channel}:${profile}`))
|
|
25963
|
+
continue;
|
|
25802
25964
|
if (params.includeRtsp && profile !== "ext") {
|
|
25803
25965
|
const streamName = profile === "main" ? "main" : "sub";
|
|
25804
25966
|
pushRtsp({
|
|
@@ -32289,6 +32451,11 @@ async function createRfc4571TcpServerInternal(options) {
|
|
|
32289
32451
|
"videoAccessUnit",
|
|
32290
32452
|
onAu
|
|
32291
32453
|
);
|
|
32454
|
+
const pendingErr = videoStream.consumePendingStartupError?.();
|
|
32455
|
+
if (pendingErr) {
|
|
32456
|
+
cleanup();
|
|
32457
|
+
reject(pendingErr);
|
|
32458
|
+
}
|
|
32292
32459
|
});
|
|
32293
32460
|
}
|
|
32294
32461
|
};
|
|
@@ -32300,24 +32467,32 @@ async function createRfc4571TcpServerInternal(options) {
|
|
|
32300
32467
|
await videoStream.stop();
|
|
32301
32468
|
} catch {
|
|
32302
32469
|
}
|
|
32303
|
-
if (
|
|
32304
|
-
|
|
32305
|
-
|
|
32470
|
+
if (dedicatedSession) {
|
|
32471
|
+
try {
|
|
32472
|
+
await dedicatedSession.release();
|
|
32473
|
+
} catch {
|
|
32474
|
+
}
|
|
32475
|
+
}
|
|
32476
|
+
if (!dedicatedSession) {
|
|
32477
|
+
if (closeApiOnTeardown) {
|
|
32478
|
+
await Promise.allSettled(
|
|
32479
|
+
Array.from(apisToClose).map(async (a) => {
|
|
32480
|
+
try {
|
|
32481
|
+
await a.close();
|
|
32482
|
+
} catch {
|
|
32483
|
+
}
|
|
32484
|
+
})
|
|
32485
|
+
);
|
|
32486
|
+
} else {
|
|
32487
|
+
const graceMs = isComposite ? 5e3 : 0;
|
|
32488
|
+
for (const a of Array.from(apisToClose)) {
|
|
32306
32489
|
try {
|
|
32307
|
-
|
|
32490
|
+
a?.client?.requestIdleDisconnectSoon?.(
|
|
32491
|
+
"rfc4571_teardown",
|
|
32492
|
+
graceMs
|
|
32493
|
+
);
|
|
32308
32494
|
} catch {
|
|
32309
32495
|
}
|
|
32310
|
-
})
|
|
32311
|
-
);
|
|
32312
|
-
} else {
|
|
32313
|
-
const graceMs = isComposite ? 5e3 : 0;
|
|
32314
|
-
for (const a of Array.from(apisToClose)) {
|
|
32315
|
-
try {
|
|
32316
|
-
a?.client?.requestIdleDisconnectSoon?.(
|
|
32317
|
-
"rfc4571_teardown",
|
|
32318
|
-
graceMs
|
|
32319
|
-
);
|
|
32320
|
-
} catch {
|
|
32321
32496
|
}
|
|
32322
32497
|
}
|
|
32323
32498
|
}
|
|
@@ -32573,7 +32748,7 @@ async function createRfc4571TcpServerInternal(options) {
|
|
|
32573
32748
|
} catch {
|
|
32574
32749
|
}
|
|
32575
32750
|
}
|
|
32576
|
-
if (closeApiOnTeardown) {
|
|
32751
|
+
if (closeApiOnTeardown && !dedicatedSession) {
|
|
32577
32752
|
await Promise.allSettled(
|
|
32578
32753
|
Array.from(apisToClose).map(async (a) => {
|
|
32579
32754
|
try {
|
|
@@ -33540,6 +33715,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33540
33715
|
gracePeriodMs;
|
|
33541
33716
|
prebufferMaxMs;
|
|
33542
33717
|
maxBufferBytes;
|
|
33718
|
+
streamTimeoutMs;
|
|
33543
33719
|
prestartStream;
|
|
33544
33720
|
active = false;
|
|
33545
33721
|
server;
|
|
@@ -33553,6 +33729,11 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33553
33729
|
connectedClients = /* @__PURE__ */ new Set();
|
|
33554
33730
|
clientSockets = /* @__PURE__ */ new Map();
|
|
33555
33731
|
stopGraceTimer;
|
|
33732
|
+
// Stream health monitoring
|
|
33733
|
+
lastFrameAt = 0;
|
|
33734
|
+
streamHealthTimer;
|
|
33735
|
+
totalFramesReceived = 0;
|
|
33736
|
+
totalVideoFramesWritten = 0;
|
|
33556
33737
|
// Prebuffer
|
|
33557
33738
|
prebuffer = [];
|
|
33558
33739
|
constructor(options) {
|
|
@@ -33568,6 +33749,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33568
33749
|
this.gracePeriodMs = options.gracePeriodMs ?? 3e4;
|
|
33569
33750
|
this.prebufferMaxMs = options.prebufferMs ?? 3e3;
|
|
33570
33751
|
this.maxBufferBytes = options.maxBufferBytes ?? 1e8;
|
|
33752
|
+
this.streamTimeoutMs = options.streamTimeoutMs ?? 15e3;
|
|
33571
33753
|
this.prestartStream = options.prestartStream ?? true;
|
|
33572
33754
|
}
|
|
33573
33755
|
// -----------------------------------------------------------------------
|
|
@@ -33606,6 +33788,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33606
33788
|
if (!this.active) return;
|
|
33607
33789
|
this.active = false;
|
|
33608
33790
|
clearTimeout(this.stopGraceTimer);
|
|
33791
|
+
this.stopStreamHealthMonitor();
|
|
33609
33792
|
for (const [id, sock] of this.clientSockets) {
|
|
33610
33793
|
sock.destroy();
|
|
33611
33794
|
this.connectedClients.delete(id);
|
|
@@ -33659,12 +33842,12 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33659
33842
|
`[Go2rtcTcpServer] feedClient error id=${clientId}: ${err}`
|
|
33660
33843
|
);
|
|
33661
33844
|
});
|
|
33662
|
-
const cleanup = () => {
|
|
33663
|
-
this.removeClient(clientId);
|
|
33845
|
+
const cleanup = (reason) => {
|
|
33846
|
+
this.removeClient(clientId, reason);
|
|
33664
33847
|
socket.destroy();
|
|
33665
33848
|
};
|
|
33666
|
-
socket.on("error", cleanup);
|
|
33667
|
-
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)"));
|
|
33668
33851
|
}
|
|
33669
33852
|
async feedClient(clientId, socket) {
|
|
33670
33853
|
const fanoutDeadline = Date.now() + 3e4;
|
|
@@ -33725,6 +33908,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33725
33908
|
}
|
|
33726
33909
|
socket.write(annexB);
|
|
33727
33910
|
liveVideoWritten++;
|
|
33911
|
+
this.totalVideoFramesWritten++;
|
|
33728
33912
|
if (Date.now() - lastLogAt > 1e4) {
|
|
33729
33913
|
this.logger.info?.(
|
|
33730
33914
|
`[Go2rtcTcpServer] live stats client=${clientId} received=${liveFrameCount} written=${liveVideoWritten} bufLen=${socket.writableLength}`
|
|
@@ -33851,6 +34035,8 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33851
34035
|
...dedicatedClient ? { client: dedicatedClient } : {}
|
|
33852
34036
|
}),
|
|
33853
34037
|
onFrame: (frame) => {
|
|
34038
|
+
this.lastFrameAt = Date.now();
|
|
34039
|
+
this.totalFramesReceived++;
|
|
33854
34040
|
if (!frame.audio && (frame.videoType === "H264" || frame.videoType === "H265")) {
|
|
33855
34041
|
this.detectedVideoType = frame.videoType;
|
|
33856
34042
|
}
|
|
@@ -33877,6 +34063,12 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33877
34063
|
if (!this.nativeStreamActive) return;
|
|
33878
34064
|
this.nativeStreamActive = false;
|
|
33879
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
|
+
);
|
|
33880
34072
|
if (this.dedicatedSessionRelease) {
|
|
33881
34073
|
this.dedicatedSessionRelease().catch(() => {
|
|
33882
34074
|
});
|
|
@@ -33884,16 +34076,18 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33884
34076
|
}
|
|
33885
34077
|
if (this.active && (this.connectedClients.size > 0 || this.prestartStream)) {
|
|
33886
34078
|
this.logger.info?.(
|
|
33887
|
-
`[Go2rtcTcpServer] native stream
|
|
34079
|
+
`[Go2rtcTcpServer] restarting native stream (clients=${this.connectedClients.size}, prestart=${this.prestartStream})`
|
|
33888
34080
|
);
|
|
33889
34081
|
this.startNativeStream();
|
|
33890
34082
|
}
|
|
33891
34083
|
}
|
|
33892
34084
|
});
|
|
33893
34085
|
this.nativeFanout.start();
|
|
34086
|
+
this.startStreamHealthMonitor();
|
|
33894
34087
|
}
|
|
33895
34088
|
async stopNativeStream() {
|
|
33896
34089
|
this.nativeStreamActive = false;
|
|
34090
|
+
this.stopStreamHealthMonitor();
|
|
33897
34091
|
const fanout = this.nativeFanout;
|
|
33898
34092
|
this.nativeFanout = null;
|
|
33899
34093
|
if (fanout) {
|
|
@@ -33907,14 +34101,50 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
33907
34101
|
}
|
|
33908
34102
|
}
|
|
33909
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
|
+
// -----------------------------------------------------------------------
|
|
33910
34138
|
// Client lifecycle
|
|
33911
34139
|
// -----------------------------------------------------------------------
|
|
33912
|
-
removeClient(clientId) {
|
|
34140
|
+
removeClient(clientId, reason) {
|
|
33913
34141
|
if (!this.connectedClients.has(clientId)) return;
|
|
33914
34142
|
this.connectedClients.delete(clientId);
|
|
33915
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` : "";
|
|
33916
34146
|
this.logger.info?.(
|
|
33917
|
-
`[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}`
|
|
33918
34148
|
);
|
|
33919
34149
|
this.emit("clientDisconnected", clientId);
|
|
33920
34150
|
if (this.connectedClients.size === 0 && !this.prestartStream) {
|
|
@@ -36249,16 +36479,16 @@ function isTcpFailureThatShouldFallbackToUdp(e) {
|
|
|
36249
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");
|
|
36250
36480
|
}
|
|
36251
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
|
+
);
|
|
36252
36491
|
return new Promise((resolve) => {
|
|
36253
|
-
const { exec } = require("child_process");
|
|
36254
|
-
const platform2 = process.platform;
|
|
36255
|
-
const pingCmd = platform2 === "win32" ? `ping -n 1 -w ${timeoutMs} ${host}` : platform2 === "darwin" ? (
|
|
36256
|
-
// macOS: -W is in milliseconds (Linux: seconds)
|
|
36257
|
-
`ping -c 1 -W ${timeoutMs} ${host}`
|
|
36258
|
-
) : (
|
|
36259
|
-
// Linux/BSD-ish: -W is in seconds on most distros
|
|
36260
|
-
`ping -c 1 -W ${Math.max(1, Math.floor(timeoutMs / 1e3))} ${host}`
|
|
36261
|
-
);
|
|
36262
36492
|
exec(pingCmd, (error) => {
|
|
36263
36493
|
resolve(!error);
|
|
36264
36494
|
});
|