@apocaliss92/nodelink-js 0.3.4 → 0.3.5

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.
@@ -3,7 +3,7 @@ import {
3
3
  BaichuanRtspServer,
4
4
  ReolinkBaichuanApi,
5
5
  autoDetectDeviceType
6
- } from "../chunk-YSEFEQYV.js";
6
+ } from "../chunk-UDS2UR4S.js";
7
7
  import {
8
8
  __require
9
9
  } from "../chunk-APEEZ4UN.js";
package/dist/index.cjs CHANGED
@@ -7444,6 +7444,7 @@ __export(index_exports, {
7444
7444
  DUAL_LENS_DUAL_MOTION_MODELS: () => DUAL_LENS_DUAL_MOTION_MODELS,
7445
7445
  DUAL_LENS_MODELS: () => DUAL_LENS_MODELS,
7446
7446
  DUAL_LENS_SINGLE_MOTION_MODELS: () => DUAL_LENS_SINGLE_MOTION_MODELS,
7447
+ Go2rtcTcpServer: () => Go2rtcTcpServer,
7447
7448
  H264RtpDepacketizer: () => H264RtpDepacketizer,
7448
7449
  H265RtpDepacketizer: () => H265RtpDepacketizer,
7449
7450
  HlsSessionManager: () => HlsSessionManager,
@@ -9261,6 +9262,22 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
9261
9262
  static coverPreviewBackoffMs = /* @__PURE__ */ new Map();
9262
9263
  static COVER_PREVIEW_INITIAL_BACKOFF_MS = 1e3;
9263
9264
  static COVER_PREVIEW_MAX_BACKOFF_MS = 3e4;
9265
+ /**
9266
+ * Per-client snapshot (cmd_id=109) serialization queue.
9267
+ *
9268
+ * WHY: On NVR/multi-camera devices sharing one socket, concurrent snapshot requests
9269
+ * can cause JPEG data to mix (even with per-request msgNum filtering):
9270
+ * - Camera A and B both send frames on same socket
9271
+ * - Frame listener is global per socket
9272
+ * - Timing quirks can cause chunk reordering or listener confusion
9273
+ *
9274
+ * FIX: Serialize all cmd_id=109 requests on THIS client instance.
9275
+ * Each snapshot waits for previous one to complete before starting.
9276
+ * This ensures clean frame sequences per request, zero data corruption.
9277
+ *
9278
+ * Impact: Snapshots are ~0–50ms slower per camera (negligible for users).
9279
+ */
9280
+ snapshotQueueTail = Promise.resolve();
9264
9281
  opts;
9265
9282
  debugCfg;
9266
9283
  logger;
@@ -11742,6 +11759,20 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
11742
11759
  });
11743
11760
  }
11744
11761
  async sendBinarySnapshot109(params) {
11762
+ const prevTail = this.snapshotQueueTail;
11763
+ let resolve;
11764
+ const newTail = new Promise((r) => {
11765
+ resolve = r;
11766
+ });
11767
+ this.snapshotQueueTail = newTail;
11768
+ try {
11769
+ await prevTail;
11770
+ return await this.sendBinarySnapshot109Impl(params);
11771
+ } finally {
11772
+ resolve();
11773
+ }
11774
+ }
11775
+ async sendBinarySnapshot109Impl(params) {
11745
11776
  await this.connect();
11746
11777
  const channel = params.channel ?? this.opts.channel ?? 0;
11747
11778
  const channelId = params.channelIdOverride ?? (params.channel == null ? this.hostChannelId : channel + 1);
@@ -11801,7 +11832,8 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
11801
11832
  };
11802
11833
  const onFrame = (frame) => {
11803
11834
  if (frame.header.cmdId !== cmdId) return;
11804
- if (frame.header.msgNum === msgNum && frame.header.responseCode >= 400) {
11835
+ if (frame.header.msgNum !== msgNum) return;
11836
+ if (frame.header.responseCode >= 400) {
11805
11837
  fail(
11806
11838
  new Error(
11807
11839
  `Baichuan snapshot request rejected (cmdId=${cmdId} msgNum=${msgNum} responseCode=${frame.header.responseCode})`
@@ -32724,8 +32756,529 @@ async function createReplayHttpServer(options) {
32724
32756
  // src/index.ts
32725
32757
  init_BaichuanVideoStream();
32726
32758
 
32727
- // src/baichuan/stream/BaichuanHttpStreamServer.ts
32759
+ // src/baichuan/stream/Go2rtcTcpServer.ts
32728
32760
  var import_node_events6 = require("events");
32761
+ var net4 = __toESM(require("net"), 1);
32762
+ init_H264Converter();
32763
+ init_H265Converter();
32764
+ var AsyncBoundedQueue2 = class {
32765
+ maxItems;
32766
+ queue = [];
32767
+ waiting;
32768
+ closed = false;
32769
+ constructor(maxItems) {
32770
+ this.maxItems = Math.max(1, maxItems | 0);
32771
+ }
32772
+ push(item) {
32773
+ if (this.closed) return;
32774
+ if (this.waiting) {
32775
+ const { resolve } = this.waiting;
32776
+ this.waiting = void 0;
32777
+ resolve({ value: item, done: false });
32778
+ return;
32779
+ }
32780
+ this.queue.push(item);
32781
+ if (this.queue.length > this.maxItems) {
32782
+ this.queue.splice(0, this.queue.length - this.maxItems);
32783
+ }
32784
+ }
32785
+ close() {
32786
+ if (this.closed) return;
32787
+ this.closed = true;
32788
+ if (this.waiting) {
32789
+ const { resolve } = this.waiting;
32790
+ this.waiting = void 0;
32791
+ resolve({ value: void 0, done: true });
32792
+ }
32793
+ }
32794
+ async next() {
32795
+ if (this.closed) return { value: void 0, done: true };
32796
+ const item = this.queue.shift();
32797
+ if (item !== void 0) return { value: item, done: false };
32798
+ return await new Promise((resolve) => {
32799
+ this.waiting = { resolve };
32800
+ });
32801
+ }
32802
+ };
32803
+ var NativeStreamFanout2 = class {
32804
+ opts;
32805
+ queues = /* @__PURE__ */ new Map();
32806
+ source = null;
32807
+ running = false;
32808
+ pumpPromise = null;
32809
+ constructor(opts) {
32810
+ this.opts = opts;
32811
+ }
32812
+ start() {
32813
+ if (this.running) return;
32814
+ this.running = true;
32815
+ this.source = this.opts.createSource();
32816
+ this.pumpPromise = (async () => {
32817
+ try {
32818
+ for await (const frame of this.source) {
32819
+ try {
32820
+ this.opts.onFrame?.(frame);
32821
+ } catch {
32822
+ }
32823
+ for (const q of this.queues.values()) {
32824
+ q.push(frame);
32825
+ }
32826
+ }
32827
+ } catch (e) {
32828
+ this.opts.onError?.(e);
32829
+ } finally {
32830
+ for (const q of this.queues.values()) q.close();
32831
+ this.queues.clear();
32832
+ this.running = false;
32833
+ this.opts.onEnd?.();
32834
+ }
32835
+ })();
32836
+ }
32837
+ subscribe(id) {
32838
+ const q = new AsyncBoundedQueue2(this.opts.maxQueueItems);
32839
+ this.queues.set(id, q);
32840
+ const self = this;
32841
+ return (async function* () {
32842
+ try {
32843
+ while (true) {
32844
+ const r = await q.next();
32845
+ if (r.done) return;
32846
+ yield r.value;
32847
+ }
32848
+ } finally {
32849
+ q.close();
32850
+ self.queues.delete(id);
32851
+ }
32852
+ })();
32853
+ }
32854
+ async stop() {
32855
+ if (!this.running) return;
32856
+ this.running = false;
32857
+ const src = this.source;
32858
+ this.source = null;
32859
+ for (const q of this.queues.values()) q.close();
32860
+ this.queues.clear();
32861
+ try {
32862
+ await src?.return(void 0);
32863
+ } catch {
32864
+ }
32865
+ try {
32866
+ await this.pumpPromise;
32867
+ } catch {
32868
+ }
32869
+ this.pumpPromise = null;
32870
+ }
32871
+ };
32872
+ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEmitter {
32873
+ api;
32874
+ channel;
32875
+ profile;
32876
+ variant;
32877
+ listenHost;
32878
+ listenPort;
32879
+ logger;
32880
+ deviceId;
32881
+ gracePeriodMs;
32882
+ prebufferMaxMs;
32883
+ maxBufferBytes;
32884
+ prestartStream;
32885
+ active = false;
32886
+ server;
32887
+ resolvedPort;
32888
+ // Native stream
32889
+ nativeFanout = null;
32890
+ nativeStreamActive = false;
32891
+ dedicatedSessionRelease;
32892
+ detectedVideoType;
32893
+ // Client tracking
32894
+ connectedClients = /* @__PURE__ */ new Set();
32895
+ clientSockets = /* @__PURE__ */ new Map();
32896
+ stopGraceTimer;
32897
+ // Prebuffer
32898
+ prebuffer = [];
32899
+ constructor(options) {
32900
+ super();
32901
+ this.api = options.api;
32902
+ this.channel = options.channel;
32903
+ this.profile = options.profile;
32904
+ this.variant = options.variant ?? "default";
32905
+ this.listenHost = options.listenHost ?? "127.0.0.1";
32906
+ this.listenPort = options.listenPort ?? 0;
32907
+ this.logger = options.logger ?? console;
32908
+ this.deviceId = options.deviceId;
32909
+ this.gracePeriodMs = options.gracePeriodMs ?? 3e4;
32910
+ this.prebufferMaxMs = options.prebufferMs ?? 3e3;
32911
+ this.maxBufferBytes = options.maxBufferBytes ?? 1e8;
32912
+ this.prestartStream = options.prestartStream ?? true;
32913
+ }
32914
+ // -----------------------------------------------------------------------
32915
+ // Public API
32916
+ // -----------------------------------------------------------------------
32917
+ /** Start listening. Resolves once the TCP server is bound. */
32918
+ async start() {
32919
+ if (this.active) return;
32920
+ this.active = true;
32921
+ this.server = net4.createServer((socket) => this.handleClient(socket));
32922
+ this.server.on("error", (err) => {
32923
+ this.logger.error?.(`[Go2rtcTcpServer] server error: ${err.message}`);
32924
+ this.emit("error", err);
32925
+ });
32926
+ await new Promise((resolve, reject) => {
32927
+ this.server.listen(this.listenPort, this.listenHost, () => {
32928
+ const addr = this.server.address();
32929
+ this.resolvedPort = addr.port;
32930
+ this.logger.info?.(
32931
+ `[Go2rtcTcpServer] listening on ${addr.address}:${addr.port} channel=${this.channel} profile=${this.profile}`
32932
+ );
32933
+ this.emit("listening", { host: addr.address, port: addr.port });
32934
+ resolve();
32935
+ });
32936
+ this.server.once("error", reject);
32937
+ });
32938
+ if (this.prestartStream) {
32939
+ this.logger.info?.(
32940
+ `[Go2rtcTcpServer] pre-starting native stream channel=${this.channel} profile=${this.profile}`
32941
+ );
32942
+ this.startNativeStream();
32943
+ }
32944
+ }
32945
+ /** Stop the server and all active streams. */
32946
+ async stop() {
32947
+ if (!this.active) return;
32948
+ this.active = false;
32949
+ clearTimeout(this.stopGraceTimer);
32950
+ for (const [id, sock] of this.clientSockets) {
32951
+ sock.destroy();
32952
+ this.connectedClients.delete(id);
32953
+ }
32954
+ this.clientSockets.clear();
32955
+ await this.stopNativeStream();
32956
+ if (this.server) {
32957
+ await new Promise((resolve) => {
32958
+ this.server.close(() => resolve());
32959
+ });
32960
+ this.server = void 0;
32961
+ }
32962
+ this.prebuffer = [];
32963
+ this.resolvedPort = void 0;
32964
+ this.emit("close");
32965
+ }
32966
+ /** The actual port the server is listening on (available after start()). */
32967
+ get port() {
32968
+ return this.resolvedPort;
32969
+ }
32970
+ /** The go2rtc-compatible source URL. */
32971
+ get go2rtcSourceUrl() {
32972
+ if (this.resolvedPort == null) return void 0;
32973
+ return `tcp://127.0.0.1:${this.resolvedPort}`;
32974
+ }
32975
+ /** Number of currently connected clients. */
32976
+ get clientCount() {
32977
+ return this.connectedClients.size;
32978
+ }
32979
+ // -----------------------------------------------------------------------
32980
+ // Client handling
32981
+ // -----------------------------------------------------------------------
32982
+ handleClient(socket) {
32983
+ const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
32984
+ socket.setNoDelay(true);
32985
+ this.connectedClients.add(clientId);
32986
+ this.clientSockets.set(clientId, socket);
32987
+ this.logger.info?.(
32988
+ `[Go2rtcTcpServer] client connected id=${clientId} total=${this.connectedClients.size}`
32989
+ );
32990
+ this.emit("client", clientId);
32991
+ if (this.stopGraceTimer) {
32992
+ clearTimeout(this.stopGraceTimer);
32993
+ this.stopGraceTimer = void 0;
32994
+ }
32995
+ if (!this.nativeStreamActive) {
32996
+ this.startNativeStream();
32997
+ }
32998
+ this.feedClient(clientId, socket).catch((err) => {
32999
+ this.logger.warn?.(
33000
+ `[Go2rtcTcpServer] feedClient error id=${clientId}: ${err}`
33001
+ );
33002
+ });
33003
+ const cleanup = () => {
33004
+ this.removeClient(clientId);
33005
+ socket.destroy();
33006
+ };
33007
+ socket.on("error", cleanup);
33008
+ socket.on("close", cleanup);
33009
+ }
33010
+ async feedClient(clientId, socket) {
33011
+ const fanoutDeadline = Date.now() + 3e4;
33012
+ while (this.active && !this.nativeFanout) {
33013
+ if (socket.destroyed) return;
33014
+ if (Date.now() > fanoutDeadline) {
33015
+ this.logger.warn?.(
33016
+ `[Go2rtcTcpServer] fanout not ready after 30s, dropping client ${clientId}`
33017
+ );
33018
+ return;
33019
+ }
33020
+ await new Promise((r) => setTimeout(r, 100));
33021
+ }
33022
+ if (!this.active || !this.nativeFanout) return;
33023
+ const subscription = this.nativeFanout.subscribe(clientId);
33024
+ const prebufferSnap = this.prebuffer.slice();
33025
+ let lastIdrIdx = -1;
33026
+ for (let i = prebufferSnap.length - 1; i >= 0; i--) {
33027
+ if (prebufferSnap[i].isKeyframe) {
33028
+ lastIdrIdx = i;
33029
+ break;
33030
+ }
33031
+ }
33032
+ if (lastIdrIdx >= 0) {
33033
+ const replay = prebufferSnap.slice(lastIdrIdx);
33034
+ this.logger.info?.(
33035
+ `[Go2rtcTcpServer] prebuffer replay client=${clientId} frames=${replay.length}`
33036
+ );
33037
+ for (const entry of replay) {
33038
+ if (socket.destroyed) return;
33039
+ socket.write(entry.data);
33040
+ }
33041
+ }
33042
+ let seenKeyframe = lastIdrIdx >= 0;
33043
+ let liveFrameCount = 0;
33044
+ let liveVideoWritten = 0;
33045
+ let lastLogAt = Date.now();
33046
+ try {
33047
+ this.logger.info?.(
33048
+ `[Go2rtcTcpServer] entering live loop client=${clientId} seenKeyframe=${seenKeyframe}`
33049
+ );
33050
+ for await (const frame of subscription) {
33051
+ if (socket.destroyed || !this.active) {
33052
+ this.logger.info?.(
33053
+ `[Go2rtcTcpServer] live loop exit client=${clientId} destroyed=${socket.destroyed} active=${this.active}`
33054
+ );
33055
+ break;
33056
+ }
33057
+ liveFrameCount++;
33058
+ const annexB = this.convertFrame(frame);
33059
+ if (!annexB) continue;
33060
+ if (!seenKeyframe) {
33061
+ if (!this.isAnnexBKeyframe(annexB, frame.videoType)) continue;
33062
+ seenKeyframe = true;
33063
+ this.logger.info?.(
33064
+ `[Go2rtcTcpServer] first live keyframe client=${clientId} after ${liveFrameCount} frames`
33065
+ );
33066
+ }
33067
+ socket.write(annexB);
33068
+ liveVideoWritten++;
33069
+ if (Date.now() - lastLogAt > 1e4) {
33070
+ this.logger.info?.(
33071
+ `[Go2rtcTcpServer] live stats client=${clientId} received=${liveFrameCount} written=${liveVideoWritten} bufLen=${socket.writableLength}`
33072
+ );
33073
+ lastLogAt = Date.now();
33074
+ }
33075
+ if (socket.writableLength > this.maxBufferBytes) {
33076
+ this.logger.warn?.(
33077
+ `[Go2rtcTcpServer] buffer overflow (${socket.writableLength} bytes), dropping client ${clientId}`
33078
+ );
33079
+ socket.destroy();
33080
+ break;
33081
+ }
33082
+ }
33083
+ this.logger.info?.(
33084
+ `[Go2rtcTcpServer] live loop ended naturally client=${clientId} received=${liveFrameCount} written=${liveVideoWritten}`
33085
+ );
33086
+ } finally {
33087
+ await subscription.return(void 0).catch(() => {
33088
+ });
33089
+ }
33090
+ }
33091
+ // -----------------------------------------------------------------------
33092
+ // Frame conversion
33093
+ // -----------------------------------------------------------------------
33094
+ /**
33095
+ * Convert a native frame to wire-ready Annex-B.
33096
+ * Audio frames are skipped — raw TCP carries only video (Annex-B).
33097
+ * go2rtc auto-detects the codec from SPS/PPS/VPS NALUs.
33098
+ */
33099
+ convertFrame(frame) {
33100
+ if (frame.audio) {
33101
+ return null;
33102
+ }
33103
+ if (frame.data.length === 0) return null;
33104
+ try {
33105
+ if (frame.videoType === "H264") {
33106
+ return convertToAnnexB(frame.data);
33107
+ }
33108
+ if (frame.videoType === "H265") {
33109
+ return convertToAnnexB2(frame.data);
33110
+ }
33111
+ } catch {
33112
+ }
33113
+ return frame.data;
33114
+ }
33115
+ /** Check if an Annex-B buffer contains a keyframe (IDR for H.264, IRAP for H.265). */
33116
+ isAnnexBKeyframe(annexB, videoType) {
33117
+ try {
33118
+ if (videoType === "H264") {
33119
+ const nals = _Go2rtcTcpServer.splitAnnexBNals(annexB);
33120
+ return nals.some((n) => n.length >= 1 && (n[0] & 31) === 5);
33121
+ }
33122
+ if (videoType === "H265") {
33123
+ const nals = splitAnnexBToNalPayloads2(annexB);
33124
+ return nals.some(
33125
+ (n) => n.length >= 2 && isH265Irap(n[0] >> 1 & 63)
33126
+ );
33127
+ }
33128
+ } catch {
33129
+ }
33130
+ return false;
33131
+ }
33132
+ /** Split Annex-B byte stream into individual NAL units. */
33133
+ static splitAnnexBNals(buf) {
33134
+ const nals = [];
33135
+ let i = 0;
33136
+ while (i < buf.length) {
33137
+ if (i + 2 < buf.length && buf[i] === 0 && buf[i + 1] === 0) {
33138
+ let scLen;
33139
+ if (buf[i + 2] === 1) {
33140
+ scLen = 3;
33141
+ } else if (i + 3 < buf.length && buf[i + 2] === 0 && buf[i + 3] === 1) {
33142
+ scLen = 4;
33143
+ } else {
33144
+ i++;
33145
+ continue;
33146
+ }
33147
+ const nalStart = i + scLen;
33148
+ let nalEnd = buf.length;
33149
+ for (let j = nalStart; j < buf.length - 2; j++) {
33150
+ if (buf[j] === 0 && buf[j + 1] === 0 && (buf[j + 2] === 1 || j + 3 < buf.length && buf[j + 2] === 0 && buf[j + 3] === 1)) {
33151
+ nalEnd = j;
33152
+ break;
33153
+ }
33154
+ }
33155
+ if (nalEnd > nalStart) {
33156
+ nals.push(buf.subarray(nalStart, nalEnd));
33157
+ }
33158
+ i = nalEnd;
33159
+ } else {
33160
+ i++;
33161
+ }
33162
+ }
33163
+ return nals;
33164
+ }
33165
+ // -----------------------------------------------------------------------
33166
+ // Native stream management
33167
+ // -----------------------------------------------------------------------
33168
+ async startNativeStream() {
33169
+ if (this.nativeStreamActive) return;
33170
+ this.nativeStreamActive = true;
33171
+ let dedicatedClient;
33172
+ if (this.deviceId) {
33173
+ try {
33174
+ const session = await this.api.createDedicatedSession(
33175
+ `live:${this.deviceId}:ch${this.channel}:${this.profile}`
33176
+ );
33177
+ dedicatedClient = session.client;
33178
+ this.dedicatedSessionRelease = session.release;
33179
+ } catch (e) {
33180
+ this.logger.warn?.(
33181
+ `[Go2rtcTcpServer] failed to acquire dedicated session, using shared socket: ${e}`
33182
+ );
33183
+ }
33184
+ }
33185
+ this.logger.info?.(
33186
+ `[Go2rtcTcpServer] native stream starting channel=${this.channel} profile=${this.profile} dedicated=${!!dedicatedClient}`
33187
+ );
33188
+ this.nativeFanout = new NativeStreamFanout2({
33189
+ maxQueueItems: 200,
33190
+ createSource: () => createNativeStream(this.api, this.channel, this.profile, {
33191
+ variant: this.variant,
33192
+ ...dedicatedClient ? { client: dedicatedClient } : {}
33193
+ }),
33194
+ onFrame: (frame) => {
33195
+ if (!frame.audio && (frame.videoType === "H264" || frame.videoType === "H265")) {
33196
+ this.detectedVideoType = frame.videoType;
33197
+ }
33198
+ const wireData = this.convertFrame(frame);
33199
+ if (!wireData || wireData.length === 0) return;
33200
+ const isKeyframe = !frame.audio && this.isAnnexBKeyframe(wireData, frame.videoType);
33201
+ this.prebuffer.push({
33202
+ data: Buffer.from(wireData),
33203
+ time: Date.now(),
33204
+ isKeyframe,
33205
+ audio: frame.audio
33206
+ });
33207
+ const cutoff = Date.now() - this.prebufferMaxMs;
33208
+ let trimIdx = 0;
33209
+ while (trimIdx < this.prebuffer.length && this.prebuffer[trimIdx].time < cutoff) {
33210
+ trimIdx++;
33211
+ }
33212
+ if (trimIdx > 0) this.prebuffer.splice(0, trimIdx);
33213
+ },
33214
+ onError: (error) => {
33215
+ this.logger.warn?.(`[Go2rtcTcpServer] native stream error: ${error}`);
33216
+ },
33217
+ onEnd: () => {
33218
+ if (!this.nativeStreamActive) return;
33219
+ this.nativeStreamActive = false;
33220
+ this.nativeFanout = null;
33221
+ if (this.dedicatedSessionRelease) {
33222
+ this.dedicatedSessionRelease().catch(() => {
33223
+ });
33224
+ this.dedicatedSessionRelease = void 0;
33225
+ }
33226
+ if (this.active && (this.connectedClients.size > 0 || this.prestartStream)) {
33227
+ this.logger.info?.(
33228
+ `[Go2rtcTcpServer] native stream ended, restarting (clients=${this.connectedClients.size}, prestart=${this.prestartStream})`
33229
+ );
33230
+ this.startNativeStream();
33231
+ }
33232
+ }
33233
+ });
33234
+ this.nativeFanout.start();
33235
+ }
33236
+ async stopNativeStream() {
33237
+ this.nativeStreamActive = false;
33238
+ const fanout = this.nativeFanout;
33239
+ this.nativeFanout = null;
33240
+ if (fanout) {
33241
+ await fanout.stop();
33242
+ }
33243
+ this.prebuffer = [];
33244
+ if (this.dedicatedSessionRelease) {
33245
+ await this.dedicatedSessionRelease().catch(() => {
33246
+ });
33247
+ this.dedicatedSessionRelease = void 0;
33248
+ }
33249
+ }
33250
+ // -----------------------------------------------------------------------
33251
+ // Client lifecycle
33252
+ // -----------------------------------------------------------------------
33253
+ removeClient(clientId) {
33254
+ if (!this.connectedClients.has(clientId)) return;
33255
+ this.connectedClients.delete(clientId);
33256
+ this.clientSockets.delete(clientId);
33257
+ this.logger.info?.(
33258
+ `[Go2rtcTcpServer] client disconnected id=${clientId} remaining=${this.connectedClients.size}`
33259
+ );
33260
+ this.emit("clientDisconnected", clientId);
33261
+ if (this.connectedClients.size === 0 && !this.prestartStream) {
33262
+ this.scheduleStop();
33263
+ }
33264
+ }
33265
+ scheduleStop() {
33266
+ if (this.stopGraceTimer) return;
33267
+ this.logger.info?.(
33268
+ `[Go2rtcTcpServer] no clients, scheduling stream stop in ${this.gracePeriodMs}ms`
33269
+ );
33270
+ this.stopGraceTimer = setTimeout(async () => {
33271
+ this.stopGraceTimer = void 0;
33272
+ if (this.connectedClients.size === 0 && this.nativeStreamActive) {
33273
+ this.logger.info?.("[Go2rtcTcpServer] grace period expired, stopping native stream");
33274
+ await this.stopNativeStream();
33275
+ }
33276
+ }, this.gracePeriodMs);
33277
+ }
33278
+ };
33279
+
33280
+ // src/baichuan/stream/BaichuanHttpStreamServer.ts
33281
+ var import_node_events7 = require("events");
32729
33282
  var import_node_child_process9 = require("child_process");
32730
33283
  var http4 = __toESM(require("http"), 1);
32731
33284
  var NAL_START_CODE_4B4 = Buffer.from([0, 0, 0, 1]);
@@ -32772,7 +33325,7 @@ function isH264KeyframeFromAnnexB(annexB) {
32772
33325
  }
32773
33326
  return false;
32774
33327
  }
32775
- var BaichuanHttpStreamServer = class extends import_node_events6.EventEmitter {
33328
+ var BaichuanHttpStreamServer = class extends import_node_events7.EventEmitter {
32776
33329
  videoStream;
32777
33330
  listenPort;
32778
33331
  path;
@@ -33039,15 +33592,15 @@ var BaichuanHttpStreamServer = class extends import_node_events6.EventEmitter {
33039
33592
  };
33040
33593
 
33041
33594
  // src/baichuan/stream/BaichuanMjpegServer.ts
33042
- var import_node_events8 = require("events");
33595
+ var import_node_events9 = require("events");
33043
33596
  var http5 = __toESM(require("http"), 1);
33044
33597
 
33045
33598
  // src/baichuan/stream/MjpegTransformer.ts
33046
- var import_node_events7 = require("events");
33599
+ var import_node_events8 = require("events");
33047
33600
  var import_node_child_process10 = require("child_process");
33048
33601
  var JPEG_SOI = Buffer.from([255, 216]);
33049
33602
  var JPEG_EOI = Buffer.from([255, 217]);
33050
- var MjpegTransformer = class extends import_node_events7.EventEmitter {
33603
+ var MjpegTransformer = class extends import_node_events8.EventEmitter {
33051
33604
  options;
33052
33605
  ffmpeg = null;
33053
33606
  started = false;
@@ -33246,7 +33799,7 @@ Content-Length: ${frame.length}\r
33246
33799
  // src/baichuan/stream/BaichuanMjpegServer.ts
33247
33800
  init_H264Converter();
33248
33801
  init_H265Converter();
33249
- var BaichuanMjpegServer = class extends import_node_events8.EventEmitter {
33802
+ var BaichuanMjpegServer = class extends import_node_events9.EventEmitter {
33250
33803
  options;
33251
33804
  clients = /* @__PURE__ */ new Map();
33252
33805
  httpServer = null;
@@ -33527,7 +34080,7 @@ var BaichuanMjpegServer = class extends import_node_events8.EventEmitter {
33527
34080
  };
33528
34081
 
33529
34082
  // src/baichuan/stream/BaichuanWebRTCServer.ts
33530
- var import_node_events9 = require("events");
34083
+ var import_node_events10 = require("events");
33531
34084
  init_BcMediaAnnexBDecoder();
33532
34085
  init_H264Converter();
33533
34086
  function parseAnnexBNalUnits(annexB) {
@@ -33564,7 +34117,7 @@ function getH264NalType(nalUnit) {
33564
34117
  function getH265NalType2(nalUnit) {
33565
34118
  return nalUnit[0] >> 1 & 63;
33566
34119
  }
33567
- var BaichuanWebRTCServer = class extends import_node_events9.EventEmitter {
34120
+ var BaichuanWebRTCServer = class extends import_node_events10.EventEmitter {
33568
34121
  options;
33569
34122
  sessions = /* @__PURE__ */ new Map();
33570
34123
  sessionIdCounter = 0;
@@ -34466,7 +35019,7 @@ Error: ${err}`
34466
35019
  };
34467
35020
 
34468
35021
  // src/baichuan/stream/BaichuanHlsServer.ts
34469
- var import_node_events10 = require("events");
35022
+ var import_node_events11 = require("events");
34470
35023
  var import_node_fs = __toESM(require("fs"), 1);
34471
35024
  var import_promises3 = __toESM(require("fs/promises"), 1);
34472
35025
  var import_node_os3 = __toESM(require("os"), 1);
@@ -34546,7 +35099,7 @@ function getNalTypes(codec, annexB) {
34546
35099
  }
34547
35100
  });
34548
35101
  }
34549
- var BaichuanHlsServer = class extends import_node_events10.EventEmitter {
35102
+ var BaichuanHlsServer = class extends import_node_events11.EventEmitter {
34550
35103
  api;
34551
35104
  channel;
34552
35105
  profile;
@@ -35547,10 +36100,10 @@ async function autoDetectDeviceType(inputs) {
35547
36100
  }
35548
36101
 
35549
36102
  // src/multifocal/compositeRtspServer.ts
35550
- var import_node_events11 = require("events");
36103
+ var import_node_events12 = require("events");
35551
36104
  var import_node_child_process12 = require("child_process");
35552
- var net4 = __toESM(require("net"), 1);
35553
- var CompositeRtspServer = class extends import_node_events11.EventEmitter {
36105
+ var net5 = __toESM(require("net"), 1);
36106
+ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
35554
36107
  options;
35555
36108
  compositeStream = null;
35556
36109
  rtspServer = null;
@@ -35616,7 +36169,7 @@ var CompositeRtspServer = class extends import_node_events11.EventEmitter {
35616
36169
  const width = widerStreamInfo?.width ?? 1920;
35617
36170
  const height = widerStreamInfo?.height ?? 1080;
35618
36171
  const fps = widerStreamInfo?.frameRate ?? 25;
35619
- this.rtspServer = net4.createServer((socket) => {
36172
+ this.rtspServer = net5.createServer((socket) => {
35620
36173
  this.handleRtspConnection(socket);
35621
36174
  });
35622
36175
  await new Promise((resolve, reject) => {
@@ -35892,6 +36445,7 @@ var CompositeRtspServer = class extends import_node_events11.EventEmitter {
35892
36445
  DUAL_LENS_DUAL_MOTION_MODELS,
35893
36446
  DUAL_LENS_MODELS,
35894
36447
  DUAL_LENS_SINGLE_MOTION_MODELS,
36448
+ Go2rtcTcpServer,
35895
36449
  H264RtpDepacketizer,
35896
36450
  H265RtpDepacketizer,
35897
36451
  HlsSessionManager,