@apocaliss92/nodelink-js 0.6.6 → 0.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -76,7 +76,7 @@ import {
76
76
  setGlobalLogger,
77
77
  tcpReachabilityProbe,
78
78
  xmlIndicatesFloodlight
79
- } from "./chunk-JQ5NSEVD.js";
79
+ } from "./chunk-T22QCNBR.js";
80
80
  import {
81
81
  ReolinkCgiApi,
82
82
  ReolinkHttpClient,
@@ -4763,809 +4763,8 @@ async function createReplayHttpServer(options) {
4763
4763
  };
4764
4764
  }
4765
4765
 
4766
- // src/baichuan/stream/Go2rtcTcpServer.ts
4767
- import { EventEmitter as EventEmitter2 } from "events";
4768
- import * as net from "net";
4769
- var AsyncBoundedQueue = class {
4770
- maxItems;
4771
- queue = [];
4772
- waiting;
4773
- closed = false;
4774
- constructor(maxItems) {
4775
- this.maxItems = Math.max(1, maxItems | 0);
4776
- }
4777
- push(item) {
4778
- if (this.closed) return;
4779
- if (this.waiting) {
4780
- const { resolve } = this.waiting;
4781
- this.waiting = void 0;
4782
- resolve({ value: item, done: false });
4783
- return;
4784
- }
4785
- this.queue.push(item);
4786
- if (this.queue.length > this.maxItems) {
4787
- this.queue.splice(0, this.queue.length - this.maxItems);
4788
- }
4789
- }
4790
- close() {
4791
- if (this.closed) return;
4792
- this.closed = true;
4793
- if (this.waiting) {
4794
- const { resolve } = this.waiting;
4795
- this.waiting = void 0;
4796
- resolve({ value: void 0, done: true });
4797
- }
4798
- }
4799
- async next() {
4800
- if (this.closed) return { value: void 0, done: true };
4801
- const item = this.queue.shift();
4802
- if (item !== void 0) return { value: item, done: false };
4803
- return await new Promise((resolve) => {
4804
- this.waiting = { resolve };
4805
- });
4806
- }
4807
- };
4808
- var NativeStreamFanout = class {
4809
- opts;
4810
- queues = /* @__PURE__ */ new Map();
4811
- source = null;
4812
- running = false;
4813
- pumpPromise = null;
4814
- constructor(opts) {
4815
- this.opts = opts;
4816
- }
4817
- start() {
4818
- if (this.running) return;
4819
- this.running = true;
4820
- this.source = this.opts.createSource();
4821
- this.pumpPromise = (async () => {
4822
- try {
4823
- for await (const frame of this.source) {
4824
- try {
4825
- this.opts.onFrame?.(frame);
4826
- } catch {
4827
- }
4828
- for (const q of this.queues.values()) {
4829
- q.push(frame);
4830
- }
4831
- }
4832
- } catch (e) {
4833
- this.opts.onError?.(e);
4834
- } finally {
4835
- for (const q of this.queues.values()) q.close();
4836
- this.queues.clear();
4837
- this.running = false;
4838
- this.opts.onEnd?.();
4839
- }
4840
- })();
4841
- }
4842
- subscribe(id) {
4843
- const q = new AsyncBoundedQueue(this.opts.maxQueueItems);
4844
- this.queues.set(id, q);
4845
- const self = this;
4846
- return (async function* () {
4847
- try {
4848
- while (true) {
4849
- const r = await q.next();
4850
- if (r.done) return;
4851
- yield r.value;
4852
- }
4853
- } finally {
4854
- q.close();
4855
- self.queues.delete(id);
4856
- }
4857
- })();
4858
- }
4859
- async stop() {
4860
- if (!this.running) return;
4861
- this.running = false;
4862
- const src = this.source;
4863
- this.source = null;
4864
- for (const q of this.queues.values()) q.close();
4865
- this.queues.clear();
4866
- try {
4867
- await src?.return(void 0);
4868
- } catch {
4869
- }
4870
- try {
4871
- await this.pumpPromise;
4872
- } catch {
4873
- }
4874
- this.pumpPromise = null;
4875
- }
4876
- };
4877
- var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
4878
- api;
4879
- channel;
4880
- profile;
4881
- variant;
4882
- listenHost;
4883
- listenPort;
4884
- logger;
4885
- deviceId;
4886
- gracePeriodMs;
4887
- prebufferMaxMs;
4888
- maxBufferBytes;
4889
- streamTimeoutMs;
4890
- prestartStream;
4891
- active = false;
4892
- server;
4893
- resolvedPort;
4894
- // Native stream
4895
- nativeFanout = null;
4896
- nativeStreamActive = false;
4897
- // Set only by stopNativeStream() (explicit teardown) so the fanout's onEnd
4898
- // callback can short-circuit cleanup/restart logic. NOT set by the inactivity-
4899
- // timeout force-restart path — that flow wants onEnd to run and decide
4900
- // whether to restart based on prestartStream / connected clients.
4901
- nativeStreamStopping = false;
4902
- // Pending retry timer for the unbounded auto-restart loop. When a stream
4903
- // start fails transiently (camera in maintenance reboot, idle-disconnect
4904
- // race, etc.) we keep trying with exponential backoff until either the
4905
- // server is stopped or a frame finally arrives.
4906
- nativeStreamRetryTimer;
4907
- nativeStreamRetryDelayMs = 0;
4908
- dedicatedSessionRelease;
4909
- detectedVideoType;
4910
- // Client tracking
4911
- connectedClients = /* @__PURE__ */ new Set();
4912
- clientSockets = /* @__PURE__ */ new Map();
4913
- stopGraceTimer;
4914
- // Stream health monitoring
4915
- lastFrameAt = 0;
4916
- streamHealthTimer;
4917
- totalFramesReceived = 0;
4918
- totalVideoFramesWritten = 0;
4919
- // Prebuffer
4920
- prebuffer = [];
4921
- // Audio metadata — populated on first valid ADTS AAC frame.
4922
- // Exposed via getAudioInfo() for the stream-diagnostics feature.
4923
- audioInfo = null;
4924
- constructor(options) {
4925
- super();
4926
- this.api = options.api;
4927
- this.channel = options.channel;
4928
- this.profile = options.profile;
4929
- this.variant = options.variant ?? "default";
4930
- this.listenHost = options.listenHost ?? "127.0.0.1";
4931
- this.listenPort = options.listenPort ?? 0;
4932
- this.logger = options.logger ?? console;
4933
- this.deviceId = options.deviceId;
4934
- this.gracePeriodMs = options.gracePeriodMs ?? 3e4;
4935
- this.prebufferMaxMs = options.prebufferMs ?? 3e3;
4936
- this.maxBufferBytes = options.maxBufferBytes ?? 1e8;
4937
- this.streamTimeoutMs = options.streamTimeoutMs ?? 15e3;
4938
- this.prestartStream = options.prestartStream ?? true;
4939
- }
4940
- // -----------------------------------------------------------------------
4941
- // Public API
4942
- // -----------------------------------------------------------------------
4943
- /** Start listening. Resolves once the TCP server is bound. */
4944
- async start() {
4945
- if (this.active) return;
4946
- this.active = true;
4947
- this.server = net.createServer((socket) => this.handleClient(socket));
4948
- this.server.on("error", (err) => {
4949
- this.logger.error?.(`[Go2rtcTcpServer] server error: ${err.message}`);
4950
- this.emit("error", err);
4951
- });
4952
- await new Promise((resolve, reject) => {
4953
- this.server.listen(this.listenPort, this.listenHost, () => {
4954
- const addr = this.server.address();
4955
- this.resolvedPort = addr.port;
4956
- this.logger.info?.(
4957
- `[Go2rtcTcpServer] listening on ${addr.address}:${addr.port} channel=${this.channel} profile=${this.profile}`
4958
- );
4959
- this.emit("listening", { host: addr.address, port: addr.port });
4960
- resolve();
4961
- });
4962
- this.server.once("error", reject);
4963
- });
4964
- if (this.prestartStream) {
4965
- this.logger.info?.(
4966
- `[Go2rtcTcpServer] pre-starting native stream channel=${this.channel} profile=${this.profile}`
4967
- );
4968
- this.startNativeStream();
4969
- }
4970
- }
4971
- /** Stop the server and all active streams. */
4972
- async stop() {
4973
- if (!this.active) return;
4974
- this.active = false;
4975
- clearTimeout(this.stopGraceTimer);
4976
- this.clearNativeStreamRetry();
4977
- this.stopStreamHealthMonitor();
4978
- for (const [id, sock] of this.clientSockets) {
4979
- sock.destroy();
4980
- this.connectedClients.delete(id);
4981
- }
4982
- this.clientSockets.clear();
4983
- await this.stopNativeStream();
4984
- if (this.server) {
4985
- await new Promise((resolve) => {
4986
- this.server.close(() => resolve());
4987
- });
4988
- this.server = void 0;
4989
- }
4990
- this.prebuffer = [];
4991
- this.resolvedPort = void 0;
4992
- this.emit("close");
4993
- }
4994
- /** The actual port the server is listening on (available after start()). */
4995
- get port() {
4996
- return this.resolvedPort;
4997
- }
4998
- /** The go2rtc-compatible source URL. */
4999
- get go2rtcSourceUrl() {
5000
- if (this.resolvedPort == null) return void 0;
5001
- return `tcp://127.0.0.1:${this.resolvedPort}`;
5002
- }
5003
- /** Number of currently connected clients. */
5004
- get clientCount() {
5005
- return this.connectedClients.size;
5006
- }
5007
- // -----------------------------------------------------------------------
5008
- // Diagnostic subscription API (implements DiagnosticStreamServer)
5009
- //
5010
- // Matches the shape of BaichuanRtspServer's diagnostic API so the
5011
- // stream-diagnostic feature in the Manager app can drive either backend
5012
- // with identical code.
5013
- // -----------------------------------------------------------------------
5014
- /**
5015
- * Subscribe to the raw native stream for diagnostic purposes.
5016
- * The subscriber receives the same frames the MPEG-TS muxer consumes
5017
- * (pre-muxing). Counts as a "consumer" so the native stream is kept alive
5018
- * for the lifetime of the subscription. If the stream is not already
5019
- * running (battery camera, prestart=false), this starts it.
5020
- */
5021
- async subscribeDiagnostic(id) {
5022
- this.connectedClients.add(`diag:${id}`);
5023
- if (!this.nativeStreamActive) {
5024
- await this.startNativeStream();
5025
- }
5026
- if (!this.nativeFanout) {
5027
- this.connectedClients.delete(`diag:${id}`);
5028
- throw new Error(
5029
- "Go2rtcTcpServer: native stream failed to start \u2014 cannot subscribe diagnostic"
5030
- );
5031
- }
5032
- return this.nativeFanout.subscribe(`diag:${id}`);
5033
- }
5034
- /** Unsubscribe a diagnostic session and release its consumer slot. */
5035
- unsubscribeDiagnostic(id) {
5036
- this.removeClient(`diag:${id}`, "diagnostic unsubscribe");
5037
- }
5038
- /**
5039
- * Returns ADTS AAC audio metadata detected from the native stream, or
5040
- * null if no audio frame has been observed yet (e.g. video-only cameras
5041
- * or before the first audio packet arrives).
5042
- */
5043
- getAudioInfo() {
5044
- return this.audioInfo;
5045
- }
5046
- // -----------------------------------------------------------------------
5047
- // Client handling
5048
- // -----------------------------------------------------------------------
5049
- handleClient(socket) {
5050
- const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
5051
- socket.setNoDelay(true);
5052
- this.connectedClients.add(clientId);
5053
- this.clientSockets.set(clientId, socket);
5054
- this.logger.info?.(
5055
- `[Go2rtcTcpServer] client connected id=${clientId} total=${this.connectedClients.size}`
5056
- );
5057
- this.emit("client", clientId);
5058
- if (this.stopGraceTimer) {
5059
- clearTimeout(this.stopGraceTimer);
5060
- this.stopGraceTimer = void 0;
5061
- }
5062
- if (!this.nativeStreamActive) {
5063
- this.startNativeStream();
5064
- }
5065
- this.feedClient(clientId, socket).catch((err) => {
5066
- this.logger.warn?.(
5067
- `[Go2rtcTcpServer] feedClient error id=${clientId}: ${err}`
5068
- );
5069
- });
5070
- const cleanup = (reason) => {
5071
- this.removeClient(clientId, reason);
5072
- socket.destroy();
5073
- };
5074
- socket.on("error", (err) => cleanup(`error: ${err.message}`));
5075
- socket.on("close", (hadError) => cleanup(hadError ? "close (with error)" : "close (clean)"));
5076
- }
5077
- async feedClient(clientId, socket) {
5078
- const fanoutDeadline = Date.now() + 3e4;
5079
- while (this.active && !this.nativeFanout) {
5080
- if (socket.destroyed) return;
5081
- if (Date.now() > fanoutDeadline) {
5082
- this.logger.warn?.(
5083
- `[Go2rtcTcpServer] fanout not ready after 30s, dropping client ${clientId}`
5084
- );
5085
- return;
5086
- }
5087
- await new Promise((r) => setTimeout(r, 100));
5088
- }
5089
- if (!this.active || !this.nativeFanout) return;
5090
- const subscription = this.nativeFanout.subscribe(clientId);
5091
- let muxer = null;
5092
- const prebufferSnap = this.prebuffer.slice();
5093
- let lastIdrIdx = -1;
5094
- for (let i = prebufferSnap.length - 1; i >= 0; i--) {
5095
- if (prebufferSnap[i].isKeyframe) {
5096
- lastIdrIdx = i;
5097
- break;
5098
- }
5099
- }
5100
- if (lastIdrIdx >= 0) {
5101
- const replay = prebufferSnap.slice(lastIdrIdx);
5102
- this.logger.info?.(
5103
- `[Go2rtcTcpServer] prebuffer replay client=${clientId} frames=${replay.length}`
5104
- );
5105
- if (!muxer) {
5106
- muxer = new MpegTsMuxer({
5107
- videoType: this.detectedVideoType ?? "H264",
5108
- includeAudio: true
5109
- });
5110
- }
5111
- for (const entry of replay) {
5112
- if (socket.destroyed) return;
5113
- let ts;
5114
- if (!entry.audio) {
5115
- ts = muxer.muxVideo(entry.data, entry.pts, entry.isKeyframe);
5116
- } else {
5117
- ts = muxer.muxAudio(entry.data, entry.pts);
5118
- }
5119
- if (ts.length > 0) socket.write(ts);
5120
- }
5121
- }
5122
- let seenKeyframe = lastIdrIdx >= 0;
5123
- let liveFrameCount = 0;
5124
- let liveVideoWritten = 0;
5125
- let lastLogAt = Date.now();
5126
- try {
5127
- this.logger.info?.(
5128
- `[Go2rtcTcpServer] entering live loop client=${clientId} seenKeyframe=${seenKeyframe}`
5129
- );
5130
- for await (const frame of subscription) {
5131
- if (socket.destroyed || !this.active) {
5132
- this.logger.info?.(
5133
- `[Go2rtcTcpServer] live loop exit client=${clientId} destroyed=${socket.destroyed} active=${this.active}`
5134
- );
5135
- break;
5136
- }
5137
- liveFrameCount++;
5138
- if (frame.audio) {
5139
- if (muxer) {
5140
- const pts2 = frame.microseconds ?? Date.now() * 1e3;
5141
- const ts2 = muxer.muxAudio(frame.data, pts2);
5142
- if (ts2.length > 0) socket.write(ts2);
5143
- }
5144
- continue;
5145
- }
5146
- const annexB = this.convertVideoFrame(frame);
5147
- if (!annexB) continue;
5148
- const isKf = this.isAnnexBKeyframe(annexB, frame.videoType);
5149
- if (!seenKeyframe) {
5150
- if (!isKf) continue;
5151
- seenKeyframe = true;
5152
- this.logger.info?.(
5153
- `[Go2rtcTcpServer] first live keyframe client=${clientId} after ${liveFrameCount} frames`
5154
- );
5155
- if (!muxer) {
5156
- muxer = new MpegTsMuxer({
5157
- videoType: frame.videoType ?? this.detectedVideoType ?? "H264",
5158
- includeAudio: true
5159
- });
5160
- }
5161
- }
5162
- const pts = frame.microseconds ?? Date.now() * 1e3;
5163
- const ts = muxer.muxVideo(annexB, pts, isKf);
5164
- socket.write(ts);
5165
- liveVideoWritten++;
5166
- this.totalVideoFramesWritten++;
5167
- if (Date.now() - lastLogAt > 1e4) {
5168
- this.logger.info?.(
5169
- `[Go2rtcTcpServer] live stats client=${clientId} received=${liveFrameCount} written=${liveVideoWritten} bufLen=${socket.writableLength}`
5170
- );
5171
- lastLogAt = Date.now();
5172
- }
5173
- if (socket.writableLength > this.maxBufferBytes) {
5174
- this.logger.warn?.(
5175
- `[Go2rtcTcpServer] buffer overflow (${socket.writableLength} bytes), dropping client ${clientId}`
5176
- );
5177
- socket.destroy();
5178
- break;
5179
- }
5180
- }
5181
- this.logger.info?.(
5182
- `[Go2rtcTcpServer] live loop ended naturally client=${clientId} received=${liveFrameCount} written=${liveVideoWritten}`
5183
- );
5184
- } finally {
5185
- await subscription.return(void 0).catch(() => {
5186
- });
5187
- }
5188
- }
5189
- // -----------------------------------------------------------------------
5190
- // Frame conversion
5191
- // -----------------------------------------------------------------------
5192
- /**
5193
- * Convert a native video frame to Annex-B.
5194
- * Returns null for audio frames (handled separately by muxAudio).
5195
- */
5196
- convertVideoFrame(frame) {
5197
- if (frame.audio) return null;
5198
- if (frame.data.length === 0) return null;
5199
- try {
5200
- if (frame.videoType === "H264") {
5201
- return convertToAnnexB(frame.data);
5202
- }
5203
- if (frame.videoType === "H265") {
5204
- return convertToAnnexB2(frame.data);
5205
- }
5206
- } catch {
5207
- }
5208
- return frame.data;
5209
- }
5210
- /** Check if an Annex-B buffer contains a keyframe (IDR for H.264, IRAP for H.265). */
5211
- isAnnexBKeyframe(annexB, videoType) {
5212
- try {
5213
- if (videoType === "H264") {
5214
- const nals = _Go2rtcTcpServer.splitAnnexBNals(annexB);
5215
- return nals.some((n) => n.length >= 1 && (n[0] & 31) === 5);
5216
- }
5217
- if (videoType === "H265") {
5218
- const nals = splitAnnexBToNalPayloads2(annexB);
5219
- return nals.some(
5220
- (n) => n.length >= 2 && isH265Irap(n[0] >> 1 & 63)
5221
- );
5222
- }
5223
- } catch {
5224
- }
5225
- return false;
5226
- }
5227
- /** Split Annex-B byte stream into individual NAL units. */
5228
- static splitAnnexBNals(buf) {
5229
- const nals = [];
5230
- let i = 0;
5231
- while (i < buf.length) {
5232
- if (i + 2 < buf.length && buf[i] === 0 && buf[i + 1] === 0) {
5233
- let scLen;
5234
- if (buf[i + 2] === 1) {
5235
- scLen = 3;
5236
- } else if (i + 3 < buf.length && buf[i + 2] === 0 && buf[i + 3] === 1) {
5237
- scLen = 4;
5238
- } else {
5239
- i++;
5240
- continue;
5241
- }
5242
- const nalStart = i + scLen;
5243
- let nalEnd = buf.length;
5244
- for (let j = nalStart; j < buf.length - 2; j++) {
5245
- if (buf[j] === 0 && buf[j + 1] === 0 && (buf[j + 2] === 1 || j + 3 < buf.length && buf[j + 2] === 0 && buf[j + 3] === 1)) {
5246
- nalEnd = j;
5247
- break;
5248
- }
5249
- }
5250
- if (nalEnd > nalStart) {
5251
- nals.push(buf.subarray(nalStart, nalEnd));
5252
- }
5253
- i = nalEnd;
5254
- } else {
5255
- i++;
5256
- }
5257
- }
5258
- return nals;
5259
- }
5260
- // -----------------------------------------------------------------------
5261
- // ADTS AAC parsing (used for audio metadata exposed via getAudioInfo)
5262
- // -----------------------------------------------------------------------
5263
- /** True if `b` starts with an ADTS AAC syncword (0xFFF). */
5264
- static isAdtsAacFrame(b) {
5265
- return b.length >= 2 && b[0] === 255 && (b[1] & 240) === 240;
5266
- }
5267
- /**
5268
- * Parse an ADTS header into {sampleRate, channels, AudioSpecificConfig hex}.
5269
- * Returns null when the buffer is not a valid ADTS frame.
5270
- */
5271
- static parseAdtsSamplingInfo(b) {
5272
- if (b.length < 7) return null;
5273
- if (!_Go2rtcTcpServer.isAdtsAacFrame(b)) return null;
5274
- const samplingIndex = b[2] >> 2 & 15;
5275
- const sampleRates = [
5276
- 96e3,
5277
- 88200,
5278
- 64e3,
5279
- 48e3,
5280
- 44100,
5281
- 32e3,
5282
- 24e3,
5283
- 22050,
5284
- 16e3,
5285
- 12e3,
5286
- 11025,
5287
- 8e3,
5288
- 7350
5289
- ];
5290
- const sampleRate = sampleRates[samplingIndex] ?? null;
5291
- if (!sampleRate) return null;
5292
- const channelConfig = (b[2] & 1) << 2 | b[3] >> 6 & 3;
5293
- const channels = channelConfig === 0 ? 1 : channelConfig;
5294
- const profile = b[2] >> 6 & 3;
5295
- const audioObjectType = profile + 1;
5296
- const asc = audioObjectType << 11 | samplingIndex << 7 | channelConfig << 3;
5297
- const configHex = Buffer.from([asc >> 8 & 255, asc & 255]).toString(
5298
- "hex"
5299
- );
5300
- return { sampleRate, channels, configHex };
5301
- }
5302
- // -----------------------------------------------------------------------
5303
- // Native stream management
5304
- // -----------------------------------------------------------------------
5305
- /**
5306
- * Schedule another startNativeStream() attempt after the given delay.
5307
- * Idempotent: a no-op if a retry is already scheduled, the server is no
5308
- * longer active, or an explicit stop is in progress. Implements unbounded
5309
- * exponential backoff (5s → 60s) so a camera that stays unreachable for
5310
- * minutes (e.g. nightly maintenance reboot) eventually recovers without
5311
- * manual intervention — see issue #16.
5312
- */
5313
- scheduleNativeStreamRetry(reason) {
5314
- if (!this.active) return;
5315
- if (this.nativeStreamStopping) return;
5316
- if (this.nativeStreamRetryTimer) return;
5317
- const delay = this.nativeStreamRetryDelayMs > 0 ? this.nativeStreamRetryDelayMs : 5e3;
5318
- this.logger.info?.(
5319
- `[Go2rtcTcpServer] scheduling native stream retry in ${(delay / 1e3).toFixed(0)}s (reason=${reason})`
5320
- );
5321
- this.nativeStreamRetryTimer = setTimeout(() => {
5322
- this.nativeStreamRetryTimer = void 0;
5323
- if (!this.active) return;
5324
- if (this.nativeStreamStopping) return;
5325
- this.startNativeStream().catch((err) => {
5326
- this.logger.warn?.(
5327
- `[Go2rtcTcpServer] retry of startNativeStream threw: ${err instanceof Error ? err.message : err}`
5328
- );
5329
- });
5330
- }, delay);
5331
- this.nativeStreamRetryDelayMs = Math.min(delay * 2, 6e4);
5332
- }
5333
- /**
5334
- * Cancel any pending retry timer and reset the backoff. Called on explicit
5335
- * stop and on first-frame-received so the next failure starts the backoff
5336
- * window from scratch.
5337
- */
5338
- clearNativeStreamRetry() {
5339
- if (this.nativeStreamRetryTimer) {
5340
- clearTimeout(this.nativeStreamRetryTimer);
5341
- this.nativeStreamRetryTimer = void 0;
5342
- }
5343
- this.nativeStreamRetryDelayMs = 0;
5344
- }
5345
- async startNativeStream() {
5346
- if (this.nativeStreamActive) return;
5347
- if (!this.api.isReady) {
5348
- if (this.api.isClosed) {
5349
- this.logger.warn?.(
5350
- `[Go2rtcTcpServer] API has been explicitly closed \u2014 stream cannot start`
5351
- );
5352
- return;
5353
- }
5354
- try {
5355
- this.logger.info?.(
5356
- `[Go2rtcTcpServer] API not ready (idle disconnect?), calling ensureConnected`
5357
- );
5358
- await this.api.ensureConnected();
5359
- } catch (e) {
5360
- this.logger.warn?.(
5361
- `[Go2rtcTcpServer] ensureConnected failed: ${e}`
5362
- );
5363
- this.scheduleNativeStreamRetry("ensureConnected failed");
5364
- return;
5365
- }
5366
- }
5367
- this.nativeStreamActive = true;
5368
- let dedicatedClient;
5369
- if (this.deviceId) {
5370
- try {
5371
- const session = await this.api.createDedicatedSession(
5372
- `live:${this.deviceId}:ch${this.channel}:${this.profile}`
5373
- );
5374
- dedicatedClient = session.client;
5375
- this.dedicatedSessionRelease = session.release;
5376
- } catch (e) {
5377
- this.logger.warn?.(
5378
- `[Go2rtcTcpServer] failed to acquire dedicated session, using shared socket: ${e}`
5379
- );
5380
- }
5381
- }
5382
- this.logger.info?.(
5383
- `[Go2rtcTcpServer] native stream starting channel=${this.channel} profile=${this.profile} dedicated=${!!dedicatedClient}`
5384
- );
5385
- let hadFrames = false;
5386
- this.nativeFanout = new NativeStreamFanout({
5387
- maxQueueItems: 200,
5388
- createSource: () => createNativeStream(this.api, this.channel, this.profile, {
5389
- variant: this.variant,
5390
- ...dedicatedClient ? { client: dedicatedClient } : {}
5391
- }),
5392
- onFrame: (frame) => {
5393
- if (!hadFrames) {
5394
- this.clearNativeStreamRetry();
5395
- }
5396
- hadFrames = true;
5397
- this.lastFrameAt = Date.now();
5398
- this.totalFramesReceived++;
5399
- if (!frame.audio && (frame.videoType === "H264" || frame.videoType === "H265")) {
5400
- this.detectedVideoType = frame.videoType;
5401
- }
5402
- let prebufData;
5403
- let isKeyframe;
5404
- if (frame.audio) {
5405
- if (frame.data.length === 0) return;
5406
- if (!this.audioInfo) {
5407
- const parsed = _Go2rtcTcpServer.parseAdtsSamplingInfo(frame.data);
5408
- if (parsed) {
5409
- this.audioInfo = { codec: "aac-adts", ...parsed };
5410
- }
5411
- }
5412
- prebufData = frame.data;
5413
- isKeyframe = false;
5414
- } else {
5415
- const annexB = this.convertVideoFrame(frame);
5416
- if (!annexB || annexB.length === 0) return;
5417
- prebufData = annexB;
5418
- isKeyframe = this.isAnnexBKeyframe(annexB, frame.videoType);
5419
- }
5420
- const pts = frame.microseconds ?? Date.now() * 1e3;
5421
- this.prebuffer.push({
5422
- data: Buffer.from(prebufData),
5423
- time: Date.now(),
5424
- isKeyframe,
5425
- audio: frame.audio,
5426
- pts
5427
- });
5428
- const cutoff = Date.now() - this.prebufferMaxMs;
5429
- let trimIdx = 0;
5430
- while (trimIdx < this.prebuffer.length && this.prebuffer[trimIdx].time < cutoff) {
5431
- trimIdx++;
5432
- }
5433
- if (trimIdx > 0) this.prebuffer.splice(0, trimIdx);
5434
- },
5435
- onError: (error) => {
5436
- this.logger.warn?.(`[Go2rtcTcpServer] native stream error: ${error}`);
5437
- },
5438
- onEnd: () => {
5439
- if (this.nativeStreamStopping) return;
5440
- this.nativeStreamActive = false;
5441
- this.nativeFanout = null;
5442
- this.stopStreamHealthMonitor();
5443
- const silenceMs = this.lastFrameAt > 0 ? Date.now() - this.lastFrameAt : -1;
5444
- const diagnosis = silenceMs > this.streamTimeoutMs ? "camera stopped sending frames" : silenceMs >= 0 ? "stream source closed" : "no frames were ever received";
5445
- this.logger.warn?.(
5446
- `[Go2rtcTcpServer] native stream ended diagnosis="${diagnosis}" lastFrame=${silenceMs >= 0 ? `${(silenceMs / 1e3).toFixed(1)}s ago` : "never"} totalRx=${this.totalFramesReceived} clients=${this.connectedClients.size}`
5447
- );
5448
- if (this.dedicatedSessionRelease) {
5449
- this.dedicatedSessionRelease().catch(() => {
5450
- });
5451
- this.dedicatedSessionRelease = void 0;
5452
- }
5453
- if (!this.prestartStream) {
5454
- this.logger.info?.(
5455
- `[Go2rtcTcpServer] battery native stream ended hadFrames=${hadFrames} channel=${this.channel} profile=${this.profile} \u2014 dropping ${this.connectedClients.size} client(s) to prevent wake loop`
5456
- );
5457
- for (const [, sock] of this.clientSockets) {
5458
- sock.destroy();
5459
- }
5460
- } else if (this.active) {
5461
- if (typeof this.api.isStreamProfileRejected === "function" && this.api.isStreamProfileRejected(this.channel, this.profile)) {
5462
- this.logger.warn?.(
5463
- `[Go2rtcTcpServer] profile rejected by device channel=${this.channel} profile=${this.profile} \u2014 not restarting`
5464
- );
5465
- for (const [, sock] of this.clientSockets) {
5466
- sock.destroy();
5467
- }
5468
- return;
5469
- }
5470
- this.logger.info?.(
5471
- `[Go2rtcTcpServer] restarting native stream (clients=${this.connectedClients.size}, prestart=${this.prestartStream})`
5472
- );
5473
- this.startNativeStream();
5474
- }
5475
- }
5476
- });
5477
- this.nativeFanout.start();
5478
- this.startStreamHealthMonitor();
5479
- }
5480
- async stopNativeStream() {
5481
- this.nativeStreamStopping = true;
5482
- this.nativeStreamActive = false;
5483
- this.clearNativeStreamRetry();
5484
- this.stopStreamHealthMonitor();
5485
- const fanout = this.nativeFanout;
5486
- this.nativeFanout = null;
5487
- try {
5488
- if (fanout) {
5489
- await fanout.stop();
5490
- }
5491
- this.prebuffer = [];
5492
- if (this.dedicatedSessionRelease) {
5493
- await this.dedicatedSessionRelease().catch(() => {
5494
- });
5495
- this.dedicatedSessionRelease = void 0;
5496
- }
5497
- } finally {
5498
- this.nativeStreamStopping = false;
5499
- }
5500
- }
5501
- // -----------------------------------------------------------------------
5502
- // Stream health monitoring
5503
- // -----------------------------------------------------------------------
5504
- startStreamHealthMonitor() {
5505
- this.stopStreamHealthMonitor();
5506
- if (this.streamTimeoutMs <= 0) return;
5507
- this.lastFrameAt = Date.now();
5508
- this.streamHealthTimer = setInterval(() => {
5509
- if (!this.nativeStreamActive || !this.active) {
5510
- this.stopStreamHealthMonitor();
5511
- return;
5512
- }
5513
- const silenceMs = Date.now() - this.lastFrameAt;
5514
- if (silenceMs > this.streamTimeoutMs) {
5515
- this.logger.warn?.(
5516
- `[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`
5517
- );
5518
- this.stopStreamHealthMonitor();
5519
- const fanout = this.nativeFanout;
5520
- if (fanout) {
5521
- this.nativeStreamActive = false;
5522
- this.nativeFanout = null;
5523
- fanout.stop().catch(() => {
5524
- });
5525
- }
5526
- }
5527
- }, Math.min(this.streamTimeoutMs / 2, 5e3));
5528
- }
5529
- stopStreamHealthMonitor() {
5530
- if (this.streamHealthTimer) {
5531
- clearInterval(this.streamHealthTimer);
5532
- this.streamHealthTimer = void 0;
5533
- }
5534
- }
5535
- // -----------------------------------------------------------------------
5536
- // Client lifecycle
5537
- // -----------------------------------------------------------------------
5538
- removeClient(clientId, reason) {
5539
- if (!this.connectedClients.has(clientId)) return;
5540
- this.connectedClients.delete(clientId);
5541
- this.clientSockets.delete(clientId);
5542
- const silenceMs = this.lastFrameAt > 0 ? Date.now() - this.lastFrameAt : -1;
5543
- const silenceInfo = silenceMs >= 0 ? ` lastFrame=${(silenceMs / 1e3).toFixed(1)}s ago` : "";
5544
- this.logger.info?.(
5545
- `[Go2rtcTcpServer] client disconnected id=${clientId} reason=${reason ?? "unknown"} remaining=${this.connectedClients.size} totalRx=${this.totalFramesReceived} totalTx=${this.totalVideoFramesWritten}${silenceInfo}`
5546
- );
5547
- this.emit("clientDisconnected", clientId);
5548
- if (this.connectedClients.size === 0 && !this.prestartStream) {
5549
- this.scheduleStop();
5550
- }
5551
- }
5552
- scheduleStop() {
5553
- if (this.stopGraceTimer) return;
5554
- this.logger.info?.(
5555
- `[Go2rtcTcpServer] no clients, scheduling stream stop in ${this.gracePeriodMs}ms`
5556
- );
5557
- this.stopGraceTimer = setTimeout(async () => {
5558
- this.stopGraceTimer = void 0;
5559
- if (this.connectedClients.size === 0 && this.nativeStreamActive) {
5560
- this.logger.info?.("[Go2rtcTcpServer] grace period expired, stopping native stream");
5561
- await this.stopNativeStream();
5562
- }
5563
- }, this.gracePeriodMs);
5564
- }
5565
- };
5566
-
5567
4766
  // src/baichuan/stream/BaichuanHttpStreamServer.ts
5568
- import { EventEmitter as EventEmitter3 } from "events";
4767
+ import { EventEmitter as EventEmitter2 } from "events";
5569
4768
  import { spawn as spawn5 } from "child_process";
5570
4769
  import * as http4 from "http";
5571
4770
  var NAL_START_CODE_4B = Buffer.from([0, 0, 0, 1]);
@@ -5612,7 +4811,7 @@ function isH264KeyframeFromAnnexB(annexB) {
5612
4811
  }
5613
4812
  return false;
5614
4813
  }
5615
- var BaichuanHttpStreamServer = class extends EventEmitter3 {
4814
+ var BaichuanHttpStreamServer = class extends EventEmitter2 {
5616
4815
  videoStream;
5617
4816
  listenPort;
5618
4817
  path;
@@ -5884,15 +5083,15 @@ var BaichuanHttpStreamServer = class extends EventEmitter3 {
5884
5083
  };
5885
5084
 
5886
5085
  // src/baichuan/stream/BaichuanMjpegServer.ts
5887
- import { EventEmitter as EventEmitter5 } from "events";
5086
+ import { EventEmitter as EventEmitter4 } from "events";
5888
5087
  import * as http5 from "http";
5889
5088
 
5890
5089
  // src/baichuan/stream/MjpegTransformer.ts
5891
- import { EventEmitter as EventEmitter4 } from "events";
5090
+ import { EventEmitter as EventEmitter3 } from "events";
5892
5091
  import { spawn as spawn6 } from "child_process";
5893
5092
  var JPEG_SOI = Buffer.from([255, 216]);
5894
5093
  var JPEG_EOI = Buffer.from([255, 217]);
5895
- var MjpegTransformer = class extends EventEmitter4 {
5094
+ var MjpegTransformer = class extends EventEmitter3 {
5896
5095
  options;
5897
5096
  ffmpeg = null;
5898
5097
  started = false;
@@ -6089,7 +5288,7 @@ Content-Length: ${frame.length}\r
6089
5288
  }
6090
5289
 
6091
5290
  // src/baichuan/stream/BaichuanMjpegServer.ts
6092
- var BaichuanMjpegServer = class extends EventEmitter5 {
5291
+ var BaichuanMjpegServer = class extends EventEmitter4 {
6093
5292
  options;
6094
5293
  clients = /* @__PURE__ */ new Map();
6095
5294
  httpServer = null;
@@ -6370,13 +5569,13 @@ var BaichuanMjpegServer = class extends EventEmitter5 {
6370
5569
  };
6371
5570
 
6372
5571
  // src/baichuan/stream/BaichuanWebRTCServer.ts
6373
- import { EventEmitter as EventEmitter7 } from "events";
5572
+ import { EventEmitter as EventEmitter6 } from "events";
6374
5573
 
6375
5574
  // src/baichuan/stream/AacToOpusTranscoder.ts
6376
5575
  import { spawn as spawn7 } from "child_process";
6377
5576
  import { createSocket } from "dgram";
6378
- import { EventEmitter as EventEmitter6 } from "events";
6379
- var AacToOpusTranscoder = class extends EventEmitter6 {
5577
+ import { EventEmitter as EventEmitter5 } from "events";
5578
+ var AacToOpusTranscoder = class extends EventEmitter5 {
6380
5579
  opts;
6381
5580
  socket = null;
6382
5581
  ffmpeg = null;
@@ -6592,7 +5791,7 @@ function getH264NalType(nalUnit) {
6592
5791
  function getH265NalType2(nalUnit) {
6593
5792
  return nalUnit[0] >> 1 & 63;
6594
5793
  }
6595
- var BaichuanWebRTCServer = class extends EventEmitter7 {
5794
+ var BaichuanWebRTCServer = class extends EventEmitter6 {
6596
5795
  options;
6597
5796
  sessions = /* @__PURE__ */ new Map();
6598
5797
  sessionIdCounter = 0;
@@ -7584,7 +6783,7 @@ Error: ${err}`
7584
6783
  };
7585
6784
 
7586
6785
  // src/baichuan/stream/BaichuanHlsServer.ts
7587
- import { EventEmitter as EventEmitter8 } from "events";
6786
+ import { EventEmitter as EventEmitter7 } from "events";
7588
6787
  import fs from "fs";
7589
6788
  import fsp from "fs/promises";
7590
6789
  import os from "os";
@@ -7661,7 +6860,7 @@ function getNalTypes(codec, annexB) {
7661
6860
  }
7662
6861
  });
7663
6862
  }
7664
- var BaichuanHlsServer = class extends EventEmitter8 {
6863
+ var BaichuanHlsServer = class extends EventEmitter7 {
7665
6864
  api;
7666
6865
  channel;
7667
6866
  profile;
@@ -8086,10 +7285,10 @@ var BaichuanHlsServer = class extends EventEmitter8 {
8086
7285
  };
8087
7286
 
8088
7287
  // src/multifocal/compositeRtspServer.ts
8089
- import { EventEmitter as EventEmitter9 } from "events";
7288
+ import { EventEmitter as EventEmitter8 } from "events";
8090
7289
  import { spawn as spawn9 } from "child_process";
8091
- import * as net2 from "net";
8092
- var CompositeRtspServer = class extends EventEmitter9 {
7290
+ import * as net from "net";
7291
+ var CompositeRtspServer = class extends EventEmitter8 {
8093
7292
  options;
8094
7293
  compositeStream = null;
8095
7294
  rtspServer = null;
@@ -8155,7 +7354,7 @@ var CompositeRtspServer = class extends EventEmitter9 {
8155
7354
  const width = widerStreamInfo?.width ?? 1920;
8156
7355
  const height = widerStreamInfo?.height ?? 1080;
8157
7356
  const fps = widerStreamInfo?.frameRate ?? 25;
8158
- this.rtspServer = net2.createServer((socket) => {
7357
+ this.rtspServer = net.createServer((socket) => {
8159
7358
  this.handleRtspConnection(socket);
8160
7359
  });
8161
7360
  await new Promise((resolve, reject) => {
@@ -8865,8 +8064,8 @@ var RtspBackchannel = class _RtspBackchannel {
8865
8064
  };
8866
8065
 
8867
8066
  // src/baichuan/stream/BaichuanRtspBackchannelServer.ts
8868
- import { EventEmitter as EventEmitter10 } from "events";
8869
- import * as net3 from "net";
8067
+ import { EventEmitter as EventEmitter9 } from "events";
8068
+ import * as net2 from "net";
8870
8069
  import * as crypto2 from "crypto";
8871
8070
  var md5Hex = (s) => crypto2.createHash("md5").update(s).digest("hex");
8872
8071
  var RTCP_KEEPALIVE_INTERVAL_MS = 1e4;
@@ -8922,7 +8121,7 @@ function extractPublicEndpoint(url, requestText) {
8922
8121
  if (hostHeader) return hostHeader;
8923
8122
  return null;
8924
8123
  }
8925
- var BaichuanRtspBackchannelServer = class _BaichuanRtspBackchannelServer extends EventEmitter10 {
8124
+ var BaichuanRtspBackchannelServer = class _BaichuanRtspBackchannelServer extends EventEmitter9 {
8926
8125
  listenHost;
8927
8126
  listenPort;
8928
8127
  logger;
@@ -9039,7 +8238,7 @@ var BaichuanRtspBackchannelServer = class _BaichuanRtspBackchannelServer extends
9039
8238
  async start() {
9040
8239
  if (this.server) return;
9041
8240
  await new Promise((resolve, reject) => {
9042
- const server = net3.createServer((socket) => this.handleConnection(socket));
8241
+ const server = net2.createServer((socket) => this.handleConnection(socket));
9043
8242
  const onError = (err) => {
9044
8243
  server.removeListener("error", onError);
9045
8244
  reject(err);
@@ -10066,7 +9265,6 @@ export {
10066
9265
  DUAL_LENS_DUAL_MOTION_MODELS,
10067
9266
  DUAL_LENS_MODELS,
10068
9267
  DUAL_LENS_SINGLE_MOTION_MODELS,
10069
- Go2rtcTcpServer,
10070
9268
  H264RtpDepacketizer,
10071
9269
  H265RtpDepacketizer,
10072
9270
  HlsSessionManager,