@apocaliss92/nodelink-js 0.4.10 → 0.4.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/BaichuanVideoStream-PHQG4A2L.js +7 -0
- package/dist/{DiagnosticsTools-RNIDFEJK.js → DiagnosticsTools-HGJGVQXZ.js} +3 -2
- package/dist/DiagnosticsTools-HGJGVQXZ.js.map +1 -0
- package/dist/{chunk-EDLMKBG2.js → chunk-IJG45AOT.js} +151 -2733
- package/dist/chunk-IJG45AOT.js.map +1 -0
- package/dist/{chunk-HGQ53FB3.js → chunk-ND73IJIB.js} +765 -45
- package/dist/chunk-ND73IJIB.js.map +1 -0
- package/dist/chunk-W2ANCJVM.js +2703 -0
- package/dist/chunk-W2ANCJVM.js.map +1 -0
- package/dist/cli/rtsp-server.cjs +834 -10
- 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 +1322 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +708 -142
- package/dist/index.d.ts +601 -20
- package/dist/index.js +527 -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-PHQG4A2L.js.map} +0 -0
package/dist/cli/rtsp-server.cjs
CHANGED
|
@@ -60,7 +60,7 @@ var init_urls = __esm({
|
|
|
60
60
|
function bcHeaderHasPayloadOffset(messageClass) {
|
|
61
61
|
return messageClass === BC_CLASS_MODERN_24 || messageClass === BC_CLASS_MODERN_24_ALT || messageClass === BC_CLASS_FILE_DOWNLOAD;
|
|
62
62
|
}
|
|
63
|
-
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_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_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_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_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;
|
|
63
|
+
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_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_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_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_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;
|
|
64
64
|
var init_constants = __esm({
|
|
65
65
|
"src/protocol/constants.ts"() {
|
|
66
66
|
"use strict";
|
|
@@ -126,9 +126,11 @@ var init_constants = __esm({
|
|
|
126
126
|
BC_CMD_ID_PING = 93;
|
|
127
127
|
BC_CMD_ID_CHANNEL_INFO_ALL = 145;
|
|
128
128
|
BC_CMD_ID_GET_OSD_DATETIME = 44;
|
|
129
|
+
BC_CMD_ID_SET_OSD_DATETIME = 45;
|
|
129
130
|
BC_CMD_ID_GET_RECORD_CFG = 54;
|
|
130
131
|
BC_CMD_ID_GET_ABILITY_SUPPORT = 58;
|
|
131
132
|
BC_CMD_ID_GET_FTP_TASK = 70;
|
|
133
|
+
BC_CMD_ID_GET_VERSION_INFO = 80;
|
|
132
134
|
BC_CMD_ID_GET_RECORD = 81;
|
|
133
135
|
BC_CMD_ID_GET_HDD_INFO_LIST = 102;
|
|
134
136
|
BC_CMD_ID_GET_WIFI_SIGNAL = 115;
|
|
@@ -488,10 +490,15 @@ var init_BcMediaCodec = __esm({
|
|
|
488
490
|
strict;
|
|
489
491
|
amountSkipped = 0;
|
|
490
492
|
logger;
|
|
493
|
+
onUnknownChunk;
|
|
491
494
|
constructor(strict = false, logger) {
|
|
492
495
|
this.strict = strict;
|
|
493
496
|
this.logger = logger;
|
|
494
497
|
}
|
|
498
|
+
/** Register a listener that fires for every unknown chunk before recovery. */
|
|
499
|
+
setUnknownChunkListener(listener) {
|
|
500
|
+
this.onUnknownChunk = listener;
|
|
501
|
+
}
|
|
495
502
|
/**
|
|
496
503
|
* Push data into the codec buffer and try to parse complete BcMedia packets.
|
|
497
504
|
* Returns an array of complete BcMedia packets found.
|
|
@@ -552,6 +559,13 @@ var init_BcMediaCodec = __esm({
|
|
|
552
559
|
}
|
|
553
560
|
}
|
|
554
561
|
if (next > 0) {
|
|
562
|
+
if (this.onUnknownChunk) {
|
|
563
|
+
this.onUnknownChunk({
|
|
564
|
+
magic,
|
|
565
|
+
preview: Buffer.from(this.buffer.subarray(0, Math.min(256, next))),
|
|
566
|
+
skipped: next
|
|
567
|
+
});
|
|
568
|
+
}
|
|
555
569
|
this.amountSkipped += next;
|
|
556
570
|
this.buffer = this.buffer.subarray(next);
|
|
557
571
|
continue;
|
|
@@ -1414,6 +1428,10 @@ var init_BcMediaAnnexBDecoder = __esm({
|
|
|
1414
1428
|
});
|
|
1415
1429
|
|
|
1416
1430
|
// src/baichuan/stream/BaichuanVideoStream.ts
|
|
1431
|
+
var BaichuanVideoStream_exports = {};
|
|
1432
|
+
__export(BaichuanVideoStream_exports, {
|
|
1433
|
+
BaichuanVideoStream: () => BaichuanVideoStream
|
|
1434
|
+
});
|
|
1417
1435
|
function removeEmulationPreventionBytes(rbsp) {
|
|
1418
1436
|
const out = [];
|
|
1419
1437
|
for (let i = 0; i < rbsp.length; i++) {
|
|
@@ -1547,6 +1565,15 @@ var init_BaichuanVideoStream = __esm({
|
|
|
1547
1565
|
acceptAnyStreamType;
|
|
1548
1566
|
lockedChannelId;
|
|
1549
1567
|
bcMediaCodec;
|
|
1568
|
+
/**
|
|
1569
|
+
* Diagnostic-only accessor for the BcMedia codec. Used by tools that need to
|
|
1570
|
+
* inspect unknown chunks (for example to discover undocumented audio
|
|
1571
|
+
* sub-packets the parser currently skips). Not part of the supported public
|
|
1572
|
+
* surface — do not rely on it in application code.
|
|
1573
|
+
*/
|
|
1574
|
+
get _bcMediaCodec() {
|
|
1575
|
+
return this.bcMediaCodec;
|
|
1576
|
+
}
|
|
1550
1577
|
debugH264LogsLeft;
|
|
1551
1578
|
debugSavedSamples;
|
|
1552
1579
|
warnedNonAnnexBOnce = false;
|
|
@@ -1578,6 +1605,14 @@ var init_BaichuanVideoStream = __esm({
|
|
|
1578
1605
|
// Stateful AES decryptor for fragmented BcMedia packets (full_aes mode)
|
|
1579
1606
|
// In CFB mode, continuation frames must use the cipher state from previous frames.
|
|
1580
1607
|
aesStreamDecryptor = null;
|
|
1608
|
+
// Latest frame dimensions reported by BcMedia InfoV1/V2 packets.
|
|
1609
|
+
// Used to attach width/height context to the `additionalHeader` event so
|
|
1610
|
+
// consumers can normalize box coordinates to a fraction of the stream size.
|
|
1611
|
+
latestFrameWidth;
|
|
1612
|
+
latestFrameHeight;
|
|
1613
|
+
// Teardown returned by ReolinkBaichuanApi._registerVideoStreamForDetection.
|
|
1614
|
+
// Called from stop() to detach the detection bridge.
|
|
1615
|
+
detectionTeardown;
|
|
1581
1616
|
/**
|
|
1582
1617
|
* Pending startup error stashed when emitSafeError is called before any
|
|
1583
1618
|
* "error" listener is registered (e.g. camera returns 400 during start()).
|
|
@@ -2240,6 +2275,16 @@ var init_BaichuanVideoStream = __esm({
|
|
|
2240
2275
|
}
|
|
2241
2276
|
videoType = detectedCodec;
|
|
2242
2277
|
}
|
|
2278
|
+
if (media.additionalHeader && media.additionalHeader.length > 0) {
|
|
2279
|
+
this.emit("additionalHeader", {
|
|
2280
|
+
raw: media.additionalHeader,
|
|
2281
|
+
frameType: "Iframe",
|
|
2282
|
+
videoType,
|
|
2283
|
+
microseconds: media.microseconds,
|
|
2284
|
+
...this.latestFrameWidth !== void 0 ? { frameWidth: this.latestFrameWidth } : {},
|
|
2285
|
+
...this.latestFrameHeight !== void 0 ? { frameHeight: this.latestFrameHeight } : {}
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2243
2288
|
const annexBData = videoType === "H265" ? convertToAnnexB2(media.data) : convertToAnnexB(media.data);
|
|
2244
2289
|
const isKeyframe = true;
|
|
2245
2290
|
maybeCacheParamSets(annexBData, "Iframe", videoType);
|
|
@@ -2321,6 +2366,18 @@ var init_BaichuanVideoStream = __esm({
|
|
|
2321
2366
|
}
|
|
2322
2367
|
} else if (media.type === "Pframe") {
|
|
2323
2368
|
const chunk = media.data;
|
|
2369
|
+
if (media.additionalHeader && media.additionalHeader.length > 0) {
|
|
2370
|
+
const detected = detectVideoCodecFromNal(chunk);
|
|
2371
|
+
const videoTypeForHeader = detected ?? media.videoType;
|
|
2372
|
+
this.emit("additionalHeader", {
|
|
2373
|
+
raw: media.additionalHeader,
|
|
2374
|
+
frameType: "Pframe",
|
|
2375
|
+
videoType: videoTypeForHeader,
|
|
2376
|
+
microseconds: media.microseconds,
|
|
2377
|
+
...this.latestFrameWidth !== void 0 ? { frameWidth: this.latestFrameWidth } : {},
|
|
2378
|
+
...this.latestFrameHeight !== void 0 ? { frameHeight: this.latestFrameHeight } : {}
|
|
2379
|
+
});
|
|
2380
|
+
}
|
|
2324
2381
|
let videoType = media.videoType;
|
|
2325
2382
|
const detectedCodec = detectVideoCodecFromNal(chunk);
|
|
2326
2383
|
if (detectedCodec && detectedCodec !== videoType) {
|
|
@@ -2363,6 +2420,8 @@ var init_BaichuanVideoStream = __esm({
|
|
|
2363
2420
|
this.emit("audioFrame", media.data);
|
|
2364
2421
|
}
|
|
2365
2422
|
if (media.type === "InfoV1" || media.type === "InfoV2") {
|
|
2423
|
+
if (media.videoWidth > 0) this.latestFrameWidth = media.videoWidth;
|
|
2424
|
+
if (media.videoHeight > 0) this.latestFrameHeight = media.videoHeight;
|
|
2366
2425
|
}
|
|
2367
2426
|
}
|
|
2368
2427
|
if (totalFramesReceived <= 10 || totalFramesReceived % 20 === 0 && (videoFramesEmitted > 0 || audioFramesEmitted > 0)) {
|
|
@@ -2382,6 +2441,12 @@ var init_BaichuanVideoStream = __esm({
|
|
|
2382
2441
|
this.client.on("push", this.videoFrameHandler);
|
|
2383
2442
|
this.active = true;
|
|
2384
2443
|
this.startWatchdog();
|
|
2444
|
+
if (this.api && typeof this.api._registerVideoStreamForDetection === "function") {
|
|
2445
|
+
this.detectionTeardown = this.api._registerVideoStreamForDetection(this, {
|
|
2446
|
+
channel: this.channel,
|
|
2447
|
+
profile: this.profile
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
2385
2450
|
this.lastMediaAtMs = Date.now();
|
|
2386
2451
|
if (this.api) {
|
|
2387
2452
|
try {
|
|
@@ -2479,6 +2544,13 @@ var init_BaichuanVideoStream = __esm({
|
|
|
2479
2544
|
}
|
|
2480
2545
|
}
|
|
2481
2546
|
this.active = false;
|
|
2547
|
+
if (this.detectionTeardown) {
|
|
2548
|
+
try {
|
|
2549
|
+
this.detectionTeardown();
|
|
2550
|
+
} catch {
|
|
2551
|
+
}
|
|
2552
|
+
this.detectionTeardown = void 0;
|
|
2553
|
+
}
|
|
2482
2554
|
this.emit("close");
|
|
2483
2555
|
}
|
|
2484
2556
|
isActive() {
|
|
@@ -2714,6 +2786,15 @@ function applyXmlTagPatch(xml, tag, value) {
|
|
|
2714
2786
|
const re = new RegExp(`<${tag}>[^<]*</${tag}>`);
|
|
2715
2787
|
return xml.replace(re, `<${tag}>${v}</${tag}>`);
|
|
2716
2788
|
}
|
|
2789
|
+
function upsertXmlTag(xml, tag, value) {
|
|
2790
|
+
if (value === void 0) return xml;
|
|
2791
|
+
const v = typeof value === "boolean" ? value ? 1 : 0 : value;
|
|
2792
|
+
const re = new RegExp(`<${tag}>[^<]*</${tag}>`);
|
|
2793
|
+
if (re.test(xml)) {
|
|
2794
|
+
return xml.replace(re, `<${tag}>${v}</${tag}>`);
|
|
2795
|
+
}
|
|
2796
|
+
return `${xml}<${tag}>${v}</${tag}>`;
|
|
2797
|
+
}
|
|
2717
2798
|
function patchNestedTag(xml, parent, child, value) {
|
|
2718
2799
|
if (value === void 0) return xml;
|
|
2719
2800
|
const v = typeof value === "boolean" ? value ? 1 : 0 : value;
|
|
@@ -2729,6 +2810,15 @@ function applyStreamPatch(xml, streamTag, patch) {
|
|
|
2729
2810
|
);
|
|
2730
2811
|
return xml.replace(re, (_match, open, body, close) => {
|
|
2731
2812
|
let next = body;
|
|
2813
|
+
if (patch.audio !== void 0) {
|
|
2814
|
+
next = applyXmlTagPatch(next, "audio", patch.audio);
|
|
2815
|
+
}
|
|
2816
|
+
if (patch.width !== void 0) {
|
|
2817
|
+
next = applyXmlTagPatch(next, "width", patch.width);
|
|
2818
|
+
}
|
|
2819
|
+
if (patch.height !== void 0) {
|
|
2820
|
+
next = applyXmlTagPatch(next, "height", patch.height);
|
|
2821
|
+
}
|
|
2732
2822
|
if (patch.bitRate !== void 0) {
|
|
2733
2823
|
next = applyXmlTagPatch(next, "bitRate", patch.bitRate);
|
|
2734
2824
|
}
|
|
@@ -2740,6 +2830,23 @@ function applyStreamPatch(xml, streamTag, patch) {
|
|
|
2740
2830
|
const intVal = patch.videoEncType === "h265" ? 1 : 0;
|
|
2741
2831
|
next = applyXmlTagPatch(next, "videoEncType", intVal);
|
|
2742
2832
|
}
|
|
2833
|
+
if (patch.encoderType !== void 0) {
|
|
2834
|
+
next = upsertXmlTag(next, "encoderType", patch.encoderType);
|
|
2835
|
+
}
|
|
2836
|
+
if (patch.encoderProfile !== void 0) {
|
|
2837
|
+
next = upsertXmlTag(next, "encoderProfile", patch.encoderProfile);
|
|
2838
|
+
}
|
|
2839
|
+
if (patch.gop !== void 0) {
|
|
2840
|
+
const gopBlockRe = /(<gop[^>]*>)([\s\S]*?)(<\/gop>)/;
|
|
2841
|
+
if (gopBlockRe.test(next)) {
|
|
2842
|
+
next = next.replace(
|
|
2843
|
+
gopBlockRe,
|
|
2844
|
+
(_m, gOpen, gBody, gClose) => `${gOpen}${applyXmlTagPatch(gBody, "cur", patch.gop)}${gClose}`
|
|
2845
|
+
);
|
|
2846
|
+
} else {
|
|
2847
|
+
next = `${next}<gop><cur>${patch.gop}</cur></gop>`;
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2743
2850
|
return `${open}${next}${close}`;
|
|
2744
2851
|
});
|
|
2745
2852
|
}
|
|
@@ -12313,6 +12420,23 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
|
|
|
12313
12420
|
* even if the current client instance is idle/disconnected.
|
|
12314
12421
|
*/
|
|
12315
12422
|
static streamingRegistry = /* @__PURE__ */ new Map();
|
|
12423
|
+
/**
|
|
12424
|
+
* Per-device set of live BaichuanClient instances.
|
|
12425
|
+
*
|
|
12426
|
+
* Why: when a streaming client unsubscribes (e.g. RTSP grace timer expires
|
|
12427
|
+
* and SocketPool tears the streaming socket down), the global streaming
|
|
12428
|
+
* registry decrements but the GENERAL client of the same device has no
|
|
12429
|
+
* way of knowing — its idle-disconnect timer was last evaluated while
|
|
12430
|
+
* `isDeviceStreamingActive()` was still true (because the streaming socket
|
|
12431
|
+
* was still alive) and wasn't rescheduled. Without this registry the
|
|
12432
|
+
* general socket stays connected, the 60-second session-guard timer keeps
|
|
12433
|
+
* sending getOnlineUserList() to the camera, and a battery camera ends up
|
|
12434
|
+
* waking up every minute (issue #18).
|
|
12435
|
+
*
|
|
12436
|
+
* On streamingRegistry decrement-to-zero we walk this set and kick every
|
|
12437
|
+
* sibling's idle-disconnect timer so it can re-evaluate eligibility.
|
|
12438
|
+
*/
|
|
12439
|
+
static deviceClients = /* @__PURE__ */ new Map();
|
|
12316
12440
|
/**
|
|
12317
12441
|
* Per-host D2C_DISC backoff state that persists across client instance recreation.
|
|
12318
12442
|
*
|
|
@@ -12427,6 +12551,29 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
|
|
|
12427
12551
|
// AlarmEventList (cmdId=33) can be very chatty (often sent every second).
|
|
12428
12552
|
// Track last per-channel alarm state so we only emit on transitions.
|
|
12429
12553
|
alarmEventState = /* @__PURE__ */ new Map();
|
|
12554
|
+
/** Whether this instance is currently in BaichuanClient.deviceClients. */
|
|
12555
|
+
registeredInDeviceClients = false;
|
|
12556
|
+
registerInDeviceClients() {
|
|
12557
|
+
if (this.registeredInDeviceClients) return;
|
|
12558
|
+
const key = this.getDeviceRegistryKey();
|
|
12559
|
+
let set = _BaichuanClient.deviceClients.get(key);
|
|
12560
|
+
if (!set) {
|
|
12561
|
+
set = /* @__PURE__ */ new Set();
|
|
12562
|
+
_BaichuanClient.deviceClients.set(key, set);
|
|
12563
|
+
}
|
|
12564
|
+
set.add(this);
|
|
12565
|
+
this.registeredInDeviceClients = true;
|
|
12566
|
+
}
|
|
12567
|
+
unregisterFromDeviceClients() {
|
|
12568
|
+
if (!this.registeredInDeviceClients) return;
|
|
12569
|
+
const key = this.getDeviceRegistryKey();
|
|
12570
|
+
const set = _BaichuanClient.deviceClients.get(key);
|
|
12571
|
+
if (set) {
|
|
12572
|
+
set.delete(this);
|
|
12573
|
+
if (set.size === 0) _BaichuanClient.deviceClients.delete(key);
|
|
12574
|
+
}
|
|
12575
|
+
this.registeredInDeviceClients = false;
|
|
12576
|
+
}
|
|
12430
12577
|
constructor(options) {
|
|
12431
12578
|
super();
|
|
12432
12579
|
this.opts = options;
|
|
@@ -12441,6 +12588,7 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
|
|
|
12441
12588
|
code: err?.code
|
|
12442
12589
|
});
|
|
12443
12590
|
});
|
|
12591
|
+
this.registerInDeviceClients();
|
|
12444
12592
|
}
|
|
12445
12593
|
newSocketSessionId(transport) {
|
|
12446
12594
|
const short = (0, import_node_crypto2.randomUUID)().split("-")[0] ?? (0, import_node_crypto2.randomUUID)().slice(0, 8);
|
|
@@ -12697,6 +12845,18 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
|
|
|
12697
12845
|
activeStreamClients: nextCount
|
|
12698
12846
|
});
|
|
12699
12847
|
this.contributesToGlobalStreamingRegistry = shouldContribute;
|
|
12848
|
+
if (!shouldContribute && nextCount === 0) {
|
|
12849
|
+
const siblings = _BaichuanClient.deviceClients.get(key);
|
|
12850
|
+
if (siblings) {
|
|
12851
|
+
for (const sib of siblings) {
|
|
12852
|
+
if (sib === this) continue;
|
|
12853
|
+
try {
|
|
12854
|
+
sib.kickIdleDisconnectTimer();
|
|
12855
|
+
} catch {
|
|
12856
|
+
}
|
|
12857
|
+
}
|
|
12858
|
+
}
|
|
12859
|
+
}
|
|
12700
12860
|
}
|
|
12701
12861
|
/**
|
|
12702
12862
|
* True if the device should be considered "awake" due to active streaming.
|
|
@@ -13161,6 +13321,7 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
|
|
|
13161
13321
|
`transport=tcp host=${this.opts.host} port=${port}${sid ? ` sid=${sid}` : ""}${remote ? ` remote=${remote}` : ""}${peer ? ` peer=${peer}` : ""}`
|
|
13162
13322
|
);
|
|
13163
13323
|
this.logSocketState("tcp_connected");
|
|
13324
|
+
this.registerInDeviceClients();
|
|
13164
13325
|
this.startKeepAlive();
|
|
13165
13326
|
this.kickIdleDisconnectTimer();
|
|
13166
13327
|
}
|
|
@@ -13477,6 +13638,7 @@ var BaichuanClient = class _BaichuanClient extends import_node_events4.EventEmit
|
|
|
13477
13638
|
this.logDebug("udp_close_error", e);
|
|
13478
13639
|
}
|
|
13479
13640
|
}
|
|
13641
|
+
this.unregisterFromDeviceClients();
|
|
13480
13642
|
}
|
|
13481
13643
|
handleFrame(frame) {
|
|
13482
13644
|
const now = Date.now();
|
|
@@ -15804,6 +15966,32 @@ var parseAbilityInfoXml = (xml) => {
|
|
|
15804
15966
|
return abilities;
|
|
15805
15967
|
};
|
|
15806
15968
|
|
|
15969
|
+
// src/reolink/baichuan/utils/versionInfo.ts
|
|
15970
|
+
init_xml();
|
|
15971
|
+
function parseVersionInfo(xml) {
|
|
15972
|
+
const out = {};
|
|
15973
|
+
const set = (key) => {
|
|
15974
|
+
const v = getXmlText(xml, key);
|
|
15975
|
+
if (v !== void 0) out[key] = v;
|
|
15976
|
+
};
|
|
15977
|
+
set("name");
|
|
15978
|
+
set("type");
|
|
15979
|
+
set("serialNumber");
|
|
15980
|
+
set("buildDay");
|
|
15981
|
+
set("hardwareVersion");
|
|
15982
|
+
set("cfgVersion");
|
|
15983
|
+
set("firmwareVersion");
|
|
15984
|
+
set("detail");
|
|
15985
|
+
set("IEClient");
|
|
15986
|
+
set("cc3200Version");
|
|
15987
|
+
set("spVersion");
|
|
15988
|
+
set("pakSuffix");
|
|
15989
|
+
set("itemNo");
|
|
15990
|
+
set("aiVersion");
|
|
15991
|
+
set("helpVersion");
|
|
15992
|
+
return out;
|
|
15993
|
+
}
|
|
15994
|
+
|
|
15807
15995
|
// src/reolink/baichuan/utils/aiState.ts
|
|
15808
15996
|
init_constants();
|
|
15809
15997
|
init_xml();
|
|
@@ -16110,6 +16298,204 @@ var buildChannelPushDataLogSnapshot = (channelPushData) => {
|
|
|
16110
16298
|
return { result: resultObj, storedChannels: Object.keys(resultObj) };
|
|
16111
16299
|
};
|
|
16112
16300
|
|
|
16301
|
+
// src/reolink/baichuan/utils/detection.ts
|
|
16302
|
+
var lz4 = __toESM(require("lz4js"), 1);
|
|
16303
|
+
|
|
16304
|
+
// src/reolink/baichuan/utils/aiClassMap.ts
|
|
16305
|
+
function type1ToLabel(type1) {
|
|
16306
|
+
switch (type1) {
|
|
16307
|
+
case 1:
|
|
16308
|
+
return "people";
|
|
16309
|
+
case 2:
|
|
16310
|
+
return "vehicle";
|
|
16311
|
+
case 3:
|
|
16312
|
+
return "animal";
|
|
16313
|
+
case 11259375:
|
|
16314
|
+
return "face";
|
|
16315
|
+
default:
|
|
16316
|
+
return "unknown";
|
|
16317
|
+
}
|
|
16318
|
+
}
|
|
16319
|
+
|
|
16320
|
+
// src/reolink/baichuan/utils/detection.ts
|
|
16321
|
+
var MARKER_LENGTH = 8;
|
|
16322
|
+
var IFRAME_PREFIX_LENGTH = 8;
|
|
16323
|
+
var COUNTER_OFFSET = 8;
|
|
16324
|
+
var BASELINE_SIZE = 128;
|
|
16325
|
+
var FRAME_SIZE_TLV = Buffer.from([3, 4, 0]);
|
|
16326
|
+
var LZ4F_MAGIC = Buffer.from([4, 34, 77, 24]);
|
|
16327
|
+
var CONFIDENCE_DIVISOR = 100;
|
|
16328
|
+
var DEFAULT_AI_FRAME_WIDTH = 896;
|
|
16329
|
+
var DEFAULT_AI_FRAME_HEIGHT = 480;
|
|
16330
|
+
var LZ4_DECOMPRESS_MAX = 256 * 1024;
|
|
16331
|
+
function walkBoxes(buf, start, end, type1, type2, out) {
|
|
16332
|
+
let pos = start;
|
|
16333
|
+
while (pos + 3 <= end) {
|
|
16334
|
+
const t = buf[pos];
|
|
16335
|
+
if (t === 0) return;
|
|
16336
|
+
const length = buf[pos + 1] | buf[pos + 2] << 8;
|
|
16337
|
+
const recordEnd = pos + 3 + length;
|
|
16338
|
+
if (recordEnd > end) return;
|
|
16339
|
+
const isBoxType4 = t === 4 && (length === 10 || length === 13 || length === 14);
|
|
16340
|
+
const isBoxType2 = t === 2 && length === 10;
|
|
16341
|
+
if ((isBoxType4 || isBoxType2) && type1 !== 0 && type2 !== 0) {
|
|
16342
|
+
const x1 = buf.readUInt16LE(pos + 3);
|
|
16343
|
+
const y1 = buf.readUInt16LE(pos + 5);
|
|
16344
|
+
const x2 = buf.readUInt16LE(pos + 7);
|
|
16345
|
+
const y2 = buf.readUInt16LE(pos + 9);
|
|
16346
|
+
const conf = buf.readUInt16LE(pos + 11);
|
|
16347
|
+
if (x2 > x1 && y2 > y1) {
|
|
16348
|
+
out.push({ x1, y1, x2, y2, conf, label: type1ToLabel(type1) });
|
|
16349
|
+
}
|
|
16350
|
+
pos = recordEnd;
|
|
16351
|
+
continue;
|
|
16352
|
+
}
|
|
16353
|
+
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]) {
|
|
16354
|
+
try {
|
|
16355
|
+
const decompressed = lz4.decompress(
|
|
16356
|
+
buf.subarray(pos + 3, recordEnd),
|
|
16357
|
+
LZ4_DECOMPRESS_MAX
|
|
16358
|
+
);
|
|
16359
|
+
const decBuf = Buffer.from(decompressed);
|
|
16360
|
+
walkBoxes(decBuf, 0, decBuf.length, 0, 0, out);
|
|
16361
|
+
} catch {
|
|
16362
|
+
}
|
|
16363
|
+
pos = recordEnd;
|
|
16364
|
+
continue;
|
|
16365
|
+
}
|
|
16366
|
+
if (length > 0) {
|
|
16367
|
+
let nextT1 = type1;
|
|
16368
|
+
let nextT2 = type2;
|
|
16369
|
+
if (type1 === 0) nextT1 = t;
|
|
16370
|
+
else if (type2 === 0) nextT2 = t;
|
|
16371
|
+
walkBoxes(buf, pos + 3, recordEnd, nextT1, nextT2, out);
|
|
16372
|
+
}
|
|
16373
|
+
pos = recordEnd;
|
|
16374
|
+
}
|
|
16375
|
+
}
|
|
16376
|
+
function decodeDetectionHeader(raw, frameType) {
|
|
16377
|
+
const markerOffset = frameType === "Iframe" ? IFRAME_PREFIX_LENGTH : 0;
|
|
16378
|
+
const blockLength = raw.length - markerOffset;
|
|
16379
|
+
const empty = {
|
|
16380
|
+
state: "invalid-marker",
|
|
16381
|
+
markerOffset,
|
|
16382
|
+
blockLength,
|
|
16383
|
+
boxes: []
|
|
16384
|
+
};
|
|
16385
|
+
if (blockLength < MARKER_LENGTH) return empty;
|
|
16386
|
+
if (!hasStandardMarker(raw, markerOffset)) return empty;
|
|
16387
|
+
if (blockLength < COUNTER_OFFSET + 4) return empty;
|
|
16388
|
+
const counter = raw.readUInt32LE(markerOffset + COUNTER_OFFSET);
|
|
16389
|
+
const rawBoxes = [];
|
|
16390
|
+
walkBoxes(raw, markerOffset, raw.length, 0, 0, rawBoxes);
|
|
16391
|
+
let aiFrameWidth = DEFAULT_AI_FRAME_WIDTH;
|
|
16392
|
+
let aiFrameHeight = DEFAULT_AI_FRAME_HEIGHT;
|
|
16393
|
+
let frameSizeFound = false;
|
|
16394
|
+
const searchStart = markerOffset + MARKER_LENGTH;
|
|
16395
|
+
for (let i = searchStart; i + 7 <= raw.length; i++) {
|
|
16396
|
+
if (raw[i] === FRAME_SIZE_TLV[0] && raw[i + 1] === FRAME_SIZE_TLV[1] && raw[i + 2] === FRAME_SIZE_TLV[2]) {
|
|
16397
|
+
const w = raw.readUInt16LE(i + 3);
|
|
16398
|
+
const h = raw.readUInt16LE(i + 5);
|
|
16399
|
+
if (w >= 64 && w <= 8192 && h >= 64 && h <= 8192) {
|
|
16400
|
+
aiFrameWidth = w;
|
|
16401
|
+
aiFrameHeight = h;
|
|
16402
|
+
frameSizeFound = true;
|
|
16403
|
+
break;
|
|
16404
|
+
}
|
|
16405
|
+
}
|
|
16406
|
+
}
|
|
16407
|
+
const specificity = {
|
|
16408
|
+
face: 4,
|
|
16409
|
+
animal: 3,
|
|
16410
|
+
people: 2,
|
|
16411
|
+
vehicle: 1,
|
|
16412
|
+
unknown: 0
|
|
16413
|
+
};
|
|
16414
|
+
const dedup = /* @__PURE__ */ new Map();
|
|
16415
|
+
for (const rb of rawBoxes) {
|
|
16416
|
+
if (rb.x2 > aiFrameWidth || rb.y2 > aiFrameHeight) continue;
|
|
16417
|
+
const key = `${rb.x1}_${rb.y1}_${rb.x2}_${rb.y2}`;
|
|
16418
|
+
const prev = dedup.get(key);
|
|
16419
|
+
if (!prev || (specificity[rb.label] ?? 0) > (specificity[prev.label] ?? 0)) {
|
|
16420
|
+
dedup.set(key, rb);
|
|
16421
|
+
}
|
|
16422
|
+
}
|
|
16423
|
+
const boxes = [];
|
|
16424
|
+
for (const rb of dedup.values()) {
|
|
16425
|
+
boxes.push({
|
|
16426
|
+
x: rb.x1 / aiFrameWidth,
|
|
16427
|
+
y: rb.y1 / aiFrameHeight,
|
|
16428
|
+
width: (rb.x2 - rb.x1) / aiFrameWidth,
|
|
16429
|
+
height: (rb.y2 - rb.y1) / aiFrameHeight,
|
|
16430
|
+
...rb.conf > 0 && rb.conf <= 100 ? { confidence: rb.conf / CONFIDENCE_DIVISOR } : {},
|
|
16431
|
+
...rb.label !== "unknown" ? { label: rb.label } : {}
|
|
16432
|
+
});
|
|
16433
|
+
}
|
|
16434
|
+
let state;
|
|
16435
|
+
if (boxes.length > 0) state = "overlay-decoded";
|
|
16436
|
+
else if (blockLength === BASELINE_SIZE) state = "no-overlay";
|
|
16437
|
+
else state = "overlay-undecoded";
|
|
16438
|
+
return {
|
|
16439
|
+
state,
|
|
16440
|
+
markerOffset,
|
|
16441
|
+
blockLength,
|
|
16442
|
+
counter,
|
|
16443
|
+
...frameSizeFound ? { aiFrameWidth, aiFrameHeight } : {},
|
|
16444
|
+
boxes
|
|
16445
|
+
};
|
|
16446
|
+
}
|
|
16447
|
+
function hasStandardMarker(raw, offset) {
|
|
16448
|
+
if (raw.length < offset + MARKER_LENGTH) return false;
|
|
16449
|
+
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;
|
|
16450
|
+
}
|
|
16451
|
+
|
|
16452
|
+
// src/reolink/baichuan/utils/encOptions.ts
|
|
16453
|
+
function buildEncOptions(list, channel) {
|
|
16454
|
+
const result = { channel };
|
|
16455
|
+
const main2 = aggregateByType(list, "mainStream");
|
|
16456
|
+
const sub = aggregateByType(list, "subStream");
|
|
16457
|
+
const third = aggregateByType(list, "thirdStream");
|
|
16458
|
+
if (main2) result.mainStream = main2;
|
|
16459
|
+
if (sub) result.subStream = sub;
|
|
16460
|
+
if (third) result.thirdStream = third;
|
|
16461
|
+
return result;
|
|
16462
|
+
}
|
|
16463
|
+
function aggregateByType(list, type) {
|
|
16464
|
+
const seen = /* @__PURE__ */ new Map();
|
|
16465
|
+
for (const stream of list.streams) {
|
|
16466
|
+
for (const eb of stream.encodeTables) {
|
|
16467
|
+
if (eb.type !== type) continue;
|
|
16468
|
+
const mapped = mapEncodeTable(eb);
|
|
16469
|
+
if (!mapped) continue;
|
|
16470
|
+
const key = `${mapped.width}x${mapped.height}`;
|
|
16471
|
+
if (!seen.has(key)) seen.set(key, mapped);
|
|
16472
|
+
}
|
|
16473
|
+
}
|
|
16474
|
+
if (seen.size === 0) return void 0;
|
|
16475
|
+
return {
|
|
16476
|
+
type,
|
|
16477
|
+
resolutions: [...seen.values()],
|
|
16478
|
+
encoderTypes: ["vbr", "cbr"],
|
|
16479
|
+
encoderProfiles: ["high", "main", "baseline"]
|
|
16480
|
+
};
|
|
16481
|
+
}
|
|
16482
|
+
function mapEncodeTable(eb) {
|
|
16483
|
+
if (eb.width == null || eb.height == null) return void 0;
|
|
16484
|
+
const videoEncTypes = (eb.videoEncTypeList ?? (eb.videoEncType != null ? [eb.videoEncType] : [])).map(
|
|
16485
|
+
(t) => t === 0 ? "h264" : t === 1 ? "h265" : void 0
|
|
16486
|
+
).filter((t) => t !== void 0);
|
|
16487
|
+
return {
|
|
16488
|
+
width: eb.width,
|
|
16489
|
+
height: eb.height,
|
|
16490
|
+
videoEncTypes,
|
|
16491
|
+
...eb.defaultFramerate != null ? { defaultFramerate: eb.defaultFramerate } : {},
|
|
16492
|
+
...eb.defaultBitrate != null ? { defaultBitrate: eb.defaultBitrate } : {},
|
|
16493
|
+
...eb.defaultGop != null ? { defaultGop: eb.defaultGop } : {},
|
|
16494
|
+
framerateOptions: eb.framerateTable ?? [],
|
|
16495
|
+
bitrateOptions: eb.bitrateTable ?? []
|
|
16496
|
+
};
|
|
16497
|
+
}
|
|
16498
|
+
|
|
16113
16499
|
// src/reolink/baichuan/utils/events.ts
|
|
16114
16500
|
var mapToSimpleEvent = (event) => {
|
|
16115
16501
|
const timestamp = event.timestamp ?? Date.now();
|
|
@@ -18233,6 +18619,21 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18233
18619
|
sessionGuardIntervalTimer;
|
|
18234
18620
|
simpleEventListeners = /* @__PURE__ */ new Set();
|
|
18235
18621
|
simpleEventSubscribed = false;
|
|
18622
|
+
// Detection events are sourced from BcMedia additionalHeader on active video
|
|
18623
|
+
// streams. Unlike simpleEvent, no Baichuan subscribe command is needed — the
|
|
18624
|
+
// data flows whenever a stream is open. Active streams register themselves via
|
|
18625
|
+
// _registerVideoStreamForDetection (called from BaichuanVideoStream.start).
|
|
18626
|
+
detectionEventListeners = /* @__PURE__ */ new Set();
|
|
18627
|
+
detectionEventStreamHooks = /* @__PURE__ */ new Map();
|
|
18628
|
+
// Auto-managed substream for `onObjectDetections` listeners. Reference-counted
|
|
18629
|
+
// by the listener set: the substream is opened on the first listener and torn
|
|
18630
|
+
// down with the last one. Mirrors `onSimpleEvent`'s subscribe/unsubscribe
|
|
18631
|
+
// lifecycle so a caller never has to manage a video stream just to read AI
|
|
18632
|
+
// detections.
|
|
18633
|
+
objectDetectionListeners = /* @__PURE__ */ new Set();
|
|
18634
|
+
objectDetectionStream;
|
|
18635
|
+
objectDetectionStreamStartInFlight;
|
|
18636
|
+
objectDetectionInternalListener;
|
|
18236
18637
|
simpleEventSubscribeInFlight;
|
|
18237
18638
|
simpleEventUnsubscribeInFlight;
|
|
18238
18639
|
simpleEventResubscribeTimer;
|
|
@@ -19668,6 +20069,205 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
19668
20069
|
}
|
|
19669
20070
|
}
|
|
19670
20071
|
}
|
|
20072
|
+
/**
|
|
20073
|
+
* Subscribe to per-frame detection events sourced from the BcMedia
|
|
20074
|
+
* `additionalHeader` block on active video streams.
|
|
20075
|
+
*
|
|
20076
|
+
* Mirrors {@link onSimpleEvent} but is fed by the streaming side-channel:
|
|
20077
|
+
* one event fires for every I-frame / P-frame that carries an overlay block.
|
|
20078
|
+
* Coordinates are reported in normalized [0, 1] fractions of the source
|
|
20079
|
+
* frame, so the same box renders correctly on mainStream, subStream, and
|
|
20080
|
+
* externStream.
|
|
20081
|
+
*
|
|
20082
|
+
* Unlike `onSimpleEvent`, no Baichuan subscribe command is involved — events
|
|
20083
|
+
* only flow while a video stream is open. The library hooks every
|
|
20084
|
+
* `BaichuanVideoStream` created via this API for the listener's lifetime.
|
|
20085
|
+
*/
|
|
20086
|
+
onDetection(callback) {
|
|
20087
|
+
this.detectionEventListeners.add(callback);
|
|
20088
|
+
}
|
|
20089
|
+
/**
|
|
20090
|
+
* Remove a single detection callback, or all of them if `callback` is omitted.
|
|
20091
|
+
*/
|
|
20092
|
+
offDetection(callback) {
|
|
20093
|
+
if (callback) {
|
|
20094
|
+
this.detectionEventListeners.delete(callback);
|
|
20095
|
+
} else {
|
|
20096
|
+
this.detectionEventListeners.clear();
|
|
20097
|
+
}
|
|
20098
|
+
}
|
|
20099
|
+
/**
|
|
20100
|
+
* Subscribe to AI object detections (people / vehicle / animal / face boxes
|
|
20101
|
+
* with class label and confidence) without managing a video stream yourself.
|
|
20102
|
+
*
|
|
20103
|
+
* Mirrors {@link onSimpleEvent} end-to-end: the API opens a dedicated
|
|
20104
|
+
* substream behind the scenes on the first listener, forwards every box-bearing
|
|
20105
|
+
* `additionalHeader` to your callback, and tears the stream down when the last
|
|
20106
|
+
* listener unsubscribes. The substream is the lightest profile (typically
|
|
20107
|
+
* 640×360) so the additional bandwidth/CPU overhead is minimal.
|
|
20108
|
+
*
|
|
20109
|
+
* Each event carries normalized `[0, 1]` box coordinates, a class label, and
|
|
20110
|
+
* a confidence score — render-ready without further conversion.
|
|
20111
|
+
*/
|
|
20112
|
+
async onObjectDetections(callback) {
|
|
20113
|
+
this.objectDetectionListeners.add(callback);
|
|
20114
|
+
this.logger.debug?.(
|
|
20115
|
+
`[ReolinkBaichuanApi] onObjectDetections: registering listener (total=${this.objectDetectionListeners.size})`
|
|
20116
|
+
);
|
|
20117
|
+
await this.ensureObjectDetectionStream();
|
|
20118
|
+
}
|
|
20119
|
+
/**
|
|
20120
|
+
* Remove one detection callback, or all of them if `callback` is omitted.
|
|
20121
|
+
* When the last listener is removed the auto-managed substream is closed.
|
|
20122
|
+
*/
|
|
20123
|
+
async offObjectDetections(callback) {
|
|
20124
|
+
if (callback) {
|
|
20125
|
+
this.objectDetectionListeners.delete(callback);
|
|
20126
|
+
} else {
|
|
20127
|
+
this.objectDetectionListeners.clear();
|
|
20128
|
+
}
|
|
20129
|
+
if (this.objectDetectionListeners.size === 0) {
|
|
20130
|
+
await this.tearDownObjectDetectionStream();
|
|
20131
|
+
}
|
|
20132
|
+
}
|
|
20133
|
+
async ensureObjectDetectionStream() {
|
|
20134
|
+
if (this.objectDetectionStream) return;
|
|
20135
|
+
if (this.objectDetectionStreamStartInFlight) {
|
|
20136
|
+
await this.objectDetectionStreamStartInFlight;
|
|
20137
|
+
return;
|
|
20138
|
+
}
|
|
20139
|
+
this.objectDetectionStreamStartInFlight = (async () => {
|
|
20140
|
+
const { BaichuanVideoStream: BaichuanVideoStream2 } = await Promise.resolve().then(() => (init_BaichuanVideoStream(), BaichuanVideoStream_exports));
|
|
20141
|
+
const sessionKey = `live:object-detections:ch0:sub`;
|
|
20142
|
+
const dedicated = await this.createDedicatedSession(sessionKey);
|
|
20143
|
+
const stream = new BaichuanVideoStream2({
|
|
20144
|
+
client: dedicated.client,
|
|
20145
|
+
api: this,
|
|
20146
|
+
channel: 0,
|
|
20147
|
+
profile: "sub",
|
|
20148
|
+
logger: this.logger
|
|
20149
|
+
});
|
|
20150
|
+
this.objectDetectionInternalListener = (event) => {
|
|
20151
|
+
for (const cb of this.objectDetectionListeners) {
|
|
20152
|
+
try {
|
|
20153
|
+
void Promise.resolve(cb(event)).catch((e) => {
|
|
20154
|
+
(this.logger.warn ?? this.logger.error).call(
|
|
20155
|
+
this.logger,
|
|
20156
|
+
"[ReolinkBaichuanApi] onObjectDetections handler error",
|
|
20157
|
+
formatErrorForLog(e)
|
|
20158
|
+
);
|
|
20159
|
+
});
|
|
20160
|
+
} catch (e) {
|
|
20161
|
+
(this.logger.warn ?? this.logger.error).call(
|
|
20162
|
+
this.logger,
|
|
20163
|
+
"[ReolinkBaichuanApi] onObjectDetections handler error",
|
|
20164
|
+
formatErrorForLog(e)
|
|
20165
|
+
);
|
|
20166
|
+
}
|
|
20167
|
+
}
|
|
20168
|
+
};
|
|
20169
|
+
this.detectionEventListeners.add(this.objectDetectionInternalListener);
|
|
20170
|
+
try {
|
|
20171
|
+
await stream.start();
|
|
20172
|
+
} catch (e) {
|
|
20173
|
+
if (this.objectDetectionInternalListener) {
|
|
20174
|
+
this.detectionEventListeners.delete(
|
|
20175
|
+
this.objectDetectionInternalListener
|
|
20176
|
+
);
|
|
20177
|
+
this.objectDetectionInternalListener = void 0;
|
|
20178
|
+
}
|
|
20179
|
+
await dedicated.release().catch(() => {
|
|
20180
|
+
});
|
|
20181
|
+
throw e;
|
|
20182
|
+
}
|
|
20183
|
+
this.objectDetectionStream = {
|
|
20184
|
+
stop: () => stream.stop(),
|
|
20185
|
+
release: () => dedicated.release()
|
|
20186
|
+
};
|
|
20187
|
+
this.logger.debug?.(
|
|
20188
|
+
`[ReolinkBaichuanApi] onObjectDetections: substream started (key=${sessionKey})`
|
|
20189
|
+
);
|
|
20190
|
+
})();
|
|
20191
|
+
try {
|
|
20192
|
+
await this.objectDetectionStreamStartInFlight;
|
|
20193
|
+
} finally {
|
|
20194
|
+
this.objectDetectionStreamStartInFlight = void 0;
|
|
20195
|
+
}
|
|
20196
|
+
}
|
|
20197
|
+
async tearDownObjectDetectionStream() {
|
|
20198
|
+
const handle = this.objectDetectionStream;
|
|
20199
|
+
this.objectDetectionStream = void 0;
|
|
20200
|
+
if (this.objectDetectionInternalListener) {
|
|
20201
|
+
this.detectionEventListeners.delete(this.objectDetectionInternalListener);
|
|
20202
|
+
this.objectDetectionInternalListener = void 0;
|
|
20203
|
+
}
|
|
20204
|
+
if (!handle) return;
|
|
20205
|
+
try {
|
|
20206
|
+
await handle.stop();
|
|
20207
|
+
} catch (e) {
|
|
20208
|
+
this.logger.debug?.(
|
|
20209
|
+
`[ReolinkBaichuanApi] onObjectDetections: stream stop error: ${formatErrorForLog(e)}`
|
|
20210
|
+
);
|
|
20211
|
+
}
|
|
20212
|
+
try {
|
|
20213
|
+
await handle.release();
|
|
20214
|
+
} catch (e) {
|
|
20215
|
+
this.logger.debug?.(
|
|
20216
|
+
`[ReolinkBaichuanApi] onObjectDetections: session release error: ${formatErrorForLog(e)}`
|
|
20217
|
+
);
|
|
20218
|
+
}
|
|
20219
|
+
this.logger.debug?.(
|
|
20220
|
+
`[ReolinkBaichuanApi] onObjectDetections: substream torn down`
|
|
20221
|
+
);
|
|
20222
|
+
}
|
|
20223
|
+
/**
|
|
20224
|
+
* Internal: invoked by BaichuanVideoStream when it starts so the API can hook
|
|
20225
|
+
* its `additionalHeader` event. Returns a teardown function the stream calls
|
|
20226
|
+
* on stop. Not intended for direct use by consumers.
|
|
20227
|
+
*/
|
|
20228
|
+
_registerVideoStreamForDetection(stream, context) {
|
|
20229
|
+
const listener = (info) => {
|
|
20230
|
+
if (this.detectionEventListeners.size === 0) return;
|
|
20231
|
+
const decoded = decodeDetectionHeader(info.raw, info.frameType);
|
|
20232
|
+
const event = {
|
|
20233
|
+
channel: context.channel,
|
|
20234
|
+
microseconds: info.microseconds,
|
|
20235
|
+
profile: context.profile,
|
|
20236
|
+
boxes: decoded.boxes,
|
|
20237
|
+
...info.frameWidth !== void 0 ? { frameWidth: info.frameWidth } : {},
|
|
20238
|
+
...info.frameHeight !== void 0 ? { frameHeight: info.frameHeight } : {},
|
|
20239
|
+
decodeState: decoded.state,
|
|
20240
|
+
rawHeader: info.raw
|
|
20241
|
+
};
|
|
20242
|
+
this.dispatchDetectionEvent(event);
|
|
20243
|
+
};
|
|
20244
|
+
stream.on("additionalHeader", listener);
|
|
20245
|
+
const teardown = () => {
|
|
20246
|
+
stream.off("additionalHeader", listener);
|
|
20247
|
+
this.detectionEventStreamHooks.delete(stream);
|
|
20248
|
+
};
|
|
20249
|
+
this.detectionEventStreamHooks.set(stream, teardown);
|
|
20250
|
+
return teardown;
|
|
20251
|
+
}
|
|
20252
|
+
dispatchDetectionEvent(evt) {
|
|
20253
|
+
for (const cb of this.detectionEventListeners) {
|
|
20254
|
+
try {
|
|
20255
|
+
void Promise.resolve(cb(evt)).catch((e) => {
|
|
20256
|
+
(this.logger.warn ?? this.logger.error).call(
|
|
20257
|
+
this.logger,
|
|
20258
|
+
"[ReolinkBaichuanApi] onDetection handler error",
|
|
20259
|
+
formatErrorForLog(e)
|
|
20260
|
+
);
|
|
20261
|
+
});
|
|
20262
|
+
} catch (e) {
|
|
20263
|
+
(this.logger.warn ?? this.logger.error).call(
|
|
20264
|
+
this.logger,
|
|
20265
|
+
"[ReolinkBaichuanApi] onDetection handler error",
|
|
20266
|
+
formatErrorForLog(e)
|
|
20267
|
+
);
|
|
20268
|
+
}
|
|
20269
|
+
}
|
|
20270
|
+
}
|
|
19671
20271
|
startSimpleEventResubscribeTimer() {
|
|
19672
20272
|
if (this.simpleEventResubscribeTimer) return;
|
|
19673
20273
|
if (this.simpleEventListeners.size === 0) return;
|
|
@@ -20050,6 +20650,9 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
20050
20650
|
this.stopUdpSleepInference();
|
|
20051
20651
|
this.stopSimpleEventWatchdog();
|
|
20052
20652
|
this.stopSimpleEventResubscribeTimer();
|
|
20653
|
+
this.objectDetectionListeners.clear();
|
|
20654
|
+
await this.tearDownObjectDetectionStream().catch(() => {
|
|
20655
|
+
});
|
|
20053
20656
|
await this.cleanup();
|
|
20054
20657
|
await this.stopAllActiveStreams();
|
|
20055
20658
|
await this.cleanupSocketPool();
|
|
@@ -20397,6 +21000,53 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
20397
21000
|
const xml = `<?xml version="1.0" encoding="UTF-8" ?><body><${tag} version="1.1"><enable>${params.enable ? 1 : 0}</enable></${tag}></body>`;
|
|
20398
21001
|
await this.sendXml({ cmdId: 36, payloadXml: xml });
|
|
20399
21002
|
}
|
|
21003
|
+
/**
|
|
21004
|
+
* Full port-config setter (cmd_id 36). Patches one or more of the six
|
|
21005
|
+
* service ports the camera serves — Server (Baichuan), HTTP, HTTPS,
|
|
21006
|
+
* RTSP, RTMP, ONVIF. Each entry takes an optional `port` (number) and
|
|
21007
|
+
* `enable` (boolean); fields the caller doesn't pass are left alone.
|
|
21008
|
+
*
|
|
21009
|
+
* Sends one block per port that has any field set, then issues a
|
|
21010
|
+
* single cmd_36 with the merged body. The camera accepts multiple
|
|
21011
|
+
* `<XxxPort>` siblings in the same payload.
|
|
21012
|
+
*
|
|
21013
|
+
* Wire format observed on E1 Zoom:
|
|
21014
|
+
*
|
|
21015
|
+
* <body>
|
|
21016
|
+
* <RtspPort version="1.1">
|
|
21017
|
+
* <rtspPort>554</rtspPort>
|
|
21018
|
+
* <enable>1</enable>
|
|
21019
|
+
* </RtspPort>
|
|
21020
|
+
* <HttpsPort version="1.1">
|
|
21021
|
+
* <enable>0</enable>
|
|
21022
|
+
* </HttpsPort>
|
|
21023
|
+
* ...
|
|
21024
|
+
* </body>
|
|
21025
|
+
*/
|
|
21026
|
+
async setPortConfig(patch) {
|
|
21027
|
+
const blocks = [];
|
|
21028
|
+
const append = (tag, portField, cfg) => {
|
|
21029
|
+
if (!cfg) return;
|
|
21030
|
+
if (cfg.port === void 0 && cfg.enable === void 0) return;
|
|
21031
|
+
const inner = [];
|
|
21032
|
+
if (cfg.port !== void 0) {
|
|
21033
|
+
inner.push(`<${portField}>${cfg.port}</${portField}>`);
|
|
21034
|
+
}
|
|
21035
|
+
if (cfg.enable !== void 0) {
|
|
21036
|
+
inner.push(`<enable>${cfg.enable ? 1 : 0}</enable>`);
|
|
21037
|
+
}
|
|
21038
|
+
blocks.push(`<${tag} version="1.1">${inner.join("")}</${tag}>`);
|
|
21039
|
+
};
|
|
21040
|
+
append("ServerPort", "serverPort", patch.server);
|
|
21041
|
+
append("HttpPort", "httpPort", patch.http);
|
|
21042
|
+
append("HttpsPort", "httpsPort", patch.https);
|
|
21043
|
+
append("RtspPort", "rtspPort", patch.rtsp);
|
|
21044
|
+
append("RtmpPort", "rtmpPort", patch.rtmp);
|
|
21045
|
+
append("OnvifPort", "onvifPort", patch.onvif);
|
|
21046
|
+
if (blocks.length === 0) return;
|
|
21047
|
+
const xml = `<?xml version="1.0" encoding="UTF-8" ?><body>${blocks.join("")}</body>`;
|
|
21048
|
+
await this.sendXml({ cmdId: 36, payloadXml: xml });
|
|
21049
|
+
}
|
|
20400
21050
|
/** GetDevInfo via Baichuan: host cmd_id 80, channel cmd_id 318 */
|
|
20401
21051
|
async getInfo(channel, options) {
|
|
20402
21052
|
const req = { cmdId: channel == null ? 80 : 318 };
|
|
@@ -24081,6 +24731,27 @@ ${stderr}`)
|
|
|
24081
24731
|
);
|
|
24082
24732
|
}
|
|
24083
24733
|
}
|
|
24734
|
+
async gotoPtzPreset(arg1, arg2) {
|
|
24735
|
+
const ch = arg2 === void 0 ? this.normalizeChannel(void 0) : this.normalizeChannel(arg1);
|
|
24736
|
+
const presetId = arg2 === void 0 ? arg1 : arg2;
|
|
24737
|
+
const channelId = ch;
|
|
24738
|
+
const payloadXml = buildPtzPresetXmlV2(channelId, presetId, "toPos");
|
|
24739
|
+
const extensionXml = buildChannelExtensionXml(channelId);
|
|
24740
|
+
const frame = await this.client.sendFrame({
|
|
24741
|
+
cmdId: BC_CMD_ID_PTZ_CONTROL_PRESET,
|
|
24742
|
+
channel: ch,
|
|
24743
|
+
channelIdOverride: channelId,
|
|
24744
|
+
extensionXml,
|
|
24745
|
+
payloadXml,
|
|
24746
|
+
messageClass: BC_CLASS_MODERN_24,
|
|
24747
|
+
streamType: 0
|
|
24748
|
+
});
|
|
24749
|
+
if (frame.header.responseCode !== 200) {
|
|
24750
|
+
throw new Error(
|
|
24751
|
+
`PTZ goto preset rejected (response_code ${frame.header.responseCode})`
|
|
24752
|
+
);
|
|
24753
|
+
}
|
|
24754
|
+
}
|
|
24084
24755
|
async deletePtzPreset(arg1, arg2) {
|
|
24085
24756
|
const ch = arg2 === void 0 ? this.normalizeChannel(void 0) : this.normalizeChannel(arg1);
|
|
24086
24757
|
const presetId = arg2 === void 0 ? arg1 : arg2;
|
|
@@ -24732,22 +25403,62 @@ ${stderr}`)
|
|
|
24732
25403
|
const channel = typeof arg1 === "number" ? arg1 : arg3;
|
|
24733
25404
|
const enabled = typeof arg1 === "number" ? arg2 : arg1;
|
|
24734
25405
|
const sensitivity = typeof arg1 === "number" ? arg3 : arg2;
|
|
24735
|
-
|
|
25406
|
+
return await this.setMotionAlarmFull({
|
|
25407
|
+
...channel !== void 0 ? { channel } : {},
|
|
25408
|
+
enabled,
|
|
25409
|
+
...sensitivity !== void 0 ? { sensitivity } : {}
|
|
25410
|
+
});
|
|
25411
|
+
}
|
|
25412
|
+
/**
|
|
25413
|
+
* Set motion alarm with full control, including the detection-zone grid.
|
|
25414
|
+
*
|
|
25415
|
+
* Wire format observed on E1 Zoom (cmd_id=47 SetMdAlarm body):
|
|
25416
|
+
*
|
|
25417
|
+
* <MD version="1.1">
|
|
25418
|
+
* <channelId>0</channelId>
|
|
25419
|
+
* <enable>1</enable>
|
|
25420
|
+
* <usepir>0</usepir>
|
|
25421
|
+
* <width>60</width> <height>33</height>
|
|
25422
|
+
* <scope>
|
|
25423
|
+
* <columns>96</columns> <rows>64</rows>
|
|
25424
|
+
* <valueTable>{base64 6144-bit bitmap}</valueTable>
|
|
25425
|
+
* </scope>
|
|
25426
|
+
* ... other camera-specific fields ...
|
|
25427
|
+
* </MD>
|
|
25428
|
+
*
|
|
25429
|
+
* We do a read-modify-write of the GET response so any camera-specific
|
|
25430
|
+
* extension fields are preserved untouched. Pass `valueTable` to update
|
|
25431
|
+
* the detection zone — see `encodeMotionScopeBitmap` for the bitmap layout.
|
|
25432
|
+
*
|
|
25433
|
+
* @param channel - 0-based channel
|
|
25434
|
+
* @param enabled - toggle motion detection on/off (optional)
|
|
25435
|
+
* @param sensitivity - 0-50, higher = more sensitive (optional)
|
|
25436
|
+
* @param valueTable - base64-encoded grid bitmap; size must match
|
|
25437
|
+
* `<scope><columns>×<rows></scope>` from the GET (optional)
|
|
25438
|
+
*/
|
|
25439
|
+
async setMotionAlarmFull(opts) {
|
|
25440
|
+
const ch = this.normalizeChannel(opts.channel);
|
|
24736
25441
|
const currentXml = await this.sendXml({
|
|
24737
25442
|
cmdId: BC_CMD_ID_GET_MOTION_ALARM,
|
|
24738
25443
|
channel: ch
|
|
24739
25444
|
});
|
|
24740
25445
|
let modifiedXml = currentXml;
|
|
24741
|
-
if (enabled !== void 0) {
|
|
25446
|
+
if (opts.enabled !== void 0) {
|
|
24742
25447
|
modifiedXml = modifiedXml.replace(
|
|
24743
25448
|
/<enable>[^<]*<\/enable>/,
|
|
24744
|
-
`<enable>${enabled ? "1" : "0"}</enable>`
|
|
25449
|
+
`<enable>${opts.enabled ? "1" : "0"}</enable>`
|
|
24745
25450
|
);
|
|
24746
25451
|
}
|
|
24747
|
-
if (sensitivity !== void 0) {
|
|
25452
|
+
if (opts.sensitivity !== void 0) {
|
|
24748
25453
|
modifiedXml = modifiedXml.replace(
|
|
24749
25454
|
/<sensitivityDefault>[^<]*<\/sensitivityDefault>/,
|
|
24750
|
-
`<sensitivityDefault>${sensitivity}</sensitivityDefault>`
|
|
25455
|
+
`<sensitivityDefault>${opts.sensitivity}</sensitivityDefault>`
|
|
25456
|
+
);
|
|
25457
|
+
}
|
|
25458
|
+
if (opts.valueTable !== void 0) {
|
|
25459
|
+
modifiedXml = modifiedXml.replace(
|
|
25460
|
+
/<valueTable>[^<]*<\/valueTable>/,
|
|
25461
|
+
`<valueTable>${opts.valueTable}</valueTable>`
|
|
24751
25462
|
);
|
|
24752
25463
|
}
|
|
24753
25464
|
await this.sendXml({
|
|
@@ -26091,12 +26802,24 @@ ${xml}`
|
|
|
26091
26802
|
}
|
|
26092
26803
|
/**
|
|
26093
26804
|
* SetEnc via Baichuan (cmdId=57). Read-modify-write — preserves
|
|
26094
|
-
* unspecified fields. Mirrors reolink_aio's `SetEnc
|
|
26805
|
+
* unspecified fields. Mirrors reolink_aio's `SetEnc` plus the additional
|
|
26806
|
+
* `width`/`height`/`encoderType`/`encoderProfile`/`gop`/`thirdStream`
|
|
26807
|
+
* fields observed in the official mobile app (see `pcap/resolution.pcapng`).
|
|
26808
|
+
*
|
|
26809
|
+
* Field meaning per stream:
|
|
26810
|
+
* - `audio` — 0/1 toggle
|
|
26811
|
+
* - `width`/`height` — resolution in pixels. Must be one of the
|
|
26812
|
+
* resolutions returned by {@link getStreamInfoList}.
|
|
26813
|
+
* - `bitRate` — kbps. Must match the table from `getStreamInfoList`.
|
|
26814
|
+
* - `frameRate` — fps. Must match the table from `getStreamInfoList`.
|
|
26815
|
+
* - `videoEncType` — `"h264"` or `"h265"`
|
|
26816
|
+
* - `encoderType` — `"vbr"` or `"cbr"`
|
|
26817
|
+
* - `encoderProfile` — `"high"`, `"main"`, or `"baseline"`
|
|
26818
|
+
* - `gop` — keyframe interval in seconds (sets `<gop><cur>`)
|
|
26095
26819
|
*
|
|
26096
26820
|
* @param channel - Channel number (0-based)
|
|
26097
|
-
* @param patch - Fields to update
|
|
26098
|
-
*
|
|
26099
|
-
* to change.
|
|
26821
|
+
* @param patch - Fields to update. Pass only the fields you want to change;
|
|
26822
|
+
* everything else is preserved from the device's current configuration.
|
|
26100
26823
|
*/
|
|
26101
26824
|
async setEnc(channel, patch, options) {
|
|
26102
26825
|
const ch = this.normalizeChannel(channel);
|
|
@@ -26113,6 +26836,7 @@ ${xml}`
|
|
|
26113
26836
|
}
|
|
26114
26837
|
xml = applyStreamPatch(xml, "mainStream", patch.mainStream);
|
|
26115
26838
|
xml = applyStreamPatch(xml, "subStream", patch.subStream);
|
|
26839
|
+
xml = applyStreamPatch(xml, "thirdStream", patch.thirdStream);
|
|
26116
26840
|
await this.sendXml({
|
|
26117
26841
|
cmdId: BC_CMD_ID_SET_ENC,
|
|
26118
26842
|
channel: ch,
|
|
@@ -26720,6 +27444,71 @@ ${xml}`
|
|
|
26720
27444
|
`PCAP-derived settings GET failed for cmdId=${params.cmdId}: ${String(lastErr)}`
|
|
26721
27445
|
);
|
|
26722
27446
|
}
|
|
27447
|
+
/**
|
|
27448
|
+
* Update the OSD timestamp + channel-name overlay via cmd_id=45
|
|
27449
|
+
* (SetOsdDatetime). The schema is the same `<body><OsdDatetime>` +
|
|
27450
|
+
* `<OsdChannelName>` block returned by `getOsdDatetime` — we
|
|
27451
|
+
* read-modify-write so any extension fields the camera sent are
|
|
27452
|
+
* preserved.
|
|
27453
|
+
*
|
|
27454
|
+
* Position is in **camera pixel coordinates** (e.g. (1,1) for top-left,
|
|
27455
|
+
* not preset strings). Set `enable=0` to hide the overlay; the camera
|
|
27456
|
+
* keeps the stored position so re-enabling later restores it.
|
|
27457
|
+
*/
|
|
27458
|
+
async setOsdDatetime(channel, patch, options) {
|
|
27459
|
+
const ch = this.normalizeChannel(channel);
|
|
27460
|
+
const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
|
|
27461
|
+
let xml = await this.sendPcapDerivedSettingsGetXml({
|
|
27462
|
+
cmdId: BC_CMD_ID_GET_OSD_DATETIME,
|
|
27463
|
+
channel: ch,
|
|
27464
|
+
...timeoutOpts
|
|
27465
|
+
});
|
|
27466
|
+
const patchBlock = (block, fields) => {
|
|
27467
|
+
const start = xml.indexOf(`<${block}`);
|
|
27468
|
+
if (start < 0) return;
|
|
27469
|
+
const end = xml.indexOf(`</${block}>`, start);
|
|
27470
|
+
if (end < 0) return;
|
|
27471
|
+
let body = xml.slice(start, end);
|
|
27472
|
+
for (const [tag, value] of Object.entries(fields)) {
|
|
27473
|
+
if (value === void 0) continue;
|
|
27474
|
+
const raw = typeof value === "boolean" ? value ? "1" : "0" : String(value);
|
|
27475
|
+
const escaped = raw.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
27476
|
+
if (body.includes(`<${tag}>`)) {
|
|
27477
|
+
body = body.replace(
|
|
27478
|
+
new RegExp(`<${tag}>[^<]*<\\/${tag}>`),
|
|
27479
|
+
`<${tag}>${escaped}</${tag}>`
|
|
27480
|
+
);
|
|
27481
|
+
} else {
|
|
27482
|
+
body += `<${tag}>${escaped}</${tag}>`;
|
|
27483
|
+
}
|
|
27484
|
+
}
|
|
27485
|
+
xml = xml.slice(0, start) + body + xml.slice(end);
|
|
27486
|
+
};
|
|
27487
|
+
if (patch.datetime) {
|
|
27488
|
+
patchBlock("OsdDatetime", {
|
|
27489
|
+
enable: patch.datetime.enable,
|
|
27490
|
+
topLeftX: patch.datetime.topLeftX,
|
|
27491
|
+
topLeftY: patch.datetime.topLeftY,
|
|
27492
|
+
language: patch.datetime.language
|
|
27493
|
+
});
|
|
27494
|
+
}
|
|
27495
|
+
if (patch.channelName) {
|
|
27496
|
+
patchBlock("OsdChannelName", {
|
|
27497
|
+
name: patch.channelName.name,
|
|
27498
|
+
enable: patch.channelName.enable,
|
|
27499
|
+
topLeftX: patch.channelName.topLeftX,
|
|
27500
|
+
topLeftY: patch.channelName.topLeftY,
|
|
27501
|
+
enWatermark: patch.channelName.enWatermark,
|
|
27502
|
+
enBgcolor: patch.channelName.enBgcolor
|
|
27503
|
+
});
|
|
27504
|
+
}
|
|
27505
|
+
await this.sendXml({
|
|
27506
|
+
cmdId: BC_CMD_ID_SET_OSD_DATETIME,
|
|
27507
|
+
channel: ch,
|
|
27508
|
+
payloadXml: ensureXmlHeader(xml),
|
|
27509
|
+
...timeoutOpts
|
|
27510
|
+
});
|
|
27511
|
+
}
|
|
26723
27512
|
async getOsdDatetime(channel, options) {
|
|
26724
27513
|
const rawXml = await this.sendPcapDerivedSettingsGetXml({
|
|
26725
27514
|
cmdId: BC_CMD_ID_GET_OSD_DATETIME,
|
|
@@ -26912,6 +27701,41 @@ ${xml}`
|
|
|
26912
27701
|
});
|
|
26913
27702
|
return { streams };
|
|
26914
27703
|
}
|
|
27704
|
+
/**
|
|
27705
|
+
* Return the set of values `setEnc` will accept on each stream of `channel`.
|
|
27706
|
+
* Aggregates `getStreamInfoList` (cmd_146) into a UI-friendly shape:
|
|
27707
|
+
* per-stream resolutions with their allowed codecs/framerates/bitrates plus
|
|
27708
|
+
* the enumerated encoder modes/profiles Reolink exposes.
|
|
27709
|
+
*
|
|
27710
|
+
* Useful for populating selectors and validating user input before calling
|
|
27711
|
+
* `setEnc` — picking an unsupported combination causes the camera to reject
|
|
27712
|
+
* the SET_ENC command (responseCode != 200).
|
|
27713
|
+
*/
|
|
27714
|
+
async getEncOptions(channel, options) {
|
|
27715
|
+
const list = await this.getStreamInfoList(channel, options);
|
|
27716
|
+
return buildEncOptions(list, channel);
|
|
27717
|
+
}
|
|
27718
|
+
/**
|
|
27719
|
+
* Read the camera's `<VersionInfo>` block (cmd_id=80). Returns the
|
|
27720
|
+
* friendly name, model code (e.g. `"E1 Zoom"`), serial number, firmware
|
|
27721
|
+
* version, hardware revision, build day, AI model bundle version, etc.
|
|
27722
|
+
*
|
|
27723
|
+
* This is the same info the Reolink mobile app shows in "About this
|
|
27724
|
+
* device" — distinct from `getSystemGeneral` (cmd_104) which carries
|
|
27725
|
+
* time/locale.
|
|
27726
|
+
*
|
|
27727
|
+
* No channel parameter: this command is device-global on NVRs/Hubs and
|
|
27728
|
+
* camera-global on standalone cameras. Pass an explicit channel via the
|
|
27729
|
+
* underlying `sendXml` only if a specific firmware demands it (none we've
|
|
27730
|
+
* tested do).
|
|
27731
|
+
*/
|
|
27732
|
+
async getVersionInfo(options) {
|
|
27733
|
+
const xml = await this.sendXml({
|
|
27734
|
+
cmdId: BC_CMD_ID_GET_VERSION_INFO,
|
|
27735
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
27736
|
+
});
|
|
27737
|
+
return parseVersionInfo(xml);
|
|
27738
|
+
}
|
|
26915
27739
|
async getLedState(channel, options) {
|
|
26916
27740
|
const rawXml = await this.sendPcapDerivedSettingsGetXml({
|
|
26917
27741
|
cmdId: BC_CMD_ID_GET_LED_STATE,
|