@apocaliss92/nodelink-js 0.3.4 → 0.3.9

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/index.cjs CHANGED
@@ -3103,6 +3103,7 @@ var init_urls = __esm({
3103
3103
  // src/debug/DiagnosticsTools.ts
3104
3104
  var DiagnosticsTools_exports = {};
3105
3105
  __export(DiagnosticsTools_exports, {
3106
+ captureModelFixtures: () => captureModelFixtures,
3106
3107
  collectCgiDiagnostics: () => collectCgiDiagnostics,
3107
3108
  collectMultifocalDiagnostics: () => collectMultifocalDiagnostics,
3108
3109
  collectNativeDiagnostics: () => collectNativeDiagnostics,
@@ -3136,6 +3137,10 @@ function writeJson(filePath, obj) {
3136
3137
  mkdirp(path4.dirname(filePath));
3137
3138
  fs4.writeFileSync(filePath, JSON.stringify(obj, null, 2));
3138
3139
  }
3140
+ function writeText(filePath, text) {
3141
+ mkdirp(path4.dirname(filePath));
3142
+ fs4.writeFileSync(filePath, text);
3143
+ }
3139
3144
  function appendNdjson(filePath, obj) {
3140
3145
  mkdirp(path4.dirname(filePath));
3141
3146
  fs4.appendFileSync(filePath, JSON.stringify(obj) + "\n");
@@ -5399,6 +5404,172 @@ async function runAllDiagnosticsConsecutively(params) {
5399
5404
  streamsDir
5400
5405
  };
5401
5406
  }
5407
+ async function captureModelFixtures(params) {
5408
+ const { api, channel, outDir } = params;
5409
+ const log = params.log ?? console.log;
5410
+ mkdirp(outDir);
5411
+ const calls = {};
5412
+ const errors = [];
5413
+ async function capture(name, fn, writer) {
5414
+ try {
5415
+ const value = await fn();
5416
+ calls[name] = { ok: true, value };
5417
+ if (writer && value !== void 0 && value !== null) {
5418
+ writer(value);
5419
+ }
5420
+ log(` \u2713 ${name}`);
5421
+ return value;
5422
+ } catch (e) {
5423
+ const msg = e instanceof Error ? e.message : String(e);
5424
+ calls[name] = { ok: false, error: msg };
5425
+ errors.push(`${name}: ${msg}`);
5426
+ log(` \u2717 ${name}: ${msg}`);
5427
+ return void 0;
5428
+ }
5429
+ }
5430
+ const info = await capture(
5431
+ "getInfo",
5432
+ () => api.getInfo(channel),
5433
+ (v) => writeJson(path4.join(outDir, "device-info.json"), v)
5434
+ );
5435
+ const support = await capture(
5436
+ "getSupportInfo",
5437
+ () => api.getSupportInfo(),
5438
+ (v) => writeJson(path4.join(outDir, "support-info.json"), v)
5439
+ );
5440
+ const abilities = await capture(
5441
+ "getAbilityInfo",
5442
+ () => api.getAbilityInfo(),
5443
+ (v) => writeJson(path4.join(outDir, "ability-info.json"), v)
5444
+ );
5445
+ await capture(
5446
+ "getDeviceCapabilities",
5447
+ () => api.getDeviceCapabilities(channel),
5448
+ (v) => writeJson(path4.join(outDir, "capabilities.json"), v)
5449
+ );
5450
+ await capture("cmd289-WhiteLed", () => api.sendXml({
5451
+ cmdId: BC_CMD_ID_GET_WHITE_LED,
5452
+ channel,
5453
+ timeoutMs: 3e3
5454
+ }), (v) => writeText(path4.join(outDir, "cmd289-white-led.xml"), v));
5455
+ await capture(
5456
+ "getStreamMetadata",
5457
+ () => api.getStreamMetadata(channel),
5458
+ (v) => writeJson(path4.join(outDir, "stream-metadata.json"), v)
5459
+ );
5460
+ await capture(
5461
+ "getEncXml",
5462
+ () => api.getEncXml(channel),
5463
+ (v) => writeText(path4.join(outDir, "enc-config.xml"), v)
5464
+ );
5465
+ await capture(
5466
+ "getPorts",
5467
+ () => api.getPorts(),
5468
+ (v) => writeJson(path4.join(outDir, "ports.json"), v)
5469
+ );
5470
+ await capture(
5471
+ "getTalkAbility",
5472
+ () => api.getTalkAbility(channel),
5473
+ (v) => writeJson(path4.join(outDir, "talk-ability.json"), v)
5474
+ );
5475
+ await capture(
5476
+ "getTwoWayAudioConfig",
5477
+ () => api.getTwoWayAudioConfig(channel),
5478
+ (v) => writeJson(path4.join(outDir, "two-way-audio-config.json"), v)
5479
+ );
5480
+ await capture(
5481
+ "getAiState",
5482
+ () => api.getAiState(channel),
5483
+ (v) => writeJson(path4.join(outDir, "ai-state.json"), v)
5484
+ );
5485
+ await capture(
5486
+ "getAiCfg",
5487
+ () => api.getAiCfg(channel),
5488
+ (v) => writeJson(path4.join(outDir, "ai-cfg.json"), v)
5489
+ );
5490
+ await capture(
5491
+ "getOsd",
5492
+ () => api.getOsd(channel),
5493
+ (v) => writeJson(path4.join(outDir, "osd.json"), v)
5494
+ );
5495
+ await capture(
5496
+ "getMotionAlarm",
5497
+ () => api.getMotionAlarm(channel),
5498
+ (v) => writeJson(path4.join(outDir, "motion-alarm.json"), v)
5499
+ );
5500
+ await capture(
5501
+ "getRecordCfg",
5502
+ () => api.getRecordCfg(channel),
5503
+ (v) => writeJson(path4.join(outDir, "record-cfg.json"), v)
5504
+ );
5505
+ await capture(
5506
+ "getVideoInput",
5507
+ () => api.getVideoInput(channel),
5508
+ (v) => writeJson(path4.join(outDir, "video-input.json"), v)
5509
+ );
5510
+ await capture(
5511
+ "getPtzPresets",
5512
+ () => api.getPtzPresets(channel),
5513
+ (v) => writeJson(path4.join(outDir, "ptz-presets.json"), v)
5514
+ );
5515
+ await capture(
5516
+ "getNetworkInfo",
5517
+ () => api.getNetworkInfo(),
5518
+ (v) => writeJson(path4.join(outDir, "network-info.json"), v)
5519
+ );
5520
+ await capture(
5521
+ "getSystemGeneral",
5522
+ () => api.getSystemGeneral(),
5523
+ (v) => writeJson(path4.join(outDir, "system-general.json"), v)
5524
+ );
5525
+ await capture(
5526
+ "getWifiSignal",
5527
+ () => api.getWifiSignal(channel),
5528
+ (v) => writeJson(path4.join(outDir, "wifi-signal.json"), v)
5529
+ );
5530
+ await capture(
5531
+ "getWhiteLedState",
5532
+ () => api.getWhiteLedState(channel),
5533
+ (v) => writeJson(path4.join(outDir, "white-led-state.json"), v)
5534
+ );
5535
+ await capture(
5536
+ "getFloodlightOnMotion",
5537
+ () => api.getFloodlightOnMotion(channel),
5538
+ (v) => writeJson(path4.join(outDir, "floodlight-on-motion.json"), v)
5539
+ );
5540
+ await capture(
5541
+ "buildVideoStreamOptions",
5542
+ () => api.buildVideoStreamOptions({ channel }),
5543
+ (v) => writeJson(path4.join(outDir, "video-stream-options.json"), v)
5544
+ );
5545
+ const total = Object.keys(calls).length;
5546
+ const ok = Object.values(calls).filter((c) => c.ok).length;
5547
+ const failed = total - ok;
5548
+ const summary = { total, ok, failed, errors };
5549
+ writeJson(path4.join(outDir, "_summary.json"), {
5550
+ collectedAt: (/* @__PURE__ */ new Date()).toISOString(),
5551
+ model: info?.type ?? "unknown",
5552
+ itemNo: info?.itemNo ?? "unknown",
5553
+ firmwareVersion: info?.firmwareVersion ?? "unknown",
5554
+ channel,
5555
+ ...summary,
5556
+ calls: Object.fromEntries(
5557
+ Object.entries(calls).map(([k, v]) => [
5558
+ k,
5559
+ v.ok ? "ok" : `FAILED: ${v.error}`
5560
+ ])
5561
+ )
5562
+ });
5563
+ log(`
5564
+ Summary: ${ok}/${total} ok, ${failed} failed`);
5565
+ if (errors.length) {
5566
+ log(` Errors:`);
5567
+ for (const err of errors) {
5568
+ log(` - ${err}`);
5569
+ }
5570
+ }
5571
+ return { calls, outDir, summary };
5572
+ }
5402
5573
  var fs4, path4, import_node_child_process, import_node_path;
5403
5574
  var init_DiagnosticsTools = __esm({
5404
5575
  "src/debug/DiagnosticsTools.ts"() {
@@ -7444,6 +7615,7 @@ __export(index_exports, {
7444
7615
  DUAL_LENS_DUAL_MOTION_MODELS: () => DUAL_LENS_DUAL_MOTION_MODELS,
7445
7616
  DUAL_LENS_MODELS: () => DUAL_LENS_MODELS,
7446
7617
  DUAL_LENS_SINGLE_MOTION_MODELS: () => DUAL_LENS_SINGLE_MOTION_MODELS,
7618
+ Go2rtcTcpServer: () => Go2rtcTcpServer,
7447
7619
  H264RtpDepacketizer: () => H264RtpDepacketizer,
7448
7620
  H265RtpDepacketizer: () => H265RtpDepacketizer,
7449
7621
  HlsSessionManager: () => HlsSessionManager,
@@ -7485,6 +7657,7 @@ __export(index_exports, {
7485
7657
  buildSirenTimesXml: () => buildSirenTimesXml,
7486
7658
  buildStartZoomFocusXml: () => buildStartZoomFocusXml,
7487
7659
  buildWhiteLedStateXml: () => buildWhiteLedStateXml,
7660
+ captureModelFixtures: () => captureModelFixtures,
7488
7661
  collectCgiDiagnostics: () => collectCgiDiagnostics,
7489
7662
  collectMultifocalDiagnostics: () => collectMultifocalDiagnostics,
7490
7663
  collectNativeDiagnostics: () => collectNativeDiagnostics,
@@ -7566,6 +7739,7 @@ __export(index_exports, {
7566
7739
  splitH265AnnexBToNalPayloads: () => splitAnnexBToNalPayloads2,
7567
7740
  testChannelStreams: () => testChannelStreams,
7568
7741
  xmlEscape: () => xmlEscape,
7742
+ xmlIndicatesFloodlight: () => xmlIndicatesFloodlight,
7569
7743
  zipDirectory: () => zipDirectory
7570
7744
  });
7571
7745
  module.exports = __toCommonJS(index_exports);
@@ -9261,6 +9435,22 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
9261
9435
  static coverPreviewBackoffMs = /* @__PURE__ */ new Map();
9262
9436
  static COVER_PREVIEW_INITIAL_BACKOFF_MS = 1e3;
9263
9437
  static COVER_PREVIEW_MAX_BACKOFF_MS = 3e4;
9438
+ /**
9439
+ * Per-client snapshot (cmd_id=109) serialization queue.
9440
+ *
9441
+ * WHY: On NVR/multi-camera devices sharing one socket, concurrent snapshot requests
9442
+ * can cause JPEG data to mix (even with per-request msgNum filtering):
9443
+ * - Camera A and B both send frames on same socket
9444
+ * - Frame listener is global per socket
9445
+ * - Timing quirks can cause chunk reordering or listener confusion
9446
+ *
9447
+ * FIX: Serialize all cmd_id=109 requests on THIS client instance.
9448
+ * Each snapshot waits for previous one to complete before starting.
9449
+ * This ensures clean frame sequences per request, zero data corruption.
9450
+ *
9451
+ * Impact: Snapshots are ~0–50ms slower per camera (negligible for users).
9452
+ */
9453
+ snapshotQueueTail = Promise.resolve();
9264
9454
  opts;
9265
9455
  debugCfg;
9266
9456
  logger;
@@ -11742,6 +11932,20 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
11742
11932
  });
11743
11933
  }
11744
11934
  async sendBinarySnapshot109(params) {
11935
+ const prevTail = this.snapshotQueueTail;
11936
+ let resolve;
11937
+ const newTail = new Promise((r) => {
11938
+ resolve = r;
11939
+ });
11940
+ this.snapshotQueueTail = newTail;
11941
+ try {
11942
+ await prevTail;
11943
+ return await this.sendBinarySnapshot109Impl(params);
11944
+ } finally {
11945
+ resolve();
11946
+ }
11947
+ }
11948
+ async sendBinarySnapshot109Impl(params) {
11745
11949
  await this.connect();
11746
11950
  const channel = params.channel ?? this.opts.channel ?? 0;
11747
11951
  const channelId = params.channelIdOverride ?? (params.channel == null ? this.hostChannelId : channel + 1);
@@ -11801,7 +12005,8 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
11801
12005
  };
11802
12006
  const onFrame = (frame) => {
11803
12007
  if (frame.header.cmdId !== cmdId) return;
11804
- if (frame.header.msgNum === msgNum && frame.header.responseCode >= 400) {
12008
+ if (frame.header.msgNum !== msgNum) return;
12009
+ if (frame.header.responseCode >= 400) {
11805
12010
  fail(
11806
12011
  new Error(
11807
12012
  `Baichuan snapshot request rejected (cmdId=${cmdId} msgNum=${msgNum} responseCode=${frame.header.responseCode})`
@@ -15285,6 +15490,17 @@ function computeDeviceCapabilities(params) {
15285
15490
  if (ptzMode !== void 0) result.ptzMode = ptzMode;
15286
15491
  return result;
15287
15492
  }
15493
+ function xmlIndicatesFloodlight(xml) {
15494
+ if (/(<FloodlightTask\b|<FloodlightManual\b|<FloodlightStatusList\b)/i.test(
15495
+ xml
15496
+ )) {
15497
+ return true;
15498
+ }
15499
+ if (/<WhiteLed\b/i.test(xml)) {
15500
+ return /(<brightness_cur>|<bright>|<LightingSchedule\b)/i.test(xml);
15501
+ }
15502
+ return false;
15503
+ }
15288
15504
 
15289
15505
  // src/reolink/baichuan/utils/abilityInfo.ts
15290
15506
  init_xml();
@@ -24321,9 +24537,7 @@ ${stderr}`)
24321
24537
  `probeFloodlightSupportByCmd289: received XML for channel ${ch}:
24322
24538
  ${xml}`
24323
24539
  );
24324
- return /(<FloodlightTask\b|<FloodlightManual\b|<FloodlightStatusList\b|<WhiteLed\b)/i.test(
24325
- xml
24326
- );
24540
+ return xmlIndicatesFloodlight(xml);
24327
24541
  } catch {
24328
24542
  return false;
24329
24543
  }
@@ -32724,8 +32938,529 @@ async function createReplayHttpServer(options) {
32724
32938
  // src/index.ts
32725
32939
  init_BaichuanVideoStream();
32726
32940
 
32727
- // src/baichuan/stream/BaichuanHttpStreamServer.ts
32941
+ // src/baichuan/stream/Go2rtcTcpServer.ts
32728
32942
  var import_node_events6 = require("events");
32943
+ var net4 = __toESM(require("net"), 1);
32944
+ init_H264Converter();
32945
+ init_H265Converter();
32946
+ var AsyncBoundedQueue2 = class {
32947
+ maxItems;
32948
+ queue = [];
32949
+ waiting;
32950
+ closed = false;
32951
+ constructor(maxItems) {
32952
+ this.maxItems = Math.max(1, maxItems | 0);
32953
+ }
32954
+ push(item) {
32955
+ if (this.closed) return;
32956
+ if (this.waiting) {
32957
+ const { resolve } = this.waiting;
32958
+ this.waiting = void 0;
32959
+ resolve({ value: item, done: false });
32960
+ return;
32961
+ }
32962
+ this.queue.push(item);
32963
+ if (this.queue.length > this.maxItems) {
32964
+ this.queue.splice(0, this.queue.length - this.maxItems);
32965
+ }
32966
+ }
32967
+ close() {
32968
+ if (this.closed) return;
32969
+ this.closed = true;
32970
+ if (this.waiting) {
32971
+ const { resolve } = this.waiting;
32972
+ this.waiting = void 0;
32973
+ resolve({ value: void 0, done: true });
32974
+ }
32975
+ }
32976
+ async next() {
32977
+ if (this.closed) return { value: void 0, done: true };
32978
+ const item = this.queue.shift();
32979
+ if (item !== void 0) return { value: item, done: false };
32980
+ return await new Promise((resolve) => {
32981
+ this.waiting = { resolve };
32982
+ });
32983
+ }
32984
+ };
32985
+ var NativeStreamFanout2 = class {
32986
+ opts;
32987
+ queues = /* @__PURE__ */ new Map();
32988
+ source = null;
32989
+ running = false;
32990
+ pumpPromise = null;
32991
+ constructor(opts) {
32992
+ this.opts = opts;
32993
+ }
32994
+ start() {
32995
+ if (this.running) return;
32996
+ this.running = true;
32997
+ this.source = this.opts.createSource();
32998
+ this.pumpPromise = (async () => {
32999
+ try {
33000
+ for await (const frame of this.source) {
33001
+ try {
33002
+ this.opts.onFrame?.(frame);
33003
+ } catch {
33004
+ }
33005
+ for (const q of this.queues.values()) {
33006
+ q.push(frame);
33007
+ }
33008
+ }
33009
+ } catch (e) {
33010
+ this.opts.onError?.(e);
33011
+ } finally {
33012
+ for (const q of this.queues.values()) q.close();
33013
+ this.queues.clear();
33014
+ this.running = false;
33015
+ this.opts.onEnd?.();
33016
+ }
33017
+ })();
33018
+ }
33019
+ subscribe(id) {
33020
+ const q = new AsyncBoundedQueue2(this.opts.maxQueueItems);
33021
+ this.queues.set(id, q);
33022
+ const self = this;
33023
+ return (async function* () {
33024
+ try {
33025
+ while (true) {
33026
+ const r = await q.next();
33027
+ if (r.done) return;
33028
+ yield r.value;
33029
+ }
33030
+ } finally {
33031
+ q.close();
33032
+ self.queues.delete(id);
33033
+ }
33034
+ })();
33035
+ }
33036
+ async stop() {
33037
+ if (!this.running) return;
33038
+ this.running = false;
33039
+ const src = this.source;
33040
+ this.source = null;
33041
+ for (const q of this.queues.values()) q.close();
33042
+ this.queues.clear();
33043
+ try {
33044
+ await src?.return(void 0);
33045
+ } catch {
33046
+ }
33047
+ try {
33048
+ await this.pumpPromise;
33049
+ } catch {
33050
+ }
33051
+ this.pumpPromise = null;
33052
+ }
33053
+ };
33054
+ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEmitter {
33055
+ api;
33056
+ channel;
33057
+ profile;
33058
+ variant;
33059
+ listenHost;
33060
+ listenPort;
33061
+ logger;
33062
+ deviceId;
33063
+ gracePeriodMs;
33064
+ prebufferMaxMs;
33065
+ maxBufferBytes;
33066
+ prestartStream;
33067
+ active = false;
33068
+ server;
33069
+ resolvedPort;
33070
+ // Native stream
33071
+ nativeFanout = null;
33072
+ nativeStreamActive = false;
33073
+ dedicatedSessionRelease;
33074
+ detectedVideoType;
33075
+ // Client tracking
33076
+ connectedClients = /* @__PURE__ */ new Set();
33077
+ clientSockets = /* @__PURE__ */ new Map();
33078
+ stopGraceTimer;
33079
+ // Prebuffer
33080
+ prebuffer = [];
33081
+ constructor(options) {
33082
+ super();
33083
+ this.api = options.api;
33084
+ this.channel = options.channel;
33085
+ this.profile = options.profile;
33086
+ this.variant = options.variant ?? "default";
33087
+ this.listenHost = options.listenHost ?? "127.0.0.1";
33088
+ this.listenPort = options.listenPort ?? 0;
33089
+ this.logger = options.logger ?? console;
33090
+ this.deviceId = options.deviceId;
33091
+ this.gracePeriodMs = options.gracePeriodMs ?? 3e4;
33092
+ this.prebufferMaxMs = options.prebufferMs ?? 3e3;
33093
+ this.maxBufferBytes = options.maxBufferBytes ?? 1e8;
33094
+ this.prestartStream = options.prestartStream ?? true;
33095
+ }
33096
+ // -----------------------------------------------------------------------
33097
+ // Public API
33098
+ // -----------------------------------------------------------------------
33099
+ /** Start listening. Resolves once the TCP server is bound. */
33100
+ async start() {
33101
+ if (this.active) return;
33102
+ this.active = true;
33103
+ this.server = net4.createServer((socket) => this.handleClient(socket));
33104
+ this.server.on("error", (err) => {
33105
+ this.logger.error?.(`[Go2rtcTcpServer] server error: ${err.message}`);
33106
+ this.emit("error", err);
33107
+ });
33108
+ await new Promise((resolve, reject) => {
33109
+ this.server.listen(this.listenPort, this.listenHost, () => {
33110
+ const addr = this.server.address();
33111
+ this.resolvedPort = addr.port;
33112
+ this.logger.info?.(
33113
+ `[Go2rtcTcpServer] listening on ${addr.address}:${addr.port} channel=${this.channel} profile=${this.profile}`
33114
+ );
33115
+ this.emit("listening", { host: addr.address, port: addr.port });
33116
+ resolve();
33117
+ });
33118
+ this.server.once("error", reject);
33119
+ });
33120
+ if (this.prestartStream) {
33121
+ this.logger.info?.(
33122
+ `[Go2rtcTcpServer] pre-starting native stream channel=${this.channel} profile=${this.profile}`
33123
+ );
33124
+ this.startNativeStream();
33125
+ }
33126
+ }
33127
+ /** Stop the server and all active streams. */
33128
+ async stop() {
33129
+ if (!this.active) return;
33130
+ this.active = false;
33131
+ clearTimeout(this.stopGraceTimer);
33132
+ for (const [id, sock] of this.clientSockets) {
33133
+ sock.destroy();
33134
+ this.connectedClients.delete(id);
33135
+ }
33136
+ this.clientSockets.clear();
33137
+ await this.stopNativeStream();
33138
+ if (this.server) {
33139
+ await new Promise((resolve) => {
33140
+ this.server.close(() => resolve());
33141
+ });
33142
+ this.server = void 0;
33143
+ }
33144
+ this.prebuffer = [];
33145
+ this.resolvedPort = void 0;
33146
+ this.emit("close");
33147
+ }
33148
+ /** The actual port the server is listening on (available after start()). */
33149
+ get port() {
33150
+ return this.resolvedPort;
33151
+ }
33152
+ /** The go2rtc-compatible source URL. */
33153
+ get go2rtcSourceUrl() {
33154
+ if (this.resolvedPort == null) return void 0;
33155
+ return `tcp://127.0.0.1:${this.resolvedPort}`;
33156
+ }
33157
+ /** Number of currently connected clients. */
33158
+ get clientCount() {
33159
+ return this.connectedClients.size;
33160
+ }
33161
+ // -----------------------------------------------------------------------
33162
+ // Client handling
33163
+ // -----------------------------------------------------------------------
33164
+ handleClient(socket) {
33165
+ const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
33166
+ socket.setNoDelay(true);
33167
+ this.connectedClients.add(clientId);
33168
+ this.clientSockets.set(clientId, socket);
33169
+ this.logger.info?.(
33170
+ `[Go2rtcTcpServer] client connected id=${clientId} total=${this.connectedClients.size}`
33171
+ );
33172
+ this.emit("client", clientId);
33173
+ if (this.stopGraceTimer) {
33174
+ clearTimeout(this.stopGraceTimer);
33175
+ this.stopGraceTimer = void 0;
33176
+ }
33177
+ if (!this.nativeStreamActive) {
33178
+ this.startNativeStream();
33179
+ }
33180
+ this.feedClient(clientId, socket).catch((err) => {
33181
+ this.logger.warn?.(
33182
+ `[Go2rtcTcpServer] feedClient error id=${clientId}: ${err}`
33183
+ );
33184
+ });
33185
+ const cleanup = () => {
33186
+ this.removeClient(clientId);
33187
+ socket.destroy();
33188
+ };
33189
+ socket.on("error", cleanup);
33190
+ socket.on("close", cleanup);
33191
+ }
33192
+ async feedClient(clientId, socket) {
33193
+ const fanoutDeadline = Date.now() + 3e4;
33194
+ while (this.active && !this.nativeFanout) {
33195
+ if (socket.destroyed) return;
33196
+ if (Date.now() > fanoutDeadline) {
33197
+ this.logger.warn?.(
33198
+ `[Go2rtcTcpServer] fanout not ready after 30s, dropping client ${clientId}`
33199
+ );
33200
+ return;
33201
+ }
33202
+ await new Promise((r) => setTimeout(r, 100));
33203
+ }
33204
+ if (!this.active || !this.nativeFanout) return;
33205
+ const subscription = this.nativeFanout.subscribe(clientId);
33206
+ const prebufferSnap = this.prebuffer.slice();
33207
+ let lastIdrIdx = -1;
33208
+ for (let i = prebufferSnap.length - 1; i >= 0; i--) {
33209
+ if (prebufferSnap[i].isKeyframe) {
33210
+ lastIdrIdx = i;
33211
+ break;
33212
+ }
33213
+ }
33214
+ if (lastIdrIdx >= 0) {
33215
+ const replay = prebufferSnap.slice(lastIdrIdx);
33216
+ this.logger.info?.(
33217
+ `[Go2rtcTcpServer] prebuffer replay client=${clientId} frames=${replay.length}`
33218
+ );
33219
+ for (const entry of replay) {
33220
+ if (socket.destroyed) return;
33221
+ socket.write(entry.data);
33222
+ }
33223
+ }
33224
+ let seenKeyframe = lastIdrIdx >= 0;
33225
+ let liveFrameCount = 0;
33226
+ let liveVideoWritten = 0;
33227
+ let lastLogAt = Date.now();
33228
+ try {
33229
+ this.logger.info?.(
33230
+ `[Go2rtcTcpServer] entering live loop client=${clientId} seenKeyframe=${seenKeyframe}`
33231
+ );
33232
+ for await (const frame of subscription) {
33233
+ if (socket.destroyed || !this.active) {
33234
+ this.logger.info?.(
33235
+ `[Go2rtcTcpServer] live loop exit client=${clientId} destroyed=${socket.destroyed} active=${this.active}`
33236
+ );
33237
+ break;
33238
+ }
33239
+ liveFrameCount++;
33240
+ const annexB = this.convertFrame(frame);
33241
+ if (!annexB) continue;
33242
+ if (!seenKeyframe) {
33243
+ if (!this.isAnnexBKeyframe(annexB, frame.videoType)) continue;
33244
+ seenKeyframe = true;
33245
+ this.logger.info?.(
33246
+ `[Go2rtcTcpServer] first live keyframe client=${clientId} after ${liveFrameCount} frames`
33247
+ );
33248
+ }
33249
+ socket.write(annexB);
33250
+ liveVideoWritten++;
33251
+ if (Date.now() - lastLogAt > 1e4) {
33252
+ this.logger.info?.(
33253
+ `[Go2rtcTcpServer] live stats client=${clientId} received=${liveFrameCount} written=${liveVideoWritten} bufLen=${socket.writableLength}`
33254
+ );
33255
+ lastLogAt = Date.now();
33256
+ }
33257
+ if (socket.writableLength > this.maxBufferBytes) {
33258
+ this.logger.warn?.(
33259
+ `[Go2rtcTcpServer] buffer overflow (${socket.writableLength} bytes), dropping client ${clientId}`
33260
+ );
33261
+ socket.destroy();
33262
+ break;
33263
+ }
33264
+ }
33265
+ this.logger.info?.(
33266
+ `[Go2rtcTcpServer] live loop ended naturally client=${clientId} received=${liveFrameCount} written=${liveVideoWritten}`
33267
+ );
33268
+ } finally {
33269
+ await subscription.return(void 0).catch(() => {
33270
+ });
33271
+ }
33272
+ }
33273
+ // -----------------------------------------------------------------------
33274
+ // Frame conversion
33275
+ // -----------------------------------------------------------------------
33276
+ /**
33277
+ * Convert a native frame to wire-ready Annex-B.
33278
+ * Audio frames are skipped — raw TCP carries only video (Annex-B).
33279
+ * go2rtc auto-detects the codec from SPS/PPS/VPS NALUs.
33280
+ */
33281
+ convertFrame(frame) {
33282
+ if (frame.audio) {
33283
+ return null;
33284
+ }
33285
+ if (frame.data.length === 0) return null;
33286
+ try {
33287
+ if (frame.videoType === "H264") {
33288
+ return convertToAnnexB(frame.data);
33289
+ }
33290
+ if (frame.videoType === "H265") {
33291
+ return convertToAnnexB2(frame.data);
33292
+ }
33293
+ } catch {
33294
+ }
33295
+ return frame.data;
33296
+ }
33297
+ /** Check if an Annex-B buffer contains a keyframe (IDR for H.264, IRAP for H.265). */
33298
+ isAnnexBKeyframe(annexB, videoType) {
33299
+ try {
33300
+ if (videoType === "H264") {
33301
+ const nals = _Go2rtcTcpServer.splitAnnexBNals(annexB);
33302
+ return nals.some((n) => n.length >= 1 && (n[0] & 31) === 5);
33303
+ }
33304
+ if (videoType === "H265") {
33305
+ const nals = splitAnnexBToNalPayloads2(annexB);
33306
+ return nals.some(
33307
+ (n) => n.length >= 2 && isH265Irap(n[0] >> 1 & 63)
33308
+ );
33309
+ }
33310
+ } catch {
33311
+ }
33312
+ return false;
33313
+ }
33314
+ /** Split Annex-B byte stream into individual NAL units. */
33315
+ static splitAnnexBNals(buf) {
33316
+ const nals = [];
33317
+ let i = 0;
33318
+ while (i < buf.length) {
33319
+ if (i + 2 < buf.length && buf[i] === 0 && buf[i + 1] === 0) {
33320
+ let scLen;
33321
+ if (buf[i + 2] === 1) {
33322
+ scLen = 3;
33323
+ } else if (i + 3 < buf.length && buf[i + 2] === 0 && buf[i + 3] === 1) {
33324
+ scLen = 4;
33325
+ } else {
33326
+ i++;
33327
+ continue;
33328
+ }
33329
+ const nalStart = i + scLen;
33330
+ let nalEnd = buf.length;
33331
+ for (let j = nalStart; j < buf.length - 2; j++) {
33332
+ if (buf[j] === 0 && buf[j + 1] === 0 && (buf[j + 2] === 1 || j + 3 < buf.length && buf[j + 2] === 0 && buf[j + 3] === 1)) {
33333
+ nalEnd = j;
33334
+ break;
33335
+ }
33336
+ }
33337
+ if (nalEnd > nalStart) {
33338
+ nals.push(buf.subarray(nalStart, nalEnd));
33339
+ }
33340
+ i = nalEnd;
33341
+ } else {
33342
+ i++;
33343
+ }
33344
+ }
33345
+ return nals;
33346
+ }
33347
+ // -----------------------------------------------------------------------
33348
+ // Native stream management
33349
+ // -----------------------------------------------------------------------
33350
+ async startNativeStream() {
33351
+ if (this.nativeStreamActive) return;
33352
+ this.nativeStreamActive = true;
33353
+ let dedicatedClient;
33354
+ if (this.deviceId) {
33355
+ try {
33356
+ const session = await this.api.createDedicatedSession(
33357
+ `live:${this.deviceId}:ch${this.channel}:${this.profile}`
33358
+ );
33359
+ dedicatedClient = session.client;
33360
+ this.dedicatedSessionRelease = session.release;
33361
+ } catch (e) {
33362
+ this.logger.warn?.(
33363
+ `[Go2rtcTcpServer] failed to acquire dedicated session, using shared socket: ${e}`
33364
+ );
33365
+ }
33366
+ }
33367
+ this.logger.info?.(
33368
+ `[Go2rtcTcpServer] native stream starting channel=${this.channel} profile=${this.profile} dedicated=${!!dedicatedClient}`
33369
+ );
33370
+ this.nativeFanout = new NativeStreamFanout2({
33371
+ maxQueueItems: 200,
33372
+ createSource: () => createNativeStream(this.api, this.channel, this.profile, {
33373
+ variant: this.variant,
33374
+ ...dedicatedClient ? { client: dedicatedClient } : {}
33375
+ }),
33376
+ onFrame: (frame) => {
33377
+ if (!frame.audio && (frame.videoType === "H264" || frame.videoType === "H265")) {
33378
+ this.detectedVideoType = frame.videoType;
33379
+ }
33380
+ const wireData = this.convertFrame(frame);
33381
+ if (!wireData || wireData.length === 0) return;
33382
+ const isKeyframe = !frame.audio && this.isAnnexBKeyframe(wireData, frame.videoType);
33383
+ this.prebuffer.push({
33384
+ data: Buffer.from(wireData),
33385
+ time: Date.now(),
33386
+ isKeyframe,
33387
+ audio: frame.audio
33388
+ });
33389
+ const cutoff = Date.now() - this.prebufferMaxMs;
33390
+ let trimIdx = 0;
33391
+ while (trimIdx < this.prebuffer.length && this.prebuffer[trimIdx].time < cutoff) {
33392
+ trimIdx++;
33393
+ }
33394
+ if (trimIdx > 0) this.prebuffer.splice(0, trimIdx);
33395
+ },
33396
+ onError: (error) => {
33397
+ this.logger.warn?.(`[Go2rtcTcpServer] native stream error: ${error}`);
33398
+ },
33399
+ onEnd: () => {
33400
+ if (!this.nativeStreamActive) return;
33401
+ this.nativeStreamActive = false;
33402
+ this.nativeFanout = null;
33403
+ if (this.dedicatedSessionRelease) {
33404
+ this.dedicatedSessionRelease().catch(() => {
33405
+ });
33406
+ this.dedicatedSessionRelease = void 0;
33407
+ }
33408
+ if (this.active && (this.connectedClients.size > 0 || this.prestartStream)) {
33409
+ this.logger.info?.(
33410
+ `[Go2rtcTcpServer] native stream ended, restarting (clients=${this.connectedClients.size}, prestart=${this.prestartStream})`
33411
+ );
33412
+ this.startNativeStream();
33413
+ }
33414
+ }
33415
+ });
33416
+ this.nativeFanout.start();
33417
+ }
33418
+ async stopNativeStream() {
33419
+ this.nativeStreamActive = false;
33420
+ const fanout = this.nativeFanout;
33421
+ this.nativeFanout = null;
33422
+ if (fanout) {
33423
+ await fanout.stop();
33424
+ }
33425
+ this.prebuffer = [];
33426
+ if (this.dedicatedSessionRelease) {
33427
+ await this.dedicatedSessionRelease().catch(() => {
33428
+ });
33429
+ this.dedicatedSessionRelease = void 0;
33430
+ }
33431
+ }
33432
+ // -----------------------------------------------------------------------
33433
+ // Client lifecycle
33434
+ // -----------------------------------------------------------------------
33435
+ removeClient(clientId) {
33436
+ if (!this.connectedClients.has(clientId)) return;
33437
+ this.connectedClients.delete(clientId);
33438
+ this.clientSockets.delete(clientId);
33439
+ this.logger.info?.(
33440
+ `[Go2rtcTcpServer] client disconnected id=${clientId} remaining=${this.connectedClients.size}`
33441
+ );
33442
+ this.emit("clientDisconnected", clientId);
33443
+ if (this.connectedClients.size === 0 && !this.prestartStream) {
33444
+ this.scheduleStop();
33445
+ }
33446
+ }
33447
+ scheduleStop() {
33448
+ if (this.stopGraceTimer) return;
33449
+ this.logger.info?.(
33450
+ `[Go2rtcTcpServer] no clients, scheduling stream stop in ${this.gracePeriodMs}ms`
33451
+ );
33452
+ this.stopGraceTimer = setTimeout(async () => {
33453
+ this.stopGraceTimer = void 0;
33454
+ if (this.connectedClients.size === 0 && this.nativeStreamActive) {
33455
+ this.logger.info?.("[Go2rtcTcpServer] grace period expired, stopping native stream");
33456
+ await this.stopNativeStream();
33457
+ }
33458
+ }, this.gracePeriodMs);
33459
+ }
33460
+ };
33461
+
33462
+ // src/baichuan/stream/BaichuanHttpStreamServer.ts
33463
+ var import_node_events7 = require("events");
32729
33464
  var import_node_child_process9 = require("child_process");
32730
33465
  var http4 = __toESM(require("http"), 1);
32731
33466
  var NAL_START_CODE_4B4 = Buffer.from([0, 0, 0, 1]);
@@ -32772,7 +33507,7 @@ function isH264KeyframeFromAnnexB(annexB) {
32772
33507
  }
32773
33508
  return false;
32774
33509
  }
32775
- var BaichuanHttpStreamServer = class extends import_node_events6.EventEmitter {
33510
+ var BaichuanHttpStreamServer = class extends import_node_events7.EventEmitter {
32776
33511
  videoStream;
32777
33512
  listenPort;
32778
33513
  path;
@@ -33039,15 +33774,15 @@ var BaichuanHttpStreamServer = class extends import_node_events6.EventEmitter {
33039
33774
  };
33040
33775
 
33041
33776
  // src/baichuan/stream/BaichuanMjpegServer.ts
33042
- var import_node_events8 = require("events");
33777
+ var import_node_events9 = require("events");
33043
33778
  var http5 = __toESM(require("http"), 1);
33044
33779
 
33045
33780
  // src/baichuan/stream/MjpegTransformer.ts
33046
- var import_node_events7 = require("events");
33781
+ var import_node_events8 = require("events");
33047
33782
  var import_node_child_process10 = require("child_process");
33048
33783
  var JPEG_SOI = Buffer.from([255, 216]);
33049
33784
  var JPEG_EOI = Buffer.from([255, 217]);
33050
- var MjpegTransformer = class extends import_node_events7.EventEmitter {
33785
+ var MjpegTransformer = class extends import_node_events8.EventEmitter {
33051
33786
  options;
33052
33787
  ffmpeg = null;
33053
33788
  started = false;
@@ -33246,7 +33981,7 @@ Content-Length: ${frame.length}\r
33246
33981
  // src/baichuan/stream/BaichuanMjpegServer.ts
33247
33982
  init_H264Converter();
33248
33983
  init_H265Converter();
33249
- var BaichuanMjpegServer = class extends import_node_events8.EventEmitter {
33984
+ var BaichuanMjpegServer = class extends import_node_events9.EventEmitter {
33250
33985
  options;
33251
33986
  clients = /* @__PURE__ */ new Map();
33252
33987
  httpServer = null;
@@ -33527,7 +34262,7 @@ var BaichuanMjpegServer = class extends import_node_events8.EventEmitter {
33527
34262
  };
33528
34263
 
33529
34264
  // src/baichuan/stream/BaichuanWebRTCServer.ts
33530
- var import_node_events9 = require("events");
34265
+ var import_node_events10 = require("events");
33531
34266
  init_BcMediaAnnexBDecoder();
33532
34267
  init_H264Converter();
33533
34268
  function parseAnnexBNalUnits(annexB) {
@@ -33564,7 +34299,7 @@ function getH264NalType(nalUnit) {
33564
34299
  function getH265NalType2(nalUnit) {
33565
34300
  return nalUnit[0] >> 1 & 63;
33566
34301
  }
33567
- var BaichuanWebRTCServer = class extends import_node_events9.EventEmitter {
34302
+ var BaichuanWebRTCServer = class extends import_node_events10.EventEmitter {
33568
34303
  options;
33569
34304
  sessions = /* @__PURE__ */ new Map();
33570
34305
  sessionIdCounter = 0;
@@ -34466,7 +35201,7 @@ Error: ${err}`
34466
35201
  };
34467
35202
 
34468
35203
  // src/baichuan/stream/BaichuanHlsServer.ts
34469
- var import_node_events10 = require("events");
35204
+ var import_node_events11 = require("events");
34470
35205
  var import_node_fs = __toESM(require("fs"), 1);
34471
35206
  var import_promises3 = __toESM(require("fs/promises"), 1);
34472
35207
  var import_node_os3 = __toESM(require("os"), 1);
@@ -34546,7 +35281,7 @@ function getNalTypes(codec, annexB) {
34546
35281
  }
34547
35282
  });
34548
35283
  }
34549
- var BaichuanHlsServer = class extends import_node_events10.EventEmitter {
35284
+ var BaichuanHlsServer = class extends import_node_events11.EventEmitter {
34550
35285
  api;
34551
35286
  channel;
34552
35287
  profile;
@@ -35547,10 +36282,10 @@ async function autoDetectDeviceType(inputs) {
35547
36282
  }
35548
36283
 
35549
36284
  // src/multifocal/compositeRtspServer.ts
35550
- var import_node_events11 = require("events");
36285
+ var import_node_events12 = require("events");
35551
36286
  var import_node_child_process12 = require("child_process");
35552
- var net4 = __toESM(require("net"), 1);
35553
- var CompositeRtspServer = class extends import_node_events11.EventEmitter {
36287
+ var net5 = __toESM(require("net"), 1);
36288
+ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
35554
36289
  options;
35555
36290
  compositeStream = null;
35556
36291
  rtspServer = null;
@@ -35616,7 +36351,7 @@ var CompositeRtspServer = class extends import_node_events11.EventEmitter {
35616
36351
  const width = widerStreamInfo?.width ?? 1920;
35617
36352
  const height = widerStreamInfo?.height ?? 1080;
35618
36353
  const fps = widerStreamInfo?.frameRate ?? 25;
35619
- this.rtspServer = net4.createServer((socket) => {
36354
+ this.rtspServer = net5.createServer((socket) => {
35620
36355
  this.handleRtspConnection(socket);
35621
36356
  });
35622
36357
  await new Promise((resolve, reject) => {
@@ -35892,6 +36627,7 @@ var CompositeRtspServer = class extends import_node_events11.EventEmitter {
35892
36627
  DUAL_LENS_DUAL_MOTION_MODELS,
35893
36628
  DUAL_LENS_MODELS,
35894
36629
  DUAL_LENS_SINGLE_MOTION_MODELS,
36630
+ Go2rtcTcpServer,
35895
36631
  H264RtpDepacketizer,
35896
36632
  H265RtpDepacketizer,
35897
36633
  HlsSessionManager,
@@ -35933,6 +36669,7 @@ var CompositeRtspServer = class extends import_node_events11.EventEmitter {
35933
36669
  buildSirenTimesXml,
35934
36670
  buildStartZoomFocusXml,
35935
36671
  buildWhiteLedStateXml,
36672
+ captureModelFixtures,
35936
36673
  collectCgiDiagnostics,
35937
36674
  collectMultifocalDiagnostics,
35938
36675
  collectNativeDiagnostics,
@@ -36014,6 +36751,7 @@ var CompositeRtspServer = class extends import_node_events11.EventEmitter {
36014
36751
  splitH265AnnexBToNalPayloads,
36015
36752
  testChannelStreams,
36016
36753
  xmlEscape,
36754
+ xmlIndicatesFloodlight,
36017
36755
  zipDirectory
36018
36756
  });
36019
36757
  //# sourceMappingURL=index.cjs.map