@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/{DiagnosticsTools-UMN4C7SY.js → DiagnosticsTools-HJDH4GPP.js} +2 -2
- package/dist/{chunk-GKLOJJ34.js → chunk-VBYF3BQX.js} +639 -341
- package/dist/chunk-VBYF3BQX.js.map +1 -0
- package/dist/{chunk-TR3V5FTO.js → chunk-YKKQDUKU.js} +3 -3
- package/dist/{chunk-TR3V5FTO.js.map → chunk-YKKQDUKU.js.map} +1 -1
- package/dist/cli/rtsp-server.cjs +607 -311
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +2 -2
- package/dist/index.cjs +793 -328
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +254 -6
- package/dist/index.d.ts +238 -5
- package/dist/index.js +188 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-GKLOJJ34.js.map +0 -1
- /package/dist/{DiagnosticsTools-UMN4C7SY.js.map → DiagnosticsTools-HJDH4GPP.js.map} +0 -0
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
4949
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
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(
|
|
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.
|
|
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,
|