@apocaliss92/nodelink-js 0.4.11 → 0.4.13
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/README.md +24 -0
- package/dist/BaichuanVideoStream-HGPU2MZ3.js +7 -0
- package/dist/{DiagnosticsTools-RNIDFEJK.js → DiagnosticsTools-BQOWJ23L.js} +3 -2
- package/dist/DiagnosticsTools-BQOWJ23L.js.map +1 -0
- package/dist/{chunk-EDLMKBG2.js → chunk-2JNXKT3C.js} +151 -2733
- package/dist/chunk-2JNXKT3C.js.map +1 -0
- package/dist/chunk-C57QV7IL.js +2723 -0
- package/dist/chunk-C57QV7IL.js.map +1 -0
- package/dist/{chunk-HGQ53FB3.js → chunk-R5AJV73A.js} +1348 -47
- package/dist/chunk-R5AJV73A.js.map +1 -0
- package/dist/cli/rtsp-server.cjs +1422 -12
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +3 -2
- package/dist/cli/rtsp-server.js.map +1 -1
- package/dist/index.cjs +1929 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1051 -172
- package/dist/index.d.ts +965 -46
- package/dist/index.js +547 -85
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
- package/dist/chunk-EDLMKBG2.js.map +0 -1
- package/dist/chunk-HGQ53FB3.js.map +0 -1
- /package/dist/{DiagnosticsTools-RNIDFEJK.js.map → BaichuanVideoStream-HGPU2MZ3.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -34,7 +34,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
34
34
|
function bcHeaderHasPayloadOffset(messageClass) {
|
|
35
35
|
return messageClass === BC_CLASS_MODERN_24 || messageClass === BC_CLASS_MODERN_24_ALT || messageClass === BC_CLASS_FILE_DOWNLOAD;
|
|
36
36
|
}
|
|
37
|
-
var BC_TCP_DEFAULT_PORT, BC_MAGIC, BC_MAGIC_REV, BC_XML_KEY, BC_AES_IV, BC_CLASS_LEGACY, BC_CLASS_MODERN_20, BC_CLASS_MODERN_24, BC_CLASS_MODERN_24_ALT, BC_CLASS_FILE_DOWNLOAD, BC_CMD_ID_LOGIN, BC_CMD_ID_LOGOUT, BC_CMD_ID_VIDEO, BC_CMD_ID_VIDEO_STOP, BC_CMD_ID_FILE_INFO_LIST_REPLAY, BC_CMD_ID_FILE_INFO_LIST_STOP, BC_CMD_ID_FILE_INFO_LIST_DL_VIDEO, BC_CMD_ID_FILE_INFO_LIST_DOWNLOAD, BC_CMD_ID_FILE_INFO_LIST_OPEN, BC_CMD_ID_FILE_INFO_LIST_GET, BC_CMD_ID_FILE_INFO_LIST_CLOSE, BC_CMD_ID_FIND_REC_VIDEO_OPEN, BC_CMD_ID_FIND_REC_VIDEO_GET, BC_CMD_ID_FIND_REC_VIDEO_CLOSE, BC_CMD_ID_COVER_PREVIEW, BC_CMD_ID_COVER_STANDALONE_458, BC_CMD_ID_COVER_STANDALONE_459, BC_CMD_ID_COVER_STANDALONE_460, BC_CMD_ID_COVER_STANDALONE_461, BC_CMD_ID_COVER_STANDALONE_462, BC_CMD_ID_COVER_RESPONSE, BC_CMD_ID_TALK_ABILITY, BC_CMD_ID_TALK_RESET, BC_CMD_ID_TALK_CONFIG, BC_CMD_ID_TALK, BC_CMD_ID_PTZ_CONTROL, BC_CMD_ID_PTZ_CONTROL_PRESET, BC_CMD_ID_GET_PTZ_PRESET, BC_CMD_ID_GET_PTZ_POSITION, BC_CMD_ID_GET_ZOOM_FOCUS, BC_CMD_ID_SET_ZOOM_FOCUS, BC_CMD_ID_GET_BATTERY_INFO_LIST, BC_CMD_ID_GET_BATTERY_INFO, BC_CMD_ID_UDP_KEEP_ALIVE, BC_CMD_ID_GET_PIR_INFO, BC_CMD_ID_SET_PIR_INFO, BC_CMD_ID_GET_MOTION_ALARM, BC_CMD_ID_SET_MOTION_ALARM, BC_CMD_ID_ALARM_EVENT_LIST, BC_CMD_ID_GET_AI_ALARM, BC_CMD_ID_SET_AI_ALARM, BC_CMD_ID_GET_AUDIO_ALARM, BC_CMD_ID_AUDIO_ALARM_PLAY, BC_CMD_ID_GET_WHITE_LED, BC_CMD_ID_SET_WHITE_LED_STATE, BC_CMD_ID_SET_WHITE_LED_TASK, BC_CMD_ID_FLOODLIGHT_STATUS_LIST, BC_CMD_ID_ABILITY_INFO, BC_CMD_ID_SUPPORT, BC_CMD_ID_PING, BC_CMD_ID_CHANNEL_INFO_ALL, BC_CMD_ID_GET_OSD_DATETIME, BC_CMD_ID_GET_RECORD_CFG, BC_CMD_ID_GET_ABILITY_SUPPORT, BC_CMD_ID_GET_FTP_TASK, BC_CMD_ID_GET_RECORD, BC_CMD_ID_GET_HDD_INFO_LIST, BC_CMD_ID_GET_WIFI_SIGNAL, BC_CMD_ID_GET_WIFI, BC_CMD_ID_GET_ONLINE_USER_LIST, BC_CMD_ID_GET_DAY_RECORDS, BC_CMD_ID_GET_STREAM_INFO_LIST, BC_CMD_ID_GET_LED_STATE, BC_CMD_ID_GET_EMAIL_TASK, BC_CMD_ID_GET_AUDIO_TASK, BC_CMD_ID_GET_AUDIO_CFG, BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD, BC_CMD_ID_GET_TIMELAPSE_CFG, BC_CMD_ID_GET_AI_DENOISE, BC_CMD_ID_GET_KIT_AP_CFG, BC_CMD_ID_GET_REC_ENC_CFG, BC_CMD_ID_GET_ACCESS_USER_LIST, BC_CMD_ID_GET_SLEEP_STATE, BC_CMD_ID_GET_VIDEO_INPUT, BC_CMD_ID_GET_SYSTEM_GENERAL, BC_CMD_ID_GET_SUPPORT, BC_CMD_ID_GET_AI_CFG, BC_CMD_ID_SET_AI_CFG, BC_CMD_ID_GET_SIREN_STATUS, BC_CMD_ID_SET_AUDIO_TASK, BC_CMD_ID_SET_VIDEO_INPUT, BC_CMD_ID_SET_DAY_NIGHT_THRESHOLD, BC_CMD_ID_GET_ENC, BC_CMD_ID_SET_ENC, BC_CMD_ID_GET_PRIVACY_MASK, BC_CMD_ID_SET_PRIVACY_MASK, BC_CMD_ID_SET_AI_DENOISE, BC_CMD_ID_SET_LED_STATE, BC_CMD_ID_SET_AUDIO_CFG, BC_CMD_ID_SET_RECORD, BC_CMD_ID_SET_RECORD_CFG, BC_CMD_ID_SET_EMAIL_TASK, BC_CMD_ID_GET_PUSH_TASK, BC_CMD_ID_SET_PUSH_TASK, BC_CMD_ID_GET_AUTO_FOCUS, BC_CMD_ID_SET_AUTO_FOCUS, BC_CMD_ID_CMD_123, BC_CMD_ID_CMD_209, BC_CMD_ID_CMD_265, BC_CMD_ID_CMD_440, BC_CMD_ID_PUSH_VIDEO_INPUT, BC_CMD_ID_PUSH_SERIAL, BC_CMD_ID_PUSH_NET_INFO, BC_CMD_ID_PUSH_DINGDONG_LIST, BC_CMD_ID_PUSH_SLEEP_STATUS, BC_CMD_ID_PUSH_COORDINATE_POINT_LIST, BC_CMD_ID_DING_DONG_CTRL, BC_CMD_ID_GET_DING_DONG_LIST, BC_CMD_ID_DING_DONG_OPT, BC_CMD_ID_GET_DING_DONG_CFG, BC_CMD_ID_SET_DING_DONG_CFG, BC_CMD_ID_QUICK_REPLY_PLAY, BC_CMD_ID_GET_DING_DONG_SILENT, BC_CMD_ID_SET_DING_DONG_SILENT;
|
|
37
|
+
var BC_TCP_DEFAULT_PORT, BC_MAGIC, BC_MAGIC_REV, BC_XML_KEY, BC_AES_IV, BC_CLASS_LEGACY, BC_CLASS_MODERN_20, BC_CLASS_MODERN_24, BC_CLASS_MODERN_24_ALT, BC_CLASS_FILE_DOWNLOAD, BC_CMD_ID_LOGIN, BC_CMD_ID_LOGOUT, BC_CMD_ID_VIDEO, BC_CMD_ID_VIDEO_STOP, BC_CMD_ID_FILE_INFO_LIST_REPLAY, BC_CMD_ID_FILE_INFO_LIST_STOP, BC_CMD_ID_FILE_INFO_LIST_DL_VIDEO, BC_CMD_ID_FILE_INFO_LIST_DOWNLOAD, BC_CMD_ID_FILE_INFO_LIST_OPEN, BC_CMD_ID_FILE_INFO_LIST_GET, BC_CMD_ID_FILE_INFO_LIST_CLOSE, BC_CMD_ID_FIND_REC_VIDEO_OPEN, BC_CMD_ID_FIND_REC_VIDEO_GET, BC_CMD_ID_FIND_REC_VIDEO_CLOSE, BC_CMD_ID_COVER_PREVIEW, BC_CMD_ID_COVER_STANDALONE_458, BC_CMD_ID_COVER_STANDALONE_459, BC_CMD_ID_COVER_STANDALONE_460, BC_CMD_ID_COVER_STANDALONE_461, BC_CMD_ID_COVER_STANDALONE_462, BC_CMD_ID_COVER_RESPONSE, BC_CMD_ID_TALK_ABILITY, BC_CMD_ID_TALK_RESET, BC_CMD_ID_TALK_CONFIG, BC_CMD_ID_TALK, BC_CMD_ID_PTZ_CONTROL, BC_CMD_ID_PTZ_CONTROL_PRESET, BC_CMD_ID_GET_PTZ_PRESET, BC_CMD_ID_GET_PTZ_POSITION, BC_CMD_ID_GET_ZOOM_FOCUS, BC_CMD_ID_SET_ZOOM_FOCUS, BC_CMD_ID_GET_BATTERY_INFO_LIST, BC_CMD_ID_GET_BATTERY_INFO, BC_CMD_ID_UDP_KEEP_ALIVE, BC_CMD_ID_GET_PIR_INFO, BC_CMD_ID_SET_PIR_INFO, BC_CMD_ID_GET_MOTION_ALARM, BC_CMD_ID_SET_MOTION_ALARM, BC_CMD_ID_ALARM_EVENT_LIST, BC_CMD_ID_GET_AI_ALARM, BC_CMD_ID_SET_AI_ALARM, BC_CMD_ID_GET_AUDIO_ALARM, BC_CMD_ID_AUDIO_ALARM_PLAY, BC_CMD_ID_GET_WHITE_LED, BC_CMD_ID_SET_WHITE_LED_STATE, BC_CMD_ID_SET_WHITE_LED_TASK, BC_CMD_ID_FLOODLIGHT_STATUS_LIST, BC_CMD_ID_ABILITY_INFO, BC_CMD_ID_SUPPORT, BC_CMD_ID_PING, BC_CMD_ID_CHANNEL_INFO_ALL, BC_CMD_ID_GET_OSD_DATETIME, BC_CMD_ID_SET_OSD_DATETIME, BC_CMD_ID_GET_RECORD_CFG, BC_CMD_ID_GET_ABILITY_SUPPORT, BC_CMD_ID_GET_FTP_TASK, BC_CMD_ID_GET_VERSION_INFO, BC_CMD_ID_GET_RECORD, BC_CMD_ID_GET_HDD_INFO_LIST, BC_CMD_ID_GET_WIFI_SIGNAL, BC_CMD_ID_GET_WIFI, BC_CMD_ID_GET_ONLINE_USER_LIST, BC_CMD_ID_GET_DAY_RECORDS, BC_CMD_ID_GET_STREAM_INFO_LIST, BC_CMD_ID_GET_LED_STATE, BC_CMD_ID_GET_EMAIL_TASK, BC_CMD_ID_GET_AUDIO_TASK, BC_CMD_ID_GET_AUDIO_CFG, BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD, BC_CMD_ID_GET_TIMELAPSE_CFG, BC_CMD_ID_GET_AI_DENOISE, BC_CMD_ID_GET_KIT_AP_CFG, BC_CMD_ID_GET_REC_ENC_CFG, BC_CMD_ID_GET_ACCESS_USER_LIST, BC_CMD_ID_GET_SLEEP_STATE, BC_CMD_ID_GET_VIDEO_INPUT, BC_CMD_ID_GET_SYSTEM_GENERAL, BC_CMD_ID_GET_SUPPORT, BC_CMD_ID_GET_AI_CFG, BC_CMD_ID_SET_AI_CFG, BC_CMD_ID_GET_SIREN_STATUS, BC_CMD_ID_SET_AUDIO_TASK, BC_CMD_ID_SET_VIDEO_INPUT, BC_CMD_ID_SET_DAY_NIGHT_THRESHOLD, BC_CMD_ID_GET_ENC, BC_CMD_ID_SET_ENC, BC_CMD_ID_GET_PRIVACY_MASK, BC_CMD_ID_SET_PRIVACY_MASK, BC_CMD_ID_SET_AI_DENOISE, BC_CMD_ID_SET_LED_STATE, BC_CMD_ID_SET_AUDIO_CFG, BC_CMD_ID_SET_RECORD, BC_CMD_ID_SET_RECORD_CFG, BC_CMD_ID_SET_EMAIL_TASK, BC_CMD_ID_GET_PUSH_TASK, BC_CMD_ID_SET_PUSH_TASK, BC_CMD_ID_GET_AUTO_FOCUS, BC_CMD_ID_SET_AUTO_FOCUS, BC_CMD_ID_GET_EMAIL, BC_CMD_ID_SET_EMAIL, BC_CMD_ID_TEST_EMAIL, BC_CMD_ID_GET_NTP, BC_CMD_ID_SET_NTP, BC_CMD_ID_SET_SYSTEM_GENERAL, BC_CMD_ID_GET_DST, BC_CMD_ID_SET_DST, BC_CMD_ID_GET_AUTO_REBOOT, BC_CMD_ID_SET_AUTO_REBOOT, BC_CMD_ID_CMD_123, BC_CMD_ID_CMD_209, BC_CMD_ID_CMD_265, BC_CMD_ID_CMD_440, BC_CMD_ID_PUSH_VIDEO_INPUT, BC_CMD_ID_PUSH_SERIAL, BC_CMD_ID_PUSH_NET_INFO, BC_CMD_ID_PUSH_DINGDONG_LIST, BC_CMD_ID_PUSH_SLEEP_STATUS, BC_CMD_ID_PUSH_COORDINATE_POINT_LIST, BC_CMD_ID_DING_DONG_CTRL, BC_CMD_ID_GET_DING_DONG_LIST, BC_CMD_ID_DING_DONG_OPT, BC_CMD_ID_GET_DING_DONG_CFG, BC_CMD_ID_SET_DING_DONG_CFG, BC_CMD_ID_QUICK_REPLY_PLAY, BC_CMD_ID_GET_DING_DONG_SILENT, BC_CMD_ID_SET_DING_DONG_SILENT;
|
|
38
38
|
var init_constants = __esm({
|
|
39
39
|
"src/protocol/constants.ts"() {
|
|
40
40
|
"use strict";
|
|
@@ -109,9 +109,11 @@ var init_constants = __esm({
|
|
|
109
109
|
BC_CMD_ID_PING = 93;
|
|
110
110
|
BC_CMD_ID_CHANNEL_INFO_ALL = 145;
|
|
111
111
|
BC_CMD_ID_GET_OSD_DATETIME = 44;
|
|
112
|
+
BC_CMD_ID_SET_OSD_DATETIME = 45;
|
|
112
113
|
BC_CMD_ID_GET_RECORD_CFG = 54;
|
|
113
114
|
BC_CMD_ID_GET_ABILITY_SUPPORT = 58;
|
|
114
115
|
BC_CMD_ID_GET_FTP_TASK = 70;
|
|
116
|
+
BC_CMD_ID_GET_VERSION_INFO = 80;
|
|
115
117
|
BC_CMD_ID_GET_RECORD = 81;
|
|
116
118
|
BC_CMD_ID_GET_HDD_INFO_LIST = 102;
|
|
117
119
|
BC_CMD_ID_GET_WIFI_SIGNAL = 115;
|
|
@@ -153,6 +155,16 @@ var init_constants = __esm({
|
|
|
153
155
|
BC_CMD_ID_SET_PUSH_TASK = 218;
|
|
154
156
|
BC_CMD_ID_GET_AUTO_FOCUS = 224;
|
|
155
157
|
BC_CMD_ID_SET_AUTO_FOCUS = 225;
|
|
158
|
+
BC_CMD_ID_GET_EMAIL = 42;
|
|
159
|
+
BC_CMD_ID_SET_EMAIL = 43;
|
|
160
|
+
BC_CMD_ID_TEST_EMAIL = 141;
|
|
161
|
+
BC_CMD_ID_GET_NTP = 38;
|
|
162
|
+
BC_CMD_ID_SET_NTP = 39;
|
|
163
|
+
BC_CMD_ID_SET_SYSTEM_GENERAL = 105;
|
|
164
|
+
BC_CMD_ID_GET_DST = 106;
|
|
165
|
+
BC_CMD_ID_SET_DST = 107;
|
|
166
|
+
BC_CMD_ID_GET_AUTO_REBOOT = 101;
|
|
167
|
+
BC_CMD_ID_SET_AUTO_REBOOT = 100;
|
|
156
168
|
BC_CMD_ID_CMD_123 = 123;
|
|
157
169
|
BC_CMD_ID_CMD_209 = 209;
|
|
158
170
|
BC_CMD_ID_CMD_265 = 265;
|
|
@@ -480,6 +492,15 @@ function applyXmlTagPatch(xml, tag, value) {
|
|
|
480
492
|
const re = new RegExp(`<${tag}>[^<]*</${tag}>`);
|
|
481
493
|
return xml.replace(re, `<${tag}>${v}</${tag}>`);
|
|
482
494
|
}
|
|
495
|
+
function upsertXmlTag(xml, tag, value) {
|
|
496
|
+
if (value === void 0) return xml;
|
|
497
|
+
const v = typeof value === "boolean" ? value ? 1 : 0 : value;
|
|
498
|
+
const re = new RegExp(`<${tag}>[^<]*</${tag}>`);
|
|
499
|
+
if (re.test(xml)) {
|
|
500
|
+
return xml.replace(re, `<${tag}>${v}</${tag}>`);
|
|
501
|
+
}
|
|
502
|
+
return `${xml}<${tag}>${v}</${tag}>`;
|
|
503
|
+
}
|
|
483
504
|
function patchNestedTag(xml, parent, child, value) {
|
|
484
505
|
if (value === void 0) return xml;
|
|
485
506
|
const v = typeof value === "boolean" ? value ? 1 : 0 : value;
|
|
@@ -495,6 +516,15 @@ function applyStreamPatch(xml, streamTag, patch) {
|
|
|
495
516
|
);
|
|
496
517
|
return xml.replace(re, (_match, open, body, close) => {
|
|
497
518
|
let next = body;
|
|
519
|
+
if (patch.audio !== void 0) {
|
|
520
|
+
next = applyXmlTagPatch(next, "audio", patch.audio);
|
|
521
|
+
}
|
|
522
|
+
if (patch.width !== void 0) {
|
|
523
|
+
next = applyXmlTagPatch(next, "width", patch.width);
|
|
524
|
+
}
|
|
525
|
+
if (patch.height !== void 0) {
|
|
526
|
+
next = applyXmlTagPatch(next, "height", patch.height);
|
|
527
|
+
}
|
|
498
528
|
if (patch.bitRate !== void 0) {
|
|
499
529
|
next = applyXmlTagPatch(next, "bitRate", patch.bitRate);
|
|
500
530
|
}
|
|
@@ -506,6 +536,23 @@ function applyStreamPatch(xml, streamTag, patch) {
|
|
|
506
536
|
const intVal = patch.videoEncType === "h265" ? 1 : 0;
|
|
507
537
|
next = applyXmlTagPatch(next, "videoEncType", intVal);
|
|
508
538
|
}
|
|
539
|
+
if (patch.encoderType !== void 0) {
|
|
540
|
+
next = upsertXmlTag(next, "encoderType", patch.encoderType);
|
|
541
|
+
}
|
|
542
|
+
if (patch.encoderProfile !== void 0) {
|
|
543
|
+
next = upsertXmlTag(next, "encoderProfile", patch.encoderProfile);
|
|
544
|
+
}
|
|
545
|
+
if (patch.gop !== void 0) {
|
|
546
|
+
const gopBlockRe = /(<gop[^>]*>)([\s\S]*?)(<\/gop>)/;
|
|
547
|
+
if (gopBlockRe.test(next)) {
|
|
548
|
+
next = next.replace(
|
|
549
|
+
gopBlockRe,
|
|
550
|
+
(_m, gOpen, gBody, gClose) => `${gOpen}${applyXmlTagPatch(gBody, "cur", patch.gop)}${gClose}`
|
|
551
|
+
);
|
|
552
|
+
} else {
|
|
553
|
+
next = `${next}<gop><cur>${patch.gop}</cur></gop>`;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
509
556
|
return `${open}${next}${close}`;
|
|
510
557
|
});
|
|
511
558
|
}
|
|
@@ -1117,10 +1164,15 @@ var init_BcMediaCodec = __esm({
|
|
|
1117
1164
|
strict;
|
|
1118
1165
|
amountSkipped = 0;
|
|
1119
1166
|
logger;
|
|
1167
|
+
onUnknownChunk;
|
|
1120
1168
|
constructor(strict = false, logger) {
|
|
1121
1169
|
this.strict = strict;
|
|
1122
1170
|
this.logger = logger;
|
|
1123
1171
|
}
|
|
1172
|
+
/** Register a listener that fires for every unknown chunk before recovery. */
|
|
1173
|
+
setUnknownChunkListener(listener) {
|
|
1174
|
+
this.onUnknownChunk = listener;
|
|
1175
|
+
}
|
|
1124
1176
|
/**
|
|
1125
1177
|
* Push data into the codec buffer and try to parse complete BcMedia packets.
|
|
1126
1178
|
* Returns an array of complete BcMedia packets found.
|
|
@@ -1181,6 +1233,13 @@ var init_BcMediaCodec = __esm({
|
|
|
1181
1233
|
}
|
|
1182
1234
|
}
|
|
1183
1235
|
if (next > 0) {
|
|
1236
|
+
if (this.onUnknownChunk) {
|
|
1237
|
+
this.onUnknownChunk({
|
|
1238
|
+
magic,
|
|
1239
|
+
preview: Buffer.from(this.buffer.subarray(0, Math.min(256, next))),
|
|
1240
|
+
skipped: next
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1184
1243
|
this.amountSkipped += next;
|
|
1185
1244
|
this.buffer = this.buffer.subarray(next);
|
|
1186
1245
|
continue;
|
|
@@ -2085,6 +2144,10 @@ var init_BcMediaAnnexBDecoder = __esm({
|
|
|
2085
2144
|
});
|
|
2086
2145
|
|
|
2087
2146
|
// src/baichuan/stream/BaichuanVideoStream.ts
|
|
2147
|
+
var BaichuanVideoStream_exports = {};
|
|
2148
|
+
__export(BaichuanVideoStream_exports, {
|
|
2149
|
+
BaichuanVideoStream: () => BaichuanVideoStream
|
|
2150
|
+
});
|
|
2088
2151
|
function removeEmulationPreventionBytes(rbsp) {
|
|
2089
2152
|
const out = [];
|
|
2090
2153
|
for (let i = 0; i < rbsp.length; i++) {
|
|
@@ -2218,6 +2281,15 @@ var init_BaichuanVideoStream = __esm({
|
|
|
2218
2281
|
acceptAnyStreamType;
|
|
2219
2282
|
lockedChannelId;
|
|
2220
2283
|
bcMediaCodec;
|
|
2284
|
+
/**
|
|
2285
|
+
* Diagnostic-only accessor for the BcMedia codec. Used by tools that need to
|
|
2286
|
+
* inspect unknown chunks (for example to discover undocumented audio
|
|
2287
|
+
* sub-packets the parser currently skips). Not part of the supported public
|
|
2288
|
+
* surface — do not rely on it in application code.
|
|
2289
|
+
*/
|
|
2290
|
+
get _bcMediaCodec() {
|
|
2291
|
+
return this.bcMediaCodec;
|
|
2292
|
+
}
|
|
2221
2293
|
debugH264LogsLeft;
|
|
2222
2294
|
debugSavedSamples;
|
|
2223
2295
|
warnedNonAnnexBOnce = false;
|
|
@@ -2249,6 +2321,14 @@ var init_BaichuanVideoStream = __esm({
|
|
|
2249
2321
|
// Stateful AES decryptor for fragmented BcMedia packets (full_aes mode)
|
|
2250
2322
|
// In CFB mode, continuation frames must use the cipher state from previous frames.
|
|
2251
2323
|
aesStreamDecryptor = null;
|
|
2324
|
+
// Latest frame dimensions reported by BcMedia InfoV1/V2 packets.
|
|
2325
|
+
// Used to attach width/height context to the `additionalHeader` event so
|
|
2326
|
+
// consumers can normalize box coordinates to a fraction of the stream size.
|
|
2327
|
+
latestFrameWidth;
|
|
2328
|
+
latestFrameHeight;
|
|
2329
|
+
// Teardown returned by ReolinkBaichuanApi._registerVideoStreamForDetection.
|
|
2330
|
+
// Called from stop() to detach the detection bridge.
|
|
2331
|
+
detectionTeardown;
|
|
2252
2332
|
/**
|
|
2253
2333
|
* Pending startup error stashed when emitSafeError is called before any
|
|
2254
2334
|
* "error" listener is registered (e.g. camera returns 400 during start()).
|
|
@@ -2911,6 +2991,16 @@ var init_BaichuanVideoStream = __esm({
|
|
|
2911
2991
|
}
|
|
2912
2992
|
videoType = detectedCodec;
|
|
2913
2993
|
}
|
|
2994
|
+
if (media.additionalHeader && media.additionalHeader.length > 0) {
|
|
2995
|
+
this.emit("additionalHeader", {
|
|
2996
|
+
raw: media.additionalHeader,
|
|
2997
|
+
frameType: "Iframe",
|
|
2998
|
+
videoType,
|
|
2999
|
+
microseconds: media.microseconds,
|
|
3000
|
+
...this.latestFrameWidth !== void 0 ? { frameWidth: this.latestFrameWidth } : {},
|
|
3001
|
+
...this.latestFrameHeight !== void 0 ? { frameHeight: this.latestFrameHeight } : {}
|
|
3002
|
+
});
|
|
3003
|
+
}
|
|
2914
3004
|
const annexBData = videoType === "H265" ? convertToAnnexB2(media.data) : convertToAnnexB(media.data);
|
|
2915
3005
|
const isKeyframe = true;
|
|
2916
3006
|
maybeCacheParamSets(annexBData, "Iframe", videoType);
|
|
@@ -2992,6 +3082,18 @@ var init_BaichuanVideoStream = __esm({
|
|
|
2992
3082
|
}
|
|
2993
3083
|
} else if (media.type === "Pframe") {
|
|
2994
3084
|
const chunk = media.data;
|
|
3085
|
+
if (media.additionalHeader && media.additionalHeader.length > 0) {
|
|
3086
|
+
const detected = detectVideoCodecFromNal(chunk);
|
|
3087
|
+
const videoTypeForHeader = detected ?? media.videoType;
|
|
3088
|
+
this.emit("additionalHeader", {
|
|
3089
|
+
raw: media.additionalHeader,
|
|
3090
|
+
frameType: "Pframe",
|
|
3091
|
+
videoType: videoTypeForHeader,
|
|
3092
|
+
microseconds: media.microseconds,
|
|
3093
|
+
...this.latestFrameWidth !== void 0 ? { frameWidth: this.latestFrameWidth } : {},
|
|
3094
|
+
...this.latestFrameHeight !== void 0 ? { frameHeight: this.latestFrameHeight } : {}
|
|
3095
|
+
});
|
|
3096
|
+
}
|
|
2995
3097
|
let videoType = media.videoType;
|
|
2996
3098
|
const detectedCodec = detectVideoCodecFromNal(chunk);
|
|
2997
3099
|
if (detectedCodec && detectedCodec !== videoType) {
|
|
@@ -3034,6 +3136,8 @@ var init_BaichuanVideoStream = __esm({
|
|
|
3034
3136
|
this.emit("audioFrame", media.data);
|
|
3035
3137
|
}
|
|
3036
3138
|
if (media.type === "InfoV1" || media.type === "InfoV2") {
|
|
3139
|
+
if (media.videoWidth > 0) this.latestFrameWidth = media.videoWidth;
|
|
3140
|
+
if (media.videoHeight > 0) this.latestFrameHeight = media.videoHeight;
|
|
3037
3141
|
}
|
|
3038
3142
|
}
|
|
3039
3143
|
if (totalFramesReceived <= 10 || totalFramesReceived % 20 === 0 && (videoFramesEmitted > 0 || audioFramesEmitted > 0)) {
|
|
@@ -3053,6 +3157,12 @@ var init_BaichuanVideoStream = __esm({
|
|
|
3053
3157
|
this.client.on("push", this.videoFrameHandler);
|
|
3054
3158
|
this.active = true;
|
|
3055
3159
|
this.startWatchdog();
|
|
3160
|
+
if (this.api && typeof this.api._registerVideoStreamForDetection === "function") {
|
|
3161
|
+
this.detectionTeardown = this.api._registerVideoStreamForDetection(this, {
|
|
3162
|
+
channel: this.channel,
|
|
3163
|
+
profile: this.profile
|
|
3164
|
+
});
|
|
3165
|
+
}
|
|
3056
3166
|
this.lastMediaAtMs = Date.now();
|
|
3057
3167
|
if (this.api) {
|
|
3058
3168
|
try {
|
|
@@ -3150,6 +3260,13 @@ var init_BaichuanVideoStream = __esm({
|
|
|
3150
3260
|
}
|
|
3151
3261
|
}
|
|
3152
3262
|
this.active = false;
|
|
3263
|
+
if (this.detectionTeardown) {
|
|
3264
|
+
try {
|
|
3265
|
+
this.detectionTeardown();
|
|
3266
|
+
} catch {
|
|
3267
|
+
}
|
|
3268
|
+
this.detectionTeardown = void 0;
|
|
3269
|
+
}
|
|
3153
3270
|
this.emit("close");
|
|
3154
3271
|
}
|
|
3155
3272
|
isActive() {
|
|
@@ -7377,12 +7494,12 @@ var init_ReolinkCgiApi = __esm({
|
|
|
7377
7494
|
"getVideoclipThumbnailJpeg",
|
|
7378
7495
|
`Extracting thumbnail from VOD URL (FLV): ${vodUrl.substring(0, 100)}... (seek=${seekSeconds}s)`
|
|
7379
7496
|
);
|
|
7380
|
-
const { spawn:
|
|
7497
|
+
const { spawn: spawn13 } = await import("child_process");
|
|
7381
7498
|
return new Promise((resolve, reject) => {
|
|
7382
7499
|
const chunks = [];
|
|
7383
7500
|
let stderr = "";
|
|
7384
7501
|
let timedOut = false;
|
|
7385
|
-
const ffmpeg =
|
|
7502
|
+
const ffmpeg = spawn13(ffmpegPath, [
|
|
7386
7503
|
"-y",
|
|
7387
7504
|
"-analyzeduration",
|
|
7388
7505
|
"10000000",
|
|
@@ -8010,6 +8127,7 @@ __export(index_exports, {
|
|
|
8010
8127
|
BC_CMD_ID_GET_AUDIO_CFG: () => BC_CMD_ID_GET_AUDIO_CFG,
|
|
8011
8128
|
BC_CMD_ID_GET_AUDIO_TASK: () => BC_CMD_ID_GET_AUDIO_TASK,
|
|
8012
8129
|
BC_CMD_ID_GET_AUTO_FOCUS: () => BC_CMD_ID_GET_AUTO_FOCUS,
|
|
8130
|
+
BC_CMD_ID_GET_AUTO_REBOOT: () => BC_CMD_ID_GET_AUTO_REBOOT,
|
|
8013
8131
|
BC_CMD_ID_GET_BATTERY_INFO: () => BC_CMD_ID_GET_BATTERY_INFO,
|
|
8014
8132
|
BC_CMD_ID_GET_BATTERY_INFO_LIST: () => BC_CMD_ID_GET_BATTERY_INFO_LIST,
|
|
8015
8133
|
BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD: () => BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD,
|
|
@@ -8017,6 +8135,8 @@ __export(index_exports, {
|
|
|
8017
8135
|
BC_CMD_ID_GET_DING_DONG_CFG: () => BC_CMD_ID_GET_DING_DONG_CFG,
|
|
8018
8136
|
BC_CMD_ID_GET_DING_DONG_LIST: () => BC_CMD_ID_GET_DING_DONG_LIST,
|
|
8019
8137
|
BC_CMD_ID_GET_DING_DONG_SILENT: () => BC_CMD_ID_GET_DING_DONG_SILENT,
|
|
8138
|
+
BC_CMD_ID_GET_DST: () => BC_CMD_ID_GET_DST,
|
|
8139
|
+
BC_CMD_ID_GET_EMAIL: () => BC_CMD_ID_GET_EMAIL,
|
|
8020
8140
|
BC_CMD_ID_GET_EMAIL_TASK: () => BC_CMD_ID_GET_EMAIL_TASK,
|
|
8021
8141
|
BC_CMD_ID_GET_ENC: () => BC_CMD_ID_GET_ENC,
|
|
8022
8142
|
BC_CMD_ID_GET_FTP_TASK: () => BC_CMD_ID_GET_FTP_TASK,
|
|
@@ -8024,6 +8144,7 @@ __export(index_exports, {
|
|
|
8024
8144
|
BC_CMD_ID_GET_KIT_AP_CFG: () => BC_CMD_ID_GET_KIT_AP_CFG,
|
|
8025
8145
|
BC_CMD_ID_GET_LED_STATE: () => BC_CMD_ID_GET_LED_STATE,
|
|
8026
8146
|
BC_CMD_ID_GET_MOTION_ALARM: () => BC_CMD_ID_GET_MOTION_ALARM,
|
|
8147
|
+
BC_CMD_ID_GET_NTP: () => BC_CMD_ID_GET_NTP,
|
|
8027
8148
|
BC_CMD_ID_GET_ONLINE_USER_LIST: () => BC_CMD_ID_GET_ONLINE_USER_LIST,
|
|
8028
8149
|
BC_CMD_ID_GET_OSD_DATETIME: () => BC_CMD_ID_GET_OSD_DATETIME,
|
|
8029
8150
|
BC_CMD_ID_GET_PIR_INFO: () => BC_CMD_ID_GET_PIR_INFO,
|
|
@@ -8040,6 +8161,7 @@ __export(index_exports, {
|
|
|
8040
8161
|
BC_CMD_ID_GET_SUPPORT: () => BC_CMD_ID_GET_SUPPORT,
|
|
8041
8162
|
BC_CMD_ID_GET_SYSTEM_GENERAL: () => BC_CMD_ID_GET_SYSTEM_GENERAL,
|
|
8042
8163
|
BC_CMD_ID_GET_TIMELAPSE_CFG: () => BC_CMD_ID_GET_TIMELAPSE_CFG,
|
|
8164
|
+
BC_CMD_ID_GET_VERSION_INFO: () => BC_CMD_ID_GET_VERSION_INFO,
|
|
8043
8165
|
BC_CMD_ID_GET_VIDEO_INPUT: () => BC_CMD_ID_GET_VIDEO_INPUT,
|
|
8044
8166
|
BC_CMD_ID_GET_WHITE_LED: () => BC_CMD_ID_GET_WHITE_LED,
|
|
8045
8167
|
BC_CMD_ID_GET_WIFI: () => BC_CMD_ID_GET_WIFI,
|
|
@@ -8063,18 +8185,24 @@ __export(index_exports, {
|
|
|
8063
8185
|
BC_CMD_ID_SET_AUDIO_CFG: () => BC_CMD_ID_SET_AUDIO_CFG,
|
|
8064
8186
|
BC_CMD_ID_SET_AUDIO_TASK: () => BC_CMD_ID_SET_AUDIO_TASK,
|
|
8065
8187
|
BC_CMD_ID_SET_AUTO_FOCUS: () => BC_CMD_ID_SET_AUTO_FOCUS,
|
|
8188
|
+
BC_CMD_ID_SET_AUTO_REBOOT: () => BC_CMD_ID_SET_AUTO_REBOOT,
|
|
8066
8189
|
BC_CMD_ID_SET_DAY_NIGHT_THRESHOLD: () => BC_CMD_ID_SET_DAY_NIGHT_THRESHOLD,
|
|
8067
8190
|
BC_CMD_ID_SET_DING_DONG_CFG: () => BC_CMD_ID_SET_DING_DONG_CFG,
|
|
8068
8191
|
BC_CMD_ID_SET_DING_DONG_SILENT: () => BC_CMD_ID_SET_DING_DONG_SILENT,
|
|
8192
|
+
BC_CMD_ID_SET_DST: () => BC_CMD_ID_SET_DST,
|
|
8193
|
+
BC_CMD_ID_SET_EMAIL: () => BC_CMD_ID_SET_EMAIL,
|
|
8069
8194
|
BC_CMD_ID_SET_EMAIL_TASK: () => BC_CMD_ID_SET_EMAIL_TASK,
|
|
8070
8195
|
BC_CMD_ID_SET_ENC: () => BC_CMD_ID_SET_ENC,
|
|
8071
8196
|
BC_CMD_ID_SET_LED_STATE: () => BC_CMD_ID_SET_LED_STATE,
|
|
8072
8197
|
BC_CMD_ID_SET_MOTION_ALARM: () => BC_CMD_ID_SET_MOTION_ALARM,
|
|
8198
|
+
BC_CMD_ID_SET_NTP: () => BC_CMD_ID_SET_NTP,
|
|
8199
|
+
BC_CMD_ID_SET_OSD_DATETIME: () => BC_CMD_ID_SET_OSD_DATETIME,
|
|
8073
8200
|
BC_CMD_ID_SET_PIR_INFO: () => BC_CMD_ID_SET_PIR_INFO,
|
|
8074
8201
|
BC_CMD_ID_SET_PRIVACY_MASK: () => BC_CMD_ID_SET_PRIVACY_MASK,
|
|
8075
8202
|
BC_CMD_ID_SET_PUSH_TASK: () => BC_CMD_ID_SET_PUSH_TASK,
|
|
8076
8203
|
BC_CMD_ID_SET_RECORD: () => BC_CMD_ID_SET_RECORD,
|
|
8077
8204
|
BC_CMD_ID_SET_RECORD_CFG: () => BC_CMD_ID_SET_RECORD_CFG,
|
|
8205
|
+
BC_CMD_ID_SET_SYSTEM_GENERAL: () => BC_CMD_ID_SET_SYSTEM_GENERAL,
|
|
8078
8206
|
BC_CMD_ID_SET_VIDEO_INPUT: () => BC_CMD_ID_SET_VIDEO_INPUT,
|
|
8079
8207
|
BC_CMD_ID_SET_WHITE_LED_STATE: () => BC_CMD_ID_SET_WHITE_LED_STATE,
|
|
8080
8208
|
BC_CMD_ID_SET_WHITE_LED_TASK: () => BC_CMD_ID_SET_WHITE_LED_TASK,
|
|
@@ -8084,6 +8212,7 @@ __export(index_exports, {
|
|
|
8084
8212
|
BC_CMD_ID_TALK_ABILITY: () => BC_CMD_ID_TALK_ABILITY,
|
|
8085
8213
|
BC_CMD_ID_TALK_CONFIG: () => BC_CMD_ID_TALK_CONFIG,
|
|
8086
8214
|
BC_CMD_ID_TALK_RESET: () => BC_CMD_ID_TALK_RESET,
|
|
8215
|
+
BC_CMD_ID_TEST_EMAIL: () => BC_CMD_ID_TEST_EMAIL,
|
|
8087
8216
|
BC_CMD_ID_UDP_KEEP_ALIVE: () => BC_CMD_ID_UDP_KEEP_ALIVE,
|
|
8088
8217
|
BC_CMD_ID_VIDEO: () => BC_CMD_ID_VIDEO,
|
|
8089
8218
|
BC_CMD_ID_VIDEO_STOP: () => BC_CMD_ID_VIDEO_STOP,
|
|
@@ -8178,6 +8307,7 @@ __export(index_exports, {
|
|
|
8178
8307
|
decideSleepInferenceTransition: () => decideSleepInferenceTransition,
|
|
8179
8308
|
decideVideoclipTranscodeMode: () => decideVideoclipTranscodeMode,
|
|
8180
8309
|
decodeHeader: () => decodeHeader,
|
|
8310
|
+
decodeMotionScopeBitmap: () => decodeMotionScopeBitmap,
|
|
8181
8311
|
deriveAesKey: () => deriveAesKey,
|
|
8182
8312
|
detectIosClient: () => detectIosClient,
|
|
8183
8313
|
detectVideoCodecFromNal: () => detectVideoCodecFromNal,
|
|
@@ -8190,6 +8320,7 @@ __export(index_exports, {
|
|
|
8190
8320
|
discoverViaUdpBroadcast: () => discoverViaUdpBroadcast,
|
|
8191
8321
|
discoverViaUdpDirect: () => discoverViaUdpDirect,
|
|
8192
8322
|
encodeHeader: () => encodeHeader,
|
|
8323
|
+
encodeMotionScopeBitmap: () => encodeMotionScopeBitmap,
|
|
8193
8324
|
ensureXmlHeader: () => ensureXmlHeader,
|
|
8194
8325
|
extractH264ParamSetsFromAccessUnit: () => extractH264ParamSetsFromAccessUnit,
|
|
8195
8326
|
extractH265ParamSetsFromAccessUnit: () => extractH265ParamSetsFromAccessUnit,
|
|
@@ -8198,6 +8329,7 @@ __export(index_exports, {
|
|
|
8198
8329
|
extractVpsFromAnnexB: () => extractVpsFromAnnexB,
|
|
8199
8330
|
flattenAbilitiesForChannel: () => flattenAbilitiesForChannel,
|
|
8200
8331
|
formatMjpegFrame: () => formatMjpegFrame,
|
|
8332
|
+
fullCoverageScope: () => fullCoverageScope,
|
|
8201
8333
|
getConstructedVideoStreamOptions: () => getConstructedVideoStreamOptions,
|
|
8202
8334
|
getGlobalLogger: () => getGlobalLogger,
|
|
8203
8335
|
getH265NalType: () => getH265NalType,
|
|
@@ -8241,6 +8373,7 @@ __export(index_exports, {
|
|
|
8241
8373
|
splitAnnexBToNals: () => splitAnnexBToNals,
|
|
8242
8374
|
splitH265AnnexBToNalPayloads: () => splitAnnexBToNalPayloads2,
|
|
8243
8375
|
testChannelStreams: () => testChannelStreams,
|
|
8376
|
+
upsertXmlTag: () => upsertXmlTag,
|
|
8244
8377
|
xmlEscape: () => xmlEscape,
|
|
8245
8378
|
xmlIndicatesFloodlight: () => xmlIndicatesFloodlight,
|
|
8246
8379
|
zipDirectory: () => zipDirectory
|
|
@@ -9969,6 +10102,23 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
9969
10102
|
* even if the current client instance is idle/disconnected.
|
|
9970
10103
|
*/
|
|
9971
10104
|
static streamingRegistry = /* @__PURE__ */ new Map();
|
|
10105
|
+
/**
|
|
10106
|
+
* Per-device set of live BaichuanClient instances.
|
|
10107
|
+
*
|
|
10108
|
+
* Why: when a streaming client unsubscribes (e.g. RTSP grace timer expires
|
|
10109
|
+
* and SocketPool tears the streaming socket down), the global streaming
|
|
10110
|
+
* registry decrements but the GENERAL client of the same device has no
|
|
10111
|
+
* way of knowing — its idle-disconnect timer was last evaluated while
|
|
10112
|
+
* `isDeviceStreamingActive()` was still true (because the streaming socket
|
|
10113
|
+
* was still alive) and wasn't rescheduled. Without this registry the
|
|
10114
|
+
* general socket stays connected, the 60-second session-guard timer keeps
|
|
10115
|
+
* sending getOnlineUserList() to the camera, and a battery camera ends up
|
|
10116
|
+
* waking up every minute (issue #18).
|
|
10117
|
+
*
|
|
10118
|
+
* On streamingRegistry decrement-to-zero we walk this set and kick every
|
|
10119
|
+
* sibling's idle-disconnect timer so it can re-evaluate eligibility.
|
|
10120
|
+
*/
|
|
10121
|
+
static deviceClients = /* @__PURE__ */ new Map();
|
|
9972
10122
|
/**
|
|
9973
10123
|
* Per-host D2C_DISC backoff state that persists across client instance recreation.
|
|
9974
10124
|
*
|
|
@@ -10083,6 +10233,29 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
10083
10233
|
// AlarmEventList (cmdId=33) can be very chatty (often sent every second).
|
|
10084
10234
|
// Track last per-channel alarm state so we only emit on transitions.
|
|
10085
10235
|
alarmEventState = /* @__PURE__ */ new Map();
|
|
10236
|
+
/** Whether this instance is currently in BaichuanClient.deviceClients. */
|
|
10237
|
+
registeredInDeviceClients = false;
|
|
10238
|
+
registerInDeviceClients() {
|
|
10239
|
+
if (this.registeredInDeviceClients) return;
|
|
10240
|
+
const key = this.getDeviceRegistryKey();
|
|
10241
|
+
let set = _BaichuanClient.deviceClients.get(key);
|
|
10242
|
+
if (!set) {
|
|
10243
|
+
set = /* @__PURE__ */ new Set();
|
|
10244
|
+
_BaichuanClient.deviceClients.set(key, set);
|
|
10245
|
+
}
|
|
10246
|
+
set.add(this);
|
|
10247
|
+
this.registeredInDeviceClients = true;
|
|
10248
|
+
}
|
|
10249
|
+
unregisterFromDeviceClients() {
|
|
10250
|
+
if (!this.registeredInDeviceClients) return;
|
|
10251
|
+
const key = this.getDeviceRegistryKey();
|
|
10252
|
+
const set = _BaichuanClient.deviceClients.get(key);
|
|
10253
|
+
if (set) {
|
|
10254
|
+
set.delete(this);
|
|
10255
|
+
if (set.size === 0) _BaichuanClient.deviceClients.delete(key);
|
|
10256
|
+
}
|
|
10257
|
+
this.registeredInDeviceClients = false;
|
|
10258
|
+
}
|
|
10086
10259
|
constructor(options) {
|
|
10087
10260
|
super();
|
|
10088
10261
|
this.opts = options;
|
|
@@ -10097,6 +10270,7 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
10097
10270
|
code: err?.code
|
|
10098
10271
|
});
|
|
10099
10272
|
});
|
|
10273
|
+
this.registerInDeviceClients();
|
|
10100
10274
|
}
|
|
10101
10275
|
newSocketSessionId(transport) {
|
|
10102
10276
|
const short = (0, import_node_crypto2.randomUUID)().split("-")[0] ?? (0, import_node_crypto2.randomUUID)().slice(0, 8);
|
|
@@ -10353,6 +10527,18 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
10353
10527
|
activeStreamClients: nextCount
|
|
10354
10528
|
});
|
|
10355
10529
|
this.contributesToGlobalStreamingRegistry = shouldContribute;
|
|
10530
|
+
if (!shouldContribute && nextCount === 0) {
|
|
10531
|
+
const siblings = _BaichuanClient.deviceClients.get(key);
|
|
10532
|
+
if (siblings) {
|
|
10533
|
+
for (const sib of siblings) {
|
|
10534
|
+
if (sib === this) continue;
|
|
10535
|
+
try {
|
|
10536
|
+
sib.kickIdleDisconnectTimer();
|
|
10537
|
+
} catch {
|
|
10538
|
+
}
|
|
10539
|
+
}
|
|
10540
|
+
}
|
|
10541
|
+
}
|
|
10356
10542
|
}
|
|
10357
10543
|
/**
|
|
10358
10544
|
* True if the device should be considered "awake" due to active streaming.
|
|
@@ -10817,6 +11003,7 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
10817
11003
|
`transport=tcp host=${this.opts.host} port=${port}${sid ? ` sid=${sid}` : ""}${remote ? ` remote=${remote}` : ""}${peer ? ` peer=${peer}` : ""}`
|
|
10818
11004
|
);
|
|
10819
11005
|
this.logSocketState("tcp_connected");
|
|
11006
|
+
this.registerInDeviceClients();
|
|
10820
11007
|
this.startKeepAlive();
|
|
10821
11008
|
this.kickIdleDisconnectTimer();
|
|
10822
11009
|
}
|
|
@@ -11133,6 +11320,7 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
11133
11320
|
this.logDebug("udp_close_error", e);
|
|
11134
11321
|
}
|
|
11135
11322
|
}
|
|
11323
|
+
this.unregisterFromDeviceClients();
|
|
11136
11324
|
}
|
|
11137
11325
|
handleFrame(frame) {
|
|
11138
11326
|
const now = Date.now();
|
|
@@ -16134,6 +16322,309 @@ function parseXmlFragmentToJson(xml) {
|
|
|
16134
16322
|
return parsed.root;
|
|
16135
16323
|
}
|
|
16136
16324
|
|
|
16325
|
+
// src/reolink/baichuan/utils/email.ts
|
|
16326
|
+
init_xml();
|
|
16327
|
+
var parseInt01 = (text) => {
|
|
16328
|
+
if (text === void 0) return void 0;
|
|
16329
|
+
return text.trim() === "1" ? 1 : 0;
|
|
16330
|
+
};
|
|
16331
|
+
var parseNumberSafe = (text) => {
|
|
16332
|
+
if (text === void 0) return void 0;
|
|
16333
|
+
const n = Number(text);
|
|
16334
|
+
return Number.isFinite(n) ? n : void 0;
|
|
16335
|
+
};
|
|
16336
|
+
var VALID_ATTACHMENT_TYPES = [
|
|
16337
|
+
"picture",
|
|
16338
|
+
"video",
|
|
16339
|
+
"none"
|
|
16340
|
+
];
|
|
16341
|
+
var VALID_TEXT_TYPES = ["withText", "noText"];
|
|
16342
|
+
var normalizeAttachmentType = (text) => {
|
|
16343
|
+
const t = (text ?? "").trim();
|
|
16344
|
+
return VALID_ATTACHMENT_TYPES.includes(t) ? t : "picture";
|
|
16345
|
+
};
|
|
16346
|
+
var normalizeTextType = (text) => {
|
|
16347
|
+
const t = (text ?? "").trim();
|
|
16348
|
+
return VALID_TEXT_TYPES.includes(t) ? t : "withText";
|
|
16349
|
+
};
|
|
16350
|
+
function parseEmailConfigFromXml(xml) {
|
|
16351
|
+
const cfg = {
|
|
16352
|
+
smtpServer: getXmlText(xml, "smtpServer") ?? "",
|
|
16353
|
+
userName: getXmlText(xml, "userName") ?? "",
|
|
16354
|
+
password: getXmlText(xml, "password") ?? "",
|
|
16355
|
+
address1: getXmlText(xml, "address1") ?? "",
|
|
16356
|
+
address2: getXmlText(xml, "address2") ?? "",
|
|
16357
|
+
address3: getXmlText(xml, "address3") ?? "",
|
|
16358
|
+
smtpPort: parseNumberSafe(getXmlText(xml, "smtpPort")) ?? 0,
|
|
16359
|
+
sendNickname: getXmlText(xml, "sendNickname") ?? "",
|
|
16360
|
+
attachment: parseInt01(getXmlText(xml, "attachment")) ?? 0,
|
|
16361
|
+
attachmentType: normalizeAttachmentType(getXmlText(xml, "attachmentType")),
|
|
16362
|
+
textType: normalizeTextType(getXmlText(xml, "textType")),
|
|
16363
|
+
ssl: parseInt01(getXmlText(xml, "ssl")) ?? 0,
|
|
16364
|
+
interval: parseNumberSafe(getXmlText(xml, "interval")) ?? 30
|
|
16365
|
+
};
|
|
16366
|
+
const senderMaxLen = parseNumberSafe(getXmlText(xml, "senderMaxLen"));
|
|
16367
|
+
if (senderMaxLen !== void 0) cfg.senderMaxLen = senderMaxLen;
|
|
16368
|
+
const pwdMaxLen = parseNumberSafe(getXmlText(xml, "pwdMaxLen"));
|
|
16369
|
+
if (pwdMaxLen !== void 0) cfg.pwdMaxLen = pwdMaxLen;
|
|
16370
|
+
const ability = parseNumberSafe(getXmlText(xml, "emailAttachAbility"));
|
|
16371
|
+
if (ability !== void 0) cfg.emailAttachAbility = ability;
|
|
16372
|
+
return cfg;
|
|
16373
|
+
}
|
|
16374
|
+
function buildSetEmailXml(current, patch) {
|
|
16375
|
+
const merged = { ...current, ...patch };
|
|
16376
|
+
return ensureXmlHeader(
|
|
16377
|
+
`<body><Email version="1.1"><smtpServer>${xmlEscape(merged.smtpServer)}</smtpServer><userName>${xmlEscape(merged.userName)}</userName><password>${xmlEscape(merged.password)}</password><address1>${xmlEscape(merged.address1)}</address1><address2>${xmlEscape(merged.address2)}</address2><address3>${xmlEscape(merged.address3)}</address3><smtpPort>${merged.smtpPort}</smtpPort><sendNickname>${xmlEscape(merged.sendNickname)}</sendNickname><attachment>${merged.attachment}</attachment><attachmentType>${merged.attachmentType}</attachmentType><textType>${merged.textType}</textType><ssl>${merged.ssl}</ssl><interval>${merged.interval}</interval></Email></body>`
|
|
16378
|
+
);
|
|
16379
|
+
}
|
|
16380
|
+
function parseEmailTaskFromXml(xml) {
|
|
16381
|
+
const channelId = parseNumberSafe(getXmlText(xml, "channelId")) ?? 0;
|
|
16382
|
+
const enable = parseInt01(getXmlText(xml, "enable")) ?? 0;
|
|
16383
|
+
const items = [];
|
|
16384
|
+
const itemRe = /<item>([\s\S]*?)<\/item>/g;
|
|
16385
|
+
let match;
|
|
16386
|
+
while ((match = itemRe.exec(xml)) !== null) {
|
|
16387
|
+
const block = match[1] ?? "";
|
|
16388
|
+
items.push({
|
|
16389
|
+
type: getXmlText(block, "type") ?? "none",
|
|
16390
|
+
valueTable: getXmlText(block, "valueTable") ?? ""
|
|
16391
|
+
});
|
|
16392
|
+
}
|
|
16393
|
+
return { channelId, enable, typeScheduleList: items };
|
|
16394
|
+
}
|
|
16395
|
+
function buildEmailScheduleValueTable(spec) {
|
|
16396
|
+
if (spec.kind === "always") return "1".repeat(168);
|
|
16397
|
+
const grid = new Array(168).fill("0");
|
|
16398
|
+
if (spec.kind === "never") return grid.join("");
|
|
16399
|
+
for (const w of spec.windows) {
|
|
16400
|
+
const startH = Math.max(0, Math.min(24, w.startHour));
|
|
16401
|
+
const endH = Math.max(startH, Math.min(24, w.endHour));
|
|
16402
|
+
for (const d of w.days) {
|
|
16403
|
+
if (d < 0 || d > 6) continue;
|
|
16404
|
+
for (let h = startH; h < endH; h++) {
|
|
16405
|
+
grid[d * 24 + h] = "1";
|
|
16406
|
+
}
|
|
16407
|
+
}
|
|
16408
|
+
}
|
|
16409
|
+
return grid.join("");
|
|
16410
|
+
}
|
|
16411
|
+
function buildSetEmailTaskXml(task) {
|
|
16412
|
+
const items = task.typeScheduleList.map(
|
|
16413
|
+
(item) => `<item><type>${xmlEscape(item.type)}</type><valueTable>${xmlEscape(item.valueTable)}</valueTable></item>`
|
|
16414
|
+
).join("");
|
|
16415
|
+
return ensureXmlHeader(
|
|
16416
|
+
`<body><EmailTask version="1.1"><channelId>${task.channelId}</channelId><enable>${task.enable}</enable><typeScheduleList>${items}</typeScheduleList></EmailTask></body>`
|
|
16417
|
+
);
|
|
16418
|
+
}
|
|
16419
|
+
|
|
16420
|
+
// src/reolink/baichuan/utils/ntp.ts
|
|
16421
|
+
init_xml();
|
|
16422
|
+
var parseNumberSafe2 = (text) => {
|
|
16423
|
+
if (text === void 0) return void 0;
|
|
16424
|
+
const n = Number(text);
|
|
16425
|
+
return Number.isFinite(n) ? n : void 0;
|
|
16426
|
+
};
|
|
16427
|
+
var parseInt012 = (text) => {
|
|
16428
|
+
if (text === void 0) return void 0;
|
|
16429
|
+
return text.trim() === "1" ? 1 : 0;
|
|
16430
|
+
};
|
|
16431
|
+
function parseNtpConfigFromXml(xml) {
|
|
16432
|
+
return {
|
|
16433
|
+
enable: parseInt012(getXmlText(xml, "enable")) ?? 0,
|
|
16434
|
+
server: getXmlText(xml, "server") ?? "",
|
|
16435
|
+
synchronizeInterval: parseNumberSafe2(getXmlText(xml, "synchronizeInterval")) ?? 1440,
|
|
16436
|
+
port: parseNumberSafe2(getXmlText(xml, "port")) ?? 123
|
|
16437
|
+
};
|
|
16438
|
+
}
|
|
16439
|
+
function buildSetNtpXml(current, patch) {
|
|
16440
|
+
const merged = { ...current, ...patch };
|
|
16441
|
+
return ensureXmlHeader(
|
|
16442
|
+
`<body><Ntp version="1.1"><enable>${merged.enable}</enable><server>${xmlEscape(merged.server)}</server><synchronizeInterval>${merged.synchronizeInterval}</synchronizeInterval><port>${merged.port}</port></Ntp></body>`
|
|
16443
|
+
);
|
|
16444
|
+
}
|
|
16445
|
+
|
|
16446
|
+
// src/reolink/baichuan/utils/dst.ts
|
|
16447
|
+
init_xml();
|
|
16448
|
+
var parseNumberSafe3 = (text) => {
|
|
16449
|
+
if (text === void 0) return void 0;
|
|
16450
|
+
const n = Number(text);
|
|
16451
|
+
return Number.isFinite(n) ? n : void 0;
|
|
16452
|
+
};
|
|
16453
|
+
var parseInt013 = (text) => {
|
|
16454
|
+
if (text === void 0) return void 0;
|
|
16455
|
+
return text.trim() === "1" ? 1 : 0;
|
|
16456
|
+
};
|
|
16457
|
+
var VALID_WEEKDAYS = [
|
|
16458
|
+
"Sunday",
|
|
16459
|
+
"Monday",
|
|
16460
|
+
"Tuesday",
|
|
16461
|
+
"Wednesday",
|
|
16462
|
+
"Thursday",
|
|
16463
|
+
"Friday",
|
|
16464
|
+
"Saturday"
|
|
16465
|
+
];
|
|
16466
|
+
var normalizeWeekday = (text) => {
|
|
16467
|
+
const t = (text ?? "").trim();
|
|
16468
|
+
return VALID_WEEKDAYS.includes(t) ? t : "Sunday";
|
|
16469
|
+
};
|
|
16470
|
+
function parseDstConfigFromXml(xml) {
|
|
16471
|
+
const cfg = {
|
|
16472
|
+
enable: parseInt013(getXmlText(xml, "enable")) ?? 0,
|
|
16473
|
+
offset: parseNumberSafe3(getXmlText(xml, "offset")) ?? 1,
|
|
16474
|
+
startMonth: parseNumberSafe3(getXmlText(xml, "startMonth")) ?? 3,
|
|
16475
|
+
startWeekIndex: parseNumberSafe3(getXmlText(xml, "startWeekIndex")) ?? 5,
|
|
16476
|
+
startWeekday: normalizeWeekday(getXmlText(xml, "startWeekday")),
|
|
16477
|
+
startHour: parseNumberSafe3(getXmlText(xml, "startHour")) ?? 2,
|
|
16478
|
+
startMinute: parseNumberSafe3(getXmlText(xml, "startMinute")) ?? 0,
|
|
16479
|
+
startSecond: parseNumberSafe3(getXmlText(xml, "startSecond")) ?? 0,
|
|
16480
|
+
endMonth: parseNumberSafe3(getXmlText(xml, "endMonth")) ?? 10,
|
|
16481
|
+
endWeekIndex: parseNumberSafe3(getXmlText(xml, "endWeekIndex")) ?? 4,
|
|
16482
|
+
endWeekday: normalizeWeekday(getXmlText(xml, "endWeekday")),
|
|
16483
|
+
endHour: parseNumberSafe3(getXmlText(xml, "endHour")) ?? 3,
|
|
16484
|
+
endMinute: parseNumberSafe3(getXmlText(xml, "endMinute")) ?? 0,
|
|
16485
|
+
endSecond: parseNumberSafe3(getXmlText(xml, "endSecond")) ?? 0
|
|
16486
|
+
};
|
|
16487
|
+
const version = parseNumberSafe3(getXmlText(xml, "version"));
|
|
16488
|
+
if (version !== void 0) cfg.version = version;
|
|
16489
|
+
return cfg;
|
|
16490
|
+
}
|
|
16491
|
+
function buildSetDstXml(current, patch) {
|
|
16492
|
+
const merged = { ...current, ...patch };
|
|
16493
|
+
return ensureXmlHeader(
|
|
16494
|
+
`<body><Dst version="1.1"><enable>${merged.enable}</enable><offset>${merged.offset}</offset><startMonth>${merged.startMonth}</startMonth><startWeekIndex>${merged.startWeekIndex}</startWeekIndex><startWeekday>${xmlEscape(merged.startWeekday)}</startWeekday><startHour>${merged.startHour}</startHour><startMinute>${merged.startMinute}</startMinute><startSecond>${merged.startSecond}</startSecond><endMonth>${merged.endMonth}</endMonth><endWeekIndex>${merged.endWeekIndex}</endWeekIndex><endWeekday>${xmlEscape(merged.endWeekday)}</endWeekday><endHour>${merged.endHour}</endHour><endMinute>${merged.endMinute}</endMinute><endSecond>${merged.endSecond}</endSecond></Dst></body>`
|
|
16495
|
+
);
|
|
16496
|
+
}
|
|
16497
|
+
|
|
16498
|
+
// src/reolink/baichuan/utils/autoReboot.ts
|
|
16499
|
+
init_xml();
|
|
16500
|
+
var parseNumberSafe4 = (text) => {
|
|
16501
|
+
if (text === void 0) return void 0;
|
|
16502
|
+
const n = Number(text);
|
|
16503
|
+
return Number.isFinite(n) ? n : void 0;
|
|
16504
|
+
};
|
|
16505
|
+
var parseInt014 = (text) => {
|
|
16506
|
+
if (text === void 0) return void 0;
|
|
16507
|
+
return text.trim() === "1" ? 1 : 0;
|
|
16508
|
+
};
|
|
16509
|
+
var VALID_WEEKDAYS2 = [
|
|
16510
|
+
"Sunday",
|
|
16511
|
+
"Monday",
|
|
16512
|
+
"Tuesday",
|
|
16513
|
+
"Wednesday",
|
|
16514
|
+
"Thursday",
|
|
16515
|
+
"Friday",
|
|
16516
|
+
"Saturday",
|
|
16517
|
+
"everyday"
|
|
16518
|
+
];
|
|
16519
|
+
var normalizeWeekday2 = (text) => {
|
|
16520
|
+
const t = (text ?? "").trim();
|
|
16521
|
+
return VALID_WEEKDAYS2.includes(t) ? t : "Sunday";
|
|
16522
|
+
};
|
|
16523
|
+
function parseAutoRebootFromXml(xml) {
|
|
16524
|
+
return {
|
|
16525
|
+
enable: parseInt014(getXmlText(xml, "enable")) ?? 0,
|
|
16526
|
+
weekDay: normalizeWeekday2(getXmlText(xml, "weekDay")),
|
|
16527
|
+
hour: parseNumberSafe4(getXmlText(xml, "hour")) ?? 0,
|
|
16528
|
+
minute: parseNumberSafe4(getXmlText(xml, "minute")) ?? 0,
|
|
16529
|
+
second: parseNumberSafe4(getXmlText(xml, "second")) ?? 0
|
|
16530
|
+
};
|
|
16531
|
+
}
|
|
16532
|
+
function buildSetAutoRebootXml(current, patch) {
|
|
16533
|
+
const merged = { ...current, ...patch };
|
|
16534
|
+
return ensureXmlHeader(
|
|
16535
|
+
`<body><AutoReboot version="1.1"><enable>${merged.enable}</enable><weekDay>${xmlEscape(merged.weekDay)}</weekDay><hour>${merged.hour}</hour><minute>${merged.minute}</minute><second>${merged.second}</second></AutoReboot></body>`
|
|
16536
|
+
);
|
|
16537
|
+
}
|
|
16538
|
+
|
|
16539
|
+
// src/reolink/baichuan/utils/systemGeneral.ts
|
|
16540
|
+
init_xml();
|
|
16541
|
+
var parseNumberSafe5 = (text) => {
|
|
16542
|
+
if (text === void 0) return void 0;
|
|
16543
|
+
const n = Number(text);
|
|
16544
|
+
return Number.isFinite(n) ? n : void 0;
|
|
16545
|
+
};
|
|
16546
|
+
var parseInt015 = (text) => {
|
|
16547
|
+
if (text === void 0) return void 0;
|
|
16548
|
+
return text.trim() === "1" ? 1 : 0;
|
|
16549
|
+
};
|
|
16550
|
+
var VALID_OSD_FORMATS = ["DMY", "MDY", "YMD"];
|
|
16551
|
+
var normalizeOsdFormat = (text) => {
|
|
16552
|
+
const t = (text ?? "").trim();
|
|
16553
|
+
return VALID_OSD_FORMATS.includes(t) ? t : "YMD";
|
|
16554
|
+
};
|
|
16555
|
+
function parseSystemGeneralFromXml(xml) {
|
|
16556
|
+
return {
|
|
16557
|
+
timeZone: parseNumberSafe5(getXmlText(xml, "timeZone")) ?? 0,
|
|
16558
|
+
osdFormat: normalizeOsdFormat(getXmlText(xml, "osdFormat")),
|
|
16559
|
+
year: parseNumberSafe5(getXmlText(xml, "year")) ?? 0,
|
|
16560
|
+
month: parseNumberSafe5(getXmlText(xml, "month")) ?? 0,
|
|
16561
|
+
day: parseNumberSafe5(getXmlText(xml, "day")) ?? 0,
|
|
16562
|
+
hour: parseNumberSafe5(getXmlText(xml, "hour")) ?? 0,
|
|
16563
|
+
minute: parseNumberSafe5(getXmlText(xml, "minute")) ?? 0,
|
|
16564
|
+
second: parseNumberSafe5(getXmlText(xml, "second")) ?? 0,
|
|
16565
|
+
deviceId: parseNumberSafe5(getXmlText(xml, "deviceId")) ?? 0,
|
|
16566
|
+
timeFormat: parseInt015(getXmlText(xml, "timeFormat")) ?? 0,
|
|
16567
|
+
language: getXmlText(xml, "language") ?? "English",
|
|
16568
|
+
deviceName: getXmlText(xml, "deviceName") ?? "",
|
|
16569
|
+
loginLock: parseInt015(getXmlText(xml, "loginLock")) ?? 0,
|
|
16570
|
+
lockTime: parseNumberSafe5(getXmlText(xml, "lockTime")) ?? 0,
|
|
16571
|
+
allowedTimes: parseNumberSafe5(getXmlText(xml, "allowedTimes")) ?? 0,
|
|
16572
|
+
isDst: parseInt015(getXmlText(xml, "isDst")) ?? 0
|
|
16573
|
+
};
|
|
16574
|
+
}
|
|
16575
|
+
function buildSetSystemGeneralXml(patch) {
|
|
16576
|
+
const parts = [];
|
|
16577
|
+
const isDeviceNameOnly = patch.deviceName !== void 0 && patch.timeZone === void 0 && patch.osdFormat === void 0 && patch.timeFormat === void 0 && patch.language === void 0 && patch.loginLock === void 0 && patch.lockTime === void 0 && patch.allowedTimes === void 0 && patch.manualTime === void 0;
|
|
16578
|
+
if (isDeviceNameOnly) {
|
|
16579
|
+
parts.push("<year>0</year>");
|
|
16580
|
+
parts.push(`<deviceName>${xmlEscape(patch.deviceName)}</deviceName>`);
|
|
16581
|
+
parts.push("<deviceNameOnly>1</deviceNameOnly>");
|
|
16582
|
+
} else if (patch.manualTime !== void 0) {
|
|
16583
|
+
const mt = patch.manualTime;
|
|
16584
|
+
if (patch.timeZone !== void 0)
|
|
16585
|
+
parts.push(`<timeZone>${patch.timeZone}</timeZone>`);
|
|
16586
|
+
if (patch.osdFormat !== void 0)
|
|
16587
|
+
parts.push(`<osdFormat>${patch.osdFormat}</osdFormat>`);
|
|
16588
|
+
parts.push(`<year>${mt.year}</year>`);
|
|
16589
|
+
parts.push(`<month>${mt.month}</month>`);
|
|
16590
|
+
parts.push(`<day>${mt.day}</day>`);
|
|
16591
|
+
parts.push(`<hour>${mt.hour}</hour>`);
|
|
16592
|
+
parts.push(`<minute>${mt.minute}</minute>`);
|
|
16593
|
+
parts.push(`<second>${mt.second}</second>`);
|
|
16594
|
+
if (patch.timeFormat !== void 0)
|
|
16595
|
+
parts.push(`<timeFormat>${patch.timeFormat}</timeFormat>`);
|
|
16596
|
+
if (patch.language !== void 0)
|
|
16597
|
+
parts.push(`<language>${xmlEscape(patch.language)}</language>`);
|
|
16598
|
+
if (patch.deviceName !== void 0)
|
|
16599
|
+
parts.push(`<deviceName>${xmlEscape(patch.deviceName)}</deviceName>`);
|
|
16600
|
+
if (patch.loginLock !== void 0)
|
|
16601
|
+
parts.push(`<loginLock>${patch.loginLock}</loginLock>`);
|
|
16602
|
+
if (patch.lockTime !== void 0)
|
|
16603
|
+
parts.push(`<lockTime>${patch.lockTime}</lockTime>`);
|
|
16604
|
+
if (patch.allowedTimes !== void 0)
|
|
16605
|
+
parts.push(`<allowedTimes>${patch.allowedTimes}</allowedTimes>`);
|
|
16606
|
+
} else {
|
|
16607
|
+
if (patch.timeZone !== void 0)
|
|
16608
|
+
parts.push(`<timeZone>${patch.timeZone}</timeZone>`);
|
|
16609
|
+
if (patch.osdFormat !== void 0)
|
|
16610
|
+
parts.push(`<osdFormat>${patch.osdFormat}</osdFormat>`);
|
|
16611
|
+
if (patch.timeFormat !== void 0)
|
|
16612
|
+
parts.push(`<timeFormat>${patch.timeFormat}</timeFormat>`);
|
|
16613
|
+
if (patch.language !== void 0)
|
|
16614
|
+
parts.push(`<language>${xmlEscape(patch.language)}</language>`);
|
|
16615
|
+
if (patch.loginLock !== void 0)
|
|
16616
|
+
parts.push(`<loginLock>${patch.loginLock}</loginLock>`);
|
|
16617
|
+
if (patch.lockTime !== void 0)
|
|
16618
|
+
parts.push(`<lockTime>${patch.lockTime}</lockTime>`);
|
|
16619
|
+
if (patch.allowedTimes !== void 0)
|
|
16620
|
+
parts.push(`<allowedTimes>${patch.allowedTimes}</allowedTimes>`);
|
|
16621
|
+
parts.push("<year>0</year>");
|
|
16622
|
+
}
|
|
16623
|
+
return ensureXmlHeader(
|
|
16624
|
+
`<body><SystemGeneral version="1.1">${parts.join("")}</SystemGeneral></body>`
|
|
16625
|
+
);
|
|
16626
|
+
}
|
|
16627
|
+
|
|
16137
16628
|
// src/reolink/baichuan/ReolinkBaichuanApi.ts
|
|
16138
16629
|
var import_jimp = require("jimp");
|
|
16139
16630
|
init_ReolinkCgiApi();
|
|
@@ -16410,6 +16901,32 @@ var parseAbilityInfoXml = (xml) => {
|
|
|
16410
16901
|
return abilities;
|
|
16411
16902
|
};
|
|
16412
16903
|
|
|
16904
|
+
// src/reolink/baichuan/utils/versionInfo.ts
|
|
16905
|
+
init_xml();
|
|
16906
|
+
function parseVersionInfo(xml) {
|
|
16907
|
+
const out = {};
|
|
16908
|
+
const set = (key) => {
|
|
16909
|
+
const v = getXmlText(xml, key);
|
|
16910
|
+
if (v !== void 0) out[key] = v;
|
|
16911
|
+
};
|
|
16912
|
+
set("name");
|
|
16913
|
+
set("type");
|
|
16914
|
+
set("serialNumber");
|
|
16915
|
+
set("buildDay");
|
|
16916
|
+
set("hardwareVersion");
|
|
16917
|
+
set("cfgVersion");
|
|
16918
|
+
set("firmwareVersion");
|
|
16919
|
+
set("detail");
|
|
16920
|
+
set("IEClient");
|
|
16921
|
+
set("cc3200Version");
|
|
16922
|
+
set("spVersion");
|
|
16923
|
+
set("pakSuffix");
|
|
16924
|
+
set("itemNo");
|
|
16925
|
+
set("aiVersion");
|
|
16926
|
+
set("helpVersion");
|
|
16927
|
+
return out;
|
|
16928
|
+
}
|
|
16929
|
+
|
|
16413
16930
|
// src/reolink/baichuan/utils/aiState.ts
|
|
16414
16931
|
init_constants();
|
|
16415
16932
|
init_xml();
|
|
@@ -16716,6 +17233,204 @@ var buildChannelPushDataLogSnapshot = (channelPushData) => {
|
|
|
16716
17233
|
return { result: resultObj, storedChannels: Object.keys(resultObj) };
|
|
16717
17234
|
};
|
|
16718
17235
|
|
|
17236
|
+
// src/reolink/baichuan/utils/detection.ts
|
|
17237
|
+
var lz4 = __toESM(require("lz4js"), 1);
|
|
17238
|
+
|
|
17239
|
+
// src/reolink/baichuan/utils/aiClassMap.ts
|
|
17240
|
+
function type1ToLabel(type1) {
|
|
17241
|
+
switch (type1) {
|
|
17242
|
+
case 1:
|
|
17243
|
+
return "people";
|
|
17244
|
+
case 2:
|
|
17245
|
+
return "vehicle";
|
|
17246
|
+
case 3:
|
|
17247
|
+
return "animal";
|
|
17248
|
+
case 11259375:
|
|
17249
|
+
return "face";
|
|
17250
|
+
default:
|
|
17251
|
+
return "unknown";
|
|
17252
|
+
}
|
|
17253
|
+
}
|
|
17254
|
+
|
|
17255
|
+
// src/reolink/baichuan/utils/detection.ts
|
|
17256
|
+
var MARKER_LENGTH = 8;
|
|
17257
|
+
var IFRAME_PREFIX_LENGTH = 8;
|
|
17258
|
+
var COUNTER_OFFSET = 8;
|
|
17259
|
+
var BASELINE_SIZE = 128;
|
|
17260
|
+
var FRAME_SIZE_TLV = Buffer.from([3, 4, 0]);
|
|
17261
|
+
var LZ4F_MAGIC = Buffer.from([4, 34, 77, 24]);
|
|
17262
|
+
var CONFIDENCE_DIVISOR = 100;
|
|
17263
|
+
var DEFAULT_AI_FRAME_WIDTH = 896;
|
|
17264
|
+
var DEFAULT_AI_FRAME_HEIGHT = 480;
|
|
17265
|
+
var LZ4_DECOMPRESS_MAX = 256 * 1024;
|
|
17266
|
+
function walkBoxes(buf, start, end, type1, type2, out) {
|
|
17267
|
+
let pos = start;
|
|
17268
|
+
while (pos + 3 <= end) {
|
|
17269
|
+
const t = buf[pos];
|
|
17270
|
+
if (t === 0) return;
|
|
17271
|
+
const length = buf[pos + 1] | buf[pos + 2] << 8;
|
|
17272
|
+
const recordEnd = pos + 3 + length;
|
|
17273
|
+
if (recordEnd > end) return;
|
|
17274
|
+
const isBoxType4 = t === 4 && (length === 10 || length === 13 || length === 14);
|
|
17275
|
+
const isBoxType2 = t === 2 && length === 10;
|
|
17276
|
+
if ((isBoxType4 || isBoxType2) && type1 !== 0 && type2 !== 0) {
|
|
17277
|
+
const x1 = buf.readUInt16LE(pos + 3);
|
|
17278
|
+
const y1 = buf.readUInt16LE(pos + 5);
|
|
17279
|
+
const x2 = buf.readUInt16LE(pos + 7);
|
|
17280
|
+
const y2 = buf.readUInt16LE(pos + 9);
|
|
17281
|
+
const conf = buf.readUInt16LE(pos + 11);
|
|
17282
|
+
if (x2 > x1 && y2 > y1) {
|
|
17283
|
+
out.push({ x1, y1, x2, y2, conf, label: type1ToLabel(type1) });
|
|
17284
|
+
}
|
|
17285
|
+
pos = recordEnd;
|
|
17286
|
+
continue;
|
|
17287
|
+
}
|
|
17288
|
+
if (type1 === 255 && type2 === 2 && t === 2 && length >= LZ4F_MAGIC.length && buf[pos + 3] === LZ4F_MAGIC[0] && buf[pos + 4] === LZ4F_MAGIC[1] && buf[pos + 5] === LZ4F_MAGIC[2] && buf[pos + 6] === LZ4F_MAGIC[3]) {
|
|
17289
|
+
try {
|
|
17290
|
+
const decompressed = lz4.decompress(
|
|
17291
|
+
buf.subarray(pos + 3, recordEnd),
|
|
17292
|
+
LZ4_DECOMPRESS_MAX
|
|
17293
|
+
);
|
|
17294
|
+
const decBuf = Buffer.from(decompressed);
|
|
17295
|
+
walkBoxes(decBuf, 0, decBuf.length, 0, 0, out);
|
|
17296
|
+
} catch {
|
|
17297
|
+
}
|
|
17298
|
+
pos = recordEnd;
|
|
17299
|
+
continue;
|
|
17300
|
+
}
|
|
17301
|
+
if (length > 0) {
|
|
17302
|
+
let nextT1 = type1;
|
|
17303
|
+
let nextT2 = type2;
|
|
17304
|
+
if (type1 === 0) nextT1 = t;
|
|
17305
|
+
else if (type2 === 0) nextT2 = t;
|
|
17306
|
+
walkBoxes(buf, pos + 3, recordEnd, nextT1, nextT2, out);
|
|
17307
|
+
}
|
|
17308
|
+
pos = recordEnd;
|
|
17309
|
+
}
|
|
17310
|
+
}
|
|
17311
|
+
function decodeDetectionHeader(raw, frameType) {
|
|
17312
|
+
const markerOffset = frameType === "Iframe" ? IFRAME_PREFIX_LENGTH : 0;
|
|
17313
|
+
const blockLength = raw.length - markerOffset;
|
|
17314
|
+
const empty = {
|
|
17315
|
+
state: "invalid-marker",
|
|
17316
|
+
markerOffset,
|
|
17317
|
+
blockLength,
|
|
17318
|
+
boxes: []
|
|
17319
|
+
};
|
|
17320
|
+
if (blockLength < MARKER_LENGTH) return empty;
|
|
17321
|
+
if (!hasStandardMarker(raw, markerOffset)) return empty;
|
|
17322
|
+
if (blockLength < COUNTER_OFFSET + 4) return empty;
|
|
17323
|
+
const counter = raw.readUInt32LE(markerOffset + COUNTER_OFFSET);
|
|
17324
|
+
const rawBoxes = [];
|
|
17325
|
+
walkBoxes(raw, markerOffset, raw.length, 0, 0, rawBoxes);
|
|
17326
|
+
let aiFrameWidth = DEFAULT_AI_FRAME_WIDTH;
|
|
17327
|
+
let aiFrameHeight = DEFAULT_AI_FRAME_HEIGHT;
|
|
17328
|
+
let frameSizeFound = false;
|
|
17329
|
+
const searchStart = markerOffset + MARKER_LENGTH;
|
|
17330
|
+
for (let i = searchStart; i + 7 <= raw.length; i++) {
|
|
17331
|
+
if (raw[i] === FRAME_SIZE_TLV[0] && raw[i + 1] === FRAME_SIZE_TLV[1] && raw[i + 2] === FRAME_SIZE_TLV[2]) {
|
|
17332
|
+
const w = raw.readUInt16LE(i + 3);
|
|
17333
|
+
const h = raw.readUInt16LE(i + 5);
|
|
17334
|
+
if (w >= 64 && w <= 8192 && h >= 64 && h <= 8192) {
|
|
17335
|
+
aiFrameWidth = w;
|
|
17336
|
+
aiFrameHeight = h;
|
|
17337
|
+
frameSizeFound = true;
|
|
17338
|
+
break;
|
|
17339
|
+
}
|
|
17340
|
+
}
|
|
17341
|
+
}
|
|
17342
|
+
const specificity = {
|
|
17343
|
+
face: 4,
|
|
17344
|
+
animal: 3,
|
|
17345
|
+
people: 2,
|
|
17346
|
+
vehicle: 1,
|
|
17347
|
+
unknown: 0
|
|
17348
|
+
};
|
|
17349
|
+
const dedup = /* @__PURE__ */ new Map();
|
|
17350
|
+
for (const rb of rawBoxes) {
|
|
17351
|
+
if (rb.x2 > aiFrameWidth || rb.y2 > aiFrameHeight) continue;
|
|
17352
|
+
const key = `${rb.x1}_${rb.y1}_${rb.x2}_${rb.y2}`;
|
|
17353
|
+
const prev = dedup.get(key);
|
|
17354
|
+
if (!prev || (specificity[rb.label] ?? 0) > (specificity[prev.label] ?? 0)) {
|
|
17355
|
+
dedup.set(key, rb);
|
|
17356
|
+
}
|
|
17357
|
+
}
|
|
17358
|
+
const boxes = [];
|
|
17359
|
+
for (const rb of dedup.values()) {
|
|
17360
|
+
boxes.push({
|
|
17361
|
+
x: rb.x1 / aiFrameWidth,
|
|
17362
|
+
y: rb.y1 / aiFrameHeight,
|
|
17363
|
+
width: (rb.x2 - rb.x1) / aiFrameWidth,
|
|
17364
|
+
height: (rb.y2 - rb.y1) / aiFrameHeight,
|
|
17365
|
+
...rb.conf > 0 && rb.conf <= 100 ? { confidence: rb.conf / CONFIDENCE_DIVISOR } : {},
|
|
17366
|
+
...rb.label !== "unknown" ? { label: rb.label } : {}
|
|
17367
|
+
});
|
|
17368
|
+
}
|
|
17369
|
+
let state;
|
|
17370
|
+
if (boxes.length > 0) state = "overlay-decoded";
|
|
17371
|
+
else if (blockLength === BASELINE_SIZE) state = "no-overlay";
|
|
17372
|
+
else state = "overlay-undecoded";
|
|
17373
|
+
return {
|
|
17374
|
+
state,
|
|
17375
|
+
markerOffset,
|
|
17376
|
+
blockLength,
|
|
17377
|
+
counter,
|
|
17378
|
+
...frameSizeFound ? { aiFrameWidth, aiFrameHeight } : {},
|
|
17379
|
+
boxes
|
|
17380
|
+
};
|
|
17381
|
+
}
|
|
17382
|
+
function hasStandardMarker(raw, offset) {
|
|
17383
|
+
if (raw.length < offset + MARKER_LENGTH) return false;
|
|
17384
|
+
return raw[offset] === 255 && raw[offset + 2] === 0 && raw[offset + 3] === 1 && raw[offset + 4] === 11 && raw[offset + 5] === 0 && raw[offset + 6] === 1 && raw[offset + 7] === 8;
|
|
17385
|
+
}
|
|
17386
|
+
|
|
17387
|
+
// src/reolink/baichuan/utils/encOptions.ts
|
|
17388
|
+
function buildEncOptions(list, channel) {
|
|
17389
|
+
const result = { channel };
|
|
17390
|
+
const main = aggregateByType(list, "mainStream");
|
|
17391
|
+
const sub = aggregateByType(list, "subStream");
|
|
17392
|
+
const third = aggregateByType(list, "thirdStream");
|
|
17393
|
+
if (main) result.mainStream = main;
|
|
17394
|
+
if (sub) result.subStream = sub;
|
|
17395
|
+
if (third) result.thirdStream = third;
|
|
17396
|
+
return result;
|
|
17397
|
+
}
|
|
17398
|
+
function aggregateByType(list, type) {
|
|
17399
|
+
const seen = /* @__PURE__ */ new Map();
|
|
17400
|
+
for (const stream of list.streams) {
|
|
17401
|
+
for (const eb of stream.encodeTables) {
|
|
17402
|
+
if (eb.type !== type) continue;
|
|
17403
|
+
const mapped = mapEncodeTable(eb);
|
|
17404
|
+
if (!mapped) continue;
|
|
17405
|
+
const key = `${mapped.width}x${mapped.height}`;
|
|
17406
|
+
if (!seen.has(key)) seen.set(key, mapped);
|
|
17407
|
+
}
|
|
17408
|
+
}
|
|
17409
|
+
if (seen.size === 0) return void 0;
|
|
17410
|
+
return {
|
|
17411
|
+
type,
|
|
17412
|
+
resolutions: [...seen.values()],
|
|
17413
|
+
encoderTypes: ["vbr", "cbr"],
|
|
17414
|
+
encoderProfiles: ["high", "main", "baseline"]
|
|
17415
|
+
};
|
|
17416
|
+
}
|
|
17417
|
+
function mapEncodeTable(eb) {
|
|
17418
|
+
if (eb.width == null || eb.height == null) return void 0;
|
|
17419
|
+
const videoEncTypes = (eb.videoEncTypeList ?? (eb.videoEncType != null ? [eb.videoEncType] : [])).map(
|
|
17420
|
+
(t) => t === 0 ? "h264" : t === 1 ? "h265" : void 0
|
|
17421
|
+
).filter((t) => t !== void 0);
|
|
17422
|
+
return {
|
|
17423
|
+
width: eb.width,
|
|
17424
|
+
height: eb.height,
|
|
17425
|
+
videoEncTypes,
|
|
17426
|
+
...eb.defaultFramerate != null ? { defaultFramerate: eb.defaultFramerate } : {},
|
|
17427
|
+
...eb.defaultBitrate != null ? { defaultBitrate: eb.defaultBitrate } : {},
|
|
17428
|
+
...eb.defaultGop != null ? { defaultGop: eb.defaultGop } : {},
|
|
17429
|
+
framerateOptions: eb.framerateTable ?? [],
|
|
17430
|
+
bitrateOptions: eb.bitrateTable ?? []
|
|
17431
|
+
};
|
|
17432
|
+
}
|
|
17433
|
+
|
|
16719
17434
|
// src/reolink/baichuan/utils/events.ts
|
|
16720
17435
|
var mapToSimpleEvent = (event) => {
|
|
16721
17436
|
const timestamp = event.timestamp ?? Date.now();
|
|
@@ -18839,6 +19554,21 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18839
19554
|
sessionGuardIntervalTimer;
|
|
18840
19555
|
simpleEventListeners = /* @__PURE__ */ new Set();
|
|
18841
19556
|
simpleEventSubscribed = false;
|
|
19557
|
+
// Detection events are sourced from BcMedia additionalHeader on active video
|
|
19558
|
+
// streams. Unlike simpleEvent, no Baichuan subscribe command is needed — the
|
|
19559
|
+
// data flows whenever a stream is open. Active streams register themselves via
|
|
19560
|
+
// _registerVideoStreamForDetection (called from BaichuanVideoStream.start).
|
|
19561
|
+
detectionEventListeners = /* @__PURE__ */ new Set();
|
|
19562
|
+
detectionEventStreamHooks = /* @__PURE__ */ new Map();
|
|
19563
|
+
// Auto-managed substream for `onObjectDetections` listeners. Reference-counted
|
|
19564
|
+
// by the listener set: the substream is opened on the first listener and torn
|
|
19565
|
+
// down with the last one. Mirrors `onSimpleEvent`'s subscribe/unsubscribe
|
|
19566
|
+
// lifecycle so a caller never has to manage a video stream just to read AI
|
|
19567
|
+
// detections.
|
|
19568
|
+
objectDetectionListeners = /* @__PURE__ */ new Set();
|
|
19569
|
+
objectDetectionStream;
|
|
19570
|
+
objectDetectionStreamStartInFlight;
|
|
19571
|
+
objectDetectionInternalListener;
|
|
18842
19572
|
simpleEventSubscribeInFlight;
|
|
18843
19573
|
simpleEventUnsubscribeInFlight;
|
|
18844
19574
|
simpleEventResubscribeTimer;
|
|
@@ -20274,6 +21004,205 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
20274
21004
|
}
|
|
20275
21005
|
}
|
|
20276
21006
|
}
|
|
21007
|
+
/**
|
|
21008
|
+
* Subscribe to per-frame detection events sourced from the BcMedia
|
|
21009
|
+
* `additionalHeader` block on active video streams.
|
|
21010
|
+
*
|
|
21011
|
+
* Mirrors {@link onSimpleEvent} but is fed by the streaming side-channel:
|
|
21012
|
+
* one event fires for every I-frame / P-frame that carries an overlay block.
|
|
21013
|
+
* Coordinates are reported in normalized [0, 1] fractions of the source
|
|
21014
|
+
* frame, so the same box renders correctly on mainStream, subStream, and
|
|
21015
|
+
* externStream.
|
|
21016
|
+
*
|
|
21017
|
+
* Unlike `onSimpleEvent`, no Baichuan subscribe command is involved — events
|
|
21018
|
+
* only flow while a video stream is open. The library hooks every
|
|
21019
|
+
* `BaichuanVideoStream` created via this API for the listener's lifetime.
|
|
21020
|
+
*/
|
|
21021
|
+
onDetection(callback) {
|
|
21022
|
+
this.detectionEventListeners.add(callback);
|
|
21023
|
+
}
|
|
21024
|
+
/**
|
|
21025
|
+
* Remove a single detection callback, or all of them if `callback` is omitted.
|
|
21026
|
+
*/
|
|
21027
|
+
offDetection(callback) {
|
|
21028
|
+
if (callback) {
|
|
21029
|
+
this.detectionEventListeners.delete(callback);
|
|
21030
|
+
} else {
|
|
21031
|
+
this.detectionEventListeners.clear();
|
|
21032
|
+
}
|
|
21033
|
+
}
|
|
21034
|
+
/**
|
|
21035
|
+
* Subscribe to AI object detections (people / vehicle / animal / face boxes
|
|
21036
|
+
* with class label and confidence) without managing a video stream yourself.
|
|
21037
|
+
*
|
|
21038
|
+
* Mirrors {@link onSimpleEvent} end-to-end: the API opens a dedicated
|
|
21039
|
+
* substream behind the scenes on the first listener, forwards every box-bearing
|
|
21040
|
+
* `additionalHeader` to your callback, and tears the stream down when the last
|
|
21041
|
+
* listener unsubscribes. The substream is the lightest profile (typically
|
|
21042
|
+
* 640×360) so the additional bandwidth/CPU overhead is minimal.
|
|
21043
|
+
*
|
|
21044
|
+
* Each event carries normalized `[0, 1]` box coordinates, a class label, and
|
|
21045
|
+
* a confidence score — render-ready without further conversion.
|
|
21046
|
+
*/
|
|
21047
|
+
async onObjectDetections(callback) {
|
|
21048
|
+
this.objectDetectionListeners.add(callback);
|
|
21049
|
+
this.logger.debug?.(
|
|
21050
|
+
`[ReolinkBaichuanApi] onObjectDetections: registering listener (total=${this.objectDetectionListeners.size})`
|
|
21051
|
+
);
|
|
21052
|
+
await this.ensureObjectDetectionStream();
|
|
21053
|
+
}
|
|
21054
|
+
/**
|
|
21055
|
+
* Remove one detection callback, or all of them if `callback` is omitted.
|
|
21056
|
+
* When the last listener is removed the auto-managed substream is closed.
|
|
21057
|
+
*/
|
|
21058
|
+
async offObjectDetections(callback) {
|
|
21059
|
+
if (callback) {
|
|
21060
|
+
this.objectDetectionListeners.delete(callback);
|
|
21061
|
+
} else {
|
|
21062
|
+
this.objectDetectionListeners.clear();
|
|
21063
|
+
}
|
|
21064
|
+
if (this.objectDetectionListeners.size === 0) {
|
|
21065
|
+
await this.tearDownObjectDetectionStream();
|
|
21066
|
+
}
|
|
21067
|
+
}
|
|
21068
|
+
async ensureObjectDetectionStream() {
|
|
21069
|
+
if (this.objectDetectionStream) return;
|
|
21070
|
+
if (this.objectDetectionStreamStartInFlight) {
|
|
21071
|
+
await this.objectDetectionStreamStartInFlight;
|
|
21072
|
+
return;
|
|
21073
|
+
}
|
|
21074
|
+
this.objectDetectionStreamStartInFlight = (async () => {
|
|
21075
|
+
const { BaichuanVideoStream: BaichuanVideoStream2 } = await Promise.resolve().then(() => (init_BaichuanVideoStream(), BaichuanVideoStream_exports));
|
|
21076
|
+
const sessionKey = `live:object-detections:ch0:sub`;
|
|
21077
|
+
const dedicated = await this.createDedicatedSession(sessionKey);
|
|
21078
|
+
const stream = new BaichuanVideoStream2({
|
|
21079
|
+
client: dedicated.client,
|
|
21080
|
+
api: this,
|
|
21081
|
+
channel: 0,
|
|
21082
|
+
profile: "sub",
|
|
21083
|
+
logger: this.logger
|
|
21084
|
+
});
|
|
21085
|
+
this.objectDetectionInternalListener = (event) => {
|
|
21086
|
+
for (const cb of this.objectDetectionListeners) {
|
|
21087
|
+
try {
|
|
21088
|
+
void Promise.resolve(cb(event)).catch((e) => {
|
|
21089
|
+
(this.logger.warn ?? this.logger.error).call(
|
|
21090
|
+
this.logger,
|
|
21091
|
+
"[ReolinkBaichuanApi] onObjectDetections handler error",
|
|
21092
|
+
formatErrorForLog(e)
|
|
21093
|
+
);
|
|
21094
|
+
});
|
|
21095
|
+
} catch (e) {
|
|
21096
|
+
(this.logger.warn ?? this.logger.error).call(
|
|
21097
|
+
this.logger,
|
|
21098
|
+
"[ReolinkBaichuanApi] onObjectDetections handler error",
|
|
21099
|
+
formatErrorForLog(e)
|
|
21100
|
+
);
|
|
21101
|
+
}
|
|
21102
|
+
}
|
|
21103
|
+
};
|
|
21104
|
+
this.detectionEventListeners.add(this.objectDetectionInternalListener);
|
|
21105
|
+
try {
|
|
21106
|
+
await stream.start();
|
|
21107
|
+
} catch (e) {
|
|
21108
|
+
if (this.objectDetectionInternalListener) {
|
|
21109
|
+
this.detectionEventListeners.delete(
|
|
21110
|
+
this.objectDetectionInternalListener
|
|
21111
|
+
);
|
|
21112
|
+
this.objectDetectionInternalListener = void 0;
|
|
21113
|
+
}
|
|
21114
|
+
await dedicated.release().catch(() => {
|
|
21115
|
+
});
|
|
21116
|
+
throw e;
|
|
21117
|
+
}
|
|
21118
|
+
this.objectDetectionStream = {
|
|
21119
|
+
stop: () => stream.stop(),
|
|
21120
|
+
release: () => dedicated.release()
|
|
21121
|
+
};
|
|
21122
|
+
this.logger.debug?.(
|
|
21123
|
+
`[ReolinkBaichuanApi] onObjectDetections: substream started (key=${sessionKey})`
|
|
21124
|
+
);
|
|
21125
|
+
})();
|
|
21126
|
+
try {
|
|
21127
|
+
await this.objectDetectionStreamStartInFlight;
|
|
21128
|
+
} finally {
|
|
21129
|
+
this.objectDetectionStreamStartInFlight = void 0;
|
|
21130
|
+
}
|
|
21131
|
+
}
|
|
21132
|
+
async tearDownObjectDetectionStream() {
|
|
21133
|
+
const handle = this.objectDetectionStream;
|
|
21134
|
+
this.objectDetectionStream = void 0;
|
|
21135
|
+
if (this.objectDetectionInternalListener) {
|
|
21136
|
+
this.detectionEventListeners.delete(this.objectDetectionInternalListener);
|
|
21137
|
+
this.objectDetectionInternalListener = void 0;
|
|
21138
|
+
}
|
|
21139
|
+
if (!handle) return;
|
|
21140
|
+
try {
|
|
21141
|
+
await handle.stop();
|
|
21142
|
+
} catch (e) {
|
|
21143
|
+
this.logger.debug?.(
|
|
21144
|
+
`[ReolinkBaichuanApi] onObjectDetections: stream stop error: ${formatErrorForLog(e)}`
|
|
21145
|
+
);
|
|
21146
|
+
}
|
|
21147
|
+
try {
|
|
21148
|
+
await handle.release();
|
|
21149
|
+
} catch (e) {
|
|
21150
|
+
this.logger.debug?.(
|
|
21151
|
+
`[ReolinkBaichuanApi] onObjectDetections: session release error: ${formatErrorForLog(e)}`
|
|
21152
|
+
);
|
|
21153
|
+
}
|
|
21154
|
+
this.logger.debug?.(
|
|
21155
|
+
`[ReolinkBaichuanApi] onObjectDetections: substream torn down`
|
|
21156
|
+
);
|
|
21157
|
+
}
|
|
21158
|
+
/**
|
|
21159
|
+
* Internal: invoked by BaichuanVideoStream when it starts so the API can hook
|
|
21160
|
+
* its `additionalHeader` event. Returns a teardown function the stream calls
|
|
21161
|
+
* on stop. Not intended for direct use by consumers.
|
|
21162
|
+
*/
|
|
21163
|
+
_registerVideoStreamForDetection(stream, context) {
|
|
21164
|
+
const listener = (info) => {
|
|
21165
|
+
if (this.detectionEventListeners.size === 0) return;
|
|
21166
|
+
const decoded = decodeDetectionHeader(info.raw, info.frameType);
|
|
21167
|
+
const event = {
|
|
21168
|
+
channel: context.channel,
|
|
21169
|
+
microseconds: info.microseconds,
|
|
21170
|
+
profile: context.profile,
|
|
21171
|
+
boxes: decoded.boxes,
|
|
21172
|
+
...info.frameWidth !== void 0 ? { frameWidth: info.frameWidth } : {},
|
|
21173
|
+
...info.frameHeight !== void 0 ? { frameHeight: info.frameHeight } : {},
|
|
21174
|
+
decodeState: decoded.state,
|
|
21175
|
+
rawHeader: info.raw
|
|
21176
|
+
};
|
|
21177
|
+
this.dispatchDetectionEvent(event);
|
|
21178
|
+
};
|
|
21179
|
+
stream.on("additionalHeader", listener);
|
|
21180
|
+
const teardown = () => {
|
|
21181
|
+
stream.off("additionalHeader", listener);
|
|
21182
|
+
this.detectionEventStreamHooks.delete(stream);
|
|
21183
|
+
};
|
|
21184
|
+
this.detectionEventStreamHooks.set(stream, teardown);
|
|
21185
|
+
return teardown;
|
|
21186
|
+
}
|
|
21187
|
+
dispatchDetectionEvent(evt) {
|
|
21188
|
+
for (const cb of this.detectionEventListeners) {
|
|
21189
|
+
try {
|
|
21190
|
+
void Promise.resolve(cb(evt)).catch((e) => {
|
|
21191
|
+
(this.logger.warn ?? this.logger.error).call(
|
|
21192
|
+
this.logger,
|
|
21193
|
+
"[ReolinkBaichuanApi] onDetection handler error",
|
|
21194
|
+
formatErrorForLog(e)
|
|
21195
|
+
);
|
|
21196
|
+
});
|
|
21197
|
+
} catch (e) {
|
|
21198
|
+
(this.logger.warn ?? this.logger.error).call(
|
|
21199
|
+
this.logger,
|
|
21200
|
+
"[ReolinkBaichuanApi] onDetection handler error",
|
|
21201
|
+
formatErrorForLog(e)
|
|
21202
|
+
);
|
|
21203
|
+
}
|
|
21204
|
+
}
|
|
21205
|
+
}
|
|
20277
21206
|
startSimpleEventResubscribeTimer() {
|
|
20278
21207
|
if (this.simpleEventResubscribeTimer) return;
|
|
20279
21208
|
if (this.simpleEventListeners.size === 0) return;
|
|
@@ -20656,6 +21585,9 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
20656
21585
|
this.stopUdpSleepInference();
|
|
20657
21586
|
this.stopSimpleEventWatchdog();
|
|
20658
21587
|
this.stopSimpleEventResubscribeTimer();
|
|
21588
|
+
this.objectDetectionListeners.clear();
|
|
21589
|
+
await this.tearDownObjectDetectionStream().catch(() => {
|
|
21590
|
+
});
|
|
20659
21591
|
await this.cleanup();
|
|
20660
21592
|
await this.stopAllActiveStreams();
|
|
20661
21593
|
await this.cleanupSocketPool();
|
|
@@ -21003,6 +21935,53 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
21003
21935
|
const xml = `<?xml version="1.0" encoding="UTF-8" ?><body><${tag} version="1.1"><enable>${params.enable ? 1 : 0}</enable></${tag}></body>`;
|
|
21004
21936
|
await this.sendXml({ cmdId: 36, payloadXml: xml });
|
|
21005
21937
|
}
|
|
21938
|
+
/**
|
|
21939
|
+
* Full port-config setter (cmd_id 36). Patches one or more of the six
|
|
21940
|
+
* service ports the camera serves — Server (Baichuan), HTTP, HTTPS,
|
|
21941
|
+
* RTSP, RTMP, ONVIF. Each entry takes an optional `port` (number) and
|
|
21942
|
+
* `enable` (boolean); fields the caller doesn't pass are left alone.
|
|
21943
|
+
*
|
|
21944
|
+
* Sends one block per port that has any field set, then issues a
|
|
21945
|
+
* single cmd_36 with the merged body. The camera accepts multiple
|
|
21946
|
+
* `<XxxPort>` siblings in the same payload.
|
|
21947
|
+
*
|
|
21948
|
+
* Wire format observed on E1 Zoom:
|
|
21949
|
+
*
|
|
21950
|
+
* <body>
|
|
21951
|
+
* <RtspPort version="1.1">
|
|
21952
|
+
* <rtspPort>554</rtspPort>
|
|
21953
|
+
* <enable>1</enable>
|
|
21954
|
+
* </RtspPort>
|
|
21955
|
+
* <HttpsPort version="1.1">
|
|
21956
|
+
* <enable>0</enable>
|
|
21957
|
+
* </HttpsPort>
|
|
21958
|
+
* ...
|
|
21959
|
+
* </body>
|
|
21960
|
+
*/
|
|
21961
|
+
async setPortConfig(patch) {
|
|
21962
|
+
const blocks = [];
|
|
21963
|
+
const append = (tag, portField, cfg) => {
|
|
21964
|
+
if (!cfg) return;
|
|
21965
|
+
if (cfg.port === void 0 && cfg.enable === void 0) return;
|
|
21966
|
+
const inner = [];
|
|
21967
|
+
if (cfg.port !== void 0) {
|
|
21968
|
+
inner.push(`<${portField}>${cfg.port}</${portField}>`);
|
|
21969
|
+
}
|
|
21970
|
+
if (cfg.enable !== void 0) {
|
|
21971
|
+
inner.push(`<enable>${cfg.enable ? 1 : 0}</enable>`);
|
|
21972
|
+
}
|
|
21973
|
+
blocks.push(`<${tag} version="1.1">${inner.join("")}</${tag}>`);
|
|
21974
|
+
};
|
|
21975
|
+
append("ServerPort", "serverPort", patch.server);
|
|
21976
|
+
append("HttpPort", "httpPort", patch.http);
|
|
21977
|
+
append("HttpsPort", "httpsPort", patch.https);
|
|
21978
|
+
append("RtspPort", "rtspPort", patch.rtsp);
|
|
21979
|
+
append("RtmpPort", "rtmpPort", patch.rtmp);
|
|
21980
|
+
append("OnvifPort", "onvifPort", patch.onvif);
|
|
21981
|
+
if (blocks.length === 0) return;
|
|
21982
|
+
const xml = `<?xml version="1.0" encoding="UTF-8" ?><body>${blocks.join("")}</body>`;
|
|
21983
|
+
await this.sendXml({ cmdId: 36, payloadXml: xml });
|
|
21984
|
+
}
|
|
21006
21985
|
/** GetDevInfo via Baichuan: host cmd_id 80, channel cmd_id 318 */
|
|
21007
21986
|
async getInfo(channel, options) {
|
|
21008
21987
|
const req = { cmdId: channel == null ? 80 : 318 };
|
|
@@ -23663,7 +24642,7 @@ ${stderr}`)
|
|
|
23663
24642
|
* Convert a raw video keyframe to JPEG using ffmpeg.
|
|
23664
24643
|
*/
|
|
23665
24644
|
async convertFrameToJpeg(params) {
|
|
23666
|
-
const { spawn:
|
|
24645
|
+
const { spawn: spawn13 } = await import("child_process");
|
|
23667
24646
|
const ffmpeg = params.ffmpegPath ?? "ffmpeg";
|
|
23668
24647
|
const inputFormat = params.videoCodec === "H265" ? "hevc" : "h264";
|
|
23669
24648
|
return new Promise((resolve, reject) => {
|
|
@@ -23685,7 +24664,7 @@ ${stderr}`)
|
|
|
23685
24664
|
"2",
|
|
23686
24665
|
"pipe:1"
|
|
23687
24666
|
];
|
|
23688
|
-
const proc =
|
|
24667
|
+
const proc = spawn13(ffmpeg, args, {
|
|
23689
24668
|
stdio: ["pipe", "pipe", "pipe"]
|
|
23690
24669
|
});
|
|
23691
24670
|
const chunks = [];
|
|
@@ -23828,7 +24807,7 @@ ${stderr}`)
|
|
|
23828
24807
|
* Internal helper to mux video+audio into MP4 using ffmpeg.
|
|
23829
24808
|
*/
|
|
23830
24809
|
async muxToMp4(params) {
|
|
23831
|
-
const { spawn:
|
|
24810
|
+
const { spawn: spawn13 } = await import("child_process");
|
|
23832
24811
|
const { randomUUID: randomUUID3 } = await import("crypto");
|
|
23833
24812
|
const fs6 = await import("fs/promises");
|
|
23834
24813
|
const os2 = await import("os");
|
|
@@ -23880,7 +24859,7 @@ ${stderr}`)
|
|
|
23880
24859
|
outputPath
|
|
23881
24860
|
);
|
|
23882
24861
|
await new Promise((resolve, reject) => {
|
|
23883
|
-
const p =
|
|
24862
|
+
const p = spawn13(ffmpeg, args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
23884
24863
|
let stderr = "";
|
|
23885
24864
|
p.stderr.on("data", (d) => {
|
|
23886
24865
|
stderr += d.toString();
|
|
@@ -24687,6 +25666,27 @@ ${stderr}`)
|
|
|
24687
25666
|
);
|
|
24688
25667
|
}
|
|
24689
25668
|
}
|
|
25669
|
+
async gotoPtzPreset(arg1, arg2) {
|
|
25670
|
+
const ch = arg2 === void 0 ? this.normalizeChannel(void 0) : this.normalizeChannel(arg1);
|
|
25671
|
+
const presetId = arg2 === void 0 ? arg1 : arg2;
|
|
25672
|
+
const channelId = ch;
|
|
25673
|
+
const payloadXml = buildPtzPresetXmlV2(channelId, presetId, "toPos");
|
|
25674
|
+
const extensionXml = buildChannelExtensionXml(channelId);
|
|
25675
|
+
const frame = await this.client.sendFrame({
|
|
25676
|
+
cmdId: BC_CMD_ID_PTZ_CONTROL_PRESET,
|
|
25677
|
+
channel: ch,
|
|
25678
|
+
channelIdOverride: channelId,
|
|
25679
|
+
extensionXml,
|
|
25680
|
+
payloadXml,
|
|
25681
|
+
messageClass: BC_CLASS_MODERN_24,
|
|
25682
|
+
streamType: 0
|
|
25683
|
+
});
|
|
25684
|
+
if (frame.header.responseCode !== 200) {
|
|
25685
|
+
throw new Error(
|
|
25686
|
+
`PTZ goto preset rejected (response_code ${frame.header.responseCode})`
|
|
25687
|
+
);
|
|
25688
|
+
}
|
|
25689
|
+
}
|
|
24690
25690
|
async deletePtzPreset(arg1, arg2) {
|
|
24691
25691
|
const ch = arg2 === void 0 ? this.normalizeChannel(void 0) : this.normalizeChannel(arg1);
|
|
24692
25692
|
const presetId = arg2 === void 0 ? arg1 : arg2;
|
|
@@ -25338,22 +26338,62 @@ ${stderr}`)
|
|
|
25338
26338
|
const channel = typeof arg1 === "number" ? arg1 : arg3;
|
|
25339
26339
|
const enabled = typeof arg1 === "number" ? arg2 : arg1;
|
|
25340
26340
|
const sensitivity = typeof arg1 === "number" ? arg3 : arg2;
|
|
25341
|
-
|
|
26341
|
+
return await this.setMotionAlarmFull({
|
|
26342
|
+
...channel !== void 0 ? { channel } : {},
|
|
26343
|
+
enabled,
|
|
26344
|
+
...sensitivity !== void 0 ? { sensitivity } : {}
|
|
26345
|
+
});
|
|
26346
|
+
}
|
|
26347
|
+
/**
|
|
26348
|
+
* Set motion alarm with full control, including the detection-zone grid.
|
|
26349
|
+
*
|
|
26350
|
+
* Wire format observed on E1 Zoom (cmd_id=47 SetMdAlarm body):
|
|
26351
|
+
*
|
|
26352
|
+
* <MD version="1.1">
|
|
26353
|
+
* <channelId>0</channelId>
|
|
26354
|
+
* <enable>1</enable>
|
|
26355
|
+
* <usepir>0</usepir>
|
|
26356
|
+
* <width>60</width> <height>33</height>
|
|
26357
|
+
* <scope>
|
|
26358
|
+
* <columns>96</columns> <rows>64</rows>
|
|
26359
|
+
* <valueTable>{base64 6144-bit bitmap}</valueTable>
|
|
26360
|
+
* </scope>
|
|
26361
|
+
* ... other camera-specific fields ...
|
|
26362
|
+
* </MD>
|
|
26363
|
+
*
|
|
26364
|
+
* We do a read-modify-write of the GET response so any camera-specific
|
|
26365
|
+
* extension fields are preserved untouched. Pass `valueTable` to update
|
|
26366
|
+
* the detection zone — see `encodeMotionScopeBitmap` for the bitmap layout.
|
|
26367
|
+
*
|
|
26368
|
+
* @param channel - 0-based channel
|
|
26369
|
+
* @param enabled - toggle motion detection on/off (optional)
|
|
26370
|
+
* @param sensitivity - 0-50, higher = more sensitive (optional)
|
|
26371
|
+
* @param valueTable - base64-encoded grid bitmap; size must match
|
|
26372
|
+
* `<scope><columns>×<rows></scope>` from the GET (optional)
|
|
26373
|
+
*/
|
|
26374
|
+
async setMotionAlarmFull(opts) {
|
|
26375
|
+
const ch = this.normalizeChannel(opts.channel);
|
|
25342
26376
|
const currentXml = await this.sendXml({
|
|
25343
26377
|
cmdId: BC_CMD_ID_GET_MOTION_ALARM,
|
|
25344
26378
|
channel: ch
|
|
25345
26379
|
});
|
|
25346
26380
|
let modifiedXml = currentXml;
|
|
25347
|
-
if (enabled !== void 0) {
|
|
26381
|
+
if (opts.enabled !== void 0) {
|
|
25348
26382
|
modifiedXml = modifiedXml.replace(
|
|
25349
26383
|
/<enable>[^<]*<\/enable>/,
|
|
25350
|
-
`<enable>${enabled ? "1" : "0"}</enable>`
|
|
26384
|
+
`<enable>${opts.enabled ? "1" : "0"}</enable>`
|
|
25351
26385
|
);
|
|
25352
26386
|
}
|
|
25353
|
-
if (sensitivity !== void 0) {
|
|
26387
|
+
if (opts.sensitivity !== void 0) {
|
|
25354
26388
|
modifiedXml = modifiedXml.replace(
|
|
25355
26389
|
/<sensitivityDefault>[^<]*<\/sensitivityDefault>/,
|
|
25356
|
-
`<sensitivityDefault>${sensitivity}</sensitivityDefault>`
|
|
26390
|
+
`<sensitivityDefault>${opts.sensitivity}</sensitivityDefault>`
|
|
26391
|
+
);
|
|
26392
|
+
}
|
|
26393
|
+
if (opts.valueTable !== void 0) {
|
|
26394
|
+
modifiedXml = modifiedXml.replace(
|
|
26395
|
+
/<valueTable>[^<]*<\/valueTable>/,
|
|
26396
|
+
`<valueTable>${opts.valueTable}</valueTable>`
|
|
25357
26397
|
);
|
|
25358
26398
|
}
|
|
25359
26399
|
await this.sendXml({
|
|
@@ -26697,12 +27737,24 @@ ${xml}`
|
|
|
26697
27737
|
}
|
|
26698
27738
|
/**
|
|
26699
27739
|
* SetEnc via Baichuan (cmdId=57). Read-modify-write — preserves
|
|
26700
|
-
* unspecified fields. Mirrors reolink_aio's `SetEnc
|
|
27740
|
+
* unspecified fields. Mirrors reolink_aio's `SetEnc` plus the additional
|
|
27741
|
+
* `width`/`height`/`encoderType`/`encoderProfile`/`gop`/`thirdStream`
|
|
27742
|
+
* fields observed in the official mobile app (see `pcap/resolution.pcapng`).
|
|
27743
|
+
*
|
|
27744
|
+
* Field meaning per stream:
|
|
27745
|
+
* - `audio` — 0/1 toggle
|
|
27746
|
+
* - `width`/`height` — resolution in pixels. Must be one of the
|
|
27747
|
+
* resolutions returned by {@link getStreamInfoList}.
|
|
27748
|
+
* - `bitRate` — kbps. Must match the table from `getStreamInfoList`.
|
|
27749
|
+
* - `frameRate` — fps. Must match the table from `getStreamInfoList`.
|
|
27750
|
+
* - `videoEncType` — `"h264"` or `"h265"`
|
|
27751
|
+
* - `encoderType` — `"vbr"` or `"cbr"`
|
|
27752
|
+
* - `encoderProfile` — `"high"`, `"main"`, or `"baseline"`
|
|
27753
|
+
* - `gop` — keyframe interval in seconds (sets `<gop><cur>`)
|
|
26701
27754
|
*
|
|
26702
27755
|
* @param channel - Channel number (0-based)
|
|
26703
|
-
* @param patch - Fields to update
|
|
26704
|
-
*
|
|
26705
|
-
* to change.
|
|
27756
|
+
* @param patch - Fields to update. Pass only the fields you want to change;
|
|
27757
|
+
* everything else is preserved from the device's current configuration.
|
|
26706
27758
|
*/
|
|
26707
27759
|
async setEnc(channel, patch, options) {
|
|
26708
27760
|
const ch = this.normalizeChannel(channel);
|
|
@@ -26719,6 +27771,7 @@ ${xml}`
|
|
|
26719
27771
|
}
|
|
26720
27772
|
xml = applyStreamPatch(xml, "mainStream", patch.mainStream);
|
|
26721
27773
|
xml = applyStreamPatch(xml, "subStream", patch.subStream);
|
|
27774
|
+
xml = applyStreamPatch(xml, "thirdStream", patch.thirdStream);
|
|
26722
27775
|
await this.sendXml({
|
|
26723
27776
|
cmdId: BC_CMD_ID_SET_ENC,
|
|
26724
27777
|
channel: ch,
|
|
@@ -27326,6 +28379,71 @@ ${xml}`
|
|
|
27326
28379
|
`PCAP-derived settings GET failed for cmdId=${params.cmdId}: ${String(lastErr)}`
|
|
27327
28380
|
);
|
|
27328
28381
|
}
|
|
28382
|
+
/**
|
|
28383
|
+
* Update the OSD timestamp + channel-name overlay via cmd_id=45
|
|
28384
|
+
* (SetOsdDatetime). The schema is the same `<body><OsdDatetime>` +
|
|
28385
|
+
* `<OsdChannelName>` block returned by `getOsdDatetime` — we
|
|
28386
|
+
* read-modify-write so any extension fields the camera sent are
|
|
28387
|
+
* preserved.
|
|
28388
|
+
*
|
|
28389
|
+
* Position is in **camera pixel coordinates** (e.g. (1,1) for top-left,
|
|
28390
|
+
* not preset strings). Set `enable=0` to hide the overlay; the camera
|
|
28391
|
+
* keeps the stored position so re-enabling later restores it.
|
|
28392
|
+
*/
|
|
28393
|
+
async setOsdDatetime(channel, patch, options) {
|
|
28394
|
+
const ch = this.normalizeChannel(channel);
|
|
28395
|
+
const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
|
|
28396
|
+
let xml = await this.sendPcapDerivedSettingsGetXml({
|
|
28397
|
+
cmdId: BC_CMD_ID_GET_OSD_DATETIME,
|
|
28398
|
+
channel: ch,
|
|
28399
|
+
...timeoutOpts
|
|
28400
|
+
});
|
|
28401
|
+
const patchBlock = (block, fields) => {
|
|
28402
|
+
const start = xml.indexOf(`<${block}`);
|
|
28403
|
+
if (start < 0) return;
|
|
28404
|
+
const end = xml.indexOf(`</${block}>`, start);
|
|
28405
|
+
if (end < 0) return;
|
|
28406
|
+
let body = xml.slice(start, end);
|
|
28407
|
+
for (const [tag, value] of Object.entries(fields)) {
|
|
28408
|
+
if (value === void 0) continue;
|
|
28409
|
+
const raw = typeof value === "boolean" ? value ? "1" : "0" : String(value);
|
|
28410
|
+
const escaped = raw.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
28411
|
+
if (body.includes(`<${tag}>`)) {
|
|
28412
|
+
body = body.replace(
|
|
28413
|
+
new RegExp(`<${tag}>[^<]*<\\/${tag}>`),
|
|
28414
|
+
`<${tag}>${escaped}</${tag}>`
|
|
28415
|
+
);
|
|
28416
|
+
} else {
|
|
28417
|
+
body += `<${tag}>${escaped}</${tag}>`;
|
|
28418
|
+
}
|
|
28419
|
+
}
|
|
28420
|
+
xml = xml.slice(0, start) + body + xml.slice(end);
|
|
28421
|
+
};
|
|
28422
|
+
if (patch.datetime) {
|
|
28423
|
+
patchBlock("OsdDatetime", {
|
|
28424
|
+
enable: patch.datetime.enable,
|
|
28425
|
+
topLeftX: patch.datetime.topLeftX,
|
|
28426
|
+
topLeftY: patch.datetime.topLeftY,
|
|
28427
|
+
language: patch.datetime.language
|
|
28428
|
+
});
|
|
28429
|
+
}
|
|
28430
|
+
if (patch.channelName) {
|
|
28431
|
+
patchBlock("OsdChannelName", {
|
|
28432
|
+
name: patch.channelName.name,
|
|
28433
|
+
enable: patch.channelName.enable,
|
|
28434
|
+
topLeftX: patch.channelName.topLeftX,
|
|
28435
|
+
topLeftY: patch.channelName.topLeftY,
|
|
28436
|
+
enWatermark: patch.channelName.enWatermark,
|
|
28437
|
+
enBgcolor: patch.channelName.enBgcolor
|
|
28438
|
+
});
|
|
28439
|
+
}
|
|
28440
|
+
await this.sendXml({
|
|
28441
|
+
cmdId: BC_CMD_ID_SET_OSD_DATETIME,
|
|
28442
|
+
channel: ch,
|
|
28443
|
+
payloadXml: ensureXmlHeader(xml),
|
|
28444
|
+
...timeoutOpts
|
|
28445
|
+
});
|
|
28446
|
+
}
|
|
27329
28447
|
async getOsdDatetime(channel, options) {
|
|
27330
28448
|
const rawXml = await this.sendPcapDerivedSettingsGetXml({
|
|
27331
28449
|
cmdId: BC_CMD_ID_GET_OSD_DATETIME,
|
|
@@ -27518,6 +28636,41 @@ ${xml}`
|
|
|
27518
28636
|
});
|
|
27519
28637
|
return { streams };
|
|
27520
28638
|
}
|
|
28639
|
+
/**
|
|
28640
|
+
* Return the set of values `setEnc` will accept on each stream of `channel`.
|
|
28641
|
+
* Aggregates `getStreamInfoList` (cmd_146) into a UI-friendly shape:
|
|
28642
|
+
* per-stream resolutions with their allowed codecs/framerates/bitrates plus
|
|
28643
|
+
* the enumerated encoder modes/profiles Reolink exposes.
|
|
28644
|
+
*
|
|
28645
|
+
* Useful for populating selectors and validating user input before calling
|
|
28646
|
+
* `setEnc` — picking an unsupported combination causes the camera to reject
|
|
28647
|
+
* the SET_ENC command (responseCode != 200).
|
|
28648
|
+
*/
|
|
28649
|
+
async getEncOptions(channel, options) {
|
|
28650
|
+
const list = await this.getStreamInfoList(channel, options);
|
|
28651
|
+
return buildEncOptions(list, channel);
|
|
28652
|
+
}
|
|
28653
|
+
/**
|
|
28654
|
+
* Read the camera's `<VersionInfo>` block (cmd_id=80). Returns the
|
|
28655
|
+
* friendly name, model code (e.g. `"E1 Zoom"`), serial number, firmware
|
|
28656
|
+
* version, hardware revision, build day, AI model bundle version, etc.
|
|
28657
|
+
*
|
|
28658
|
+
* This is the same info the Reolink mobile app shows in "About this
|
|
28659
|
+
* device" — distinct from `getSystemGeneral` (cmd_104) which carries
|
|
28660
|
+
* time/locale.
|
|
28661
|
+
*
|
|
28662
|
+
* No channel parameter: this command is device-global on NVRs/Hubs and
|
|
28663
|
+
* camera-global on standalone cameras. Pass an explicit channel via the
|
|
28664
|
+
* underlying `sendXml` only if a specific firmware demands it (none we've
|
|
28665
|
+
* tested do).
|
|
28666
|
+
*/
|
|
28667
|
+
async getVersionInfo(options) {
|
|
28668
|
+
const xml = await this.sendXml({
|
|
28669
|
+
cmdId: BC_CMD_ID_GET_VERSION_INFO,
|
|
28670
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
28671
|
+
});
|
|
28672
|
+
return parseVersionInfo(xml);
|
|
28673
|
+
}
|
|
27521
28674
|
async getLedState(channel, options) {
|
|
27522
28675
|
const rawXml = await this.sendPcapDerivedSettingsGetXml({
|
|
27523
28676
|
cmdId: BC_CMD_ID_GET_LED_STATE,
|
|
@@ -27600,7 +28753,279 @@ ${xml}`
|
|
|
27600
28753
|
...channel != null ? { channel } : {},
|
|
27601
28754
|
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
27602
28755
|
});
|
|
27603
|
-
return
|
|
28756
|
+
return parseEmailTaskFromXml(xml);
|
|
28757
|
+
}
|
|
28758
|
+
/**
|
|
28759
|
+
* SetEmailTask via Baichuan (cmdId=216). Updates the email alarm schedule
|
|
28760
|
+
* (per-event-type 7×24 valueTable + master enable).
|
|
28761
|
+
*
|
|
28762
|
+
* Reolink expects the FULL `typeScheduleList` — pass the array from a prior
|
|
28763
|
+
* GET and only flip the entries you care about. Slots you don't track must
|
|
28764
|
+
* be sent back unchanged to avoid the camera dropping them.
|
|
28765
|
+
*/
|
|
28766
|
+
async setEmailTask(channel, task, options) {
|
|
28767
|
+
const ch = this.normalizeChannel(channel);
|
|
28768
|
+
const payloadXml = buildSetEmailTaskXml({ ...task, channelId: ch });
|
|
28769
|
+
await this.sendXml({
|
|
28770
|
+
cmdId: BC_CMD_ID_SET_EMAIL_TASK,
|
|
28771
|
+
channel: ch,
|
|
28772
|
+
payloadXml,
|
|
28773
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
28774
|
+
});
|
|
28775
|
+
}
|
|
28776
|
+
/**
|
|
28777
|
+
* Convenience wrapper that patches the schedule of one or more trigger
|
|
28778
|
+
* types on the camera's EmailTask without touching the others.
|
|
28779
|
+
*
|
|
28780
|
+
* Pass a high-level schedule spec (`always` / `never` / explicit windows)
|
|
28781
|
+
* and the trigger types it should apply to. The method:
|
|
28782
|
+
*
|
|
28783
|
+
* 1. Reads the current EmailTask via GET (so we keep every existing slot).
|
|
28784
|
+
* 2. Builds the new `valueTable` once from `schedule`.
|
|
28785
|
+
* 3. Replaces the `valueTable` of every matching `type` in the list.
|
|
28786
|
+
* 4. Appends entries for any requested type not already present.
|
|
28787
|
+
* 5. Writes the merged list back via SET.
|
|
28788
|
+
*
|
|
28789
|
+
* Returns the list of types that were actually touched.
|
|
28790
|
+
*/
|
|
28791
|
+
async patchEmailSchedule(channel, spec, options) {
|
|
28792
|
+
const current = await this.getEmailTask(channel, options);
|
|
28793
|
+
const newValueTable = buildEmailScheduleValueTable(spec.schedule);
|
|
28794
|
+
const targetSet = new Set(spec.types);
|
|
28795
|
+
const touched = [];
|
|
28796
|
+
const updatedList = current.typeScheduleList.map((item) => {
|
|
28797
|
+
if (targetSet.has(item.type)) {
|
|
28798
|
+
touched.push(item.type);
|
|
28799
|
+
return { ...item, valueTable: newValueTable };
|
|
28800
|
+
}
|
|
28801
|
+
return item;
|
|
28802
|
+
});
|
|
28803
|
+
for (const t of spec.types) {
|
|
28804
|
+
if (!current.typeScheduleList.some((item) => item.type === t)) {
|
|
28805
|
+
updatedList.push({ type: t, valueTable: newValueTable });
|
|
28806
|
+
touched.push(t);
|
|
28807
|
+
}
|
|
28808
|
+
}
|
|
28809
|
+
await this.setEmailTask(
|
|
28810
|
+
channel,
|
|
28811
|
+
{
|
|
28812
|
+
channelId: current.channelId,
|
|
28813
|
+
enable: spec.enable ?? current.enable,
|
|
28814
|
+
typeScheduleList: updatedList
|
|
28815
|
+
},
|
|
28816
|
+
options
|
|
28817
|
+
);
|
|
28818
|
+
return { touchedTypes: touched };
|
|
28819
|
+
}
|
|
28820
|
+
// ====================================================================
|
|
28821
|
+
// Email server (cmdId 42/43/141), NTP (38/39), DST (106/107),
|
|
28822
|
+
// SystemGeneral SET (105), AutoReboot (100/101).
|
|
28823
|
+
// Schemas derived from Reolink Client pcap (2026-05-16).
|
|
28824
|
+
// ====================================================================
|
|
28825
|
+
/**
|
|
28826
|
+
* Read the SMTP email configuration (cmdId=42). Returns the full `<Email>`
|
|
28827
|
+
* block including capability hints (`senderMaxLen`, `pwdMaxLen`,
|
|
28828
|
+
* `emailAttachAbility`).
|
|
28829
|
+
*/
|
|
28830
|
+
async getEmail(options) {
|
|
28831
|
+
const xml = await this.sendXml({
|
|
28832
|
+
cmdId: BC_CMD_ID_GET_EMAIL,
|
|
28833
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
28834
|
+
});
|
|
28835
|
+
return parseEmailConfigFromXml(xml);
|
|
28836
|
+
}
|
|
28837
|
+
/**
|
|
28838
|
+
* Patch the SMTP email configuration (cmdId=43). Reads the current config
|
|
28839
|
+
* first then merges the patch — Reolink rejects partial `<Email>` blocks.
|
|
28840
|
+
*/
|
|
28841
|
+
async setEmail(patch, options) {
|
|
28842
|
+
const current = await this.getEmail(options);
|
|
28843
|
+
const payloadXml = buildSetEmailXml(current, patch);
|
|
28844
|
+
await this.sendXml({
|
|
28845
|
+
cmdId: BC_CMD_ID_SET_EMAIL,
|
|
28846
|
+
payloadXml,
|
|
28847
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
28848
|
+
});
|
|
28849
|
+
}
|
|
28850
|
+
/**
|
|
28851
|
+
* Send a test email using either the current config or an override patch
|
|
28852
|
+
* (cmdId=141). Returns true when the camera reports 200 (test succeeded),
|
|
28853
|
+
* false when it reports 482 (test failed — server unreachable / bad creds).
|
|
28854
|
+
* Other non-200 codes propagate as exceptions via `sendXml`.
|
|
28855
|
+
*/
|
|
28856
|
+
async testEmail(patch, options) {
|
|
28857
|
+
const current = await this.getEmail(options);
|
|
28858
|
+
const payloadXml = buildSetEmailXml(current, patch ?? {});
|
|
28859
|
+
const timeoutMs = options?.timeoutMs ?? 6e4;
|
|
28860
|
+
try {
|
|
28861
|
+
await this.sendXml({
|
|
28862
|
+
cmdId: BC_CMD_ID_TEST_EMAIL,
|
|
28863
|
+
payloadXml,
|
|
28864
|
+
timeoutMs
|
|
28865
|
+
});
|
|
28866
|
+
return true;
|
|
28867
|
+
} catch (err) {
|
|
28868
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
28869
|
+
if (msg.includes("response_code 482") || msg.includes("response_code=482")) {
|
|
28870
|
+
return false;
|
|
28871
|
+
}
|
|
28872
|
+
throw err;
|
|
28873
|
+
}
|
|
28874
|
+
}
|
|
28875
|
+
/**
|
|
28876
|
+
* Read the NTP server configuration (cmdId=38).
|
|
28877
|
+
*/
|
|
28878
|
+
async getNtp(options) {
|
|
28879
|
+
const xml = await this.sendXml({
|
|
28880
|
+
cmdId: BC_CMD_ID_GET_NTP,
|
|
28881
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
28882
|
+
});
|
|
28883
|
+
return parseNtpConfigFromXml(xml);
|
|
28884
|
+
}
|
|
28885
|
+
/**
|
|
28886
|
+
* Patch the NTP server configuration (cmdId=39). Reads the current state
|
|
28887
|
+
* first and merges the patch — Reolink rejects partial `<Ntp>` blocks.
|
|
28888
|
+
*/
|
|
28889
|
+
async setNtp(patch, options) {
|
|
28890
|
+
const current = await this.getNtp(options);
|
|
28891
|
+
const payloadXml = buildSetNtpXml(current, patch);
|
|
28892
|
+
await this.sendXml({
|
|
28893
|
+
cmdId: BC_CMD_ID_SET_NTP,
|
|
28894
|
+
payloadXml,
|
|
28895
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
28896
|
+
});
|
|
28897
|
+
}
|
|
28898
|
+
/**
|
|
28899
|
+
* Patch SystemGeneral (cmdId=105). Supports partial payloads: include only
|
|
28900
|
+
* the fields you want to change. By default the builder emits `<year>0</year>`
|
|
28901
|
+
* as the "do not set manual clock" marker; pass `manualTime` to actually
|
|
28902
|
+
* set the date/time. Setting only `deviceName` automatically uses the
|
|
28903
|
+
* Reolink Client's `deviceNameOnly=1` shape.
|
|
28904
|
+
*/
|
|
28905
|
+
async setSystemGeneral(patch, options) {
|
|
28906
|
+
const payloadXml = buildSetSystemGeneralXml(patch);
|
|
28907
|
+
await this.sendXml({
|
|
28908
|
+
cmdId: BC_CMD_ID_SET_SYSTEM_GENERAL,
|
|
28909
|
+
payloadXml,
|
|
28910
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
28911
|
+
});
|
|
28912
|
+
}
|
|
28913
|
+
/**
|
|
28914
|
+
* Read the Daylight Saving Time configuration (cmdId=106).
|
|
28915
|
+
*/
|
|
28916
|
+
async getDst(options) {
|
|
28917
|
+
const xml = await this.sendXml({
|
|
28918
|
+
cmdId: BC_CMD_ID_GET_DST,
|
|
28919
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
28920
|
+
});
|
|
28921
|
+
return parseDstConfigFromXml(xml);
|
|
28922
|
+
}
|
|
28923
|
+
/**
|
|
28924
|
+
* Patch the DST configuration (cmdId=107). Reads the current state first
|
|
28925
|
+
* and merges the patch.
|
|
28926
|
+
*/
|
|
28927
|
+
async setDst(patch, options) {
|
|
28928
|
+
const current = await this.getDst(options);
|
|
28929
|
+
const payloadXml = buildSetDstXml(current, patch);
|
|
28930
|
+
await this.sendXml({
|
|
28931
|
+
cmdId: BC_CMD_ID_SET_DST,
|
|
28932
|
+
payloadXml,
|
|
28933
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
28934
|
+
});
|
|
28935
|
+
}
|
|
28936
|
+
/**
|
|
28937
|
+
* Read the auto-reboot schedule (cmdId=101).
|
|
28938
|
+
*/
|
|
28939
|
+
async getAutoReboot(options) {
|
|
28940
|
+
const xml = await this.sendXml({
|
|
28941
|
+
cmdId: BC_CMD_ID_GET_AUTO_REBOOT,
|
|
28942
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
28943
|
+
});
|
|
28944
|
+
return parseAutoRebootFromXml(xml);
|
|
28945
|
+
}
|
|
28946
|
+
/**
|
|
28947
|
+
* Patch the auto-reboot schedule (cmdId=100).
|
|
28948
|
+
*/
|
|
28949
|
+
async setAutoReboot(patch, options) {
|
|
28950
|
+
const current = await this.getAutoReboot(options);
|
|
28951
|
+
const payloadXml = buildSetAutoRebootXml(current, patch);
|
|
28952
|
+
await this.sendXml({
|
|
28953
|
+
cmdId: BC_CMD_ID_SET_AUTO_REBOOT,
|
|
28954
|
+
payloadXml,
|
|
28955
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
28956
|
+
});
|
|
28957
|
+
}
|
|
28958
|
+
/**
|
|
28959
|
+
* High-level helper that configures the camera to deliver motion alerts via
|
|
28960
|
+
* SMTP to the local nodelink manager. Orchestrates `setEmail` + `setEmailTask`
|
|
28961
|
+
* in a single call so UI code can offer "auto-configure" without juggling
|
|
28962
|
+
* the underlying commands.
|
|
28963
|
+
*
|
|
28964
|
+
* Pass `runTest: true` to also send a test email (cmdId=141). Returns a
|
|
28965
|
+
* structured result describing each leg of the flow so the caller can show
|
|
28966
|
+
* granular feedback.
|
|
28967
|
+
*
|
|
28968
|
+
* @param params Auto-configuration parameters
|
|
28969
|
+
* @param channel Logical channel (default 0). Used for the EmailTask SET.
|
|
28970
|
+
*/
|
|
28971
|
+
async setupEmailPushToManager(params, channel, options) {
|
|
28972
|
+
const port = params.managerPort ?? 2525;
|
|
28973
|
+
const domain = params.domain ?? "nodelink.local";
|
|
28974
|
+
const recipient = `${params.recipientLocalPart}@${domain}`;
|
|
28975
|
+
const triggers = params.triggerTypes ?? ["MD", "people", "vehicle"];
|
|
28976
|
+
const attachmentType = params.attachmentType ?? "picture";
|
|
28977
|
+
const interval = params.interval ?? 30;
|
|
28978
|
+
const emailPatch = {
|
|
28979
|
+
smtpServer: params.managerHost,
|
|
28980
|
+
smtpPort: port,
|
|
28981
|
+
userName: params.authUsername ?? recipient,
|
|
28982
|
+
password: params.authPassword ?? "",
|
|
28983
|
+
address1: recipient,
|
|
28984
|
+
address2: "",
|
|
28985
|
+
address3: "",
|
|
28986
|
+
sendNickname: params.sendNickname ?? params.recipientLocalPart,
|
|
28987
|
+
attachment: attachmentType === "none" ? 0 : 1,
|
|
28988
|
+
attachmentType,
|
|
28989
|
+
textType: "withText",
|
|
28990
|
+
ssl: 0,
|
|
28991
|
+
interval
|
|
28992
|
+
};
|
|
28993
|
+
await this.setEmail(emailPatch, options);
|
|
28994
|
+
const fullWeekOn = "1".repeat(168);
|
|
28995
|
+
const current = await this.getEmailTask(channel, options);
|
|
28996
|
+
const triggerSet = new Set(triggers);
|
|
28997
|
+
const touched = [];
|
|
28998
|
+
const updatedList = current.typeScheduleList.map((item) => {
|
|
28999
|
+
if (triggerSet.has(item.type)) {
|
|
29000
|
+
touched.push(item.type);
|
|
29001
|
+
return { ...item, valueTable: fullWeekOn };
|
|
29002
|
+
}
|
|
29003
|
+
return item;
|
|
29004
|
+
});
|
|
29005
|
+
for (const t of triggers) {
|
|
29006
|
+
if (!current.typeScheduleList.some((item) => item.type === t)) {
|
|
29007
|
+
updatedList.push({ type: t, valueTable: fullWeekOn });
|
|
29008
|
+
touched.push(t);
|
|
29009
|
+
}
|
|
29010
|
+
}
|
|
29011
|
+
await this.setEmailTask(
|
|
29012
|
+
channel,
|
|
29013
|
+
{
|
|
29014
|
+
channelId: current.channelId,
|
|
29015
|
+
enable: 1,
|
|
29016
|
+
typeScheduleList: updatedList
|
|
29017
|
+
},
|
|
29018
|
+
options
|
|
29019
|
+
);
|
|
29020
|
+
const result = {
|
|
29021
|
+
setEmail: { applied: true },
|
|
29022
|
+
setEmailTask: { applied: true, touchedTypes: touched }
|
|
29023
|
+
};
|
|
29024
|
+
if (params.runTest) {
|
|
29025
|
+
const ok = await this.testEmail(emailPatch, options);
|
|
29026
|
+
result.testEmail = { success: ok };
|
|
29027
|
+
}
|
|
29028
|
+
return result;
|
|
27604
29029
|
}
|
|
27605
29030
|
/**
|
|
27606
29031
|
* Get siren-on-motion state via AudioTask (cmdId=232).
|
|
@@ -27869,7 +29294,7 @@ ${xml}`
|
|
|
27869
29294
|
cmdId: BC_CMD_ID_GET_SYSTEM_GENERAL,
|
|
27870
29295
|
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
27871
29296
|
});
|
|
27872
|
-
return
|
|
29297
|
+
return parseSystemGeneralFromXml(xml);
|
|
27873
29298
|
}
|
|
27874
29299
|
/**
|
|
27875
29300
|
* Get device support/capability flags.
|
|
@@ -34596,6 +36021,17 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
34596
36021
|
// Native stream
|
|
34597
36022
|
nativeFanout = null;
|
|
34598
36023
|
nativeStreamActive = false;
|
|
36024
|
+
// Set only by stopNativeStream() (explicit teardown) so the fanout's onEnd
|
|
36025
|
+
// callback can short-circuit cleanup/restart logic. NOT set by the inactivity-
|
|
36026
|
+
// timeout force-restart path — that flow wants onEnd to run and decide
|
|
36027
|
+
// whether to restart based on prestartStream / connected clients.
|
|
36028
|
+
nativeStreamStopping = false;
|
|
36029
|
+
// Pending retry timer for the unbounded auto-restart loop. When a stream
|
|
36030
|
+
// start fails transiently (camera in maintenance reboot, idle-disconnect
|
|
36031
|
+
// race, etc.) we keep trying with exponential backoff until either the
|
|
36032
|
+
// server is stopped or a frame finally arrives.
|
|
36033
|
+
nativeStreamRetryTimer;
|
|
36034
|
+
nativeStreamRetryDelayMs = 0;
|
|
34599
36035
|
dedicatedSessionRelease;
|
|
34600
36036
|
detectedVideoType;
|
|
34601
36037
|
// Client tracking
|
|
@@ -34664,6 +36100,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
34664
36100
|
if (!this.active) return;
|
|
34665
36101
|
this.active = false;
|
|
34666
36102
|
clearTimeout(this.stopGraceTimer);
|
|
36103
|
+
this.clearNativeStreamRetry();
|
|
34667
36104
|
this.stopStreamHealthMonitor();
|
|
34668
36105
|
for (const [id, sock] of this.clientSockets) {
|
|
34669
36106
|
sock.destroy();
|
|
@@ -34992,6 +36429,46 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
34992
36429
|
// -----------------------------------------------------------------------
|
|
34993
36430
|
// Native stream management
|
|
34994
36431
|
// -----------------------------------------------------------------------
|
|
36432
|
+
/**
|
|
36433
|
+
* Schedule another startNativeStream() attempt after the given delay.
|
|
36434
|
+
* Idempotent: a no-op if a retry is already scheduled, the server is no
|
|
36435
|
+
* longer active, or an explicit stop is in progress. Implements unbounded
|
|
36436
|
+
* exponential backoff (5s → 60s) so a camera that stays unreachable for
|
|
36437
|
+
* minutes (e.g. nightly maintenance reboot) eventually recovers without
|
|
36438
|
+
* manual intervention — see issue #16.
|
|
36439
|
+
*/
|
|
36440
|
+
scheduleNativeStreamRetry(reason) {
|
|
36441
|
+
if (!this.active) return;
|
|
36442
|
+
if (this.nativeStreamStopping) return;
|
|
36443
|
+
if (this.nativeStreamRetryTimer) return;
|
|
36444
|
+
const delay = this.nativeStreamRetryDelayMs > 0 ? this.nativeStreamRetryDelayMs : 5e3;
|
|
36445
|
+
this.logger.info?.(
|
|
36446
|
+
`[Go2rtcTcpServer] scheduling native stream retry in ${(delay / 1e3).toFixed(0)}s (reason=${reason})`
|
|
36447
|
+
);
|
|
36448
|
+
this.nativeStreamRetryTimer = setTimeout(() => {
|
|
36449
|
+
this.nativeStreamRetryTimer = void 0;
|
|
36450
|
+
if (!this.active) return;
|
|
36451
|
+
if (this.nativeStreamStopping) return;
|
|
36452
|
+
this.startNativeStream().catch((err) => {
|
|
36453
|
+
this.logger.warn?.(
|
|
36454
|
+
`[Go2rtcTcpServer] retry of startNativeStream threw: ${err instanceof Error ? err.message : err}`
|
|
36455
|
+
);
|
|
36456
|
+
});
|
|
36457
|
+
}, delay);
|
|
36458
|
+
this.nativeStreamRetryDelayMs = Math.min(delay * 2, 6e4);
|
|
36459
|
+
}
|
|
36460
|
+
/**
|
|
36461
|
+
* Cancel any pending retry timer and reset the backoff. Called on explicit
|
|
36462
|
+
* stop and on first-frame-received so the next failure starts the backoff
|
|
36463
|
+
* window from scratch.
|
|
36464
|
+
*/
|
|
36465
|
+
clearNativeStreamRetry() {
|
|
36466
|
+
if (this.nativeStreamRetryTimer) {
|
|
36467
|
+
clearTimeout(this.nativeStreamRetryTimer);
|
|
36468
|
+
this.nativeStreamRetryTimer = void 0;
|
|
36469
|
+
}
|
|
36470
|
+
this.nativeStreamRetryDelayMs = 0;
|
|
36471
|
+
}
|
|
34995
36472
|
async startNativeStream() {
|
|
34996
36473
|
if (this.nativeStreamActive) return;
|
|
34997
36474
|
if (!this.api.isReady) {
|
|
@@ -35008,8 +36485,9 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
35008
36485
|
await this.api.ensureConnected();
|
|
35009
36486
|
} catch (e) {
|
|
35010
36487
|
this.logger.warn?.(
|
|
35011
|
-
`[Go2rtcTcpServer] ensureConnected failed
|
|
36488
|
+
`[Go2rtcTcpServer] ensureConnected failed: ${e}`
|
|
35012
36489
|
);
|
|
36490
|
+
this.scheduleNativeStreamRetry("ensureConnected failed");
|
|
35013
36491
|
return;
|
|
35014
36492
|
}
|
|
35015
36493
|
}
|
|
@@ -35039,6 +36517,9 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
35039
36517
|
...dedicatedClient ? { client: dedicatedClient } : {}
|
|
35040
36518
|
}),
|
|
35041
36519
|
onFrame: (frame) => {
|
|
36520
|
+
if (!hadFrames) {
|
|
36521
|
+
this.clearNativeStreamRetry();
|
|
36522
|
+
}
|
|
35042
36523
|
hadFrames = true;
|
|
35043
36524
|
this.lastFrameAt = Date.now();
|
|
35044
36525
|
this.totalFramesReceived++;
|
|
@@ -35082,7 +36563,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
35082
36563
|
this.logger.warn?.(`[Go2rtcTcpServer] native stream error: ${error}`);
|
|
35083
36564
|
},
|
|
35084
36565
|
onEnd: () => {
|
|
35085
|
-
if (
|
|
36566
|
+
if (this.nativeStreamStopping) return;
|
|
35086
36567
|
this.nativeStreamActive = false;
|
|
35087
36568
|
this.nativeFanout = null;
|
|
35088
36569
|
this.stopStreamHealthMonitor();
|
|
@@ -35124,18 +36605,24 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
35124
36605
|
this.startStreamHealthMonitor();
|
|
35125
36606
|
}
|
|
35126
36607
|
async stopNativeStream() {
|
|
36608
|
+
this.nativeStreamStopping = true;
|
|
35127
36609
|
this.nativeStreamActive = false;
|
|
36610
|
+
this.clearNativeStreamRetry();
|
|
35128
36611
|
this.stopStreamHealthMonitor();
|
|
35129
36612
|
const fanout = this.nativeFanout;
|
|
35130
36613
|
this.nativeFanout = null;
|
|
35131
|
-
|
|
35132
|
-
|
|
35133
|
-
|
|
35134
|
-
|
|
35135
|
-
|
|
35136
|
-
|
|
35137
|
-
|
|
35138
|
-
|
|
36614
|
+
try {
|
|
36615
|
+
if (fanout) {
|
|
36616
|
+
await fanout.stop();
|
|
36617
|
+
}
|
|
36618
|
+
this.prebuffer = [];
|
|
36619
|
+
if (this.dedicatedSessionRelease) {
|
|
36620
|
+
await this.dedicatedSessionRelease().catch(() => {
|
|
36621
|
+
});
|
|
36622
|
+
this.dedicatedSessionRelease = void 0;
|
|
36623
|
+
}
|
|
36624
|
+
} finally {
|
|
36625
|
+
this.nativeStreamStopping = false;
|
|
35139
36626
|
}
|
|
35140
36627
|
}
|
|
35141
36628
|
// -----------------------------------------------------------------------
|
|
@@ -35325,10 +36812,15 @@ var BaichuanHttpStreamServer = class extends import_node_events7.EventEmitter {
|
|
|
35325
36812
|
// Force a known frame rate on raw H.264 input so the muxer gets valid PTS/DTS.
|
|
35326
36813
|
"-r",
|
|
35327
36814
|
String(this.inputFps),
|
|
36815
|
+
// `+genpts` generates uniform PTS from `-r` for raw Annex-B input. We
|
|
36816
|
+
// deliberately do NOT pass `-use_wallclock_as_timestamps 1`: that
|
|
36817
|
+
// overrides the generated PTS with the host wallclock at frame ARRIVAL
|
|
36818
|
+
// time, and the network stream is bursty so the resulting PTS is
|
|
36819
|
+
// uneven. With `-r` forcing a target rate downstream, ffmpeg then
|
|
36820
|
+
// drops/duplicates frames to match — visible as the periodic stutter
|
|
36821
|
+
// reported on local-restreamer recordings (issue #11).
|
|
35328
36822
|
"-fflags",
|
|
35329
36823
|
"+genpts",
|
|
35330
|
-
"-use_wallclock_as_timestamps",
|
|
35331
|
-
"1",
|
|
35332
36824
|
"-f",
|
|
35333
36825
|
"h264",
|
|
35334
36826
|
// Input format (H.264 Annex-B)
|
|
@@ -36007,8 +37499,195 @@ var BaichuanMjpegServer = class extends import_node_events9.EventEmitter {
|
|
|
36007
37499
|
};
|
|
36008
37500
|
|
|
36009
37501
|
// src/baichuan/stream/BaichuanWebRTCServer.ts
|
|
36010
|
-
var
|
|
37502
|
+
var import_node_events11 = require("events");
|
|
36011
37503
|
init_BcMediaAnnexBDecoder();
|
|
37504
|
+
|
|
37505
|
+
// src/baichuan/stream/AacToOpusTranscoder.ts
|
|
37506
|
+
var import_node_child_process11 = require("child_process");
|
|
37507
|
+
var import_node_dgram3 = require("dgram");
|
|
37508
|
+
var import_node_events10 = require("events");
|
|
37509
|
+
var AacToOpusTranscoder = class extends import_node_events10.EventEmitter {
|
|
37510
|
+
opts;
|
|
37511
|
+
socket = null;
|
|
37512
|
+
ffmpeg = null;
|
|
37513
|
+
port = 0;
|
|
37514
|
+
starting = null;
|
|
37515
|
+
stopped = false;
|
|
37516
|
+
constructor(options = {}) {
|
|
37517
|
+
super();
|
|
37518
|
+
this.opts = {
|
|
37519
|
+
ffmpegPath: options.ffmpegPath ?? "ffmpeg",
|
|
37520
|
+
opusSampleRate: options.opusSampleRate ?? 48e3,
|
|
37521
|
+
opusChannels: options.opusChannels ?? 2,
|
|
37522
|
+
opusFrameMs: options.opusFrameMs ?? 20,
|
|
37523
|
+
opusBitrate: options.opusBitrate ?? 64e3,
|
|
37524
|
+
...options.logger !== void 0 ? { logger: options.logger } : {}
|
|
37525
|
+
};
|
|
37526
|
+
}
|
|
37527
|
+
log(level, message) {
|
|
37528
|
+
this.opts.logger?.(level, `[AacToOpusTranscoder] ${message}`);
|
|
37529
|
+
}
|
|
37530
|
+
/**
|
|
37531
|
+
* Allocate the UDP loopback socket and spawn ffmpeg. Must be awaited before
|
|
37532
|
+
* the first call to `feedAac`.
|
|
37533
|
+
*/
|
|
37534
|
+
async start() {
|
|
37535
|
+
if (this.starting) return this.starting;
|
|
37536
|
+
this.starting = this._start();
|
|
37537
|
+
return this.starting;
|
|
37538
|
+
}
|
|
37539
|
+
async _start() {
|
|
37540
|
+
if (this.stopped) throw new Error("transcoder stopped");
|
|
37541
|
+
this.socket = (0, import_node_dgram3.createSocket)("udp4");
|
|
37542
|
+
await new Promise((resolve, reject) => {
|
|
37543
|
+
this.socket.once("error", reject);
|
|
37544
|
+
this.socket.bind({ address: "127.0.0.1", port: 0 }, () => {
|
|
37545
|
+
this.socket.removeListener("error", reject);
|
|
37546
|
+
this.port = this.socket.address().port;
|
|
37547
|
+
resolve();
|
|
37548
|
+
});
|
|
37549
|
+
});
|
|
37550
|
+
this.log("info", `UDP loopback bound on 127.0.0.1:${this.port}`);
|
|
37551
|
+
this.socket.on("message", (rtpPacket) => this.handleRtp(rtpPacket));
|
|
37552
|
+
this.socket.on("error", (err) => {
|
|
37553
|
+
this.log("error", `socket error: ${err.message}`);
|
|
37554
|
+
this.emit("error", err);
|
|
37555
|
+
});
|
|
37556
|
+
const args = [
|
|
37557
|
+
"-loglevel",
|
|
37558
|
+
"warning",
|
|
37559
|
+
"-fflags",
|
|
37560
|
+
"nobuffer",
|
|
37561
|
+
"-f",
|
|
37562
|
+
"aac",
|
|
37563
|
+
"-i",
|
|
37564
|
+
"pipe:0",
|
|
37565
|
+
"-c:a",
|
|
37566
|
+
"libopus",
|
|
37567
|
+
"-ar",
|
|
37568
|
+
String(this.opts.opusSampleRate),
|
|
37569
|
+
"-ac",
|
|
37570
|
+
String(this.opts.opusChannels),
|
|
37571
|
+
"-application",
|
|
37572
|
+
"audio",
|
|
37573
|
+
"-frame_duration",
|
|
37574
|
+
String(this.opts.opusFrameMs),
|
|
37575
|
+
"-b:a",
|
|
37576
|
+
String(this.opts.opusBitrate),
|
|
37577
|
+
// Important: disable VBR so the output rate matches the configured
|
|
37578
|
+
// bitrate consistently — easier on the browser jitter buffer.
|
|
37579
|
+
"-vbr",
|
|
37580
|
+
"off",
|
|
37581
|
+
"-f",
|
|
37582
|
+
"rtp",
|
|
37583
|
+
`rtp://127.0.0.1:${this.port}`
|
|
37584
|
+
];
|
|
37585
|
+
this.log("info", `spawning ffmpeg with: ${this.opts.ffmpegPath} ${args.join(" ")}`);
|
|
37586
|
+
this.ffmpeg = (0, import_node_child_process11.spawn)(this.opts.ffmpegPath, args, {
|
|
37587
|
+
stdio: ["pipe", "ignore", "pipe"]
|
|
37588
|
+
});
|
|
37589
|
+
this.ffmpeg.on("error", (err) => {
|
|
37590
|
+
this.log("error", `ffmpeg spawn error: ${err.message}`);
|
|
37591
|
+
this.emit("error", err);
|
|
37592
|
+
});
|
|
37593
|
+
this.ffmpeg.stderr?.on("data", (chunk) => {
|
|
37594
|
+
this.log("debug", `ffmpeg stderr: ${chunk.toString().trim()}`);
|
|
37595
|
+
});
|
|
37596
|
+
this.ffmpeg.on("exit", (code) => {
|
|
37597
|
+
this.log(code === 0 ? "info" : "warn", `ffmpeg exited code=${code}`);
|
|
37598
|
+
this.emit("exit", code);
|
|
37599
|
+
});
|
|
37600
|
+
}
|
|
37601
|
+
/**
|
|
37602
|
+
* Feed one or more concatenated ADTS AAC frames to ffmpeg. Returns the
|
|
37603
|
+
* number of bytes written. Safe to call before a frame is fully buffered.
|
|
37604
|
+
*/
|
|
37605
|
+
feedAac(buf) {
|
|
37606
|
+
if (this.stopped) return false;
|
|
37607
|
+
if (!this.ffmpeg?.stdin || !this.ffmpeg.stdin.writable) {
|
|
37608
|
+
this.log("debug", "drop AAC frame \u2014 ffmpeg stdin not writable");
|
|
37609
|
+
return false;
|
|
37610
|
+
}
|
|
37611
|
+
return this.ffmpeg.stdin.write(buf);
|
|
37612
|
+
}
|
|
37613
|
+
/**
|
|
37614
|
+
* Close stdin (ffmpeg exits cleanly on EOF) and tear down the UDP socket.
|
|
37615
|
+
*/
|
|
37616
|
+
async stop() {
|
|
37617
|
+
if (this.stopped) return;
|
|
37618
|
+
this.stopped = true;
|
|
37619
|
+
try {
|
|
37620
|
+
this.ffmpeg?.stdin?.end();
|
|
37621
|
+
} catch {
|
|
37622
|
+
}
|
|
37623
|
+
if (this.ffmpeg && this.ffmpeg.exitCode === null) {
|
|
37624
|
+
await new Promise((resolve) => {
|
|
37625
|
+
const t = setTimeout(() => {
|
|
37626
|
+
this.ffmpeg?.kill("SIGKILL");
|
|
37627
|
+
resolve();
|
|
37628
|
+
}, 500);
|
|
37629
|
+
this.ffmpeg?.once("exit", () => {
|
|
37630
|
+
clearTimeout(t);
|
|
37631
|
+
resolve();
|
|
37632
|
+
});
|
|
37633
|
+
});
|
|
37634
|
+
}
|
|
37635
|
+
this.ffmpeg = null;
|
|
37636
|
+
if (this.socket) {
|
|
37637
|
+
try {
|
|
37638
|
+
this.socket.close();
|
|
37639
|
+
} catch {
|
|
37640
|
+
}
|
|
37641
|
+
this.socket = null;
|
|
37642
|
+
}
|
|
37643
|
+
this.log("info", "stopped");
|
|
37644
|
+
}
|
|
37645
|
+
/**
|
|
37646
|
+
* Parse an RTP packet ffmpeg emitted on the loopback socket and surface
|
|
37647
|
+
* its Opus payload. RTP header layout (RFC 3550):
|
|
37648
|
+
*
|
|
37649
|
+
* 0 1 2 3
|
|
37650
|
+
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
37651
|
+
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
37652
|
+
* |V=2|P|X| CC |M| PT | sequence number |
|
|
37653
|
+
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
37654
|
+
* | timestamp |
|
|
37655
|
+
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
37656
|
+
* | synchronization source (SSRC) identifier |
|
|
37657
|
+
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
37658
|
+
*
|
|
37659
|
+
* We honor the CC, X (extension) and P (padding) bits to compute the
|
|
37660
|
+
* payload offset / length correctly even if ffmpeg ever emits those.
|
|
37661
|
+
*/
|
|
37662
|
+
handleRtp(packet) {
|
|
37663
|
+
if (packet.length < 12) return;
|
|
37664
|
+
const b0 = packet[0];
|
|
37665
|
+
const b1 = packet[1];
|
|
37666
|
+
const version = b0 >> 6;
|
|
37667
|
+
if (version !== 2) return;
|
|
37668
|
+
const padding = (b0 & 32) !== 0;
|
|
37669
|
+
const extension = (b0 & 16) !== 0;
|
|
37670
|
+
const csrcCount = b0 & 15;
|
|
37671
|
+
const marker = (b1 & 128) !== 0;
|
|
37672
|
+
let offset = 12 + csrcCount * 4;
|
|
37673
|
+
if (extension) {
|
|
37674
|
+
if (packet.length < offset + 4) return;
|
|
37675
|
+
const extLen = packet.readUInt16BE(offset + 2);
|
|
37676
|
+
offset += 4 + extLen * 4;
|
|
37677
|
+
}
|
|
37678
|
+
let end = packet.length;
|
|
37679
|
+
if (padding) {
|
|
37680
|
+
const pad = packet[packet.length - 1];
|
|
37681
|
+
end -= pad;
|
|
37682
|
+
}
|
|
37683
|
+
if (offset >= end) return;
|
|
37684
|
+
const timestamp = packet.readUInt32BE(4);
|
|
37685
|
+
const payload = Buffer.from(packet.subarray(offset, end));
|
|
37686
|
+
this.emit("packet", { payload, timestamp, marker });
|
|
37687
|
+
}
|
|
37688
|
+
};
|
|
37689
|
+
|
|
37690
|
+
// src/baichuan/stream/BaichuanWebRTCServer.ts
|
|
36012
37691
|
init_H264Converter();
|
|
36013
37692
|
function parseAnnexBNalUnits(annexB) {
|
|
36014
37693
|
const nalUnits = [];
|
|
@@ -36044,7 +37723,7 @@ function getH264NalType(nalUnit) {
|
|
|
36044
37723
|
function getH265NalType2(nalUnit) {
|
|
36045
37724
|
return nalUnit[0] >> 1 & 63;
|
|
36046
37725
|
}
|
|
36047
|
-
var BaichuanWebRTCServer = class extends
|
|
37726
|
+
var BaichuanWebRTCServer = class extends import_node_events11.EventEmitter {
|
|
36048
37727
|
options;
|
|
36049
37728
|
sessions = /* @__PURE__ */ new Map();
|
|
36050
37729
|
sessionIdCounter = 0;
|
|
@@ -36282,6 +37961,14 @@ Error: ${err}`
|
|
|
36282
37961
|
}
|
|
36283
37962
|
session.dataChannel = null;
|
|
36284
37963
|
}
|
|
37964
|
+
if (session.audioTranscoder) {
|
|
37965
|
+
try {
|
|
37966
|
+
await session.audioTranscoder.stop();
|
|
37967
|
+
} catch (err) {
|
|
37968
|
+
this.log("debug", `Error stopping audio transcoder: ${err}`);
|
|
37969
|
+
}
|
|
37970
|
+
session.audioTranscoder = null;
|
|
37971
|
+
}
|
|
36285
37972
|
if (session.cleanup) {
|
|
36286
37973
|
session.cleanup();
|
|
36287
37974
|
}
|
|
@@ -36408,6 +38095,17 @@ Error: ${err}`
|
|
|
36408
38095
|
}
|
|
36409
38096
|
if (frame.audio) {
|
|
36410
38097
|
session.stats.audioFrames++;
|
|
38098
|
+
if (session.stats.audioFrames === 1) {
|
|
38099
|
+
const head = frame.data && frame.data.length > 0 ? frame.data.subarray(0, Math.min(8, frame.data.length)).toString("hex") : "(empty)";
|
|
38100
|
+
this.log(
|
|
38101
|
+
"info",
|
|
38102
|
+
`First audio frame for ${session.id}: codec=${frame.codec ?? "?"} bytes=${frame.data?.length ?? 0} head=${head}`
|
|
38103
|
+
);
|
|
38104
|
+
}
|
|
38105
|
+
if (this.options.ffmpegPath !== "" && frame.data && frame.data.length > 0) {
|
|
38106
|
+
await this.ensureAudioTranscoder(session, werift);
|
|
38107
|
+
session.audioTranscoder?.feedAac(frame.data);
|
|
38108
|
+
}
|
|
36411
38109
|
} else {
|
|
36412
38110
|
if (frame.data) {
|
|
36413
38111
|
if (!session.videoCodec && frame.videoType) {
|
|
@@ -36478,7 +38176,7 @@ Error: ${err}`
|
|
|
36478
38176
|
if (now - lastLogTime >= 5e3) {
|
|
36479
38177
|
this.log(
|
|
36480
38178
|
"debug",
|
|
36481
|
-
`WebRTC session ${session.id} [${session.videoCodec}]: sent ${session.stats.videoFrames} frames, ${packetsSentSinceLastLog} packets, ${Math.round(session.stats.bytesSent / 1024)} KB`
|
|
38179
|
+
`WebRTC session ${session.id} [${session.videoCodec}]: sent ${session.stats.videoFrames} video frames, ${packetsSentSinceLastLog} packets, ${Math.round(session.stats.bytesSent / 1024)} KB | audio frames=${session.stats.audioFrames}`
|
|
36482
38180
|
);
|
|
36483
38181
|
lastLogTime = now;
|
|
36484
38182
|
packetsSentSinceLastLog = 0;
|
|
@@ -36494,6 +38192,77 @@ Error: ${err}`
|
|
|
36494
38192
|
}
|
|
36495
38193
|
this.log("info", `Native stream ended for session ${session.id}`);
|
|
36496
38194
|
}
|
|
38195
|
+
/**
|
|
38196
|
+
* Lazily start the AAC → Opus transcoder for `session` and wire it to the
|
|
38197
|
+
* audio RTP track. ffmpeg writes RTP-formatted Opus packets back to a UDP
|
|
38198
|
+
* loopback the transcoder owns; we strip the RTP header and rewrap the
|
|
38199
|
+
* Opus payload with our audioTrack's SSRC so the browser receives a
|
|
38200
|
+
* coherent stream.
|
|
38201
|
+
*/
|
|
38202
|
+
async ensureAudioTranscoder(session, werift) {
|
|
38203
|
+
if (session.audioTranscoder !== void 0) return;
|
|
38204
|
+
const { RtpPacket, RtpHeader } = werift;
|
|
38205
|
+
const ssrc = session.audioTrack?.ssrc ?? Math.floor(Math.random() * 4294967295);
|
|
38206
|
+
const transcoder = new AacToOpusTranscoder({
|
|
38207
|
+
...this.options.ffmpegPath ? { ffmpegPath: this.options.ffmpegPath } : {},
|
|
38208
|
+
logger: (level, msg) => this.log(level, msg)
|
|
38209
|
+
});
|
|
38210
|
+
session.audioRtpSequence = Math.floor(Math.random() * 65535);
|
|
38211
|
+
session.audioRtpTimestampBase = 0;
|
|
38212
|
+
transcoder.on("packet", ({ payload, timestamp, marker }) => {
|
|
38213
|
+
try {
|
|
38214
|
+
if (session.audioRtpTimestampBase === void 0 || session.audioRtpTimestampBase === 0) {
|
|
38215
|
+
session.audioRtpTimestampBase = timestamp;
|
|
38216
|
+
}
|
|
38217
|
+
const localTs = timestamp - session.audioRtpTimestampBase >>> 0;
|
|
38218
|
+
const seq = session.audioRtpSequence;
|
|
38219
|
+
session.audioRtpSequence = seq + 1 & 65535;
|
|
38220
|
+
const header = new RtpHeader({
|
|
38221
|
+
version: 2,
|
|
38222
|
+
padding: false,
|
|
38223
|
+
extension: false,
|
|
38224
|
+
marker,
|
|
38225
|
+
// Werift assigns 111 to Opus by default in the receiver SDP, but it
|
|
38226
|
+
// also accepts other PTs. Use 111 for compatibility with the offer
|
|
38227
|
+
// we generated earlier.
|
|
38228
|
+
payloadType: 111,
|
|
38229
|
+
sequenceNumber: seq,
|
|
38230
|
+
timestamp: localTs,
|
|
38231
|
+
ssrc
|
|
38232
|
+
});
|
|
38233
|
+
const rtp = new RtpPacket(header, payload);
|
|
38234
|
+
session.audioTrack?.writeRtp(rtp);
|
|
38235
|
+
if (!session.audioStartedLogged) {
|
|
38236
|
+
session.audioStartedLogged = true;
|
|
38237
|
+
this.log(
|
|
38238
|
+
"info",
|
|
38239
|
+
`Audio RTP started for ${session.id} (PT=111, ssrc=${ssrc})`
|
|
38240
|
+
);
|
|
38241
|
+
}
|
|
38242
|
+
} catch (err) {
|
|
38243
|
+
this.log(
|
|
38244
|
+
"warn",
|
|
38245
|
+
`audio writeRtp failed for ${session.id}: ${err.message}`
|
|
38246
|
+
);
|
|
38247
|
+
}
|
|
38248
|
+
});
|
|
38249
|
+
transcoder.on("error", (err) => {
|
|
38250
|
+
this.log("error", `audio transcoder error for ${session.id}: ${err.message}`);
|
|
38251
|
+
});
|
|
38252
|
+
transcoder.on("exit", (code) => {
|
|
38253
|
+
this.log("info", `audio transcoder exited (${code}) for ${session.id}`);
|
|
38254
|
+
});
|
|
38255
|
+
try {
|
|
38256
|
+
await transcoder.start();
|
|
38257
|
+
session.audioTranscoder = transcoder;
|
|
38258
|
+
} catch (err) {
|
|
38259
|
+
this.log(
|
|
38260
|
+
"error",
|
|
38261
|
+
`failed to start audio transcoder for ${session.id}: ${err.message}`
|
|
38262
|
+
);
|
|
38263
|
+
session.audioTranscoder = null;
|
|
38264
|
+
}
|
|
38265
|
+
}
|
|
36497
38266
|
/**
|
|
36498
38267
|
* Send H.264 frame via RTP media track
|
|
36499
38268
|
* Returns the number of RTP packets sent
|
|
@@ -36781,21 +38550,21 @@ Error: ${err}`
|
|
|
36781
38550
|
`Sending ${codec} frame ${frameNumber}: ${packet.length} bytes, keyframe=${isKeyframe}`
|
|
36782
38551
|
);
|
|
36783
38552
|
}
|
|
36784
|
-
const
|
|
38553
|
+
const CHUNK_HEADER_LEN = 4;
|
|
38554
|
+
const MAX_PAYLOAD_PER_CHUNK = 16e3 - CHUNK_HEADER_LEN;
|
|
36785
38555
|
try {
|
|
36786
|
-
|
|
36787
|
-
|
|
36788
|
-
|
|
36789
|
-
|
|
36790
|
-
|
|
36791
|
-
|
|
36792
|
-
|
|
36793
|
-
|
|
36794
|
-
|
|
36795
|
-
|
|
36796
|
-
|
|
36797
|
-
|
|
36798
|
-
}
|
|
38556
|
+
const totalChunks = Math.max(
|
|
38557
|
+
1,
|
|
38558
|
+
Math.ceil(packet.length / MAX_PAYLOAD_PER_CHUNK)
|
|
38559
|
+
);
|
|
38560
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
38561
|
+
const start = i * MAX_PAYLOAD_PER_CHUNK;
|
|
38562
|
+
const end = Math.min(start + MAX_PAYLOAD_PER_CHUNK, packet.length);
|
|
38563
|
+
const chunk = packet.subarray(start, end);
|
|
38564
|
+
const chunkHeader = Buffer.alloc(CHUNK_HEADER_LEN);
|
|
38565
|
+
chunkHeader.writeUInt16BE(i, 0);
|
|
38566
|
+
chunkHeader.writeUInt16BE(totalChunks, 2);
|
|
38567
|
+
session.videoDataChannel.send(Buffer.concat([chunkHeader, chunk]));
|
|
36799
38568
|
}
|
|
36800
38569
|
return true;
|
|
36801
38570
|
} catch (err) {
|
|
@@ -36946,12 +38715,12 @@ Error: ${err}`
|
|
|
36946
38715
|
};
|
|
36947
38716
|
|
|
36948
38717
|
// src/baichuan/stream/BaichuanHlsServer.ts
|
|
36949
|
-
var
|
|
38718
|
+
var import_node_events12 = require("events");
|
|
36950
38719
|
var import_node_fs = __toESM(require("fs"), 1);
|
|
36951
38720
|
var import_promises3 = __toESM(require("fs/promises"), 1);
|
|
36952
38721
|
var import_node_os3 = __toESM(require("os"), 1);
|
|
36953
38722
|
var import_node_path3 = __toESM(require("path"), 1);
|
|
36954
|
-
var
|
|
38723
|
+
var import_node_child_process12 = require("child_process");
|
|
36955
38724
|
init_BcMediaAnnexBDecoder();
|
|
36956
38725
|
init_H264Converter();
|
|
36957
38726
|
init_H265Converter();
|
|
@@ -37026,7 +38795,7 @@ function getNalTypes(codec, annexB) {
|
|
|
37026
38795
|
}
|
|
37027
38796
|
});
|
|
37028
38797
|
}
|
|
37029
|
-
var BaichuanHlsServer = class extends
|
|
38798
|
+
var BaichuanHlsServer = class extends import_node_events12.EventEmitter {
|
|
37030
38799
|
api;
|
|
37031
38800
|
channel;
|
|
37032
38801
|
profile;
|
|
@@ -37383,10 +39152,17 @@ var BaichuanHlsServer = class extends import_node_events11.EventEmitter {
|
|
|
37383
39152
|
"-hide_banner",
|
|
37384
39153
|
"-loglevel",
|
|
37385
39154
|
"warning",
|
|
39155
|
+
// `+genpts` makes ffmpeg generate uniform PTS from the declared `-r`
|
|
39156
|
+
// when the raw H.264/H.265 input has none. We deliberately do NOT use
|
|
39157
|
+
// `-use_wallclock_as_timestamps 1` here: it replaces the generated
|
|
39158
|
+
// PTS with the host wallclock at FRAME ARRIVAL time, and because the
|
|
39159
|
+
// camera ships frames in bursty network reads, the resulting PTS
|
|
39160
|
+
// sequence is uneven. With `-r 25` (or anything else) forcing a
|
|
39161
|
+
// target rate downstream, ffmpeg then drops/duplicates frames to
|
|
39162
|
+
// match — visible as the periodic stutter / pulsing reported on
|
|
39163
|
+
// local-restreamer recordings (issue #11).
|
|
37386
39164
|
"-fflags",
|
|
37387
39165
|
"+genpts",
|
|
37388
|
-
"-use_wallclock_as_timestamps",
|
|
37389
|
-
"1",
|
|
37390
39166
|
"-r",
|
|
37391
39167
|
"25",
|
|
37392
39168
|
"-f",
|
|
@@ -37421,7 +39197,7 @@ var BaichuanHlsServer = class extends import_node_events11.EventEmitter {
|
|
|
37421
39197
|
this.segmentPattern,
|
|
37422
39198
|
this.playlistPath
|
|
37423
39199
|
);
|
|
37424
|
-
const p = (0,
|
|
39200
|
+
const p = (0, import_node_child_process12.spawn)(this.ffmpegPath, args, {
|
|
37425
39201
|
stdio: ["pipe", "ignore", "pipe"]
|
|
37426
39202
|
});
|
|
37427
39203
|
p.on("error", (err) => {
|
|
@@ -38043,10 +39819,10 @@ async function autoDetectDeviceType(inputs) {
|
|
|
38043
39819
|
}
|
|
38044
39820
|
|
|
38045
39821
|
// src/multifocal/compositeRtspServer.ts
|
|
38046
|
-
var
|
|
38047
|
-
var
|
|
39822
|
+
var import_node_events13 = require("events");
|
|
39823
|
+
var import_node_child_process13 = require("child_process");
|
|
38048
39824
|
var net5 = __toESM(require("net"), 1);
|
|
38049
|
-
var CompositeRtspServer = class extends
|
|
39825
|
+
var CompositeRtspServer = class extends import_node_events13.EventEmitter {
|
|
38050
39826
|
options;
|
|
38051
39827
|
compositeStream = null;
|
|
38052
39828
|
rtspServer = null;
|
|
@@ -38151,7 +39927,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
|
|
|
38151
39927
|
this.logger.log?.(
|
|
38152
39928
|
`[CompositeRtspServer] Starting ffmpeg RTSP server: ${ffmpegArgs.join(" ")}`
|
|
38153
39929
|
);
|
|
38154
|
-
this.ffmpegProcess = (0,
|
|
39930
|
+
this.ffmpegProcess = (0, import_node_child_process13.spawn)("ffmpeg", ffmpegArgs, {
|
|
38155
39931
|
stdio: ["pipe", "pipe", "pipe"]
|
|
38156
39932
|
});
|
|
38157
39933
|
this.ffmpegProcess.on("error", (error) => {
|
|
@@ -38259,6 +40035,86 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
|
|
|
38259
40035
|
return this.connectedClients.size;
|
|
38260
40036
|
}
|
|
38261
40037
|
};
|
|
40038
|
+
|
|
40039
|
+
// src/reolink/baichuan/utils/motionZone.ts
|
|
40040
|
+
function decodeMotionScopeBitmap(valueTable, columns, rows, width = columns, height = rows) {
|
|
40041
|
+
const trimmed = valueTable.trim().replace(/[^A-Za-z0-9+/=]/g, "");
|
|
40042
|
+
const bytes = base64DecodeToBytes(trimmed);
|
|
40043
|
+
const totalBits = columns * rows;
|
|
40044
|
+
if (bytes.length * 8 < totalBits) {
|
|
40045
|
+
throw new Error(
|
|
40046
|
+
`valueTable too short: have ${bytes.length * 8} bits, need ${totalBits}`
|
|
40047
|
+
);
|
|
40048
|
+
}
|
|
40049
|
+
const w = Math.min(width, columns);
|
|
40050
|
+
const h = Math.min(height, rows);
|
|
40051
|
+
const cells = new Array(w * h);
|
|
40052
|
+
for (let r = 0; r < h; r++) {
|
|
40053
|
+
for (let c = 0; c < w; c++) {
|
|
40054
|
+
const bitIndex = r * columns + c;
|
|
40055
|
+
const byteIdx = bitIndex >> 3;
|
|
40056
|
+
const bitIdx = 7 - (bitIndex & 7);
|
|
40057
|
+
cells[r * w + c] = (bytes[byteIdx] ?? 0) >> bitIdx & 1 ? true : false;
|
|
40058
|
+
}
|
|
40059
|
+
}
|
|
40060
|
+
return { width: w, height: h, columns, rows, cells };
|
|
40061
|
+
}
|
|
40062
|
+
function encodeMotionScopeBitmap(scope) {
|
|
40063
|
+
const bytes = new Uint8Array(Math.ceil(scope.columns * scope.rows / 8));
|
|
40064
|
+
for (let r = 0; r < scope.height; r++) {
|
|
40065
|
+
for (let c = 0; c < scope.width; c++) {
|
|
40066
|
+
const on = scope.cells[r * scope.width + c];
|
|
40067
|
+
if (!on) continue;
|
|
40068
|
+
const bitIndex = r * scope.columns + c;
|
|
40069
|
+
const byteIdx = bitIndex >> 3;
|
|
40070
|
+
const bitIdx = 7 - (bitIndex & 7);
|
|
40071
|
+
bytes[byteIdx] = (bytes[byteIdx] ?? 0) | 1 << bitIdx;
|
|
40072
|
+
}
|
|
40073
|
+
}
|
|
40074
|
+
return base64EncodeBytes(bytes);
|
|
40075
|
+
}
|
|
40076
|
+
function fullCoverageScope(columns, rows, width = columns, height = rows) {
|
|
40077
|
+
return {
|
|
40078
|
+
width,
|
|
40079
|
+
height,
|
|
40080
|
+
columns,
|
|
40081
|
+
rows,
|
|
40082
|
+
cells: new Array(width * height).fill(true)
|
|
40083
|
+
};
|
|
40084
|
+
}
|
|
40085
|
+
var B64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
40086
|
+
function base64EncodeBytes(bytes) {
|
|
40087
|
+
let out = "";
|
|
40088
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
40089
|
+
const b0 = bytes[i] ?? 0;
|
|
40090
|
+
const b1 = bytes[i + 1] ?? 0;
|
|
40091
|
+
const b2 = bytes[i + 2] ?? 0;
|
|
40092
|
+
out += B64_ALPHABET[b0 >> 2];
|
|
40093
|
+
out += B64_ALPHABET[(b0 & 3) << 4 | b1 >> 4];
|
|
40094
|
+
out += i + 1 < bytes.length ? B64_ALPHABET[(b1 & 15) << 2 | b2 >> 6] : "=";
|
|
40095
|
+
out += i + 2 < bytes.length ? B64_ALPHABET[b2 & 63] : "=";
|
|
40096
|
+
}
|
|
40097
|
+
return out;
|
|
40098
|
+
}
|
|
40099
|
+
function base64DecodeToBytes(b64) {
|
|
40100
|
+
const padded = b64.padEnd(Math.ceil(b64.length / 4) * 4, "=");
|
|
40101
|
+
const stripped = padded.replace(/=+$/, "");
|
|
40102
|
+
const out = new Uint8Array(Math.floor(stripped.length * 6 / 8));
|
|
40103
|
+
let bits = 0;
|
|
40104
|
+
let value = 0;
|
|
40105
|
+
let outIdx = 0;
|
|
40106
|
+
for (const ch of stripped) {
|
|
40107
|
+
const idx = B64_ALPHABET.indexOf(ch);
|
|
40108
|
+
if (idx < 0) continue;
|
|
40109
|
+
value = value << 6 | idx;
|
|
40110
|
+
bits += 6;
|
|
40111
|
+
if (bits >= 8) {
|
|
40112
|
+
bits -= 8;
|
|
40113
|
+
out[outIdx++] = value >> bits & 255;
|
|
40114
|
+
}
|
|
40115
|
+
}
|
|
40116
|
+
return out.subarray(0, outIdx);
|
|
40117
|
+
}
|
|
38262
40118
|
// Annotate the CommonJS export names for ESM import in node:
|
|
38263
40119
|
0 && (module.exports = {
|
|
38264
40120
|
AesStreamDecryptor,
|
|
@@ -38306,6 +40162,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
|
|
|
38306
40162
|
BC_CMD_ID_GET_AUDIO_CFG,
|
|
38307
40163
|
BC_CMD_ID_GET_AUDIO_TASK,
|
|
38308
40164
|
BC_CMD_ID_GET_AUTO_FOCUS,
|
|
40165
|
+
BC_CMD_ID_GET_AUTO_REBOOT,
|
|
38309
40166
|
BC_CMD_ID_GET_BATTERY_INFO,
|
|
38310
40167
|
BC_CMD_ID_GET_BATTERY_INFO_LIST,
|
|
38311
40168
|
BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD,
|
|
@@ -38313,6 +40170,8 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
|
|
|
38313
40170
|
BC_CMD_ID_GET_DING_DONG_CFG,
|
|
38314
40171
|
BC_CMD_ID_GET_DING_DONG_LIST,
|
|
38315
40172
|
BC_CMD_ID_GET_DING_DONG_SILENT,
|
|
40173
|
+
BC_CMD_ID_GET_DST,
|
|
40174
|
+
BC_CMD_ID_GET_EMAIL,
|
|
38316
40175
|
BC_CMD_ID_GET_EMAIL_TASK,
|
|
38317
40176
|
BC_CMD_ID_GET_ENC,
|
|
38318
40177
|
BC_CMD_ID_GET_FTP_TASK,
|
|
@@ -38320,6 +40179,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
|
|
|
38320
40179
|
BC_CMD_ID_GET_KIT_AP_CFG,
|
|
38321
40180
|
BC_CMD_ID_GET_LED_STATE,
|
|
38322
40181
|
BC_CMD_ID_GET_MOTION_ALARM,
|
|
40182
|
+
BC_CMD_ID_GET_NTP,
|
|
38323
40183
|
BC_CMD_ID_GET_ONLINE_USER_LIST,
|
|
38324
40184
|
BC_CMD_ID_GET_OSD_DATETIME,
|
|
38325
40185
|
BC_CMD_ID_GET_PIR_INFO,
|
|
@@ -38336,6 +40196,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
|
|
|
38336
40196
|
BC_CMD_ID_GET_SUPPORT,
|
|
38337
40197
|
BC_CMD_ID_GET_SYSTEM_GENERAL,
|
|
38338
40198
|
BC_CMD_ID_GET_TIMELAPSE_CFG,
|
|
40199
|
+
BC_CMD_ID_GET_VERSION_INFO,
|
|
38339
40200
|
BC_CMD_ID_GET_VIDEO_INPUT,
|
|
38340
40201
|
BC_CMD_ID_GET_WHITE_LED,
|
|
38341
40202
|
BC_CMD_ID_GET_WIFI,
|
|
@@ -38359,18 +40220,24 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
|
|
|
38359
40220
|
BC_CMD_ID_SET_AUDIO_CFG,
|
|
38360
40221
|
BC_CMD_ID_SET_AUDIO_TASK,
|
|
38361
40222
|
BC_CMD_ID_SET_AUTO_FOCUS,
|
|
40223
|
+
BC_CMD_ID_SET_AUTO_REBOOT,
|
|
38362
40224
|
BC_CMD_ID_SET_DAY_NIGHT_THRESHOLD,
|
|
38363
40225
|
BC_CMD_ID_SET_DING_DONG_CFG,
|
|
38364
40226
|
BC_CMD_ID_SET_DING_DONG_SILENT,
|
|
40227
|
+
BC_CMD_ID_SET_DST,
|
|
40228
|
+
BC_CMD_ID_SET_EMAIL,
|
|
38365
40229
|
BC_CMD_ID_SET_EMAIL_TASK,
|
|
38366
40230
|
BC_CMD_ID_SET_ENC,
|
|
38367
40231
|
BC_CMD_ID_SET_LED_STATE,
|
|
38368
40232
|
BC_CMD_ID_SET_MOTION_ALARM,
|
|
40233
|
+
BC_CMD_ID_SET_NTP,
|
|
40234
|
+
BC_CMD_ID_SET_OSD_DATETIME,
|
|
38369
40235
|
BC_CMD_ID_SET_PIR_INFO,
|
|
38370
40236
|
BC_CMD_ID_SET_PRIVACY_MASK,
|
|
38371
40237
|
BC_CMD_ID_SET_PUSH_TASK,
|
|
38372
40238
|
BC_CMD_ID_SET_RECORD,
|
|
38373
40239
|
BC_CMD_ID_SET_RECORD_CFG,
|
|
40240
|
+
BC_CMD_ID_SET_SYSTEM_GENERAL,
|
|
38374
40241
|
BC_CMD_ID_SET_VIDEO_INPUT,
|
|
38375
40242
|
BC_CMD_ID_SET_WHITE_LED_STATE,
|
|
38376
40243
|
BC_CMD_ID_SET_WHITE_LED_TASK,
|
|
@@ -38380,6 +40247,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
|
|
|
38380
40247
|
BC_CMD_ID_TALK_ABILITY,
|
|
38381
40248
|
BC_CMD_ID_TALK_CONFIG,
|
|
38382
40249
|
BC_CMD_ID_TALK_RESET,
|
|
40250
|
+
BC_CMD_ID_TEST_EMAIL,
|
|
38383
40251
|
BC_CMD_ID_UDP_KEEP_ALIVE,
|
|
38384
40252
|
BC_CMD_ID_VIDEO,
|
|
38385
40253
|
BC_CMD_ID_VIDEO_STOP,
|
|
@@ -38474,6 +40342,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
|
|
|
38474
40342
|
decideSleepInferenceTransition,
|
|
38475
40343
|
decideVideoclipTranscodeMode,
|
|
38476
40344
|
decodeHeader,
|
|
40345
|
+
decodeMotionScopeBitmap,
|
|
38477
40346
|
deriveAesKey,
|
|
38478
40347
|
detectIosClient,
|
|
38479
40348
|
detectVideoCodecFromNal,
|
|
@@ -38486,6 +40355,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
|
|
|
38486
40355
|
discoverViaUdpBroadcast,
|
|
38487
40356
|
discoverViaUdpDirect,
|
|
38488
40357
|
encodeHeader,
|
|
40358
|
+
encodeMotionScopeBitmap,
|
|
38489
40359
|
ensureXmlHeader,
|
|
38490
40360
|
extractH264ParamSetsFromAccessUnit,
|
|
38491
40361
|
extractH265ParamSetsFromAccessUnit,
|
|
@@ -38494,6 +40364,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
|
|
|
38494
40364
|
extractVpsFromAnnexB,
|
|
38495
40365
|
flattenAbilitiesForChannel,
|
|
38496
40366
|
formatMjpegFrame,
|
|
40367
|
+
fullCoverageScope,
|
|
38497
40368
|
getConstructedVideoStreamOptions,
|
|
38498
40369
|
getGlobalLogger,
|
|
38499
40370
|
getH265NalType,
|
|
@@ -38537,6 +40408,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
|
|
|
38537
40408
|
splitAnnexBToNals,
|
|
38538
40409
|
splitH265AnnexBToNalPayloads,
|
|
38539
40410
|
testChannelStreams,
|
|
40411
|
+
upsertXmlTag,
|
|
38540
40412
|
xmlEscape,
|
|
38541
40413
|
xmlIndicatesFloodlight,
|
|
38542
40414
|
zipDirectory
|