@apocaliss92/nodelink-js 0.4.7 → 0.4.8

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
@@ -8,6 +8,7 @@ import {
8
8
  DUAL_LENS_MODELS,
9
9
  DUAL_LENS_SINGLE_MOTION_MODELS,
10
10
  Intercom,
11
+ MpegTsMuxer,
11
12
  NVR_HUB_EXACT_TYPES,
12
13
  NVR_HUB_MODEL_PATTERNS,
13
14
  ReolinkBaichuanApi,
@@ -20,6 +21,7 @@ import {
20
21
  createNativeStream,
21
22
  createNullLogger,
22
23
  createTaggedLogger,
24
+ decideSleepInferenceTransition,
23
25
  decodeHeader,
24
26
  discoverReolinkDevices,
25
27
  discoverViaArpTable,
@@ -43,7 +45,7 @@ import {
43
45
  parseSupportXml,
44
46
  setGlobalLogger,
45
47
  xmlIndicatesFloodlight
46
- } from "./chunk-GKLOJJ34.js";
48
+ } from "./chunk-VBYF3BQX.js";
47
49
  import {
48
50
  AesStreamDecryptor,
49
51
  BC_AES_IV,
@@ -223,7 +225,7 @@ import {
223
225
  testChannelStreams,
224
226
  xmlEscape,
225
227
  zipDirectory
226
- } from "./chunk-TR3V5FTO.js";
228
+ } from "./chunk-YKKQDUKU.js";
227
229
 
228
230
  // src/reolink/baichuan/HlsSessionManager.ts
229
231
  var withTimeout = async (p, ms, label) => {
@@ -4746,6 +4748,9 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
4746
4748
  totalVideoFramesWritten = 0;
4747
4749
  // Prebuffer
4748
4750
  prebuffer = [];
4751
+ // Audio metadata — populated on first valid ADTS AAC frame.
4752
+ // Exposed via getAudioInfo() for the stream-diagnostics feature.
4753
+ audioInfo = null;
4749
4754
  constructor(options) {
4750
4755
  super();
4751
4756
  this.api = options.api;
@@ -4829,6 +4834,45 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
4829
4834
  return this.connectedClients.size;
4830
4835
  }
4831
4836
  // -----------------------------------------------------------------------
4837
+ // Diagnostic subscription API (implements DiagnosticStreamServer)
4838
+ //
4839
+ // Matches the shape of BaichuanRtspServer's diagnostic API so the
4840
+ // stream-diagnostic feature in the Manager app can drive either backend
4841
+ // with identical code.
4842
+ // -----------------------------------------------------------------------
4843
+ /**
4844
+ * Subscribe to the raw native stream for diagnostic purposes.
4845
+ * The subscriber receives the same frames the MPEG-TS muxer consumes
4846
+ * (pre-muxing). Counts as a "consumer" so the native stream is kept alive
4847
+ * for the lifetime of the subscription. If the stream is not already
4848
+ * running (battery camera, prestart=false), this starts it.
4849
+ */
4850
+ async subscribeDiagnostic(id) {
4851
+ this.connectedClients.add(`diag:${id}`);
4852
+ if (!this.nativeStreamActive) {
4853
+ await this.startNativeStream();
4854
+ }
4855
+ if (!this.nativeFanout) {
4856
+ this.connectedClients.delete(`diag:${id}`);
4857
+ throw new Error(
4858
+ "Go2rtcTcpServer: native stream failed to start \u2014 cannot subscribe diagnostic"
4859
+ );
4860
+ }
4861
+ return this.nativeFanout.subscribe(`diag:${id}`);
4862
+ }
4863
+ /** Unsubscribe a diagnostic session and release its consumer slot. */
4864
+ unsubscribeDiagnostic(id) {
4865
+ this.removeClient(`diag:${id}`, "diagnostic unsubscribe");
4866
+ }
4867
+ /**
4868
+ * Returns ADTS AAC audio metadata detected from the native stream, or
4869
+ * null if no audio frame has been observed yet (e.g. video-only cameras
4870
+ * or before the first audio packet arrives).
4871
+ */
4872
+ getAudioInfo() {
4873
+ return this.audioInfo;
4874
+ }
4875
+ // -----------------------------------------------------------------------
4832
4876
  // Client handling
4833
4877
  // -----------------------------------------------------------------------
4834
4878
  handleClient(socket) {
@@ -4873,6 +4917,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
4873
4917
  }
4874
4918
  if (!this.active || !this.nativeFanout) return;
4875
4919
  const subscription = this.nativeFanout.subscribe(clientId);
4920
+ let muxer = null;
4876
4921
  const prebufferSnap = this.prebuffer.slice();
4877
4922
  let lastIdrIdx = -1;
4878
4923
  for (let i = prebufferSnap.length - 1; i >= 0; i--) {
@@ -4886,9 +4931,21 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
4886
4931
  this.logger.info?.(
4887
4932
  `[Go2rtcTcpServer] prebuffer replay client=${clientId} frames=${replay.length}`
4888
4933
  );
4934
+ if (!muxer) {
4935
+ muxer = new MpegTsMuxer({
4936
+ videoType: this.detectedVideoType ?? "H264",
4937
+ includeAudio: true
4938
+ });
4939
+ }
4889
4940
  for (const entry of replay) {
4890
4941
  if (socket.destroyed) return;
4891
- socket.write(entry.data);
4942
+ let ts;
4943
+ if (!entry.audio) {
4944
+ ts = muxer.muxVideo(entry.data, entry.pts, entry.isKeyframe);
4945
+ } else {
4946
+ ts = muxer.muxAudio(entry.data, entry.pts);
4947
+ }
4948
+ if (ts.length > 0) socket.write(ts);
4892
4949
  }
4893
4950
  }
4894
4951
  let seenKeyframe = lastIdrIdx >= 0;
@@ -4907,16 +4964,33 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
4907
4964
  break;
4908
4965
  }
4909
4966
  liveFrameCount++;
4910
- const annexB = this.convertFrame(frame);
4967
+ if (frame.audio) {
4968
+ if (muxer) {
4969
+ const pts2 = frame.microseconds ?? Date.now() * 1e3;
4970
+ const ts2 = muxer.muxAudio(frame.data, pts2);
4971
+ if (ts2.length > 0) socket.write(ts2);
4972
+ }
4973
+ continue;
4974
+ }
4975
+ const annexB = this.convertVideoFrame(frame);
4911
4976
  if (!annexB) continue;
4977
+ const isKf = this.isAnnexBKeyframe(annexB, frame.videoType);
4912
4978
  if (!seenKeyframe) {
4913
- if (!this.isAnnexBKeyframe(annexB, frame.videoType)) continue;
4979
+ if (!isKf) continue;
4914
4980
  seenKeyframe = true;
4915
4981
  this.logger.info?.(
4916
4982
  `[Go2rtcTcpServer] first live keyframe client=${clientId} after ${liveFrameCount} frames`
4917
4983
  );
4984
+ if (!muxer) {
4985
+ muxer = new MpegTsMuxer({
4986
+ videoType: frame.videoType ?? this.detectedVideoType ?? "H264",
4987
+ includeAudio: true
4988
+ });
4989
+ }
4918
4990
  }
4919
- socket.write(annexB);
4991
+ const pts = frame.microseconds ?? Date.now() * 1e3;
4992
+ const ts = muxer.muxVideo(annexB, pts, isKf);
4993
+ socket.write(ts);
4920
4994
  liveVideoWritten++;
4921
4995
  this.totalVideoFramesWritten++;
4922
4996
  if (Date.now() - lastLogAt > 1e4) {
@@ -4945,14 +5019,11 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
4945
5019
  // Frame conversion
4946
5020
  // -----------------------------------------------------------------------
4947
5021
  /**
4948
- * Convert a native frame to wire-ready Annex-B.
4949
- * Audio frames are skipped raw TCP carries only video (Annex-B).
4950
- * go2rtc auto-detects the codec from SPS/PPS/VPS NALUs.
5022
+ * Convert a native video frame to Annex-B.
5023
+ * Returns null for audio frames (handled separately by muxAudio).
4951
5024
  */
4952
- convertFrame(frame) {
4953
- if (frame.audio) {
4954
- return null;
4955
- }
5025
+ convertVideoFrame(frame) {
5026
+ if (frame.audio) return null;
4956
5027
  if (frame.data.length === 0) return null;
4957
5028
  try {
4958
5029
  if (frame.videoType === "H264") {
@@ -5016,10 +5087,71 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
5016
5087
  return nals;
5017
5088
  }
5018
5089
  // -----------------------------------------------------------------------
5090
+ // ADTS AAC parsing (used for audio metadata exposed via getAudioInfo)
5091
+ // -----------------------------------------------------------------------
5092
+ /** True if `b` starts with an ADTS AAC syncword (0xFFF). */
5093
+ static isAdtsAacFrame(b) {
5094
+ return b.length >= 2 && b[0] === 255 && (b[1] & 240) === 240;
5095
+ }
5096
+ /**
5097
+ * Parse an ADTS header into {sampleRate, channels, AudioSpecificConfig hex}.
5098
+ * Returns null when the buffer is not a valid ADTS frame.
5099
+ */
5100
+ static parseAdtsSamplingInfo(b) {
5101
+ if (b.length < 7) return null;
5102
+ if (!_Go2rtcTcpServer.isAdtsAacFrame(b)) return null;
5103
+ const samplingIndex = b[2] >> 2 & 15;
5104
+ const sampleRates = [
5105
+ 96e3,
5106
+ 88200,
5107
+ 64e3,
5108
+ 48e3,
5109
+ 44100,
5110
+ 32e3,
5111
+ 24e3,
5112
+ 22050,
5113
+ 16e3,
5114
+ 12e3,
5115
+ 11025,
5116
+ 8e3,
5117
+ 7350
5118
+ ];
5119
+ const sampleRate = sampleRates[samplingIndex] ?? null;
5120
+ if (!sampleRate) return null;
5121
+ const channelConfig = (b[2] & 1) << 2 | b[3] >> 6 & 3;
5122
+ const channels = channelConfig === 0 ? 1 : channelConfig;
5123
+ const profile = b[2] >> 6 & 3;
5124
+ const audioObjectType = profile + 1;
5125
+ const asc = audioObjectType << 11 | samplingIndex << 7 | channelConfig << 3;
5126
+ const configHex = Buffer.from([asc >> 8 & 255, asc & 255]).toString(
5127
+ "hex"
5128
+ );
5129
+ return { sampleRate, channels, configHex };
5130
+ }
5131
+ // -----------------------------------------------------------------------
5019
5132
  // Native stream management
5020
5133
  // -----------------------------------------------------------------------
5021
5134
  async startNativeStream() {
5022
5135
  if (this.nativeStreamActive) return;
5136
+ if (!this.api.isReady) {
5137
+ if (this.api.isClosed) {
5138
+ this.logger.warn?.(
5139
+ `[Go2rtcTcpServer] API has been explicitly closed \u2014 stream cannot start`
5140
+ );
5141
+ return;
5142
+ }
5143
+ try {
5144
+ this.logger.info?.(
5145
+ `[Go2rtcTcpServer] API not ready (idle disconnect?), calling ensureConnected`
5146
+ );
5147
+ await this.api.ensureConnected();
5148
+ } catch (e) {
5149
+ this.logger.warn?.(
5150
+ `[Go2rtcTcpServer] ensureConnected failed, aborting stream start: ${e}`
5151
+ );
5152
+ return;
5153
+ }
5154
+ }
5023
5155
  this.nativeStreamActive = true;
5024
5156
  let dedicatedClient;
5025
5157
  if (this.deviceId) {
@@ -5038,6 +5170,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
5038
5170
  this.logger.info?.(
5039
5171
  `[Go2rtcTcpServer] native stream starting channel=${this.channel} profile=${this.profile} dedicated=${!!dedicatedClient}`
5040
5172
  );
5173
+ let hadFrames = false;
5041
5174
  this.nativeFanout = new NativeStreamFanout({
5042
5175
  maxQueueItems: 200,
5043
5176
  createSource: () => createNativeStream(this.api, this.channel, this.profile, {
@@ -5045,19 +5178,37 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
5045
5178
  ...dedicatedClient ? { client: dedicatedClient } : {}
5046
5179
  }),
5047
5180
  onFrame: (frame) => {
5181
+ hadFrames = true;
5048
5182
  this.lastFrameAt = Date.now();
5049
5183
  this.totalFramesReceived++;
5050
5184
  if (!frame.audio && (frame.videoType === "H264" || frame.videoType === "H265")) {
5051
5185
  this.detectedVideoType = frame.videoType;
5052
5186
  }
5053
- const wireData = this.convertFrame(frame);
5054
- if (!wireData || wireData.length === 0) return;
5055
- const isKeyframe = !frame.audio && this.isAnnexBKeyframe(wireData, frame.videoType);
5187
+ let prebufData;
5188
+ let isKeyframe;
5189
+ if (frame.audio) {
5190
+ if (frame.data.length === 0) return;
5191
+ if (!this.audioInfo) {
5192
+ const parsed = _Go2rtcTcpServer.parseAdtsSamplingInfo(frame.data);
5193
+ if (parsed) {
5194
+ this.audioInfo = { codec: "aac-adts", ...parsed };
5195
+ }
5196
+ }
5197
+ prebufData = frame.data;
5198
+ isKeyframe = false;
5199
+ } else {
5200
+ const annexB = this.convertVideoFrame(frame);
5201
+ if (!annexB || annexB.length === 0) return;
5202
+ prebufData = annexB;
5203
+ isKeyframe = this.isAnnexBKeyframe(annexB, frame.videoType);
5204
+ }
5205
+ const pts = frame.microseconds ?? Date.now() * 1e3;
5056
5206
  this.prebuffer.push({
5057
- data: Buffer.from(wireData),
5207
+ data: Buffer.from(prebufData),
5058
5208
  time: Date.now(),
5059
5209
  isKeyframe,
5060
- audio: frame.audio
5210
+ audio: frame.audio,
5211
+ pts
5061
5212
  });
5062
5213
  const cutoff = Date.now() - this.prebufferMaxMs;
5063
5214
  let trimIdx = 0;
@@ -5084,7 +5235,23 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
5084
5235
  });
5085
5236
  this.dedicatedSessionRelease = void 0;
5086
5237
  }
5087
- if (this.active && (this.connectedClients.size > 0 || this.prestartStream)) {
5238
+ if (!this.prestartStream) {
5239
+ this.logger.info?.(
5240
+ `[Go2rtcTcpServer] battery native stream ended hadFrames=${hadFrames} channel=${this.channel} profile=${this.profile} \u2014 dropping ${this.connectedClients.size} client(s) to prevent wake loop`
5241
+ );
5242
+ for (const [, sock] of this.clientSockets) {
5243
+ sock.destroy();
5244
+ }
5245
+ } else if (this.active) {
5246
+ if (typeof this.api.isStreamProfileRejected === "function" && this.api.isStreamProfileRejected(this.channel, this.profile)) {
5247
+ this.logger.warn?.(
5248
+ `[Go2rtcTcpServer] profile rejected by device channel=${this.channel} profile=${this.profile} \u2014 not restarting`
5249
+ );
5250
+ for (const [, sock] of this.clientSockets) {
5251
+ sock.destroy();
5252
+ }
5253
+ return;
5254
+ }
5088
5255
  this.logger.info?.(
5089
5256
  `[Go2rtcTcpServer] restarting native stream (clients=${this.connectedClients.size}, prestart=${this.prestartStream})`
5090
5257
  );
@@ -7759,6 +7926,7 @@ export {
7759
7926
  HlsSessionManager,
7760
7927
  Intercom,
7761
7928
  MjpegTransformer,
7929
+ MpegTsMuxer,
7762
7930
  NVR_HUB_EXACT_TYPES,
7763
7931
  NVR_HUB_MODEL_PATTERNS,
7764
7932
  ReolinkBaichuanApi,
@@ -7817,6 +7985,7 @@ export {
7817
7985
  createRfc4571TcpServerForReplay,
7818
7986
  createRtspProxyServer,
7819
7987
  createTaggedLogger,
7988
+ decideSleepInferenceTransition,
7820
7989
  decideVideoclipTranscodeMode,
7821
7990
  decodeHeader,
7822
7991
  deriveAesKey,