@apocaliss92/nodelink-js 0.6.6 → 0.6.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-JQ5NSEVD.js → chunk-T22QCNBR.js} +54 -16
- package/dist/chunk-T22QCNBR.js.map +1 -0
- package/dist/cli/rtsp-server.cjs +53 -15
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +1 -1
- package/dist/index.cjs +75 -842
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -192
- package/dist/index.d.ts +4 -171
- package/dist/index.js +21 -823
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-JQ5NSEVD.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -8431,7 +8431,6 @@ __export(index_exports, {
|
|
|
8431
8431
|
DUAL_LENS_DUAL_MOTION_MODELS: () => DUAL_LENS_DUAL_MOTION_MODELS,
|
|
8432
8432
|
DUAL_LENS_MODELS: () => DUAL_LENS_MODELS,
|
|
8433
8433
|
DUAL_LENS_SINGLE_MOTION_MODELS: () => DUAL_LENS_SINGLE_MOTION_MODELS,
|
|
8434
|
-
Go2rtcTcpServer: () => Go2rtcTcpServer,
|
|
8435
8434
|
H264RtpDepacketizer: () => H264RtpDepacketizer,
|
|
8436
8435
|
H265RtpDepacketizer: () => H265RtpDepacketizer,
|
|
8437
8436
|
HlsSessionManager: () => HlsSessionManager,
|
|
@@ -14898,6 +14897,38 @@ function createRtspFlow(transport, videoType) {
|
|
|
14898
14897
|
return new H265Flow(transport);
|
|
14899
14898
|
}
|
|
14900
14899
|
|
|
14900
|
+
// src/baichuan/stream/rtpVideoTimestamp.ts
|
|
14901
|
+
var U32 = 4294967296;
|
|
14902
|
+
var DEFAULT_VIDEO_CLOCK_RATE = 9e4;
|
|
14903
|
+
function deriveRtpVideoTimestamp(state, frameMicroseconds, clockRate = DEFAULT_VIDEO_CLOCK_RATE) {
|
|
14904
|
+
if (frameMicroseconds === null || frameMicroseconds === void 0 || !Number.isFinite(frameMicroseconds)) {
|
|
14905
|
+
return { timestamp: state.timestamp, state };
|
|
14906
|
+
}
|
|
14907
|
+
const curRaw = frameMicroseconds >>> 0;
|
|
14908
|
+
if (state.baseUnwrappedUs === void 0) {
|
|
14909
|
+
return {
|
|
14910
|
+
timestamp: state.timestamp,
|
|
14911
|
+
state: {
|
|
14912
|
+
...state,
|
|
14913
|
+
unwrappedUs: curRaw,
|
|
14914
|
+
lastRawUs: curRaw,
|
|
14915
|
+
baseUnwrappedUs: curRaw,
|
|
14916
|
+
baseTimestamp: state.timestamp
|
|
14917
|
+
}
|
|
14918
|
+
};
|
|
14919
|
+
}
|
|
14920
|
+
const lastRaw = state.lastRawUs ?? curRaw;
|
|
14921
|
+
let forwardDelta = curRaw - lastRaw >>> 0;
|
|
14922
|
+
if (forwardDelta > U32 / 2) forwardDelta = 0;
|
|
14923
|
+
const unwrappedUs = (state.unwrappedUs ?? curRaw) + forwardDelta;
|
|
14924
|
+
const deltaUs = unwrappedUs - state.baseUnwrappedUs;
|
|
14925
|
+
const timestamp = state.baseTimestamp + Math.round(deltaUs * clockRate / 1e6) >>> 0;
|
|
14926
|
+
return {
|
|
14927
|
+
timestamp,
|
|
14928
|
+
state: { ...state, unwrappedUs, lastRawUs: curRaw, timestamp }
|
|
14929
|
+
};
|
|
14930
|
+
}
|
|
14931
|
+
|
|
14901
14932
|
// src/baichuan/stream/BaichuanRtspServer.ts
|
|
14902
14933
|
init_H264Converter();
|
|
14903
14934
|
init_H265Converter();
|
|
@@ -15472,7 +15503,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events5.E
|
|
|
15472
15503
|
let useTcpInterleaved = false;
|
|
15473
15504
|
let clientUdpSocket = null;
|
|
15474
15505
|
let clientUdpSocketAudio = null;
|
|
15506
|
+
let cleanedUp = false;
|
|
15475
15507
|
const cleanup = () => {
|
|
15508
|
+
if (cleanedUp) return;
|
|
15509
|
+
cleanedUp = true;
|
|
15476
15510
|
const sessionDurationMs = Date.now() - connectTime;
|
|
15477
15511
|
const res = this.clientResources.get(clientId);
|
|
15478
15512
|
const framesSent = res?.framesSent ?? 0;
|
|
@@ -16220,22 +16254,25 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events5.E
|
|
|
16220
16254
|
resources.rtpVideoTimestamp = 0;
|
|
16221
16255
|
if (resources.rtpVideoBaseTimestamp === void 0)
|
|
16222
16256
|
resources.rtpVideoBaseTimestamp = resources.rtpVideoTimestamp;
|
|
16223
|
-
|
|
16257
|
+
const { timestamp, state } = deriveRtpVideoTimestamp(
|
|
16258
|
+
{
|
|
16259
|
+
timestamp: resources.rtpVideoTimestamp,
|
|
16260
|
+
baseTimestamp: resources.rtpVideoBaseTimestamp,
|
|
16261
|
+
unwrappedUs: resources.rtpVideoUnwrappedUs,
|
|
16262
|
+
lastRawUs: resources.rtpVideoLastRawUs,
|
|
16263
|
+
baseUnwrappedUs: resources.rtpVideoBaseUnwrappedUs
|
|
16264
|
+
},
|
|
16265
|
+
frameMicroseconds,
|
|
16266
|
+
videoClockRate
|
|
16267
|
+
);
|
|
16268
|
+
resources.rtpVideoTimestamp = timestamp;
|
|
16269
|
+
resources.rtpVideoBaseTimestamp = state.baseTimestamp;
|
|
16270
|
+
resources.rtpVideoUnwrappedUs = state.unwrappedUs;
|
|
16271
|
+
resources.rtpVideoLastRawUs = state.lastRawUs;
|
|
16272
|
+
resources.rtpVideoBaseUnwrappedUs = state.baseUnwrappedUs;
|
|
16273
|
+
resources.rtpVideoLastTimestamp = timestamp;
|
|
16274
|
+
if (resources.rtpVideoBaseMicroseconds === void 0)
|
|
16224
16275
|
resources.rtpVideoBaseMicroseconds = frameMicroseconds >>> 0;
|
|
16225
|
-
resources.rtpVideoLastTimestamp = resources.rtpVideoTimestamp;
|
|
16226
|
-
return;
|
|
16227
|
-
}
|
|
16228
|
-
const baseUs = resources.rtpVideoBaseMicroseconds >>> 0;
|
|
16229
|
-
const curUs = frameMicroseconds >>> 0;
|
|
16230
|
-
const deltaUs = curUs - baseUs >>> 0;
|
|
16231
|
-
const baseTs = (resources.rtpVideoBaseTimestamp ?? 0) >>> 0;
|
|
16232
|
-
let ts = baseTs + Math.round(deltaUs * videoClockRate / 1e6) >>> 0;
|
|
16233
|
-
const last = resources.rtpVideoLastTimestamp;
|
|
16234
|
-
if (last !== void 0 && ts <= last >>> 0) {
|
|
16235
|
-
ts = (last >>> 0) + 1 >>> 0;
|
|
16236
|
-
}
|
|
16237
|
-
resources.rtpVideoTimestamp = ts;
|
|
16238
|
-
resources.rtpVideoLastTimestamp = ts;
|
|
16239
16276
|
};
|
|
16240
16277
|
const sendVideoAccessUnit = (videoType, accessUnitAnnexB, advanceTimestamp = true) => {
|
|
16241
16278
|
const nals = _BaichuanRtspServer.splitAnnexBNals(accessUnitAnnexB);
|
|
@@ -38230,811 +38267,8 @@ async function createReplayHttpServer(options) {
|
|
|
38230
38267
|
// src/index.ts
|
|
38231
38268
|
init_BaichuanVideoStream();
|
|
38232
38269
|
|
|
38233
|
-
// src/baichuan/stream/Go2rtcTcpServer.ts
|
|
38234
|
-
var import_node_events8 = require("events");
|
|
38235
|
-
var net4 = __toESM(require("net"), 1);
|
|
38236
|
-
init_H264Converter();
|
|
38237
|
-
init_H265Converter();
|
|
38238
|
-
var AsyncBoundedQueue2 = class {
|
|
38239
|
-
maxItems;
|
|
38240
|
-
queue = [];
|
|
38241
|
-
waiting;
|
|
38242
|
-
closed = false;
|
|
38243
|
-
constructor(maxItems) {
|
|
38244
|
-
this.maxItems = Math.max(1, maxItems | 0);
|
|
38245
|
-
}
|
|
38246
|
-
push(item) {
|
|
38247
|
-
if (this.closed) return;
|
|
38248
|
-
if (this.waiting) {
|
|
38249
|
-
const { resolve } = this.waiting;
|
|
38250
|
-
this.waiting = void 0;
|
|
38251
|
-
resolve({ value: item, done: false });
|
|
38252
|
-
return;
|
|
38253
|
-
}
|
|
38254
|
-
this.queue.push(item);
|
|
38255
|
-
if (this.queue.length > this.maxItems) {
|
|
38256
|
-
this.queue.splice(0, this.queue.length - this.maxItems);
|
|
38257
|
-
}
|
|
38258
|
-
}
|
|
38259
|
-
close() {
|
|
38260
|
-
if (this.closed) return;
|
|
38261
|
-
this.closed = true;
|
|
38262
|
-
if (this.waiting) {
|
|
38263
|
-
const { resolve } = this.waiting;
|
|
38264
|
-
this.waiting = void 0;
|
|
38265
|
-
resolve({ value: void 0, done: true });
|
|
38266
|
-
}
|
|
38267
|
-
}
|
|
38268
|
-
async next() {
|
|
38269
|
-
if (this.closed) return { value: void 0, done: true };
|
|
38270
|
-
const item = this.queue.shift();
|
|
38271
|
-
if (item !== void 0) return { value: item, done: false };
|
|
38272
|
-
return await new Promise((resolve) => {
|
|
38273
|
-
this.waiting = { resolve };
|
|
38274
|
-
});
|
|
38275
|
-
}
|
|
38276
|
-
};
|
|
38277
|
-
var NativeStreamFanout2 = class {
|
|
38278
|
-
opts;
|
|
38279
|
-
queues = /* @__PURE__ */ new Map();
|
|
38280
|
-
source = null;
|
|
38281
|
-
running = false;
|
|
38282
|
-
pumpPromise = null;
|
|
38283
|
-
constructor(opts) {
|
|
38284
|
-
this.opts = opts;
|
|
38285
|
-
}
|
|
38286
|
-
start() {
|
|
38287
|
-
if (this.running) return;
|
|
38288
|
-
this.running = true;
|
|
38289
|
-
this.source = this.opts.createSource();
|
|
38290
|
-
this.pumpPromise = (async () => {
|
|
38291
|
-
try {
|
|
38292
|
-
for await (const frame of this.source) {
|
|
38293
|
-
try {
|
|
38294
|
-
this.opts.onFrame?.(frame);
|
|
38295
|
-
} catch {
|
|
38296
|
-
}
|
|
38297
|
-
for (const q of this.queues.values()) {
|
|
38298
|
-
q.push(frame);
|
|
38299
|
-
}
|
|
38300
|
-
}
|
|
38301
|
-
} catch (e) {
|
|
38302
|
-
this.opts.onError?.(e);
|
|
38303
|
-
} finally {
|
|
38304
|
-
for (const q of this.queues.values()) q.close();
|
|
38305
|
-
this.queues.clear();
|
|
38306
|
-
this.running = false;
|
|
38307
|
-
this.opts.onEnd?.();
|
|
38308
|
-
}
|
|
38309
|
-
})();
|
|
38310
|
-
}
|
|
38311
|
-
subscribe(id) {
|
|
38312
|
-
const q = new AsyncBoundedQueue2(this.opts.maxQueueItems);
|
|
38313
|
-
this.queues.set(id, q);
|
|
38314
|
-
const self = this;
|
|
38315
|
-
return (async function* () {
|
|
38316
|
-
try {
|
|
38317
|
-
while (true) {
|
|
38318
|
-
const r = await q.next();
|
|
38319
|
-
if (r.done) return;
|
|
38320
|
-
yield r.value;
|
|
38321
|
-
}
|
|
38322
|
-
} finally {
|
|
38323
|
-
q.close();
|
|
38324
|
-
self.queues.delete(id);
|
|
38325
|
-
}
|
|
38326
|
-
})();
|
|
38327
|
-
}
|
|
38328
|
-
async stop() {
|
|
38329
|
-
if (!this.running) return;
|
|
38330
|
-
this.running = false;
|
|
38331
|
-
const src = this.source;
|
|
38332
|
-
this.source = null;
|
|
38333
|
-
for (const q of this.queues.values()) q.close();
|
|
38334
|
-
this.queues.clear();
|
|
38335
|
-
try {
|
|
38336
|
-
await src?.return(void 0);
|
|
38337
|
-
} catch {
|
|
38338
|
-
}
|
|
38339
|
-
try {
|
|
38340
|
-
await this.pumpPromise;
|
|
38341
|
-
} catch {
|
|
38342
|
-
}
|
|
38343
|
-
this.pumpPromise = null;
|
|
38344
|
-
}
|
|
38345
|
-
};
|
|
38346
|
-
var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events8.EventEmitter {
|
|
38347
|
-
api;
|
|
38348
|
-
channel;
|
|
38349
|
-
profile;
|
|
38350
|
-
variant;
|
|
38351
|
-
listenHost;
|
|
38352
|
-
listenPort;
|
|
38353
|
-
logger;
|
|
38354
|
-
deviceId;
|
|
38355
|
-
gracePeriodMs;
|
|
38356
|
-
prebufferMaxMs;
|
|
38357
|
-
maxBufferBytes;
|
|
38358
|
-
streamTimeoutMs;
|
|
38359
|
-
prestartStream;
|
|
38360
|
-
active = false;
|
|
38361
|
-
server;
|
|
38362
|
-
resolvedPort;
|
|
38363
|
-
// Native stream
|
|
38364
|
-
nativeFanout = null;
|
|
38365
|
-
nativeStreamActive = false;
|
|
38366
|
-
// Set only by stopNativeStream() (explicit teardown) so the fanout's onEnd
|
|
38367
|
-
// callback can short-circuit cleanup/restart logic. NOT set by the inactivity-
|
|
38368
|
-
// timeout force-restart path — that flow wants onEnd to run and decide
|
|
38369
|
-
// whether to restart based on prestartStream / connected clients.
|
|
38370
|
-
nativeStreamStopping = false;
|
|
38371
|
-
// Pending retry timer for the unbounded auto-restart loop. When a stream
|
|
38372
|
-
// start fails transiently (camera in maintenance reboot, idle-disconnect
|
|
38373
|
-
// race, etc.) we keep trying with exponential backoff until either the
|
|
38374
|
-
// server is stopped or a frame finally arrives.
|
|
38375
|
-
nativeStreamRetryTimer;
|
|
38376
|
-
nativeStreamRetryDelayMs = 0;
|
|
38377
|
-
dedicatedSessionRelease;
|
|
38378
|
-
detectedVideoType;
|
|
38379
|
-
// Client tracking
|
|
38380
|
-
connectedClients = /* @__PURE__ */ new Set();
|
|
38381
|
-
clientSockets = /* @__PURE__ */ new Map();
|
|
38382
|
-
stopGraceTimer;
|
|
38383
|
-
// Stream health monitoring
|
|
38384
|
-
lastFrameAt = 0;
|
|
38385
|
-
streamHealthTimer;
|
|
38386
|
-
totalFramesReceived = 0;
|
|
38387
|
-
totalVideoFramesWritten = 0;
|
|
38388
|
-
// Prebuffer
|
|
38389
|
-
prebuffer = [];
|
|
38390
|
-
// Audio metadata — populated on first valid ADTS AAC frame.
|
|
38391
|
-
// Exposed via getAudioInfo() for the stream-diagnostics feature.
|
|
38392
|
-
audioInfo = null;
|
|
38393
|
-
constructor(options) {
|
|
38394
|
-
super();
|
|
38395
|
-
this.api = options.api;
|
|
38396
|
-
this.channel = options.channel;
|
|
38397
|
-
this.profile = options.profile;
|
|
38398
|
-
this.variant = options.variant ?? "default";
|
|
38399
|
-
this.listenHost = options.listenHost ?? "127.0.0.1";
|
|
38400
|
-
this.listenPort = options.listenPort ?? 0;
|
|
38401
|
-
this.logger = options.logger ?? console;
|
|
38402
|
-
this.deviceId = options.deviceId;
|
|
38403
|
-
this.gracePeriodMs = options.gracePeriodMs ?? 3e4;
|
|
38404
|
-
this.prebufferMaxMs = options.prebufferMs ?? 3e3;
|
|
38405
|
-
this.maxBufferBytes = options.maxBufferBytes ?? 1e8;
|
|
38406
|
-
this.streamTimeoutMs = options.streamTimeoutMs ?? 15e3;
|
|
38407
|
-
this.prestartStream = options.prestartStream ?? true;
|
|
38408
|
-
}
|
|
38409
|
-
// -----------------------------------------------------------------------
|
|
38410
|
-
// Public API
|
|
38411
|
-
// -----------------------------------------------------------------------
|
|
38412
|
-
/** Start listening. Resolves once the TCP server is bound. */
|
|
38413
|
-
async start() {
|
|
38414
|
-
if (this.active) return;
|
|
38415
|
-
this.active = true;
|
|
38416
|
-
this.server = net4.createServer((socket) => this.handleClient(socket));
|
|
38417
|
-
this.server.on("error", (err) => {
|
|
38418
|
-
this.logger.error?.(`[Go2rtcTcpServer] server error: ${err.message}`);
|
|
38419
|
-
this.emit("error", err);
|
|
38420
|
-
});
|
|
38421
|
-
await new Promise((resolve, reject) => {
|
|
38422
|
-
this.server.listen(this.listenPort, this.listenHost, () => {
|
|
38423
|
-
const addr = this.server.address();
|
|
38424
|
-
this.resolvedPort = addr.port;
|
|
38425
|
-
this.logger.info?.(
|
|
38426
|
-
`[Go2rtcTcpServer] listening on ${addr.address}:${addr.port} channel=${this.channel} profile=${this.profile}`
|
|
38427
|
-
);
|
|
38428
|
-
this.emit("listening", { host: addr.address, port: addr.port });
|
|
38429
|
-
resolve();
|
|
38430
|
-
});
|
|
38431
|
-
this.server.once("error", reject);
|
|
38432
|
-
});
|
|
38433
|
-
if (this.prestartStream) {
|
|
38434
|
-
this.logger.info?.(
|
|
38435
|
-
`[Go2rtcTcpServer] pre-starting native stream channel=${this.channel} profile=${this.profile}`
|
|
38436
|
-
);
|
|
38437
|
-
this.startNativeStream();
|
|
38438
|
-
}
|
|
38439
|
-
}
|
|
38440
|
-
/** Stop the server and all active streams. */
|
|
38441
|
-
async stop() {
|
|
38442
|
-
if (!this.active) return;
|
|
38443
|
-
this.active = false;
|
|
38444
|
-
clearTimeout(this.stopGraceTimer);
|
|
38445
|
-
this.clearNativeStreamRetry();
|
|
38446
|
-
this.stopStreamHealthMonitor();
|
|
38447
|
-
for (const [id, sock] of this.clientSockets) {
|
|
38448
|
-
sock.destroy();
|
|
38449
|
-
this.connectedClients.delete(id);
|
|
38450
|
-
}
|
|
38451
|
-
this.clientSockets.clear();
|
|
38452
|
-
await this.stopNativeStream();
|
|
38453
|
-
if (this.server) {
|
|
38454
|
-
await new Promise((resolve) => {
|
|
38455
|
-
this.server.close(() => resolve());
|
|
38456
|
-
});
|
|
38457
|
-
this.server = void 0;
|
|
38458
|
-
}
|
|
38459
|
-
this.prebuffer = [];
|
|
38460
|
-
this.resolvedPort = void 0;
|
|
38461
|
-
this.emit("close");
|
|
38462
|
-
}
|
|
38463
|
-
/** The actual port the server is listening on (available after start()). */
|
|
38464
|
-
get port() {
|
|
38465
|
-
return this.resolvedPort;
|
|
38466
|
-
}
|
|
38467
|
-
/** The go2rtc-compatible source URL. */
|
|
38468
|
-
get go2rtcSourceUrl() {
|
|
38469
|
-
if (this.resolvedPort == null) return void 0;
|
|
38470
|
-
return `tcp://127.0.0.1:${this.resolvedPort}`;
|
|
38471
|
-
}
|
|
38472
|
-
/** Number of currently connected clients. */
|
|
38473
|
-
get clientCount() {
|
|
38474
|
-
return this.connectedClients.size;
|
|
38475
|
-
}
|
|
38476
|
-
// -----------------------------------------------------------------------
|
|
38477
|
-
// Diagnostic subscription API (implements DiagnosticStreamServer)
|
|
38478
|
-
//
|
|
38479
|
-
// Matches the shape of BaichuanRtspServer's diagnostic API so the
|
|
38480
|
-
// stream-diagnostic feature in the Manager app can drive either backend
|
|
38481
|
-
// with identical code.
|
|
38482
|
-
// -----------------------------------------------------------------------
|
|
38483
|
-
/**
|
|
38484
|
-
* Subscribe to the raw native stream for diagnostic purposes.
|
|
38485
|
-
* The subscriber receives the same frames the MPEG-TS muxer consumes
|
|
38486
|
-
* (pre-muxing). Counts as a "consumer" so the native stream is kept alive
|
|
38487
|
-
* for the lifetime of the subscription. If the stream is not already
|
|
38488
|
-
* running (battery camera, prestart=false), this starts it.
|
|
38489
|
-
*/
|
|
38490
|
-
async subscribeDiagnostic(id) {
|
|
38491
|
-
this.connectedClients.add(`diag:${id}`);
|
|
38492
|
-
if (!this.nativeStreamActive) {
|
|
38493
|
-
await this.startNativeStream();
|
|
38494
|
-
}
|
|
38495
|
-
if (!this.nativeFanout) {
|
|
38496
|
-
this.connectedClients.delete(`diag:${id}`);
|
|
38497
|
-
throw new Error(
|
|
38498
|
-
"Go2rtcTcpServer: native stream failed to start \u2014 cannot subscribe diagnostic"
|
|
38499
|
-
);
|
|
38500
|
-
}
|
|
38501
|
-
return this.nativeFanout.subscribe(`diag:${id}`);
|
|
38502
|
-
}
|
|
38503
|
-
/** Unsubscribe a diagnostic session and release its consumer slot. */
|
|
38504
|
-
unsubscribeDiagnostic(id) {
|
|
38505
|
-
this.removeClient(`diag:${id}`, "diagnostic unsubscribe");
|
|
38506
|
-
}
|
|
38507
|
-
/**
|
|
38508
|
-
* Returns ADTS AAC audio metadata detected from the native stream, or
|
|
38509
|
-
* null if no audio frame has been observed yet (e.g. video-only cameras
|
|
38510
|
-
* or before the first audio packet arrives).
|
|
38511
|
-
*/
|
|
38512
|
-
getAudioInfo() {
|
|
38513
|
-
return this.audioInfo;
|
|
38514
|
-
}
|
|
38515
|
-
// -----------------------------------------------------------------------
|
|
38516
|
-
// Client handling
|
|
38517
|
-
// -----------------------------------------------------------------------
|
|
38518
|
-
handleClient(socket) {
|
|
38519
|
-
const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
|
|
38520
|
-
socket.setNoDelay(true);
|
|
38521
|
-
this.connectedClients.add(clientId);
|
|
38522
|
-
this.clientSockets.set(clientId, socket);
|
|
38523
|
-
this.logger.info?.(
|
|
38524
|
-
`[Go2rtcTcpServer] client connected id=${clientId} total=${this.connectedClients.size}`
|
|
38525
|
-
);
|
|
38526
|
-
this.emit("client", clientId);
|
|
38527
|
-
if (this.stopGraceTimer) {
|
|
38528
|
-
clearTimeout(this.stopGraceTimer);
|
|
38529
|
-
this.stopGraceTimer = void 0;
|
|
38530
|
-
}
|
|
38531
|
-
if (!this.nativeStreamActive) {
|
|
38532
|
-
this.startNativeStream();
|
|
38533
|
-
}
|
|
38534
|
-
this.feedClient(clientId, socket).catch((err) => {
|
|
38535
|
-
this.logger.warn?.(
|
|
38536
|
-
`[Go2rtcTcpServer] feedClient error id=${clientId}: ${err}`
|
|
38537
|
-
);
|
|
38538
|
-
});
|
|
38539
|
-
const cleanup = (reason) => {
|
|
38540
|
-
this.removeClient(clientId, reason);
|
|
38541
|
-
socket.destroy();
|
|
38542
|
-
};
|
|
38543
|
-
socket.on("error", (err) => cleanup(`error: ${err.message}`));
|
|
38544
|
-
socket.on("close", (hadError) => cleanup(hadError ? "close (with error)" : "close (clean)"));
|
|
38545
|
-
}
|
|
38546
|
-
async feedClient(clientId, socket) {
|
|
38547
|
-
const fanoutDeadline = Date.now() + 3e4;
|
|
38548
|
-
while (this.active && !this.nativeFanout) {
|
|
38549
|
-
if (socket.destroyed) return;
|
|
38550
|
-
if (Date.now() > fanoutDeadline) {
|
|
38551
|
-
this.logger.warn?.(
|
|
38552
|
-
`[Go2rtcTcpServer] fanout not ready after 30s, dropping client ${clientId}`
|
|
38553
|
-
);
|
|
38554
|
-
return;
|
|
38555
|
-
}
|
|
38556
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
38557
|
-
}
|
|
38558
|
-
if (!this.active || !this.nativeFanout) return;
|
|
38559
|
-
const subscription = this.nativeFanout.subscribe(clientId);
|
|
38560
|
-
let muxer = null;
|
|
38561
|
-
const prebufferSnap = this.prebuffer.slice();
|
|
38562
|
-
let lastIdrIdx = -1;
|
|
38563
|
-
for (let i = prebufferSnap.length - 1; i >= 0; i--) {
|
|
38564
|
-
if (prebufferSnap[i].isKeyframe) {
|
|
38565
|
-
lastIdrIdx = i;
|
|
38566
|
-
break;
|
|
38567
|
-
}
|
|
38568
|
-
}
|
|
38569
|
-
if (lastIdrIdx >= 0) {
|
|
38570
|
-
const replay = prebufferSnap.slice(lastIdrIdx);
|
|
38571
|
-
this.logger.info?.(
|
|
38572
|
-
`[Go2rtcTcpServer] prebuffer replay client=${clientId} frames=${replay.length}`
|
|
38573
|
-
);
|
|
38574
|
-
if (!muxer) {
|
|
38575
|
-
muxer = new MpegTsMuxer({
|
|
38576
|
-
videoType: this.detectedVideoType ?? "H264",
|
|
38577
|
-
includeAudio: true
|
|
38578
|
-
});
|
|
38579
|
-
}
|
|
38580
|
-
for (const entry of replay) {
|
|
38581
|
-
if (socket.destroyed) return;
|
|
38582
|
-
let ts;
|
|
38583
|
-
if (!entry.audio) {
|
|
38584
|
-
ts = muxer.muxVideo(entry.data, entry.pts, entry.isKeyframe);
|
|
38585
|
-
} else {
|
|
38586
|
-
ts = muxer.muxAudio(entry.data, entry.pts);
|
|
38587
|
-
}
|
|
38588
|
-
if (ts.length > 0) socket.write(ts);
|
|
38589
|
-
}
|
|
38590
|
-
}
|
|
38591
|
-
let seenKeyframe = lastIdrIdx >= 0;
|
|
38592
|
-
let liveFrameCount = 0;
|
|
38593
|
-
let liveVideoWritten = 0;
|
|
38594
|
-
let lastLogAt = Date.now();
|
|
38595
|
-
try {
|
|
38596
|
-
this.logger.info?.(
|
|
38597
|
-
`[Go2rtcTcpServer] entering live loop client=${clientId} seenKeyframe=${seenKeyframe}`
|
|
38598
|
-
);
|
|
38599
|
-
for await (const frame of subscription) {
|
|
38600
|
-
if (socket.destroyed || !this.active) {
|
|
38601
|
-
this.logger.info?.(
|
|
38602
|
-
`[Go2rtcTcpServer] live loop exit client=${clientId} destroyed=${socket.destroyed} active=${this.active}`
|
|
38603
|
-
);
|
|
38604
|
-
break;
|
|
38605
|
-
}
|
|
38606
|
-
liveFrameCount++;
|
|
38607
|
-
if (frame.audio) {
|
|
38608
|
-
if (muxer) {
|
|
38609
|
-
const pts2 = frame.microseconds ?? Date.now() * 1e3;
|
|
38610
|
-
const ts2 = muxer.muxAudio(frame.data, pts2);
|
|
38611
|
-
if (ts2.length > 0) socket.write(ts2);
|
|
38612
|
-
}
|
|
38613
|
-
continue;
|
|
38614
|
-
}
|
|
38615
|
-
const annexB = this.convertVideoFrame(frame);
|
|
38616
|
-
if (!annexB) continue;
|
|
38617
|
-
const isKf = this.isAnnexBKeyframe(annexB, frame.videoType);
|
|
38618
|
-
if (!seenKeyframe) {
|
|
38619
|
-
if (!isKf) continue;
|
|
38620
|
-
seenKeyframe = true;
|
|
38621
|
-
this.logger.info?.(
|
|
38622
|
-
`[Go2rtcTcpServer] first live keyframe client=${clientId} after ${liveFrameCount} frames`
|
|
38623
|
-
);
|
|
38624
|
-
if (!muxer) {
|
|
38625
|
-
muxer = new MpegTsMuxer({
|
|
38626
|
-
videoType: frame.videoType ?? this.detectedVideoType ?? "H264",
|
|
38627
|
-
includeAudio: true
|
|
38628
|
-
});
|
|
38629
|
-
}
|
|
38630
|
-
}
|
|
38631
|
-
const pts = frame.microseconds ?? Date.now() * 1e3;
|
|
38632
|
-
const ts = muxer.muxVideo(annexB, pts, isKf);
|
|
38633
|
-
socket.write(ts);
|
|
38634
|
-
liveVideoWritten++;
|
|
38635
|
-
this.totalVideoFramesWritten++;
|
|
38636
|
-
if (Date.now() - lastLogAt > 1e4) {
|
|
38637
|
-
this.logger.info?.(
|
|
38638
|
-
`[Go2rtcTcpServer] live stats client=${clientId} received=${liveFrameCount} written=${liveVideoWritten} bufLen=${socket.writableLength}`
|
|
38639
|
-
);
|
|
38640
|
-
lastLogAt = Date.now();
|
|
38641
|
-
}
|
|
38642
|
-
if (socket.writableLength > this.maxBufferBytes) {
|
|
38643
|
-
this.logger.warn?.(
|
|
38644
|
-
`[Go2rtcTcpServer] buffer overflow (${socket.writableLength} bytes), dropping client ${clientId}`
|
|
38645
|
-
);
|
|
38646
|
-
socket.destroy();
|
|
38647
|
-
break;
|
|
38648
|
-
}
|
|
38649
|
-
}
|
|
38650
|
-
this.logger.info?.(
|
|
38651
|
-
`[Go2rtcTcpServer] live loop ended naturally client=${clientId} received=${liveFrameCount} written=${liveVideoWritten}`
|
|
38652
|
-
);
|
|
38653
|
-
} finally {
|
|
38654
|
-
await subscription.return(void 0).catch(() => {
|
|
38655
|
-
});
|
|
38656
|
-
}
|
|
38657
|
-
}
|
|
38658
|
-
// -----------------------------------------------------------------------
|
|
38659
|
-
// Frame conversion
|
|
38660
|
-
// -----------------------------------------------------------------------
|
|
38661
|
-
/**
|
|
38662
|
-
* Convert a native video frame to Annex-B.
|
|
38663
|
-
* Returns null for audio frames (handled separately by muxAudio).
|
|
38664
|
-
*/
|
|
38665
|
-
convertVideoFrame(frame) {
|
|
38666
|
-
if (frame.audio) return null;
|
|
38667
|
-
if (frame.data.length === 0) return null;
|
|
38668
|
-
try {
|
|
38669
|
-
if (frame.videoType === "H264") {
|
|
38670
|
-
return convertToAnnexB(frame.data);
|
|
38671
|
-
}
|
|
38672
|
-
if (frame.videoType === "H265") {
|
|
38673
|
-
return convertToAnnexB2(frame.data);
|
|
38674
|
-
}
|
|
38675
|
-
} catch {
|
|
38676
|
-
}
|
|
38677
|
-
return frame.data;
|
|
38678
|
-
}
|
|
38679
|
-
/** Check if an Annex-B buffer contains a keyframe (IDR for H.264, IRAP for H.265). */
|
|
38680
|
-
isAnnexBKeyframe(annexB, videoType) {
|
|
38681
|
-
try {
|
|
38682
|
-
if (videoType === "H264") {
|
|
38683
|
-
const nals = _Go2rtcTcpServer.splitAnnexBNals(annexB);
|
|
38684
|
-
return nals.some((n) => n.length >= 1 && (n[0] & 31) === 5);
|
|
38685
|
-
}
|
|
38686
|
-
if (videoType === "H265") {
|
|
38687
|
-
const nals = splitAnnexBToNalPayloads2(annexB);
|
|
38688
|
-
return nals.some(
|
|
38689
|
-
(n) => n.length >= 2 && isH265Irap(n[0] >> 1 & 63)
|
|
38690
|
-
);
|
|
38691
|
-
}
|
|
38692
|
-
} catch {
|
|
38693
|
-
}
|
|
38694
|
-
return false;
|
|
38695
|
-
}
|
|
38696
|
-
/** Split Annex-B byte stream into individual NAL units. */
|
|
38697
|
-
static splitAnnexBNals(buf) {
|
|
38698
|
-
const nals = [];
|
|
38699
|
-
let i = 0;
|
|
38700
|
-
while (i < buf.length) {
|
|
38701
|
-
if (i + 2 < buf.length && buf[i] === 0 && buf[i + 1] === 0) {
|
|
38702
|
-
let scLen;
|
|
38703
|
-
if (buf[i + 2] === 1) {
|
|
38704
|
-
scLen = 3;
|
|
38705
|
-
} else if (i + 3 < buf.length && buf[i + 2] === 0 && buf[i + 3] === 1) {
|
|
38706
|
-
scLen = 4;
|
|
38707
|
-
} else {
|
|
38708
|
-
i++;
|
|
38709
|
-
continue;
|
|
38710
|
-
}
|
|
38711
|
-
const nalStart = i + scLen;
|
|
38712
|
-
let nalEnd = buf.length;
|
|
38713
|
-
for (let j = nalStart; j < buf.length - 2; j++) {
|
|
38714
|
-
if (buf[j] === 0 && buf[j + 1] === 0 && (buf[j + 2] === 1 || j + 3 < buf.length && buf[j + 2] === 0 && buf[j + 3] === 1)) {
|
|
38715
|
-
nalEnd = j;
|
|
38716
|
-
break;
|
|
38717
|
-
}
|
|
38718
|
-
}
|
|
38719
|
-
if (nalEnd > nalStart) {
|
|
38720
|
-
nals.push(buf.subarray(nalStart, nalEnd));
|
|
38721
|
-
}
|
|
38722
|
-
i = nalEnd;
|
|
38723
|
-
} else {
|
|
38724
|
-
i++;
|
|
38725
|
-
}
|
|
38726
|
-
}
|
|
38727
|
-
return nals;
|
|
38728
|
-
}
|
|
38729
|
-
// -----------------------------------------------------------------------
|
|
38730
|
-
// ADTS AAC parsing (used for audio metadata exposed via getAudioInfo)
|
|
38731
|
-
// -----------------------------------------------------------------------
|
|
38732
|
-
/** True if `b` starts with an ADTS AAC syncword (0xFFF). */
|
|
38733
|
-
static isAdtsAacFrame(b) {
|
|
38734
|
-
return b.length >= 2 && b[0] === 255 && (b[1] & 240) === 240;
|
|
38735
|
-
}
|
|
38736
|
-
/**
|
|
38737
|
-
* Parse an ADTS header into {sampleRate, channels, AudioSpecificConfig hex}.
|
|
38738
|
-
* Returns null when the buffer is not a valid ADTS frame.
|
|
38739
|
-
*/
|
|
38740
|
-
static parseAdtsSamplingInfo(b) {
|
|
38741
|
-
if (b.length < 7) return null;
|
|
38742
|
-
if (!_Go2rtcTcpServer.isAdtsAacFrame(b)) return null;
|
|
38743
|
-
const samplingIndex = b[2] >> 2 & 15;
|
|
38744
|
-
const sampleRates = [
|
|
38745
|
-
96e3,
|
|
38746
|
-
88200,
|
|
38747
|
-
64e3,
|
|
38748
|
-
48e3,
|
|
38749
|
-
44100,
|
|
38750
|
-
32e3,
|
|
38751
|
-
24e3,
|
|
38752
|
-
22050,
|
|
38753
|
-
16e3,
|
|
38754
|
-
12e3,
|
|
38755
|
-
11025,
|
|
38756
|
-
8e3,
|
|
38757
|
-
7350
|
|
38758
|
-
];
|
|
38759
|
-
const sampleRate = sampleRates[samplingIndex] ?? null;
|
|
38760
|
-
if (!sampleRate) return null;
|
|
38761
|
-
const channelConfig = (b[2] & 1) << 2 | b[3] >> 6 & 3;
|
|
38762
|
-
const channels = channelConfig === 0 ? 1 : channelConfig;
|
|
38763
|
-
const profile = b[2] >> 6 & 3;
|
|
38764
|
-
const audioObjectType = profile + 1;
|
|
38765
|
-
const asc = audioObjectType << 11 | samplingIndex << 7 | channelConfig << 3;
|
|
38766
|
-
const configHex = Buffer.from([asc >> 8 & 255, asc & 255]).toString(
|
|
38767
|
-
"hex"
|
|
38768
|
-
);
|
|
38769
|
-
return { sampleRate, channels, configHex };
|
|
38770
|
-
}
|
|
38771
|
-
// -----------------------------------------------------------------------
|
|
38772
|
-
// Native stream management
|
|
38773
|
-
// -----------------------------------------------------------------------
|
|
38774
|
-
/**
|
|
38775
|
-
* Schedule another startNativeStream() attempt after the given delay.
|
|
38776
|
-
* Idempotent: a no-op if a retry is already scheduled, the server is no
|
|
38777
|
-
* longer active, or an explicit stop is in progress. Implements unbounded
|
|
38778
|
-
* exponential backoff (5s → 60s) so a camera that stays unreachable for
|
|
38779
|
-
* minutes (e.g. nightly maintenance reboot) eventually recovers without
|
|
38780
|
-
* manual intervention — see issue #16.
|
|
38781
|
-
*/
|
|
38782
|
-
scheduleNativeStreamRetry(reason) {
|
|
38783
|
-
if (!this.active) return;
|
|
38784
|
-
if (this.nativeStreamStopping) return;
|
|
38785
|
-
if (this.nativeStreamRetryTimer) return;
|
|
38786
|
-
const delay = this.nativeStreamRetryDelayMs > 0 ? this.nativeStreamRetryDelayMs : 5e3;
|
|
38787
|
-
this.logger.info?.(
|
|
38788
|
-
`[Go2rtcTcpServer] scheduling native stream retry in ${(delay / 1e3).toFixed(0)}s (reason=${reason})`
|
|
38789
|
-
);
|
|
38790
|
-
this.nativeStreamRetryTimer = setTimeout(() => {
|
|
38791
|
-
this.nativeStreamRetryTimer = void 0;
|
|
38792
|
-
if (!this.active) return;
|
|
38793
|
-
if (this.nativeStreamStopping) return;
|
|
38794
|
-
this.startNativeStream().catch((err) => {
|
|
38795
|
-
this.logger.warn?.(
|
|
38796
|
-
`[Go2rtcTcpServer] retry of startNativeStream threw: ${err instanceof Error ? err.message : err}`
|
|
38797
|
-
);
|
|
38798
|
-
});
|
|
38799
|
-
}, delay);
|
|
38800
|
-
this.nativeStreamRetryDelayMs = Math.min(delay * 2, 6e4);
|
|
38801
|
-
}
|
|
38802
|
-
/**
|
|
38803
|
-
* Cancel any pending retry timer and reset the backoff. Called on explicit
|
|
38804
|
-
* stop and on first-frame-received so the next failure starts the backoff
|
|
38805
|
-
* window from scratch.
|
|
38806
|
-
*/
|
|
38807
|
-
clearNativeStreamRetry() {
|
|
38808
|
-
if (this.nativeStreamRetryTimer) {
|
|
38809
|
-
clearTimeout(this.nativeStreamRetryTimer);
|
|
38810
|
-
this.nativeStreamRetryTimer = void 0;
|
|
38811
|
-
}
|
|
38812
|
-
this.nativeStreamRetryDelayMs = 0;
|
|
38813
|
-
}
|
|
38814
|
-
async startNativeStream() {
|
|
38815
|
-
if (this.nativeStreamActive) return;
|
|
38816
|
-
if (!this.api.isReady) {
|
|
38817
|
-
if (this.api.isClosed) {
|
|
38818
|
-
this.logger.warn?.(
|
|
38819
|
-
`[Go2rtcTcpServer] API has been explicitly closed \u2014 stream cannot start`
|
|
38820
|
-
);
|
|
38821
|
-
return;
|
|
38822
|
-
}
|
|
38823
|
-
try {
|
|
38824
|
-
this.logger.info?.(
|
|
38825
|
-
`[Go2rtcTcpServer] API not ready (idle disconnect?), calling ensureConnected`
|
|
38826
|
-
);
|
|
38827
|
-
await this.api.ensureConnected();
|
|
38828
|
-
} catch (e) {
|
|
38829
|
-
this.logger.warn?.(
|
|
38830
|
-
`[Go2rtcTcpServer] ensureConnected failed: ${e}`
|
|
38831
|
-
);
|
|
38832
|
-
this.scheduleNativeStreamRetry("ensureConnected failed");
|
|
38833
|
-
return;
|
|
38834
|
-
}
|
|
38835
|
-
}
|
|
38836
|
-
this.nativeStreamActive = true;
|
|
38837
|
-
let dedicatedClient;
|
|
38838
|
-
if (this.deviceId) {
|
|
38839
|
-
try {
|
|
38840
|
-
const session = await this.api.createDedicatedSession(
|
|
38841
|
-
`live:${this.deviceId}:ch${this.channel}:${this.profile}`
|
|
38842
|
-
);
|
|
38843
|
-
dedicatedClient = session.client;
|
|
38844
|
-
this.dedicatedSessionRelease = session.release;
|
|
38845
|
-
} catch (e) {
|
|
38846
|
-
this.logger.warn?.(
|
|
38847
|
-
`[Go2rtcTcpServer] failed to acquire dedicated session, using shared socket: ${e}`
|
|
38848
|
-
);
|
|
38849
|
-
}
|
|
38850
|
-
}
|
|
38851
|
-
this.logger.info?.(
|
|
38852
|
-
`[Go2rtcTcpServer] native stream starting channel=${this.channel} profile=${this.profile} dedicated=${!!dedicatedClient}`
|
|
38853
|
-
);
|
|
38854
|
-
let hadFrames = false;
|
|
38855
|
-
this.nativeFanout = new NativeStreamFanout2({
|
|
38856
|
-
maxQueueItems: 200,
|
|
38857
|
-
createSource: () => createNativeStream(this.api, this.channel, this.profile, {
|
|
38858
|
-
variant: this.variant,
|
|
38859
|
-
...dedicatedClient ? { client: dedicatedClient } : {}
|
|
38860
|
-
}),
|
|
38861
|
-
onFrame: (frame) => {
|
|
38862
|
-
if (!hadFrames) {
|
|
38863
|
-
this.clearNativeStreamRetry();
|
|
38864
|
-
}
|
|
38865
|
-
hadFrames = true;
|
|
38866
|
-
this.lastFrameAt = Date.now();
|
|
38867
|
-
this.totalFramesReceived++;
|
|
38868
|
-
if (!frame.audio && (frame.videoType === "H264" || frame.videoType === "H265")) {
|
|
38869
|
-
this.detectedVideoType = frame.videoType;
|
|
38870
|
-
}
|
|
38871
|
-
let prebufData;
|
|
38872
|
-
let isKeyframe;
|
|
38873
|
-
if (frame.audio) {
|
|
38874
|
-
if (frame.data.length === 0) return;
|
|
38875
|
-
if (!this.audioInfo) {
|
|
38876
|
-
const parsed = _Go2rtcTcpServer.parseAdtsSamplingInfo(frame.data);
|
|
38877
|
-
if (parsed) {
|
|
38878
|
-
this.audioInfo = { codec: "aac-adts", ...parsed };
|
|
38879
|
-
}
|
|
38880
|
-
}
|
|
38881
|
-
prebufData = frame.data;
|
|
38882
|
-
isKeyframe = false;
|
|
38883
|
-
} else {
|
|
38884
|
-
const annexB = this.convertVideoFrame(frame);
|
|
38885
|
-
if (!annexB || annexB.length === 0) return;
|
|
38886
|
-
prebufData = annexB;
|
|
38887
|
-
isKeyframe = this.isAnnexBKeyframe(annexB, frame.videoType);
|
|
38888
|
-
}
|
|
38889
|
-
const pts = frame.microseconds ?? Date.now() * 1e3;
|
|
38890
|
-
this.prebuffer.push({
|
|
38891
|
-
data: Buffer.from(prebufData),
|
|
38892
|
-
time: Date.now(),
|
|
38893
|
-
isKeyframe,
|
|
38894
|
-
audio: frame.audio,
|
|
38895
|
-
pts
|
|
38896
|
-
});
|
|
38897
|
-
const cutoff = Date.now() - this.prebufferMaxMs;
|
|
38898
|
-
let trimIdx = 0;
|
|
38899
|
-
while (trimIdx < this.prebuffer.length && this.prebuffer[trimIdx].time < cutoff) {
|
|
38900
|
-
trimIdx++;
|
|
38901
|
-
}
|
|
38902
|
-
if (trimIdx > 0) this.prebuffer.splice(0, trimIdx);
|
|
38903
|
-
},
|
|
38904
|
-
onError: (error) => {
|
|
38905
|
-
this.logger.warn?.(`[Go2rtcTcpServer] native stream error: ${error}`);
|
|
38906
|
-
},
|
|
38907
|
-
onEnd: () => {
|
|
38908
|
-
if (this.nativeStreamStopping) return;
|
|
38909
|
-
this.nativeStreamActive = false;
|
|
38910
|
-
this.nativeFanout = null;
|
|
38911
|
-
this.stopStreamHealthMonitor();
|
|
38912
|
-
const silenceMs = this.lastFrameAt > 0 ? Date.now() - this.lastFrameAt : -1;
|
|
38913
|
-
const diagnosis = silenceMs > this.streamTimeoutMs ? "camera stopped sending frames" : silenceMs >= 0 ? "stream source closed" : "no frames were ever received";
|
|
38914
|
-
this.logger.warn?.(
|
|
38915
|
-
`[Go2rtcTcpServer] native stream ended diagnosis="${diagnosis}" lastFrame=${silenceMs >= 0 ? `${(silenceMs / 1e3).toFixed(1)}s ago` : "never"} totalRx=${this.totalFramesReceived} clients=${this.connectedClients.size}`
|
|
38916
|
-
);
|
|
38917
|
-
if (this.dedicatedSessionRelease) {
|
|
38918
|
-
this.dedicatedSessionRelease().catch(() => {
|
|
38919
|
-
});
|
|
38920
|
-
this.dedicatedSessionRelease = void 0;
|
|
38921
|
-
}
|
|
38922
|
-
if (!this.prestartStream) {
|
|
38923
|
-
this.logger.info?.(
|
|
38924
|
-
`[Go2rtcTcpServer] battery native stream ended hadFrames=${hadFrames} channel=${this.channel} profile=${this.profile} \u2014 dropping ${this.connectedClients.size} client(s) to prevent wake loop`
|
|
38925
|
-
);
|
|
38926
|
-
for (const [, sock] of this.clientSockets) {
|
|
38927
|
-
sock.destroy();
|
|
38928
|
-
}
|
|
38929
|
-
} else if (this.active) {
|
|
38930
|
-
if (typeof this.api.isStreamProfileRejected === "function" && this.api.isStreamProfileRejected(this.channel, this.profile)) {
|
|
38931
|
-
this.logger.warn?.(
|
|
38932
|
-
`[Go2rtcTcpServer] profile rejected by device channel=${this.channel} profile=${this.profile} \u2014 not restarting`
|
|
38933
|
-
);
|
|
38934
|
-
for (const [, sock] of this.clientSockets) {
|
|
38935
|
-
sock.destroy();
|
|
38936
|
-
}
|
|
38937
|
-
return;
|
|
38938
|
-
}
|
|
38939
|
-
this.logger.info?.(
|
|
38940
|
-
`[Go2rtcTcpServer] restarting native stream (clients=${this.connectedClients.size}, prestart=${this.prestartStream})`
|
|
38941
|
-
);
|
|
38942
|
-
this.startNativeStream();
|
|
38943
|
-
}
|
|
38944
|
-
}
|
|
38945
|
-
});
|
|
38946
|
-
this.nativeFanout.start();
|
|
38947
|
-
this.startStreamHealthMonitor();
|
|
38948
|
-
}
|
|
38949
|
-
async stopNativeStream() {
|
|
38950
|
-
this.nativeStreamStopping = true;
|
|
38951
|
-
this.nativeStreamActive = false;
|
|
38952
|
-
this.clearNativeStreamRetry();
|
|
38953
|
-
this.stopStreamHealthMonitor();
|
|
38954
|
-
const fanout = this.nativeFanout;
|
|
38955
|
-
this.nativeFanout = null;
|
|
38956
|
-
try {
|
|
38957
|
-
if (fanout) {
|
|
38958
|
-
await fanout.stop();
|
|
38959
|
-
}
|
|
38960
|
-
this.prebuffer = [];
|
|
38961
|
-
if (this.dedicatedSessionRelease) {
|
|
38962
|
-
await this.dedicatedSessionRelease().catch(() => {
|
|
38963
|
-
});
|
|
38964
|
-
this.dedicatedSessionRelease = void 0;
|
|
38965
|
-
}
|
|
38966
|
-
} finally {
|
|
38967
|
-
this.nativeStreamStopping = false;
|
|
38968
|
-
}
|
|
38969
|
-
}
|
|
38970
|
-
// -----------------------------------------------------------------------
|
|
38971
|
-
// Stream health monitoring
|
|
38972
|
-
// -----------------------------------------------------------------------
|
|
38973
|
-
startStreamHealthMonitor() {
|
|
38974
|
-
this.stopStreamHealthMonitor();
|
|
38975
|
-
if (this.streamTimeoutMs <= 0) return;
|
|
38976
|
-
this.lastFrameAt = Date.now();
|
|
38977
|
-
this.streamHealthTimer = setInterval(() => {
|
|
38978
|
-
if (!this.nativeStreamActive || !this.active) {
|
|
38979
|
-
this.stopStreamHealthMonitor();
|
|
38980
|
-
return;
|
|
38981
|
-
}
|
|
38982
|
-
const silenceMs = Date.now() - this.lastFrameAt;
|
|
38983
|
-
if (silenceMs > this.streamTimeoutMs) {
|
|
38984
|
-
this.logger.warn?.(
|
|
38985
|
-
`[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`
|
|
38986
|
-
);
|
|
38987
|
-
this.stopStreamHealthMonitor();
|
|
38988
|
-
const fanout = this.nativeFanout;
|
|
38989
|
-
if (fanout) {
|
|
38990
|
-
this.nativeStreamActive = false;
|
|
38991
|
-
this.nativeFanout = null;
|
|
38992
|
-
fanout.stop().catch(() => {
|
|
38993
|
-
});
|
|
38994
|
-
}
|
|
38995
|
-
}
|
|
38996
|
-
}, Math.min(this.streamTimeoutMs / 2, 5e3));
|
|
38997
|
-
}
|
|
38998
|
-
stopStreamHealthMonitor() {
|
|
38999
|
-
if (this.streamHealthTimer) {
|
|
39000
|
-
clearInterval(this.streamHealthTimer);
|
|
39001
|
-
this.streamHealthTimer = void 0;
|
|
39002
|
-
}
|
|
39003
|
-
}
|
|
39004
|
-
// -----------------------------------------------------------------------
|
|
39005
|
-
// Client lifecycle
|
|
39006
|
-
// -----------------------------------------------------------------------
|
|
39007
|
-
removeClient(clientId, reason) {
|
|
39008
|
-
if (!this.connectedClients.has(clientId)) return;
|
|
39009
|
-
this.connectedClients.delete(clientId);
|
|
39010
|
-
this.clientSockets.delete(clientId);
|
|
39011
|
-
const silenceMs = this.lastFrameAt > 0 ? Date.now() - this.lastFrameAt : -1;
|
|
39012
|
-
const silenceInfo = silenceMs >= 0 ? ` lastFrame=${(silenceMs / 1e3).toFixed(1)}s ago` : "";
|
|
39013
|
-
this.logger.info?.(
|
|
39014
|
-
`[Go2rtcTcpServer] client disconnected id=${clientId} reason=${reason ?? "unknown"} remaining=${this.connectedClients.size} totalRx=${this.totalFramesReceived} totalTx=${this.totalVideoFramesWritten}${silenceInfo}`
|
|
39015
|
-
);
|
|
39016
|
-
this.emit("clientDisconnected", clientId);
|
|
39017
|
-
if (this.connectedClients.size === 0 && !this.prestartStream) {
|
|
39018
|
-
this.scheduleStop();
|
|
39019
|
-
}
|
|
39020
|
-
}
|
|
39021
|
-
scheduleStop() {
|
|
39022
|
-
if (this.stopGraceTimer) return;
|
|
39023
|
-
this.logger.info?.(
|
|
39024
|
-
`[Go2rtcTcpServer] no clients, scheduling stream stop in ${this.gracePeriodMs}ms`
|
|
39025
|
-
);
|
|
39026
|
-
this.stopGraceTimer = setTimeout(async () => {
|
|
39027
|
-
this.stopGraceTimer = void 0;
|
|
39028
|
-
if (this.connectedClients.size === 0 && this.nativeStreamActive) {
|
|
39029
|
-
this.logger.info?.("[Go2rtcTcpServer] grace period expired, stopping native stream");
|
|
39030
|
-
await this.stopNativeStream();
|
|
39031
|
-
}
|
|
39032
|
-
}, this.gracePeriodMs);
|
|
39033
|
-
}
|
|
39034
|
-
};
|
|
39035
|
-
|
|
39036
38270
|
// src/baichuan/stream/BaichuanHttpStreamServer.ts
|
|
39037
|
-
var
|
|
38271
|
+
var import_node_events8 = require("events");
|
|
39038
38272
|
var import_node_child_process10 = require("child_process");
|
|
39039
38273
|
var http4 = __toESM(require("http"), 1);
|
|
39040
38274
|
var NAL_START_CODE_4B4 = Buffer.from([0, 0, 0, 1]);
|
|
@@ -39081,7 +38315,7 @@ function isH264KeyframeFromAnnexB(annexB) {
|
|
|
39081
38315
|
}
|
|
39082
38316
|
return false;
|
|
39083
38317
|
}
|
|
39084
|
-
var BaichuanHttpStreamServer = class extends
|
|
38318
|
+
var BaichuanHttpStreamServer = class extends import_node_events8.EventEmitter {
|
|
39085
38319
|
videoStream;
|
|
39086
38320
|
listenPort;
|
|
39087
38321
|
path;
|
|
@@ -39353,15 +38587,15 @@ var BaichuanHttpStreamServer = class extends import_node_events9.EventEmitter {
|
|
|
39353
38587
|
};
|
|
39354
38588
|
|
|
39355
38589
|
// src/baichuan/stream/BaichuanMjpegServer.ts
|
|
39356
|
-
var
|
|
38590
|
+
var import_node_events10 = require("events");
|
|
39357
38591
|
var http5 = __toESM(require("http"), 1);
|
|
39358
38592
|
|
|
39359
38593
|
// src/baichuan/stream/MjpegTransformer.ts
|
|
39360
|
-
var
|
|
38594
|
+
var import_node_events9 = require("events");
|
|
39361
38595
|
var import_node_child_process11 = require("child_process");
|
|
39362
38596
|
var JPEG_SOI = Buffer.from([255, 216]);
|
|
39363
38597
|
var JPEG_EOI = Buffer.from([255, 217]);
|
|
39364
|
-
var MjpegTransformer = class extends
|
|
38598
|
+
var MjpegTransformer = class extends import_node_events9.EventEmitter {
|
|
39365
38599
|
options;
|
|
39366
38600
|
ffmpeg = null;
|
|
39367
38601
|
started = false;
|
|
@@ -39560,7 +38794,7 @@ Content-Length: ${frame.length}\r
|
|
|
39560
38794
|
// src/baichuan/stream/BaichuanMjpegServer.ts
|
|
39561
38795
|
init_H264Converter();
|
|
39562
38796
|
init_H265Converter();
|
|
39563
|
-
var BaichuanMjpegServer = class extends
|
|
38797
|
+
var BaichuanMjpegServer = class extends import_node_events10.EventEmitter {
|
|
39564
38798
|
options;
|
|
39565
38799
|
clients = /* @__PURE__ */ new Map();
|
|
39566
38800
|
httpServer = null;
|
|
@@ -39841,14 +39075,14 @@ var BaichuanMjpegServer = class extends import_node_events11.EventEmitter {
|
|
|
39841
39075
|
};
|
|
39842
39076
|
|
|
39843
39077
|
// src/baichuan/stream/BaichuanWebRTCServer.ts
|
|
39844
|
-
var
|
|
39078
|
+
var import_node_events12 = require("events");
|
|
39845
39079
|
init_BcMediaAnnexBDecoder();
|
|
39846
39080
|
|
|
39847
39081
|
// src/baichuan/stream/AacToOpusTranscoder.ts
|
|
39848
39082
|
var import_node_child_process12 = require("child_process");
|
|
39849
39083
|
var import_node_dgram3 = require("dgram");
|
|
39850
|
-
var
|
|
39851
|
-
var AacToOpusTranscoder = class extends
|
|
39084
|
+
var import_node_events11 = require("events");
|
|
39085
|
+
var AacToOpusTranscoder = class extends import_node_events11.EventEmitter {
|
|
39852
39086
|
opts;
|
|
39853
39087
|
socket = null;
|
|
39854
39088
|
ffmpeg = null;
|
|
@@ -40065,7 +39299,7 @@ function getH264NalType(nalUnit) {
|
|
|
40065
39299
|
function getH265NalType2(nalUnit) {
|
|
40066
39300
|
return nalUnit[0] >> 1 & 63;
|
|
40067
39301
|
}
|
|
40068
|
-
var BaichuanWebRTCServer = class extends
|
|
39302
|
+
var BaichuanWebRTCServer = class extends import_node_events12.EventEmitter {
|
|
40069
39303
|
options;
|
|
40070
39304
|
sessions = /* @__PURE__ */ new Map();
|
|
40071
39305
|
sessionIdCounter = 0;
|
|
@@ -41057,7 +40291,7 @@ Error: ${err}`
|
|
|
41057
40291
|
};
|
|
41058
40292
|
|
|
41059
40293
|
// src/baichuan/stream/BaichuanHlsServer.ts
|
|
41060
|
-
var
|
|
40294
|
+
var import_node_events13 = require("events");
|
|
41061
40295
|
var import_node_fs = __toESM(require("fs"), 1);
|
|
41062
40296
|
var import_promises3 = __toESM(require("fs/promises"), 1);
|
|
41063
40297
|
var import_node_os3 = __toESM(require("os"), 1);
|
|
@@ -41137,7 +40371,7 @@ function getNalTypes(codec, annexB) {
|
|
|
41137
40371
|
}
|
|
41138
40372
|
});
|
|
41139
40373
|
}
|
|
41140
|
-
var BaichuanHlsServer = class extends
|
|
40374
|
+
var BaichuanHlsServer = class extends import_node_events13.EventEmitter {
|
|
41141
40375
|
api;
|
|
41142
40376
|
channel;
|
|
41143
40377
|
profile;
|
|
@@ -41706,10 +40940,10 @@ async function pingHost(host, timeoutMs = 3e3) {
|
|
|
41706
40940
|
return false;
|
|
41707
40941
|
}
|
|
41708
40942
|
async function tcpReachabilityProbe(host, port, timeoutMs) {
|
|
41709
|
-
const
|
|
40943
|
+
const net6 = await import("net");
|
|
41710
40944
|
return new Promise((resolve) => {
|
|
41711
40945
|
let settled = false;
|
|
41712
|
-
const socket = new
|
|
40946
|
+
const socket = new net6.Socket();
|
|
41713
40947
|
const timer = setTimeout(() => {
|
|
41714
40948
|
if (settled) return;
|
|
41715
40949
|
settled = true;
|
|
@@ -42302,10 +41536,10 @@ async function autoDetectDeviceType(inputs) {
|
|
|
42302
41536
|
}
|
|
42303
41537
|
|
|
42304
41538
|
// src/multifocal/compositeRtspServer.ts
|
|
42305
|
-
var
|
|
41539
|
+
var import_node_events14 = require("events");
|
|
42306
41540
|
var import_node_child_process14 = require("child_process");
|
|
42307
|
-
var
|
|
42308
|
-
var CompositeRtspServer = class extends
|
|
41541
|
+
var net4 = __toESM(require("net"), 1);
|
|
41542
|
+
var CompositeRtspServer = class extends import_node_events14.EventEmitter {
|
|
42309
41543
|
options;
|
|
42310
41544
|
compositeStream = null;
|
|
42311
41545
|
rtspServer = null;
|
|
@@ -42371,7 +41605,7 @@ var CompositeRtspServer = class extends import_node_events15.EventEmitter {
|
|
|
42371
41605
|
const width = widerStreamInfo?.width ?? 1920;
|
|
42372
41606
|
const height = widerStreamInfo?.height ?? 1080;
|
|
42373
41607
|
const fps = widerStreamInfo?.frameRate ?? 25;
|
|
42374
|
-
this.rtspServer =
|
|
41608
|
+
this.rtspServer = net4.createServer((socket) => {
|
|
42375
41609
|
this.handleRtspConnection(socket);
|
|
42376
41610
|
});
|
|
42377
41611
|
await new Promise((resolve, reject) => {
|
|
@@ -43081,8 +42315,8 @@ var RtspBackchannel = class _RtspBackchannel {
|
|
|
43081
42315
|
};
|
|
43082
42316
|
|
|
43083
42317
|
// src/baichuan/stream/BaichuanRtspBackchannelServer.ts
|
|
43084
|
-
var
|
|
43085
|
-
var
|
|
42318
|
+
var import_node_events15 = require("events");
|
|
42319
|
+
var net5 = __toESM(require("net"), 1);
|
|
43086
42320
|
var crypto3 = __toESM(require("crypto"), 1);
|
|
43087
42321
|
var md5Hex = (s) => crypto3.createHash("md5").update(s).digest("hex");
|
|
43088
42322
|
var RTCP_KEEPALIVE_INTERVAL_MS = 1e4;
|
|
@@ -43138,7 +42372,7 @@ function extractPublicEndpoint(url, requestText) {
|
|
|
43138
42372
|
if (hostHeader) return hostHeader;
|
|
43139
42373
|
return null;
|
|
43140
42374
|
}
|
|
43141
|
-
var BaichuanRtspBackchannelServer = class _BaichuanRtspBackchannelServer extends
|
|
42375
|
+
var BaichuanRtspBackchannelServer = class _BaichuanRtspBackchannelServer extends import_node_events15.EventEmitter {
|
|
43142
42376
|
listenHost;
|
|
43143
42377
|
listenPort;
|
|
43144
42378
|
logger;
|
|
@@ -43255,7 +42489,7 @@ var BaichuanRtspBackchannelServer = class _BaichuanRtspBackchannelServer extends
|
|
|
43255
42489
|
async start() {
|
|
43256
42490
|
if (this.server) return;
|
|
43257
42491
|
await new Promise((resolve, reject) => {
|
|
43258
|
-
const server =
|
|
42492
|
+
const server = net5.createServer((socket) => this.handleConnection(socket));
|
|
43259
42493
|
const onError = (err) => {
|
|
43260
42494
|
server.removeListener("error", onError);
|
|
43261
42495
|
reject(err);
|
|
@@ -44283,7 +43517,6 @@ function buildInitialStatus(config) {
|
|
|
44283
43517
|
DUAL_LENS_DUAL_MOTION_MODELS,
|
|
44284
43518
|
DUAL_LENS_MODELS,
|
|
44285
43519
|
DUAL_LENS_SINGLE_MOTION_MODELS,
|
|
44286
|
-
Go2rtcTcpServer,
|
|
44287
43520
|
H264RtpDepacketizer,
|
|
44288
43521
|
H265RtpDepacketizer,
|
|
44289
43522
|
HlsSessionManager,
|