@apocaliss92/nodelink-js 0.6.5 → 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-WQ2TQCYP.js → chunk-T22QCNBR.js} +114 -28
- package/dist/chunk-T22QCNBR.js.map +1 -0
- package/dist/cli/rtsp-server.cjs +113 -27
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +1 -1
- package/dist/index.cjs +135 -854
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -196
- package/dist/index.d.ts +10 -175
- package/dist/index.js +21 -823
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-WQ2TQCYP.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);
|
|
@@ -17700,6 +17737,30 @@ function buildSetNtpXml(current, patch) {
|
|
|
17700
17737
|
);
|
|
17701
17738
|
}
|
|
17702
17739
|
|
|
17740
|
+
// src/reolink/baichuan/utils/channelEnumeration.ts
|
|
17741
|
+
async function resolveBaichuanChannels(deps) {
|
|
17742
|
+
const fromPush = dedupeSorted(deps.pushChannels);
|
|
17743
|
+
if (fromPush.length > 0) return fromPush;
|
|
17744
|
+
const slots = dedupeSorted(deps.supportChnIds);
|
|
17745
|
+
const candidates = slots.length > 0 ? slots : [0];
|
|
17746
|
+
const probed = await Promise.all(
|
|
17747
|
+
candidates.map(
|
|
17748
|
+
async (channel) => await deps.probe(channel) ? channel : void 0
|
|
17749
|
+
)
|
|
17750
|
+
);
|
|
17751
|
+
return dedupeSorted(
|
|
17752
|
+
probed.filter((c) => c !== void 0)
|
|
17753
|
+
);
|
|
17754
|
+
}
|
|
17755
|
+
function dedupeSorted(values) {
|
|
17756
|
+
const set = /* @__PURE__ */ new Set();
|
|
17757
|
+
for (const v of values) {
|
|
17758
|
+
const n = Number(v);
|
|
17759
|
+
if (Number.isFinite(n) && n >= 0) set.add(n);
|
|
17760
|
+
}
|
|
17761
|
+
return [...set].sort((a, b) => a - b);
|
|
17762
|
+
}
|
|
17763
|
+
|
|
17703
17764
|
// src/reolink/baichuan/utils/dst.ts
|
|
17704
17765
|
init_xml();
|
|
17705
17766
|
var parseNumberSafe3 = (text) => {
|
|
@@ -24109,13 +24170,21 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
24109
24170
|
* @param options.source - Data source for the channel list (default: `"cgi"`):
|
|
24110
24171
|
* - `"cgi"`: Uses HTTP `GetChannelstatus` — returns the channel list immediately,
|
|
24111
24172
|
* no dependency on async push messages. Recommended for first-call discovery.
|
|
24112
|
-
* - `"baichuan"`:
|
|
24113
|
-
*
|
|
24114
|
-
*
|
|
24115
|
-
*
|
|
24173
|
+
* - `"baichuan"`: HTTP-free discovery. Prefers the cmd_id 145 push cache when
|
|
24174
|
+
* populated; otherwise actively probes the channel slots advertised by Support
|
|
24175
|
+
* (`items[].chnID`) via `getInfo`. Use this for hubs with HTTP disabled.
|
|
24176
|
+
*
|
|
24177
|
+
* When the api was constructed with `nativeOnly`, the source is forced to
|
|
24178
|
+
* `"baichuan"` regardless of this option (no HTTP/CGI is ever attempted).
|
|
24116
24179
|
*/
|
|
24117
24180
|
async getNvrChannelsSummary(options) {
|
|
24118
|
-
const source = options?.source ?? "cgi";
|
|
24181
|
+
const source = this.nativeOnly ? "baichuan" : options?.source ?? "cgi";
|
|
24182
|
+
const support = await this.getSupportInfo().catch(() => {
|
|
24183
|
+
this.logger.error?.(
|
|
24184
|
+
"[ReolinkBaichuanApi] getNvrChannelsSummary: failed to get support info"
|
|
24185
|
+
);
|
|
24186
|
+
return void 0;
|
|
24187
|
+
});
|
|
24119
24188
|
let channels;
|
|
24120
24189
|
const cgiStatusByChannel = /* @__PURE__ */ new Map();
|
|
24121
24190
|
if (options?.channels?.length) {
|
|
@@ -24145,15 +24214,31 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
24145
24214
|
channels = [];
|
|
24146
24215
|
}
|
|
24147
24216
|
} else {
|
|
24148
|
-
const
|
|
24149
|
-
|
|
24217
|
+
const pushChannels = Array.from(
|
|
24218
|
+
this.getChannelInfoFromPushCache().keys()
|
|
24219
|
+
).map((c) => Number(c)).filter((n) => Number.isFinite(n));
|
|
24220
|
+
const supportChnIds = (support?.items ?? []).map((i) => Number(i.chnID)).filter((n) => Number.isFinite(n));
|
|
24221
|
+
const probeTimeoutMs = options?.timeoutMs ?? 2500;
|
|
24222
|
+
channels = await resolveBaichuanChannels({
|
|
24223
|
+
pushChannels,
|
|
24224
|
+
supportChnIds,
|
|
24225
|
+
probe: async (channel) => {
|
|
24226
|
+
try {
|
|
24227
|
+
await this.getInfo(channel, {
|
|
24228
|
+
timeoutMs: probeTimeoutMs,
|
|
24229
|
+
tags: ["type", "name"]
|
|
24230
|
+
});
|
|
24231
|
+
return true;
|
|
24232
|
+
} catch {
|
|
24233
|
+
return false;
|
|
24234
|
+
}
|
|
24235
|
+
}
|
|
24236
|
+
});
|
|
24237
|
+
this.logger.debug?.(
|
|
24238
|
+
`[ReolinkBaichuanApi] getNvrChannelsSummary: baichuan resolved ${channels.length} channel(s): [${channels.join(", ")}]`
|
|
24239
|
+
);
|
|
24150
24240
|
}
|
|
24151
24241
|
channels = channels.sort((a, b) => a - b);
|
|
24152
|
-
const support = await this.getSupportInfo().catch(() => {
|
|
24153
|
-
this.logger.error?.(
|
|
24154
|
-
"[ReolinkBaichuanApi] getNvrChannelsSummary: failed to get support info"
|
|
24155
|
-
);
|
|
24156
|
-
});
|
|
24157
24242
|
const truthyNumberLike = (v) => {
|
|
24158
24243
|
if (typeof v === "number") return v > 0;
|
|
24159
24244
|
if (typeof v === "string") {
|
|
@@ -38182,811 +38267,8 @@ async function createReplayHttpServer(options) {
|
|
|
38182
38267
|
// src/index.ts
|
|
38183
38268
|
init_BaichuanVideoStream();
|
|
38184
38269
|
|
|
38185
|
-
// src/baichuan/stream/Go2rtcTcpServer.ts
|
|
38186
|
-
var import_node_events8 = require("events");
|
|
38187
|
-
var net4 = __toESM(require("net"), 1);
|
|
38188
|
-
init_H264Converter();
|
|
38189
|
-
init_H265Converter();
|
|
38190
|
-
var AsyncBoundedQueue2 = class {
|
|
38191
|
-
maxItems;
|
|
38192
|
-
queue = [];
|
|
38193
|
-
waiting;
|
|
38194
|
-
closed = false;
|
|
38195
|
-
constructor(maxItems) {
|
|
38196
|
-
this.maxItems = Math.max(1, maxItems | 0);
|
|
38197
|
-
}
|
|
38198
|
-
push(item) {
|
|
38199
|
-
if (this.closed) return;
|
|
38200
|
-
if (this.waiting) {
|
|
38201
|
-
const { resolve } = this.waiting;
|
|
38202
|
-
this.waiting = void 0;
|
|
38203
|
-
resolve({ value: item, done: false });
|
|
38204
|
-
return;
|
|
38205
|
-
}
|
|
38206
|
-
this.queue.push(item);
|
|
38207
|
-
if (this.queue.length > this.maxItems) {
|
|
38208
|
-
this.queue.splice(0, this.queue.length - this.maxItems);
|
|
38209
|
-
}
|
|
38210
|
-
}
|
|
38211
|
-
close() {
|
|
38212
|
-
if (this.closed) return;
|
|
38213
|
-
this.closed = true;
|
|
38214
|
-
if (this.waiting) {
|
|
38215
|
-
const { resolve } = this.waiting;
|
|
38216
|
-
this.waiting = void 0;
|
|
38217
|
-
resolve({ value: void 0, done: true });
|
|
38218
|
-
}
|
|
38219
|
-
}
|
|
38220
|
-
async next() {
|
|
38221
|
-
if (this.closed) return { value: void 0, done: true };
|
|
38222
|
-
const item = this.queue.shift();
|
|
38223
|
-
if (item !== void 0) return { value: item, done: false };
|
|
38224
|
-
return await new Promise((resolve) => {
|
|
38225
|
-
this.waiting = { resolve };
|
|
38226
|
-
});
|
|
38227
|
-
}
|
|
38228
|
-
};
|
|
38229
|
-
var NativeStreamFanout2 = class {
|
|
38230
|
-
opts;
|
|
38231
|
-
queues = /* @__PURE__ */ new Map();
|
|
38232
|
-
source = null;
|
|
38233
|
-
running = false;
|
|
38234
|
-
pumpPromise = null;
|
|
38235
|
-
constructor(opts) {
|
|
38236
|
-
this.opts = opts;
|
|
38237
|
-
}
|
|
38238
|
-
start() {
|
|
38239
|
-
if (this.running) return;
|
|
38240
|
-
this.running = true;
|
|
38241
|
-
this.source = this.opts.createSource();
|
|
38242
|
-
this.pumpPromise = (async () => {
|
|
38243
|
-
try {
|
|
38244
|
-
for await (const frame of this.source) {
|
|
38245
|
-
try {
|
|
38246
|
-
this.opts.onFrame?.(frame);
|
|
38247
|
-
} catch {
|
|
38248
|
-
}
|
|
38249
|
-
for (const q of this.queues.values()) {
|
|
38250
|
-
q.push(frame);
|
|
38251
|
-
}
|
|
38252
|
-
}
|
|
38253
|
-
} catch (e) {
|
|
38254
|
-
this.opts.onError?.(e);
|
|
38255
|
-
} finally {
|
|
38256
|
-
for (const q of this.queues.values()) q.close();
|
|
38257
|
-
this.queues.clear();
|
|
38258
|
-
this.running = false;
|
|
38259
|
-
this.opts.onEnd?.();
|
|
38260
|
-
}
|
|
38261
|
-
})();
|
|
38262
|
-
}
|
|
38263
|
-
subscribe(id) {
|
|
38264
|
-
const q = new AsyncBoundedQueue2(this.opts.maxQueueItems);
|
|
38265
|
-
this.queues.set(id, q);
|
|
38266
|
-
const self = this;
|
|
38267
|
-
return (async function* () {
|
|
38268
|
-
try {
|
|
38269
|
-
while (true) {
|
|
38270
|
-
const r = await q.next();
|
|
38271
|
-
if (r.done) return;
|
|
38272
|
-
yield r.value;
|
|
38273
|
-
}
|
|
38274
|
-
} finally {
|
|
38275
|
-
q.close();
|
|
38276
|
-
self.queues.delete(id);
|
|
38277
|
-
}
|
|
38278
|
-
})();
|
|
38279
|
-
}
|
|
38280
|
-
async stop() {
|
|
38281
|
-
if (!this.running) return;
|
|
38282
|
-
this.running = false;
|
|
38283
|
-
const src = this.source;
|
|
38284
|
-
this.source = null;
|
|
38285
|
-
for (const q of this.queues.values()) q.close();
|
|
38286
|
-
this.queues.clear();
|
|
38287
|
-
try {
|
|
38288
|
-
await src?.return(void 0);
|
|
38289
|
-
} catch {
|
|
38290
|
-
}
|
|
38291
|
-
try {
|
|
38292
|
-
await this.pumpPromise;
|
|
38293
|
-
} catch {
|
|
38294
|
-
}
|
|
38295
|
-
this.pumpPromise = null;
|
|
38296
|
-
}
|
|
38297
|
-
};
|
|
38298
|
-
var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events8.EventEmitter {
|
|
38299
|
-
api;
|
|
38300
|
-
channel;
|
|
38301
|
-
profile;
|
|
38302
|
-
variant;
|
|
38303
|
-
listenHost;
|
|
38304
|
-
listenPort;
|
|
38305
|
-
logger;
|
|
38306
|
-
deviceId;
|
|
38307
|
-
gracePeriodMs;
|
|
38308
|
-
prebufferMaxMs;
|
|
38309
|
-
maxBufferBytes;
|
|
38310
|
-
streamTimeoutMs;
|
|
38311
|
-
prestartStream;
|
|
38312
|
-
active = false;
|
|
38313
|
-
server;
|
|
38314
|
-
resolvedPort;
|
|
38315
|
-
// Native stream
|
|
38316
|
-
nativeFanout = null;
|
|
38317
|
-
nativeStreamActive = false;
|
|
38318
|
-
// Set only by stopNativeStream() (explicit teardown) so the fanout's onEnd
|
|
38319
|
-
// callback can short-circuit cleanup/restart logic. NOT set by the inactivity-
|
|
38320
|
-
// timeout force-restart path — that flow wants onEnd to run and decide
|
|
38321
|
-
// whether to restart based on prestartStream / connected clients.
|
|
38322
|
-
nativeStreamStopping = false;
|
|
38323
|
-
// Pending retry timer for the unbounded auto-restart loop. When a stream
|
|
38324
|
-
// start fails transiently (camera in maintenance reboot, idle-disconnect
|
|
38325
|
-
// race, etc.) we keep trying with exponential backoff until either the
|
|
38326
|
-
// server is stopped or a frame finally arrives.
|
|
38327
|
-
nativeStreamRetryTimer;
|
|
38328
|
-
nativeStreamRetryDelayMs = 0;
|
|
38329
|
-
dedicatedSessionRelease;
|
|
38330
|
-
detectedVideoType;
|
|
38331
|
-
// Client tracking
|
|
38332
|
-
connectedClients = /* @__PURE__ */ new Set();
|
|
38333
|
-
clientSockets = /* @__PURE__ */ new Map();
|
|
38334
|
-
stopGraceTimer;
|
|
38335
|
-
// Stream health monitoring
|
|
38336
|
-
lastFrameAt = 0;
|
|
38337
|
-
streamHealthTimer;
|
|
38338
|
-
totalFramesReceived = 0;
|
|
38339
|
-
totalVideoFramesWritten = 0;
|
|
38340
|
-
// Prebuffer
|
|
38341
|
-
prebuffer = [];
|
|
38342
|
-
// Audio metadata — populated on first valid ADTS AAC frame.
|
|
38343
|
-
// Exposed via getAudioInfo() for the stream-diagnostics feature.
|
|
38344
|
-
audioInfo = null;
|
|
38345
|
-
constructor(options) {
|
|
38346
|
-
super();
|
|
38347
|
-
this.api = options.api;
|
|
38348
|
-
this.channel = options.channel;
|
|
38349
|
-
this.profile = options.profile;
|
|
38350
|
-
this.variant = options.variant ?? "default";
|
|
38351
|
-
this.listenHost = options.listenHost ?? "127.0.0.1";
|
|
38352
|
-
this.listenPort = options.listenPort ?? 0;
|
|
38353
|
-
this.logger = options.logger ?? console;
|
|
38354
|
-
this.deviceId = options.deviceId;
|
|
38355
|
-
this.gracePeriodMs = options.gracePeriodMs ?? 3e4;
|
|
38356
|
-
this.prebufferMaxMs = options.prebufferMs ?? 3e3;
|
|
38357
|
-
this.maxBufferBytes = options.maxBufferBytes ?? 1e8;
|
|
38358
|
-
this.streamTimeoutMs = options.streamTimeoutMs ?? 15e3;
|
|
38359
|
-
this.prestartStream = options.prestartStream ?? true;
|
|
38360
|
-
}
|
|
38361
|
-
// -----------------------------------------------------------------------
|
|
38362
|
-
// Public API
|
|
38363
|
-
// -----------------------------------------------------------------------
|
|
38364
|
-
/** Start listening. Resolves once the TCP server is bound. */
|
|
38365
|
-
async start() {
|
|
38366
|
-
if (this.active) return;
|
|
38367
|
-
this.active = true;
|
|
38368
|
-
this.server = net4.createServer((socket) => this.handleClient(socket));
|
|
38369
|
-
this.server.on("error", (err) => {
|
|
38370
|
-
this.logger.error?.(`[Go2rtcTcpServer] server error: ${err.message}`);
|
|
38371
|
-
this.emit("error", err);
|
|
38372
|
-
});
|
|
38373
|
-
await new Promise((resolve, reject) => {
|
|
38374
|
-
this.server.listen(this.listenPort, this.listenHost, () => {
|
|
38375
|
-
const addr = this.server.address();
|
|
38376
|
-
this.resolvedPort = addr.port;
|
|
38377
|
-
this.logger.info?.(
|
|
38378
|
-
`[Go2rtcTcpServer] listening on ${addr.address}:${addr.port} channel=${this.channel} profile=${this.profile}`
|
|
38379
|
-
);
|
|
38380
|
-
this.emit("listening", { host: addr.address, port: addr.port });
|
|
38381
|
-
resolve();
|
|
38382
|
-
});
|
|
38383
|
-
this.server.once("error", reject);
|
|
38384
|
-
});
|
|
38385
|
-
if (this.prestartStream) {
|
|
38386
|
-
this.logger.info?.(
|
|
38387
|
-
`[Go2rtcTcpServer] pre-starting native stream channel=${this.channel} profile=${this.profile}`
|
|
38388
|
-
);
|
|
38389
|
-
this.startNativeStream();
|
|
38390
|
-
}
|
|
38391
|
-
}
|
|
38392
|
-
/** Stop the server and all active streams. */
|
|
38393
|
-
async stop() {
|
|
38394
|
-
if (!this.active) return;
|
|
38395
|
-
this.active = false;
|
|
38396
|
-
clearTimeout(this.stopGraceTimer);
|
|
38397
|
-
this.clearNativeStreamRetry();
|
|
38398
|
-
this.stopStreamHealthMonitor();
|
|
38399
|
-
for (const [id, sock] of this.clientSockets) {
|
|
38400
|
-
sock.destroy();
|
|
38401
|
-
this.connectedClients.delete(id);
|
|
38402
|
-
}
|
|
38403
|
-
this.clientSockets.clear();
|
|
38404
|
-
await this.stopNativeStream();
|
|
38405
|
-
if (this.server) {
|
|
38406
|
-
await new Promise((resolve) => {
|
|
38407
|
-
this.server.close(() => resolve());
|
|
38408
|
-
});
|
|
38409
|
-
this.server = void 0;
|
|
38410
|
-
}
|
|
38411
|
-
this.prebuffer = [];
|
|
38412
|
-
this.resolvedPort = void 0;
|
|
38413
|
-
this.emit("close");
|
|
38414
|
-
}
|
|
38415
|
-
/** The actual port the server is listening on (available after start()). */
|
|
38416
|
-
get port() {
|
|
38417
|
-
return this.resolvedPort;
|
|
38418
|
-
}
|
|
38419
|
-
/** The go2rtc-compatible source URL. */
|
|
38420
|
-
get go2rtcSourceUrl() {
|
|
38421
|
-
if (this.resolvedPort == null) return void 0;
|
|
38422
|
-
return `tcp://127.0.0.1:${this.resolvedPort}`;
|
|
38423
|
-
}
|
|
38424
|
-
/** Number of currently connected clients. */
|
|
38425
|
-
get clientCount() {
|
|
38426
|
-
return this.connectedClients.size;
|
|
38427
|
-
}
|
|
38428
|
-
// -----------------------------------------------------------------------
|
|
38429
|
-
// Diagnostic subscription API (implements DiagnosticStreamServer)
|
|
38430
|
-
//
|
|
38431
|
-
// Matches the shape of BaichuanRtspServer's diagnostic API so the
|
|
38432
|
-
// stream-diagnostic feature in the Manager app can drive either backend
|
|
38433
|
-
// with identical code.
|
|
38434
|
-
// -----------------------------------------------------------------------
|
|
38435
|
-
/**
|
|
38436
|
-
* Subscribe to the raw native stream for diagnostic purposes.
|
|
38437
|
-
* The subscriber receives the same frames the MPEG-TS muxer consumes
|
|
38438
|
-
* (pre-muxing). Counts as a "consumer" so the native stream is kept alive
|
|
38439
|
-
* for the lifetime of the subscription. If the stream is not already
|
|
38440
|
-
* running (battery camera, prestart=false), this starts it.
|
|
38441
|
-
*/
|
|
38442
|
-
async subscribeDiagnostic(id) {
|
|
38443
|
-
this.connectedClients.add(`diag:${id}`);
|
|
38444
|
-
if (!this.nativeStreamActive) {
|
|
38445
|
-
await this.startNativeStream();
|
|
38446
|
-
}
|
|
38447
|
-
if (!this.nativeFanout) {
|
|
38448
|
-
this.connectedClients.delete(`diag:${id}`);
|
|
38449
|
-
throw new Error(
|
|
38450
|
-
"Go2rtcTcpServer: native stream failed to start \u2014 cannot subscribe diagnostic"
|
|
38451
|
-
);
|
|
38452
|
-
}
|
|
38453
|
-
return this.nativeFanout.subscribe(`diag:${id}`);
|
|
38454
|
-
}
|
|
38455
|
-
/** Unsubscribe a diagnostic session and release its consumer slot. */
|
|
38456
|
-
unsubscribeDiagnostic(id) {
|
|
38457
|
-
this.removeClient(`diag:${id}`, "diagnostic unsubscribe");
|
|
38458
|
-
}
|
|
38459
|
-
/**
|
|
38460
|
-
* Returns ADTS AAC audio metadata detected from the native stream, or
|
|
38461
|
-
* null if no audio frame has been observed yet (e.g. video-only cameras
|
|
38462
|
-
* or before the first audio packet arrives).
|
|
38463
|
-
*/
|
|
38464
|
-
getAudioInfo() {
|
|
38465
|
-
return this.audioInfo;
|
|
38466
|
-
}
|
|
38467
|
-
// -----------------------------------------------------------------------
|
|
38468
|
-
// Client handling
|
|
38469
|
-
// -----------------------------------------------------------------------
|
|
38470
|
-
handleClient(socket) {
|
|
38471
|
-
const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
|
|
38472
|
-
socket.setNoDelay(true);
|
|
38473
|
-
this.connectedClients.add(clientId);
|
|
38474
|
-
this.clientSockets.set(clientId, socket);
|
|
38475
|
-
this.logger.info?.(
|
|
38476
|
-
`[Go2rtcTcpServer] client connected id=${clientId} total=${this.connectedClients.size}`
|
|
38477
|
-
);
|
|
38478
|
-
this.emit("client", clientId);
|
|
38479
|
-
if (this.stopGraceTimer) {
|
|
38480
|
-
clearTimeout(this.stopGraceTimer);
|
|
38481
|
-
this.stopGraceTimer = void 0;
|
|
38482
|
-
}
|
|
38483
|
-
if (!this.nativeStreamActive) {
|
|
38484
|
-
this.startNativeStream();
|
|
38485
|
-
}
|
|
38486
|
-
this.feedClient(clientId, socket).catch((err) => {
|
|
38487
|
-
this.logger.warn?.(
|
|
38488
|
-
`[Go2rtcTcpServer] feedClient error id=${clientId}: ${err}`
|
|
38489
|
-
);
|
|
38490
|
-
});
|
|
38491
|
-
const cleanup = (reason) => {
|
|
38492
|
-
this.removeClient(clientId, reason);
|
|
38493
|
-
socket.destroy();
|
|
38494
|
-
};
|
|
38495
|
-
socket.on("error", (err) => cleanup(`error: ${err.message}`));
|
|
38496
|
-
socket.on("close", (hadError) => cleanup(hadError ? "close (with error)" : "close (clean)"));
|
|
38497
|
-
}
|
|
38498
|
-
async feedClient(clientId, socket) {
|
|
38499
|
-
const fanoutDeadline = Date.now() + 3e4;
|
|
38500
|
-
while (this.active && !this.nativeFanout) {
|
|
38501
|
-
if (socket.destroyed) return;
|
|
38502
|
-
if (Date.now() > fanoutDeadline) {
|
|
38503
|
-
this.logger.warn?.(
|
|
38504
|
-
`[Go2rtcTcpServer] fanout not ready after 30s, dropping client ${clientId}`
|
|
38505
|
-
);
|
|
38506
|
-
return;
|
|
38507
|
-
}
|
|
38508
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
38509
|
-
}
|
|
38510
|
-
if (!this.active || !this.nativeFanout) return;
|
|
38511
|
-
const subscription = this.nativeFanout.subscribe(clientId);
|
|
38512
|
-
let muxer = null;
|
|
38513
|
-
const prebufferSnap = this.prebuffer.slice();
|
|
38514
|
-
let lastIdrIdx = -1;
|
|
38515
|
-
for (let i = prebufferSnap.length - 1; i >= 0; i--) {
|
|
38516
|
-
if (prebufferSnap[i].isKeyframe) {
|
|
38517
|
-
lastIdrIdx = i;
|
|
38518
|
-
break;
|
|
38519
|
-
}
|
|
38520
|
-
}
|
|
38521
|
-
if (lastIdrIdx >= 0) {
|
|
38522
|
-
const replay = prebufferSnap.slice(lastIdrIdx);
|
|
38523
|
-
this.logger.info?.(
|
|
38524
|
-
`[Go2rtcTcpServer] prebuffer replay client=${clientId} frames=${replay.length}`
|
|
38525
|
-
);
|
|
38526
|
-
if (!muxer) {
|
|
38527
|
-
muxer = new MpegTsMuxer({
|
|
38528
|
-
videoType: this.detectedVideoType ?? "H264",
|
|
38529
|
-
includeAudio: true
|
|
38530
|
-
});
|
|
38531
|
-
}
|
|
38532
|
-
for (const entry of replay) {
|
|
38533
|
-
if (socket.destroyed) return;
|
|
38534
|
-
let ts;
|
|
38535
|
-
if (!entry.audio) {
|
|
38536
|
-
ts = muxer.muxVideo(entry.data, entry.pts, entry.isKeyframe);
|
|
38537
|
-
} else {
|
|
38538
|
-
ts = muxer.muxAudio(entry.data, entry.pts);
|
|
38539
|
-
}
|
|
38540
|
-
if (ts.length > 0) socket.write(ts);
|
|
38541
|
-
}
|
|
38542
|
-
}
|
|
38543
|
-
let seenKeyframe = lastIdrIdx >= 0;
|
|
38544
|
-
let liveFrameCount = 0;
|
|
38545
|
-
let liveVideoWritten = 0;
|
|
38546
|
-
let lastLogAt = Date.now();
|
|
38547
|
-
try {
|
|
38548
|
-
this.logger.info?.(
|
|
38549
|
-
`[Go2rtcTcpServer] entering live loop client=${clientId} seenKeyframe=${seenKeyframe}`
|
|
38550
|
-
);
|
|
38551
|
-
for await (const frame of subscription) {
|
|
38552
|
-
if (socket.destroyed || !this.active) {
|
|
38553
|
-
this.logger.info?.(
|
|
38554
|
-
`[Go2rtcTcpServer] live loop exit client=${clientId} destroyed=${socket.destroyed} active=${this.active}`
|
|
38555
|
-
);
|
|
38556
|
-
break;
|
|
38557
|
-
}
|
|
38558
|
-
liveFrameCount++;
|
|
38559
|
-
if (frame.audio) {
|
|
38560
|
-
if (muxer) {
|
|
38561
|
-
const pts2 = frame.microseconds ?? Date.now() * 1e3;
|
|
38562
|
-
const ts2 = muxer.muxAudio(frame.data, pts2);
|
|
38563
|
-
if (ts2.length > 0) socket.write(ts2);
|
|
38564
|
-
}
|
|
38565
|
-
continue;
|
|
38566
|
-
}
|
|
38567
|
-
const annexB = this.convertVideoFrame(frame);
|
|
38568
|
-
if (!annexB) continue;
|
|
38569
|
-
const isKf = this.isAnnexBKeyframe(annexB, frame.videoType);
|
|
38570
|
-
if (!seenKeyframe) {
|
|
38571
|
-
if (!isKf) continue;
|
|
38572
|
-
seenKeyframe = true;
|
|
38573
|
-
this.logger.info?.(
|
|
38574
|
-
`[Go2rtcTcpServer] first live keyframe client=${clientId} after ${liveFrameCount} frames`
|
|
38575
|
-
);
|
|
38576
|
-
if (!muxer) {
|
|
38577
|
-
muxer = new MpegTsMuxer({
|
|
38578
|
-
videoType: frame.videoType ?? this.detectedVideoType ?? "H264",
|
|
38579
|
-
includeAudio: true
|
|
38580
|
-
});
|
|
38581
|
-
}
|
|
38582
|
-
}
|
|
38583
|
-
const pts = frame.microseconds ?? Date.now() * 1e3;
|
|
38584
|
-
const ts = muxer.muxVideo(annexB, pts, isKf);
|
|
38585
|
-
socket.write(ts);
|
|
38586
|
-
liveVideoWritten++;
|
|
38587
|
-
this.totalVideoFramesWritten++;
|
|
38588
|
-
if (Date.now() - lastLogAt > 1e4) {
|
|
38589
|
-
this.logger.info?.(
|
|
38590
|
-
`[Go2rtcTcpServer] live stats client=${clientId} received=${liveFrameCount} written=${liveVideoWritten} bufLen=${socket.writableLength}`
|
|
38591
|
-
);
|
|
38592
|
-
lastLogAt = Date.now();
|
|
38593
|
-
}
|
|
38594
|
-
if (socket.writableLength > this.maxBufferBytes) {
|
|
38595
|
-
this.logger.warn?.(
|
|
38596
|
-
`[Go2rtcTcpServer] buffer overflow (${socket.writableLength} bytes), dropping client ${clientId}`
|
|
38597
|
-
);
|
|
38598
|
-
socket.destroy();
|
|
38599
|
-
break;
|
|
38600
|
-
}
|
|
38601
|
-
}
|
|
38602
|
-
this.logger.info?.(
|
|
38603
|
-
`[Go2rtcTcpServer] live loop ended naturally client=${clientId} received=${liveFrameCount} written=${liveVideoWritten}`
|
|
38604
|
-
);
|
|
38605
|
-
} finally {
|
|
38606
|
-
await subscription.return(void 0).catch(() => {
|
|
38607
|
-
});
|
|
38608
|
-
}
|
|
38609
|
-
}
|
|
38610
|
-
// -----------------------------------------------------------------------
|
|
38611
|
-
// Frame conversion
|
|
38612
|
-
// -----------------------------------------------------------------------
|
|
38613
|
-
/**
|
|
38614
|
-
* Convert a native video frame to Annex-B.
|
|
38615
|
-
* Returns null for audio frames (handled separately by muxAudio).
|
|
38616
|
-
*/
|
|
38617
|
-
convertVideoFrame(frame) {
|
|
38618
|
-
if (frame.audio) return null;
|
|
38619
|
-
if (frame.data.length === 0) return null;
|
|
38620
|
-
try {
|
|
38621
|
-
if (frame.videoType === "H264") {
|
|
38622
|
-
return convertToAnnexB(frame.data);
|
|
38623
|
-
}
|
|
38624
|
-
if (frame.videoType === "H265") {
|
|
38625
|
-
return convertToAnnexB2(frame.data);
|
|
38626
|
-
}
|
|
38627
|
-
} catch {
|
|
38628
|
-
}
|
|
38629
|
-
return frame.data;
|
|
38630
|
-
}
|
|
38631
|
-
/** Check if an Annex-B buffer contains a keyframe (IDR for H.264, IRAP for H.265). */
|
|
38632
|
-
isAnnexBKeyframe(annexB, videoType) {
|
|
38633
|
-
try {
|
|
38634
|
-
if (videoType === "H264") {
|
|
38635
|
-
const nals = _Go2rtcTcpServer.splitAnnexBNals(annexB);
|
|
38636
|
-
return nals.some((n) => n.length >= 1 && (n[0] & 31) === 5);
|
|
38637
|
-
}
|
|
38638
|
-
if (videoType === "H265") {
|
|
38639
|
-
const nals = splitAnnexBToNalPayloads2(annexB);
|
|
38640
|
-
return nals.some(
|
|
38641
|
-
(n) => n.length >= 2 && isH265Irap(n[0] >> 1 & 63)
|
|
38642
|
-
);
|
|
38643
|
-
}
|
|
38644
|
-
} catch {
|
|
38645
|
-
}
|
|
38646
|
-
return false;
|
|
38647
|
-
}
|
|
38648
|
-
/** Split Annex-B byte stream into individual NAL units. */
|
|
38649
|
-
static splitAnnexBNals(buf) {
|
|
38650
|
-
const nals = [];
|
|
38651
|
-
let i = 0;
|
|
38652
|
-
while (i < buf.length) {
|
|
38653
|
-
if (i + 2 < buf.length && buf[i] === 0 && buf[i + 1] === 0) {
|
|
38654
|
-
let scLen;
|
|
38655
|
-
if (buf[i + 2] === 1) {
|
|
38656
|
-
scLen = 3;
|
|
38657
|
-
} else if (i + 3 < buf.length && buf[i + 2] === 0 && buf[i + 3] === 1) {
|
|
38658
|
-
scLen = 4;
|
|
38659
|
-
} else {
|
|
38660
|
-
i++;
|
|
38661
|
-
continue;
|
|
38662
|
-
}
|
|
38663
|
-
const nalStart = i + scLen;
|
|
38664
|
-
let nalEnd = buf.length;
|
|
38665
|
-
for (let j = nalStart; j < buf.length - 2; j++) {
|
|
38666
|
-
if (buf[j] === 0 && buf[j + 1] === 0 && (buf[j + 2] === 1 || j + 3 < buf.length && buf[j + 2] === 0 && buf[j + 3] === 1)) {
|
|
38667
|
-
nalEnd = j;
|
|
38668
|
-
break;
|
|
38669
|
-
}
|
|
38670
|
-
}
|
|
38671
|
-
if (nalEnd > nalStart) {
|
|
38672
|
-
nals.push(buf.subarray(nalStart, nalEnd));
|
|
38673
|
-
}
|
|
38674
|
-
i = nalEnd;
|
|
38675
|
-
} else {
|
|
38676
|
-
i++;
|
|
38677
|
-
}
|
|
38678
|
-
}
|
|
38679
|
-
return nals;
|
|
38680
|
-
}
|
|
38681
|
-
// -----------------------------------------------------------------------
|
|
38682
|
-
// ADTS AAC parsing (used for audio metadata exposed via getAudioInfo)
|
|
38683
|
-
// -----------------------------------------------------------------------
|
|
38684
|
-
/** True if `b` starts with an ADTS AAC syncword (0xFFF). */
|
|
38685
|
-
static isAdtsAacFrame(b) {
|
|
38686
|
-
return b.length >= 2 && b[0] === 255 && (b[1] & 240) === 240;
|
|
38687
|
-
}
|
|
38688
|
-
/**
|
|
38689
|
-
* Parse an ADTS header into {sampleRate, channels, AudioSpecificConfig hex}.
|
|
38690
|
-
* Returns null when the buffer is not a valid ADTS frame.
|
|
38691
|
-
*/
|
|
38692
|
-
static parseAdtsSamplingInfo(b) {
|
|
38693
|
-
if (b.length < 7) return null;
|
|
38694
|
-
if (!_Go2rtcTcpServer.isAdtsAacFrame(b)) return null;
|
|
38695
|
-
const samplingIndex = b[2] >> 2 & 15;
|
|
38696
|
-
const sampleRates = [
|
|
38697
|
-
96e3,
|
|
38698
|
-
88200,
|
|
38699
|
-
64e3,
|
|
38700
|
-
48e3,
|
|
38701
|
-
44100,
|
|
38702
|
-
32e3,
|
|
38703
|
-
24e3,
|
|
38704
|
-
22050,
|
|
38705
|
-
16e3,
|
|
38706
|
-
12e3,
|
|
38707
|
-
11025,
|
|
38708
|
-
8e3,
|
|
38709
|
-
7350
|
|
38710
|
-
];
|
|
38711
|
-
const sampleRate = sampleRates[samplingIndex] ?? null;
|
|
38712
|
-
if (!sampleRate) return null;
|
|
38713
|
-
const channelConfig = (b[2] & 1) << 2 | b[3] >> 6 & 3;
|
|
38714
|
-
const channels = channelConfig === 0 ? 1 : channelConfig;
|
|
38715
|
-
const profile = b[2] >> 6 & 3;
|
|
38716
|
-
const audioObjectType = profile + 1;
|
|
38717
|
-
const asc = audioObjectType << 11 | samplingIndex << 7 | channelConfig << 3;
|
|
38718
|
-
const configHex = Buffer.from([asc >> 8 & 255, asc & 255]).toString(
|
|
38719
|
-
"hex"
|
|
38720
|
-
);
|
|
38721
|
-
return { sampleRate, channels, configHex };
|
|
38722
|
-
}
|
|
38723
|
-
// -----------------------------------------------------------------------
|
|
38724
|
-
// Native stream management
|
|
38725
|
-
// -----------------------------------------------------------------------
|
|
38726
|
-
/**
|
|
38727
|
-
* Schedule another startNativeStream() attempt after the given delay.
|
|
38728
|
-
* Idempotent: a no-op if a retry is already scheduled, the server is no
|
|
38729
|
-
* longer active, or an explicit stop is in progress. Implements unbounded
|
|
38730
|
-
* exponential backoff (5s → 60s) so a camera that stays unreachable for
|
|
38731
|
-
* minutes (e.g. nightly maintenance reboot) eventually recovers without
|
|
38732
|
-
* manual intervention — see issue #16.
|
|
38733
|
-
*/
|
|
38734
|
-
scheduleNativeStreamRetry(reason) {
|
|
38735
|
-
if (!this.active) return;
|
|
38736
|
-
if (this.nativeStreamStopping) return;
|
|
38737
|
-
if (this.nativeStreamRetryTimer) return;
|
|
38738
|
-
const delay = this.nativeStreamRetryDelayMs > 0 ? this.nativeStreamRetryDelayMs : 5e3;
|
|
38739
|
-
this.logger.info?.(
|
|
38740
|
-
`[Go2rtcTcpServer] scheduling native stream retry in ${(delay / 1e3).toFixed(0)}s (reason=${reason})`
|
|
38741
|
-
);
|
|
38742
|
-
this.nativeStreamRetryTimer = setTimeout(() => {
|
|
38743
|
-
this.nativeStreamRetryTimer = void 0;
|
|
38744
|
-
if (!this.active) return;
|
|
38745
|
-
if (this.nativeStreamStopping) return;
|
|
38746
|
-
this.startNativeStream().catch((err) => {
|
|
38747
|
-
this.logger.warn?.(
|
|
38748
|
-
`[Go2rtcTcpServer] retry of startNativeStream threw: ${err instanceof Error ? err.message : err}`
|
|
38749
|
-
);
|
|
38750
|
-
});
|
|
38751
|
-
}, delay);
|
|
38752
|
-
this.nativeStreamRetryDelayMs = Math.min(delay * 2, 6e4);
|
|
38753
|
-
}
|
|
38754
|
-
/**
|
|
38755
|
-
* Cancel any pending retry timer and reset the backoff. Called on explicit
|
|
38756
|
-
* stop and on first-frame-received so the next failure starts the backoff
|
|
38757
|
-
* window from scratch.
|
|
38758
|
-
*/
|
|
38759
|
-
clearNativeStreamRetry() {
|
|
38760
|
-
if (this.nativeStreamRetryTimer) {
|
|
38761
|
-
clearTimeout(this.nativeStreamRetryTimer);
|
|
38762
|
-
this.nativeStreamRetryTimer = void 0;
|
|
38763
|
-
}
|
|
38764
|
-
this.nativeStreamRetryDelayMs = 0;
|
|
38765
|
-
}
|
|
38766
|
-
async startNativeStream() {
|
|
38767
|
-
if (this.nativeStreamActive) return;
|
|
38768
|
-
if (!this.api.isReady) {
|
|
38769
|
-
if (this.api.isClosed) {
|
|
38770
|
-
this.logger.warn?.(
|
|
38771
|
-
`[Go2rtcTcpServer] API has been explicitly closed \u2014 stream cannot start`
|
|
38772
|
-
);
|
|
38773
|
-
return;
|
|
38774
|
-
}
|
|
38775
|
-
try {
|
|
38776
|
-
this.logger.info?.(
|
|
38777
|
-
`[Go2rtcTcpServer] API not ready (idle disconnect?), calling ensureConnected`
|
|
38778
|
-
);
|
|
38779
|
-
await this.api.ensureConnected();
|
|
38780
|
-
} catch (e) {
|
|
38781
|
-
this.logger.warn?.(
|
|
38782
|
-
`[Go2rtcTcpServer] ensureConnected failed: ${e}`
|
|
38783
|
-
);
|
|
38784
|
-
this.scheduleNativeStreamRetry("ensureConnected failed");
|
|
38785
|
-
return;
|
|
38786
|
-
}
|
|
38787
|
-
}
|
|
38788
|
-
this.nativeStreamActive = true;
|
|
38789
|
-
let dedicatedClient;
|
|
38790
|
-
if (this.deviceId) {
|
|
38791
|
-
try {
|
|
38792
|
-
const session = await this.api.createDedicatedSession(
|
|
38793
|
-
`live:${this.deviceId}:ch${this.channel}:${this.profile}`
|
|
38794
|
-
);
|
|
38795
|
-
dedicatedClient = session.client;
|
|
38796
|
-
this.dedicatedSessionRelease = session.release;
|
|
38797
|
-
} catch (e) {
|
|
38798
|
-
this.logger.warn?.(
|
|
38799
|
-
`[Go2rtcTcpServer] failed to acquire dedicated session, using shared socket: ${e}`
|
|
38800
|
-
);
|
|
38801
|
-
}
|
|
38802
|
-
}
|
|
38803
|
-
this.logger.info?.(
|
|
38804
|
-
`[Go2rtcTcpServer] native stream starting channel=${this.channel} profile=${this.profile} dedicated=${!!dedicatedClient}`
|
|
38805
|
-
);
|
|
38806
|
-
let hadFrames = false;
|
|
38807
|
-
this.nativeFanout = new NativeStreamFanout2({
|
|
38808
|
-
maxQueueItems: 200,
|
|
38809
|
-
createSource: () => createNativeStream(this.api, this.channel, this.profile, {
|
|
38810
|
-
variant: this.variant,
|
|
38811
|
-
...dedicatedClient ? { client: dedicatedClient } : {}
|
|
38812
|
-
}),
|
|
38813
|
-
onFrame: (frame) => {
|
|
38814
|
-
if (!hadFrames) {
|
|
38815
|
-
this.clearNativeStreamRetry();
|
|
38816
|
-
}
|
|
38817
|
-
hadFrames = true;
|
|
38818
|
-
this.lastFrameAt = Date.now();
|
|
38819
|
-
this.totalFramesReceived++;
|
|
38820
|
-
if (!frame.audio && (frame.videoType === "H264" || frame.videoType === "H265")) {
|
|
38821
|
-
this.detectedVideoType = frame.videoType;
|
|
38822
|
-
}
|
|
38823
|
-
let prebufData;
|
|
38824
|
-
let isKeyframe;
|
|
38825
|
-
if (frame.audio) {
|
|
38826
|
-
if (frame.data.length === 0) return;
|
|
38827
|
-
if (!this.audioInfo) {
|
|
38828
|
-
const parsed = _Go2rtcTcpServer.parseAdtsSamplingInfo(frame.data);
|
|
38829
|
-
if (parsed) {
|
|
38830
|
-
this.audioInfo = { codec: "aac-adts", ...parsed };
|
|
38831
|
-
}
|
|
38832
|
-
}
|
|
38833
|
-
prebufData = frame.data;
|
|
38834
|
-
isKeyframe = false;
|
|
38835
|
-
} else {
|
|
38836
|
-
const annexB = this.convertVideoFrame(frame);
|
|
38837
|
-
if (!annexB || annexB.length === 0) return;
|
|
38838
|
-
prebufData = annexB;
|
|
38839
|
-
isKeyframe = this.isAnnexBKeyframe(annexB, frame.videoType);
|
|
38840
|
-
}
|
|
38841
|
-
const pts = frame.microseconds ?? Date.now() * 1e3;
|
|
38842
|
-
this.prebuffer.push({
|
|
38843
|
-
data: Buffer.from(prebufData),
|
|
38844
|
-
time: Date.now(),
|
|
38845
|
-
isKeyframe,
|
|
38846
|
-
audio: frame.audio,
|
|
38847
|
-
pts
|
|
38848
|
-
});
|
|
38849
|
-
const cutoff = Date.now() - this.prebufferMaxMs;
|
|
38850
|
-
let trimIdx = 0;
|
|
38851
|
-
while (trimIdx < this.prebuffer.length && this.prebuffer[trimIdx].time < cutoff) {
|
|
38852
|
-
trimIdx++;
|
|
38853
|
-
}
|
|
38854
|
-
if (trimIdx > 0) this.prebuffer.splice(0, trimIdx);
|
|
38855
|
-
},
|
|
38856
|
-
onError: (error) => {
|
|
38857
|
-
this.logger.warn?.(`[Go2rtcTcpServer] native stream error: ${error}`);
|
|
38858
|
-
},
|
|
38859
|
-
onEnd: () => {
|
|
38860
|
-
if (this.nativeStreamStopping) return;
|
|
38861
|
-
this.nativeStreamActive = false;
|
|
38862
|
-
this.nativeFanout = null;
|
|
38863
|
-
this.stopStreamHealthMonitor();
|
|
38864
|
-
const silenceMs = this.lastFrameAt > 0 ? Date.now() - this.lastFrameAt : -1;
|
|
38865
|
-
const diagnosis = silenceMs > this.streamTimeoutMs ? "camera stopped sending frames" : silenceMs >= 0 ? "stream source closed" : "no frames were ever received";
|
|
38866
|
-
this.logger.warn?.(
|
|
38867
|
-
`[Go2rtcTcpServer] native stream ended diagnosis="${diagnosis}" lastFrame=${silenceMs >= 0 ? `${(silenceMs / 1e3).toFixed(1)}s ago` : "never"} totalRx=${this.totalFramesReceived} clients=${this.connectedClients.size}`
|
|
38868
|
-
);
|
|
38869
|
-
if (this.dedicatedSessionRelease) {
|
|
38870
|
-
this.dedicatedSessionRelease().catch(() => {
|
|
38871
|
-
});
|
|
38872
|
-
this.dedicatedSessionRelease = void 0;
|
|
38873
|
-
}
|
|
38874
|
-
if (!this.prestartStream) {
|
|
38875
|
-
this.logger.info?.(
|
|
38876
|
-
`[Go2rtcTcpServer] battery native stream ended hadFrames=${hadFrames} channel=${this.channel} profile=${this.profile} \u2014 dropping ${this.connectedClients.size} client(s) to prevent wake loop`
|
|
38877
|
-
);
|
|
38878
|
-
for (const [, sock] of this.clientSockets) {
|
|
38879
|
-
sock.destroy();
|
|
38880
|
-
}
|
|
38881
|
-
} else if (this.active) {
|
|
38882
|
-
if (typeof this.api.isStreamProfileRejected === "function" && this.api.isStreamProfileRejected(this.channel, this.profile)) {
|
|
38883
|
-
this.logger.warn?.(
|
|
38884
|
-
`[Go2rtcTcpServer] profile rejected by device channel=${this.channel} profile=${this.profile} \u2014 not restarting`
|
|
38885
|
-
);
|
|
38886
|
-
for (const [, sock] of this.clientSockets) {
|
|
38887
|
-
sock.destroy();
|
|
38888
|
-
}
|
|
38889
|
-
return;
|
|
38890
|
-
}
|
|
38891
|
-
this.logger.info?.(
|
|
38892
|
-
`[Go2rtcTcpServer] restarting native stream (clients=${this.connectedClients.size}, prestart=${this.prestartStream})`
|
|
38893
|
-
);
|
|
38894
|
-
this.startNativeStream();
|
|
38895
|
-
}
|
|
38896
|
-
}
|
|
38897
|
-
});
|
|
38898
|
-
this.nativeFanout.start();
|
|
38899
|
-
this.startStreamHealthMonitor();
|
|
38900
|
-
}
|
|
38901
|
-
async stopNativeStream() {
|
|
38902
|
-
this.nativeStreamStopping = true;
|
|
38903
|
-
this.nativeStreamActive = false;
|
|
38904
|
-
this.clearNativeStreamRetry();
|
|
38905
|
-
this.stopStreamHealthMonitor();
|
|
38906
|
-
const fanout = this.nativeFanout;
|
|
38907
|
-
this.nativeFanout = null;
|
|
38908
|
-
try {
|
|
38909
|
-
if (fanout) {
|
|
38910
|
-
await fanout.stop();
|
|
38911
|
-
}
|
|
38912
|
-
this.prebuffer = [];
|
|
38913
|
-
if (this.dedicatedSessionRelease) {
|
|
38914
|
-
await this.dedicatedSessionRelease().catch(() => {
|
|
38915
|
-
});
|
|
38916
|
-
this.dedicatedSessionRelease = void 0;
|
|
38917
|
-
}
|
|
38918
|
-
} finally {
|
|
38919
|
-
this.nativeStreamStopping = false;
|
|
38920
|
-
}
|
|
38921
|
-
}
|
|
38922
|
-
// -----------------------------------------------------------------------
|
|
38923
|
-
// Stream health monitoring
|
|
38924
|
-
// -----------------------------------------------------------------------
|
|
38925
|
-
startStreamHealthMonitor() {
|
|
38926
|
-
this.stopStreamHealthMonitor();
|
|
38927
|
-
if (this.streamTimeoutMs <= 0) return;
|
|
38928
|
-
this.lastFrameAt = Date.now();
|
|
38929
|
-
this.streamHealthTimer = setInterval(() => {
|
|
38930
|
-
if (!this.nativeStreamActive || !this.active) {
|
|
38931
|
-
this.stopStreamHealthMonitor();
|
|
38932
|
-
return;
|
|
38933
|
-
}
|
|
38934
|
-
const silenceMs = Date.now() - this.lastFrameAt;
|
|
38935
|
-
if (silenceMs > this.streamTimeoutMs) {
|
|
38936
|
-
this.logger.warn?.(
|
|
38937
|
-
`[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`
|
|
38938
|
-
);
|
|
38939
|
-
this.stopStreamHealthMonitor();
|
|
38940
|
-
const fanout = this.nativeFanout;
|
|
38941
|
-
if (fanout) {
|
|
38942
|
-
this.nativeStreamActive = false;
|
|
38943
|
-
this.nativeFanout = null;
|
|
38944
|
-
fanout.stop().catch(() => {
|
|
38945
|
-
});
|
|
38946
|
-
}
|
|
38947
|
-
}
|
|
38948
|
-
}, Math.min(this.streamTimeoutMs / 2, 5e3));
|
|
38949
|
-
}
|
|
38950
|
-
stopStreamHealthMonitor() {
|
|
38951
|
-
if (this.streamHealthTimer) {
|
|
38952
|
-
clearInterval(this.streamHealthTimer);
|
|
38953
|
-
this.streamHealthTimer = void 0;
|
|
38954
|
-
}
|
|
38955
|
-
}
|
|
38956
|
-
// -----------------------------------------------------------------------
|
|
38957
|
-
// Client lifecycle
|
|
38958
|
-
// -----------------------------------------------------------------------
|
|
38959
|
-
removeClient(clientId, reason) {
|
|
38960
|
-
if (!this.connectedClients.has(clientId)) return;
|
|
38961
|
-
this.connectedClients.delete(clientId);
|
|
38962
|
-
this.clientSockets.delete(clientId);
|
|
38963
|
-
const silenceMs = this.lastFrameAt > 0 ? Date.now() - this.lastFrameAt : -1;
|
|
38964
|
-
const silenceInfo = silenceMs >= 0 ? ` lastFrame=${(silenceMs / 1e3).toFixed(1)}s ago` : "";
|
|
38965
|
-
this.logger.info?.(
|
|
38966
|
-
`[Go2rtcTcpServer] client disconnected id=${clientId} reason=${reason ?? "unknown"} remaining=${this.connectedClients.size} totalRx=${this.totalFramesReceived} totalTx=${this.totalVideoFramesWritten}${silenceInfo}`
|
|
38967
|
-
);
|
|
38968
|
-
this.emit("clientDisconnected", clientId);
|
|
38969
|
-
if (this.connectedClients.size === 0 && !this.prestartStream) {
|
|
38970
|
-
this.scheduleStop();
|
|
38971
|
-
}
|
|
38972
|
-
}
|
|
38973
|
-
scheduleStop() {
|
|
38974
|
-
if (this.stopGraceTimer) return;
|
|
38975
|
-
this.logger.info?.(
|
|
38976
|
-
`[Go2rtcTcpServer] no clients, scheduling stream stop in ${this.gracePeriodMs}ms`
|
|
38977
|
-
);
|
|
38978
|
-
this.stopGraceTimer = setTimeout(async () => {
|
|
38979
|
-
this.stopGraceTimer = void 0;
|
|
38980
|
-
if (this.connectedClients.size === 0 && this.nativeStreamActive) {
|
|
38981
|
-
this.logger.info?.("[Go2rtcTcpServer] grace period expired, stopping native stream");
|
|
38982
|
-
await this.stopNativeStream();
|
|
38983
|
-
}
|
|
38984
|
-
}, this.gracePeriodMs);
|
|
38985
|
-
}
|
|
38986
|
-
};
|
|
38987
|
-
|
|
38988
38270
|
// src/baichuan/stream/BaichuanHttpStreamServer.ts
|
|
38989
|
-
var
|
|
38271
|
+
var import_node_events8 = require("events");
|
|
38990
38272
|
var import_node_child_process10 = require("child_process");
|
|
38991
38273
|
var http4 = __toESM(require("http"), 1);
|
|
38992
38274
|
var NAL_START_CODE_4B4 = Buffer.from([0, 0, 0, 1]);
|
|
@@ -39033,7 +38315,7 @@ function isH264KeyframeFromAnnexB(annexB) {
|
|
|
39033
38315
|
}
|
|
39034
38316
|
return false;
|
|
39035
38317
|
}
|
|
39036
|
-
var BaichuanHttpStreamServer = class extends
|
|
38318
|
+
var BaichuanHttpStreamServer = class extends import_node_events8.EventEmitter {
|
|
39037
38319
|
videoStream;
|
|
39038
38320
|
listenPort;
|
|
39039
38321
|
path;
|
|
@@ -39305,15 +38587,15 @@ var BaichuanHttpStreamServer = class extends import_node_events9.EventEmitter {
|
|
|
39305
38587
|
};
|
|
39306
38588
|
|
|
39307
38589
|
// src/baichuan/stream/BaichuanMjpegServer.ts
|
|
39308
|
-
var
|
|
38590
|
+
var import_node_events10 = require("events");
|
|
39309
38591
|
var http5 = __toESM(require("http"), 1);
|
|
39310
38592
|
|
|
39311
38593
|
// src/baichuan/stream/MjpegTransformer.ts
|
|
39312
|
-
var
|
|
38594
|
+
var import_node_events9 = require("events");
|
|
39313
38595
|
var import_node_child_process11 = require("child_process");
|
|
39314
38596
|
var JPEG_SOI = Buffer.from([255, 216]);
|
|
39315
38597
|
var JPEG_EOI = Buffer.from([255, 217]);
|
|
39316
|
-
var MjpegTransformer = class extends
|
|
38598
|
+
var MjpegTransformer = class extends import_node_events9.EventEmitter {
|
|
39317
38599
|
options;
|
|
39318
38600
|
ffmpeg = null;
|
|
39319
38601
|
started = false;
|
|
@@ -39512,7 +38794,7 @@ Content-Length: ${frame.length}\r
|
|
|
39512
38794
|
// src/baichuan/stream/BaichuanMjpegServer.ts
|
|
39513
38795
|
init_H264Converter();
|
|
39514
38796
|
init_H265Converter();
|
|
39515
|
-
var BaichuanMjpegServer = class extends
|
|
38797
|
+
var BaichuanMjpegServer = class extends import_node_events10.EventEmitter {
|
|
39516
38798
|
options;
|
|
39517
38799
|
clients = /* @__PURE__ */ new Map();
|
|
39518
38800
|
httpServer = null;
|
|
@@ -39793,14 +39075,14 @@ var BaichuanMjpegServer = class extends import_node_events11.EventEmitter {
|
|
|
39793
39075
|
};
|
|
39794
39076
|
|
|
39795
39077
|
// src/baichuan/stream/BaichuanWebRTCServer.ts
|
|
39796
|
-
var
|
|
39078
|
+
var import_node_events12 = require("events");
|
|
39797
39079
|
init_BcMediaAnnexBDecoder();
|
|
39798
39080
|
|
|
39799
39081
|
// src/baichuan/stream/AacToOpusTranscoder.ts
|
|
39800
39082
|
var import_node_child_process12 = require("child_process");
|
|
39801
39083
|
var import_node_dgram3 = require("dgram");
|
|
39802
|
-
var
|
|
39803
|
-
var AacToOpusTranscoder = class extends
|
|
39084
|
+
var import_node_events11 = require("events");
|
|
39085
|
+
var AacToOpusTranscoder = class extends import_node_events11.EventEmitter {
|
|
39804
39086
|
opts;
|
|
39805
39087
|
socket = null;
|
|
39806
39088
|
ffmpeg = null;
|
|
@@ -40017,7 +39299,7 @@ function getH264NalType(nalUnit) {
|
|
|
40017
39299
|
function getH265NalType2(nalUnit) {
|
|
40018
39300
|
return nalUnit[0] >> 1 & 63;
|
|
40019
39301
|
}
|
|
40020
|
-
var BaichuanWebRTCServer = class extends
|
|
39302
|
+
var BaichuanWebRTCServer = class extends import_node_events12.EventEmitter {
|
|
40021
39303
|
options;
|
|
40022
39304
|
sessions = /* @__PURE__ */ new Map();
|
|
40023
39305
|
sessionIdCounter = 0;
|
|
@@ -41009,7 +40291,7 @@ Error: ${err}`
|
|
|
41009
40291
|
};
|
|
41010
40292
|
|
|
41011
40293
|
// src/baichuan/stream/BaichuanHlsServer.ts
|
|
41012
|
-
var
|
|
40294
|
+
var import_node_events13 = require("events");
|
|
41013
40295
|
var import_node_fs = __toESM(require("fs"), 1);
|
|
41014
40296
|
var import_promises3 = __toESM(require("fs/promises"), 1);
|
|
41015
40297
|
var import_node_os3 = __toESM(require("os"), 1);
|
|
@@ -41089,7 +40371,7 @@ function getNalTypes(codec, annexB) {
|
|
|
41089
40371
|
}
|
|
41090
40372
|
});
|
|
41091
40373
|
}
|
|
41092
|
-
var BaichuanHlsServer = class extends
|
|
40374
|
+
var BaichuanHlsServer = class extends import_node_events13.EventEmitter {
|
|
41093
40375
|
api;
|
|
41094
40376
|
channel;
|
|
41095
40377
|
profile;
|
|
@@ -41658,10 +40940,10 @@ async function pingHost(host, timeoutMs = 3e3) {
|
|
|
41658
40940
|
return false;
|
|
41659
40941
|
}
|
|
41660
40942
|
async function tcpReachabilityProbe(host, port, timeoutMs) {
|
|
41661
|
-
const
|
|
40943
|
+
const net6 = await import("net");
|
|
41662
40944
|
return new Promise((resolve) => {
|
|
41663
40945
|
let settled = false;
|
|
41664
|
-
const socket = new
|
|
40946
|
+
const socket = new net6.Socket();
|
|
41665
40947
|
const timer = setTimeout(() => {
|
|
41666
40948
|
if (settled) return;
|
|
41667
40949
|
settled = true;
|
|
@@ -42254,10 +41536,10 @@ async function autoDetectDeviceType(inputs) {
|
|
|
42254
41536
|
}
|
|
42255
41537
|
|
|
42256
41538
|
// src/multifocal/compositeRtspServer.ts
|
|
42257
|
-
var
|
|
41539
|
+
var import_node_events14 = require("events");
|
|
42258
41540
|
var import_node_child_process14 = require("child_process");
|
|
42259
|
-
var
|
|
42260
|
-
var CompositeRtspServer = class extends
|
|
41541
|
+
var net4 = __toESM(require("net"), 1);
|
|
41542
|
+
var CompositeRtspServer = class extends import_node_events14.EventEmitter {
|
|
42261
41543
|
options;
|
|
42262
41544
|
compositeStream = null;
|
|
42263
41545
|
rtspServer = null;
|
|
@@ -42323,7 +41605,7 @@ var CompositeRtspServer = class extends import_node_events15.EventEmitter {
|
|
|
42323
41605
|
const width = widerStreamInfo?.width ?? 1920;
|
|
42324
41606
|
const height = widerStreamInfo?.height ?? 1080;
|
|
42325
41607
|
const fps = widerStreamInfo?.frameRate ?? 25;
|
|
42326
|
-
this.rtspServer =
|
|
41608
|
+
this.rtspServer = net4.createServer((socket) => {
|
|
42327
41609
|
this.handleRtspConnection(socket);
|
|
42328
41610
|
});
|
|
42329
41611
|
await new Promise((resolve, reject) => {
|
|
@@ -43033,8 +42315,8 @@ var RtspBackchannel = class _RtspBackchannel {
|
|
|
43033
42315
|
};
|
|
43034
42316
|
|
|
43035
42317
|
// src/baichuan/stream/BaichuanRtspBackchannelServer.ts
|
|
43036
|
-
var
|
|
43037
|
-
var
|
|
42318
|
+
var import_node_events15 = require("events");
|
|
42319
|
+
var net5 = __toESM(require("net"), 1);
|
|
43038
42320
|
var crypto3 = __toESM(require("crypto"), 1);
|
|
43039
42321
|
var md5Hex = (s) => crypto3.createHash("md5").update(s).digest("hex");
|
|
43040
42322
|
var RTCP_KEEPALIVE_INTERVAL_MS = 1e4;
|
|
@@ -43090,7 +42372,7 @@ function extractPublicEndpoint(url, requestText) {
|
|
|
43090
42372
|
if (hostHeader) return hostHeader;
|
|
43091
42373
|
return null;
|
|
43092
42374
|
}
|
|
43093
|
-
var BaichuanRtspBackchannelServer = class _BaichuanRtspBackchannelServer extends
|
|
42375
|
+
var BaichuanRtspBackchannelServer = class _BaichuanRtspBackchannelServer extends import_node_events15.EventEmitter {
|
|
43094
42376
|
listenHost;
|
|
43095
42377
|
listenPort;
|
|
43096
42378
|
logger;
|
|
@@ -43207,7 +42489,7 @@ var BaichuanRtspBackchannelServer = class _BaichuanRtspBackchannelServer extends
|
|
|
43207
42489
|
async start() {
|
|
43208
42490
|
if (this.server) return;
|
|
43209
42491
|
await new Promise((resolve, reject) => {
|
|
43210
|
-
const server =
|
|
42492
|
+
const server = net5.createServer((socket) => this.handleConnection(socket));
|
|
43211
42493
|
const onError = (err) => {
|
|
43212
42494
|
server.removeListener("error", onError);
|
|
43213
42495
|
reject(err);
|
|
@@ -44235,7 +43517,6 @@ function buildInitialStatus(config) {
|
|
|
44235
43517
|
DUAL_LENS_DUAL_MOTION_MODELS,
|
|
44236
43518
|
DUAL_LENS_MODELS,
|
|
44237
43519
|
DUAL_LENS_SINGLE_MOTION_MODELS,
|
|
44238
|
-
Go2rtcTcpServer,
|
|
44239
43520
|
H264RtpDepacketizer,
|
|
44240
43521
|
H265RtpDepacketizer,
|
|
44241
43522
|
HlsSessionManager,
|