@apocaliss92/nodelink-js 0.4.6 → 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-F2Y5U3YP.js → chunk-VBYF3BQX.js} +730 -357
- 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 +696 -325
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +2 -2
- package/dist/index.cjs +946 -351
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +299 -6
- package/dist/index.d.ts +283 -5
- package/dist/index.js +250 -26
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-F2Y5U3YP.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) => {
|
|
@@ -4725,6 +4727,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
|
|
|
4725
4727
|
gracePeriodMs;
|
|
4726
4728
|
prebufferMaxMs;
|
|
4727
4729
|
maxBufferBytes;
|
|
4730
|
+
streamTimeoutMs;
|
|
4728
4731
|
prestartStream;
|
|
4729
4732
|
active = false;
|
|
4730
4733
|
server;
|
|
@@ -4738,8 +4741,16 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
|
|
|
4738
4741
|
connectedClients = /* @__PURE__ */ new Set();
|
|
4739
4742
|
clientSockets = /* @__PURE__ */ new Map();
|
|
4740
4743
|
stopGraceTimer;
|
|
4744
|
+
// Stream health monitoring
|
|
4745
|
+
lastFrameAt = 0;
|
|
4746
|
+
streamHealthTimer;
|
|
4747
|
+
totalFramesReceived = 0;
|
|
4748
|
+
totalVideoFramesWritten = 0;
|
|
4741
4749
|
// Prebuffer
|
|
4742
4750
|
prebuffer = [];
|
|
4751
|
+
// Audio metadata — populated on first valid ADTS AAC frame.
|
|
4752
|
+
// Exposed via getAudioInfo() for the stream-diagnostics feature.
|
|
4753
|
+
audioInfo = null;
|
|
4743
4754
|
constructor(options) {
|
|
4744
4755
|
super();
|
|
4745
4756
|
this.api = options.api;
|
|
@@ -4753,6 +4764,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
|
|
|
4753
4764
|
this.gracePeriodMs = options.gracePeriodMs ?? 3e4;
|
|
4754
4765
|
this.prebufferMaxMs = options.prebufferMs ?? 3e3;
|
|
4755
4766
|
this.maxBufferBytes = options.maxBufferBytes ?? 1e8;
|
|
4767
|
+
this.streamTimeoutMs = options.streamTimeoutMs ?? 15e3;
|
|
4756
4768
|
this.prestartStream = options.prestartStream ?? true;
|
|
4757
4769
|
}
|
|
4758
4770
|
// -----------------------------------------------------------------------
|
|
@@ -4791,6 +4803,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
|
|
|
4791
4803
|
if (!this.active) return;
|
|
4792
4804
|
this.active = false;
|
|
4793
4805
|
clearTimeout(this.stopGraceTimer);
|
|
4806
|
+
this.stopStreamHealthMonitor();
|
|
4794
4807
|
for (const [id, sock] of this.clientSockets) {
|
|
4795
4808
|
sock.destroy();
|
|
4796
4809
|
this.connectedClients.delete(id);
|
|
@@ -4821,6 +4834,45 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
|
|
|
4821
4834
|
return this.connectedClients.size;
|
|
4822
4835
|
}
|
|
4823
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
|
+
// -----------------------------------------------------------------------
|
|
4824
4876
|
// Client handling
|
|
4825
4877
|
// -----------------------------------------------------------------------
|
|
4826
4878
|
handleClient(socket) {
|
|
@@ -4844,12 +4896,12 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
|
|
|
4844
4896
|
`[Go2rtcTcpServer] feedClient error id=${clientId}: ${err}`
|
|
4845
4897
|
);
|
|
4846
4898
|
});
|
|
4847
|
-
const cleanup = () => {
|
|
4848
|
-
this.removeClient(clientId);
|
|
4899
|
+
const cleanup = (reason) => {
|
|
4900
|
+
this.removeClient(clientId, reason);
|
|
4849
4901
|
socket.destroy();
|
|
4850
4902
|
};
|
|
4851
|
-
socket.on("error", cleanup);
|
|
4852
|
-
socket.on("close", cleanup);
|
|
4903
|
+
socket.on("error", (err) => cleanup(`error: ${err.message}`));
|
|
4904
|
+
socket.on("close", (hadError) => cleanup(hadError ? "close (with error)" : "close (clean)"));
|
|
4853
4905
|
}
|
|
4854
4906
|
async feedClient(clientId, socket) {
|
|
4855
4907
|
const fanoutDeadline = Date.now() + 3e4;
|
|
@@ -4865,6 +4917,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
|
|
|
4865
4917
|
}
|
|
4866
4918
|
if (!this.active || !this.nativeFanout) return;
|
|
4867
4919
|
const subscription = this.nativeFanout.subscribe(clientId);
|
|
4920
|
+
let muxer = null;
|
|
4868
4921
|
const prebufferSnap = this.prebuffer.slice();
|
|
4869
4922
|
let lastIdrIdx = -1;
|
|
4870
4923
|
for (let i = prebufferSnap.length - 1; i >= 0; i--) {
|
|
@@ -4878,9 +4931,21 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
|
|
|
4878
4931
|
this.logger.info?.(
|
|
4879
4932
|
`[Go2rtcTcpServer] prebuffer replay client=${clientId} frames=${replay.length}`
|
|
4880
4933
|
);
|
|
4934
|
+
if (!muxer) {
|
|
4935
|
+
muxer = new MpegTsMuxer({
|
|
4936
|
+
videoType: this.detectedVideoType ?? "H264",
|
|
4937
|
+
includeAudio: true
|
|
4938
|
+
});
|
|
4939
|
+
}
|
|
4881
4940
|
for (const entry of replay) {
|
|
4882
4941
|
if (socket.destroyed) return;
|
|
4883
|
-
|
|
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);
|
|
4884
4949
|
}
|
|
4885
4950
|
}
|
|
4886
4951
|
let seenKeyframe = lastIdrIdx >= 0;
|
|
@@ -4899,17 +4964,35 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
|
|
|
4899
4964
|
break;
|
|
4900
4965
|
}
|
|
4901
4966
|
liveFrameCount++;
|
|
4902
|
-
|
|
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);
|
|
4903
4976
|
if (!annexB) continue;
|
|
4977
|
+
const isKf = this.isAnnexBKeyframe(annexB, frame.videoType);
|
|
4904
4978
|
if (!seenKeyframe) {
|
|
4905
|
-
if (!
|
|
4979
|
+
if (!isKf) continue;
|
|
4906
4980
|
seenKeyframe = true;
|
|
4907
4981
|
this.logger.info?.(
|
|
4908
4982
|
`[Go2rtcTcpServer] first live keyframe client=${clientId} after ${liveFrameCount} frames`
|
|
4909
4983
|
);
|
|
4984
|
+
if (!muxer) {
|
|
4985
|
+
muxer = new MpegTsMuxer({
|
|
4986
|
+
videoType: frame.videoType ?? this.detectedVideoType ?? "H264",
|
|
4987
|
+
includeAudio: true
|
|
4988
|
+
});
|
|
4989
|
+
}
|
|
4910
4990
|
}
|
|
4911
|
-
|
|
4991
|
+
const pts = frame.microseconds ?? Date.now() * 1e3;
|
|
4992
|
+
const ts = muxer.muxVideo(annexB, pts, isKf);
|
|
4993
|
+
socket.write(ts);
|
|
4912
4994
|
liveVideoWritten++;
|
|
4995
|
+
this.totalVideoFramesWritten++;
|
|
4913
4996
|
if (Date.now() - lastLogAt > 1e4) {
|
|
4914
4997
|
this.logger.info?.(
|
|
4915
4998
|
`[Go2rtcTcpServer] live stats client=${clientId} received=${liveFrameCount} written=${liveVideoWritten} bufLen=${socket.writableLength}`
|
|
@@ -4936,14 +5019,11 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
|
|
|
4936
5019
|
// Frame conversion
|
|
4937
5020
|
// -----------------------------------------------------------------------
|
|
4938
5021
|
/**
|
|
4939
|
-
* Convert a native frame to
|
|
4940
|
-
*
|
|
4941
|
-
* 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).
|
|
4942
5024
|
*/
|
|
4943
|
-
|
|
4944
|
-
if (frame.audio)
|
|
4945
|
-
return null;
|
|
4946
|
-
}
|
|
5025
|
+
convertVideoFrame(frame) {
|
|
5026
|
+
if (frame.audio) return null;
|
|
4947
5027
|
if (frame.data.length === 0) return null;
|
|
4948
5028
|
try {
|
|
4949
5029
|
if (frame.videoType === "H264") {
|
|
@@ -5007,10 +5087,71 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
|
|
|
5007
5087
|
return nals;
|
|
5008
5088
|
}
|
|
5009
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
|
+
// -----------------------------------------------------------------------
|
|
5010
5132
|
// Native stream management
|
|
5011
5133
|
// -----------------------------------------------------------------------
|
|
5012
5134
|
async startNativeStream() {
|
|
5013
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
|
+
}
|
|
5014
5155
|
this.nativeStreamActive = true;
|
|
5015
5156
|
let dedicatedClient;
|
|
5016
5157
|
if (this.deviceId) {
|
|
@@ -5029,6 +5170,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
|
|
|
5029
5170
|
this.logger.info?.(
|
|
5030
5171
|
`[Go2rtcTcpServer] native stream starting channel=${this.channel} profile=${this.profile} dedicated=${!!dedicatedClient}`
|
|
5031
5172
|
);
|
|
5173
|
+
let hadFrames = false;
|
|
5032
5174
|
this.nativeFanout = new NativeStreamFanout({
|
|
5033
5175
|
maxQueueItems: 200,
|
|
5034
5176
|
createSource: () => createNativeStream(this.api, this.channel, this.profile, {
|
|
@@ -5036,17 +5178,37 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
|
|
|
5036
5178
|
...dedicatedClient ? { client: dedicatedClient } : {}
|
|
5037
5179
|
}),
|
|
5038
5180
|
onFrame: (frame) => {
|
|
5181
|
+
hadFrames = true;
|
|
5182
|
+
this.lastFrameAt = Date.now();
|
|
5183
|
+
this.totalFramesReceived++;
|
|
5039
5184
|
if (!frame.audio && (frame.videoType === "H264" || frame.videoType === "H265")) {
|
|
5040
5185
|
this.detectedVideoType = frame.videoType;
|
|
5041
5186
|
}
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
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;
|
|
5045
5206
|
this.prebuffer.push({
|
|
5046
|
-
data: Buffer.from(
|
|
5207
|
+
data: Buffer.from(prebufData),
|
|
5047
5208
|
time: Date.now(),
|
|
5048
5209
|
isKeyframe,
|
|
5049
|
-
audio: frame.audio
|
|
5210
|
+
audio: frame.audio,
|
|
5211
|
+
pts
|
|
5050
5212
|
});
|
|
5051
5213
|
const cutoff = Date.now() - this.prebufferMaxMs;
|
|
5052
5214
|
let trimIdx = 0;
|
|
@@ -5062,23 +5224,47 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
|
|
|
5062
5224
|
if (!this.nativeStreamActive) return;
|
|
5063
5225
|
this.nativeStreamActive = false;
|
|
5064
5226
|
this.nativeFanout = null;
|
|
5227
|
+
this.stopStreamHealthMonitor();
|
|
5228
|
+
const silenceMs = this.lastFrameAt > 0 ? Date.now() - this.lastFrameAt : -1;
|
|
5229
|
+
const diagnosis = silenceMs > this.streamTimeoutMs ? "camera stopped sending frames" : silenceMs >= 0 ? "stream source closed" : "no frames were ever received";
|
|
5230
|
+
this.logger.warn?.(
|
|
5231
|
+
`[Go2rtcTcpServer] native stream ended diagnosis="${diagnosis}" lastFrame=${silenceMs >= 0 ? `${(silenceMs / 1e3).toFixed(1)}s ago` : "never"} totalRx=${this.totalFramesReceived} clients=${this.connectedClients.size}`
|
|
5232
|
+
);
|
|
5065
5233
|
if (this.dedicatedSessionRelease) {
|
|
5066
5234
|
this.dedicatedSessionRelease().catch(() => {
|
|
5067
5235
|
});
|
|
5068
5236
|
this.dedicatedSessionRelease = void 0;
|
|
5069
5237
|
}
|
|
5070
|
-
if (this.
|
|
5238
|
+
if (!this.prestartStream) {
|
|
5071
5239
|
this.logger.info?.(
|
|
5072
|
-
`[Go2rtcTcpServer] native stream ended
|
|
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
|
+
}
|
|
5255
|
+
this.logger.info?.(
|
|
5256
|
+
`[Go2rtcTcpServer] restarting native stream (clients=${this.connectedClients.size}, prestart=${this.prestartStream})`
|
|
5073
5257
|
);
|
|
5074
5258
|
this.startNativeStream();
|
|
5075
5259
|
}
|
|
5076
5260
|
}
|
|
5077
5261
|
});
|
|
5078
5262
|
this.nativeFanout.start();
|
|
5263
|
+
this.startStreamHealthMonitor();
|
|
5079
5264
|
}
|
|
5080
5265
|
async stopNativeStream() {
|
|
5081
5266
|
this.nativeStreamActive = false;
|
|
5267
|
+
this.stopStreamHealthMonitor();
|
|
5082
5268
|
const fanout = this.nativeFanout;
|
|
5083
5269
|
this.nativeFanout = null;
|
|
5084
5270
|
if (fanout) {
|
|
@@ -5092,14 +5278,50 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends EventEmitter2 {
|
|
|
5092
5278
|
}
|
|
5093
5279
|
}
|
|
5094
5280
|
// -----------------------------------------------------------------------
|
|
5281
|
+
// Stream health monitoring
|
|
5282
|
+
// -----------------------------------------------------------------------
|
|
5283
|
+
startStreamHealthMonitor() {
|
|
5284
|
+
this.stopStreamHealthMonitor();
|
|
5285
|
+
if (this.streamTimeoutMs <= 0) return;
|
|
5286
|
+
this.lastFrameAt = Date.now();
|
|
5287
|
+
this.streamHealthTimer = setInterval(() => {
|
|
5288
|
+
if (!this.nativeStreamActive || !this.active) {
|
|
5289
|
+
this.stopStreamHealthMonitor();
|
|
5290
|
+
return;
|
|
5291
|
+
}
|
|
5292
|
+
const silenceMs = Date.now() - this.lastFrameAt;
|
|
5293
|
+
if (silenceMs > this.streamTimeoutMs) {
|
|
5294
|
+
this.logger.warn?.(
|
|
5295
|
+
`[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`
|
|
5296
|
+
);
|
|
5297
|
+
this.stopStreamHealthMonitor();
|
|
5298
|
+
const fanout = this.nativeFanout;
|
|
5299
|
+
if (fanout) {
|
|
5300
|
+
this.nativeStreamActive = false;
|
|
5301
|
+
this.nativeFanout = null;
|
|
5302
|
+
fanout.stop().catch(() => {
|
|
5303
|
+
});
|
|
5304
|
+
}
|
|
5305
|
+
}
|
|
5306
|
+
}, Math.min(this.streamTimeoutMs / 2, 5e3));
|
|
5307
|
+
}
|
|
5308
|
+
stopStreamHealthMonitor() {
|
|
5309
|
+
if (this.streamHealthTimer) {
|
|
5310
|
+
clearInterval(this.streamHealthTimer);
|
|
5311
|
+
this.streamHealthTimer = void 0;
|
|
5312
|
+
}
|
|
5313
|
+
}
|
|
5314
|
+
// -----------------------------------------------------------------------
|
|
5095
5315
|
// Client lifecycle
|
|
5096
5316
|
// -----------------------------------------------------------------------
|
|
5097
|
-
removeClient(clientId) {
|
|
5317
|
+
removeClient(clientId, reason) {
|
|
5098
5318
|
if (!this.connectedClients.has(clientId)) return;
|
|
5099
5319
|
this.connectedClients.delete(clientId);
|
|
5100
5320
|
this.clientSockets.delete(clientId);
|
|
5321
|
+
const silenceMs = this.lastFrameAt > 0 ? Date.now() - this.lastFrameAt : -1;
|
|
5322
|
+
const silenceInfo = silenceMs >= 0 ? ` lastFrame=${(silenceMs / 1e3).toFixed(1)}s ago` : "";
|
|
5101
5323
|
this.logger.info?.(
|
|
5102
|
-
`[Go2rtcTcpServer] client disconnected id=${clientId} remaining=${this.connectedClients.size}`
|
|
5324
|
+
`[Go2rtcTcpServer] client disconnected id=${clientId} reason=${reason ?? "unknown"} remaining=${this.connectedClients.size} totalRx=${this.totalFramesReceived} totalTx=${this.totalVideoFramesWritten}${silenceInfo}`
|
|
5103
5325
|
);
|
|
5104
5326
|
this.emit("clientDisconnected", clientId);
|
|
5105
5327
|
if (this.connectedClients.size === 0 && !this.prestartStream) {
|
|
@@ -7704,6 +7926,7 @@ export {
|
|
|
7704
7926
|
HlsSessionManager,
|
|
7705
7927
|
Intercom,
|
|
7706
7928
|
MjpegTransformer,
|
|
7929
|
+
MpegTsMuxer,
|
|
7707
7930
|
NVR_HUB_EXACT_TYPES,
|
|
7708
7931
|
NVR_HUB_MODEL_PATTERNS,
|
|
7709
7932
|
ReolinkBaichuanApi,
|
|
@@ -7762,6 +7985,7 @@ export {
|
|
|
7762
7985
|
createRfc4571TcpServerForReplay,
|
|
7763
7986
|
createRtspProxyServer,
|
|
7764
7987
|
createTaggedLogger,
|
|
7988
|
+
decideSleepInferenceTransition,
|
|
7765
7989
|
decideVideoclipTranscodeMode,
|
|
7766
7990
|
decodeHeader,
|
|
7767
7991
|
deriveAesKey,
|