@apocaliss92/nodelink-js 0.4.7 → 0.4.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DiagnosticsTools-UMN4C7SY.js → DiagnosticsTools-RNIDFEJK.js} +2 -2
- package/dist/{chunk-TR3V5FTO.js → chunk-EDLMKBG2.js} +226 -3
- package/dist/chunk-EDLMKBG2.js.map +1 -0
- package/dist/{chunk-GKLOJJ34.js → chunk-HGQ53FB3.js} +1023 -341
- package/dist/chunk-HGQ53FB3.js.map +1 -0
- package/dist/cli/rtsp-server.cjs +1172 -312
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +2 -2
- package/dist/index.cjs +1407 -329
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +989 -6
- package/dist/index.d.ts +1050 -5
- package/dist/index.js +232 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-GKLOJJ34.js.map +0 -1
- package/dist/chunk-TR3V5FTO.js.map +0 -1
- /package/dist/{DiagnosticsTools-UMN4C7SY.js.map → DiagnosticsTools-RNIDFEJK.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_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_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;
|
|
64
64
|
var init_constants = __esm({
|
|
65
65
|
"src/protocol/constants.ts"() {
|
|
66
66
|
"use strict";
|
|
@@ -154,6 +154,17 @@ var init_constants = __esm({
|
|
|
154
154
|
BC_CMD_ID_SET_AI_CFG = 300;
|
|
155
155
|
BC_CMD_ID_GET_SIREN_STATUS = 547;
|
|
156
156
|
BC_CMD_ID_SET_AUDIO_TASK = 231;
|
|
157
|
+
BC_CMD_ID_SET_VIDEO_INPUT = 25;
|
|
158
|
+
BC_CMD_ID_SET_DAY_NIGHT_THRESHOLD = 297;
|
|
159
|
+
BC_CMD_ID_GET_ENC = 56;
|
|
160
|
+
BC_CMD_ID_SET_ENC = 57;
|
|
161
|
+
BC_CMD_ID_GET_PRIVACY_MASK = 52;
|
|
162
|
+
BC_CMD_ID_SET_PRIVACY_MASK = 53;
|
|
163
|
+
BC_CMD_ID_SET_AI_DENOISE = 440;
|
|
164
|
+
BC_CMD_ID_SET_LED_STATE = 209;
|
|
165
|
+
BC_CMD_ID_SET_AUDIO_CFG = 265;
|
|
166
|
+
BC_CMD_ID_GET_AUTO_FOCUS = 224;
|
|
167
|
+
BC_CMD_ID_SET_AUTO_FOCUS = 225;
|
|
157
168
|
BC_CMD_ID_CMD_123 = 123;
|
|
158
169
|
BC_CMD_ID_CMD_209 = 209;
|
|
159
170
|
BC_CMD_ID_CMD_265 = 265;
|
|
@@ -1851,7 +1862,7 @@ var init_BaichuanVideoStream = __esm({
|
|
|
1851
1862
|
const allowMsgNum0Fallback = this.acceptAnyStreamType && frame.header.msgNum === 0;
|
|
1852
1863
|
if (!allowMsgNum0Fallback) {
|
|
1853
1864
|
const frameCount = this._msgNumMismatchCount = (this._msgNumMismatchCount || 0) + 1;
|
|
1854
|
-
if (frameCount <= 5) {
|
|
1865
|
+
if (frameCount <= 5 && this.client.getDebugConfig().general) {
|
|
1855
1866
|
this.logger?.log(
|
|
1856
1867
|
`[BaichuanVideoStream] Frame msgNum mismatch: received=${frame.header.msgNum}, expected=${this.activeMsgNum}, channel=${this.channel}, profile=${this.profile}, variant=${this.variant} (frame discarded)`
|
|
1857
1868
|
);
|
|
@@ -1861,7 +1872,7 @@ var init_BaichuanVideoStream = __esm({
|
|
|
1861
1872
|
}
|
|
1862
1873
|
if (!this.acceptAnyStreamType && !this.expectedStreamTypes.has(frame.header.streamType)) {
|
|
1863
1874
|
const frameCount = this._streamTypeMismatchCount = (this._streamTypeMismatchCount || 0) + 1;
|
|
1864
|
-
if (frameCount <= 5) {
|
|
1875
|
+
if (frameCount <= 5 && this.client.getDebugConfig().general) {
|
|
1865
1876
|
this.logger?.log(
|
|
1866
1877
|
`[BaichuanVideoStream] Frame streamType mismatch: received=${frame.header.streamType}, expectedAny=[${[
|
|
1867
1878
|
...this.expectedStreamTypes
|
|
@@ -2691,6 +2702,59 @@ function buildFloodlightManualXml(channelId, status, durationSeconds = 180) {
|
|
|
2691
2702
|
</FloodlightManual>
|
|
2692
2703
|
</body>`;
|
|
2693
2704
|
}
|
|
2705
|
+
function ensureXmlHeader(xml) {
|
|
2706
|
+
const trimmed = xml.trimStart();
|
|
2707
|
+
if (trimmed.startsWith("<?xml")) return xml;
|
|
2708
|
+
return `${XML_HEADER}
|
|
2709
|
+
${xml}`;
|
|
2710
|
+
}
|
|
2711
|
+
function applyXmlTagPatch(xml, tag, value) {
|
|
2712
|
+
if (value === void 0) return xml;
|
|
2713
|
+
const v = typeof value === "boolean" ? value ? 1 : 0 : value;
|
|
2714
|
+
const re = new RegExp(`<${tag}>[^<]*</${tag}>`);
|
|
2715
|
+
return xml.replace(re, `<${tag}>${v}</${tag}>`);
|
|
2716
|
+
}
|
|
2717
|
+
function patchNestedTag(xml, parent, child, value) {
|
|
2718
|
+
if (value === void 0) return xml;
|
|
2719
|
+
const v = typeof value === "boolean" ? value ? 1 : 0 : value;
|
|
2720
|
+
const re = new RegExp(
|
|
2721
|
+
`(<${parent}[^>]*>[\\s\\S]*?<${child}>)[^<]*(</${child}>[\\s\\S]*?</${parent}>)`
|
|
2722
|
+
);
|
|
2723
|
+
return xml.replace(re, `$1${v}$2`);
|
|
2724
|
+
}
|
|
2725
|
+
function applyStreamPatch(xml, streamTag, patch) {
|
|
2726
|
+
if (!patch) return xml;
|
|
2727
|
+
const re = new RegExp(
|
|
2728
|
+
`(<${streamTag}[^>]*>)([\\s\\S]*?)(</${streamTag}>)`
|
|
2729
|
+
);
|
|
2730
|
+
return xml.replace(re, (_match, open, body, close) => {
|
|
2731
|
+
let next = body;
|
|
2732
|
+
if (patch.bitRate !== void 0) {
|
|
2733
|
+
next = applyXmlTagPatch(next, "bitRate", patch.bitRate);
|
|
2734
|
+
}
|
|
2735
|
+
if (patch.frameRate !== void 0) {
|
|
2736
|
+
next = applyXmlTagPatch(next, "frameRate", patch.frameRate);
|
|
2737
|
+
next = applyXmlTagPatch(next, "frame", patch.frameRate);
|
|
2738
|
+
}
|
|
2739
|
+
if (patch.videoEncType !== void 0) {
|
|
2740
|
+
const intVal = patch.videoEncType === "h265" ? 1 : 0;
|
|
2741
|
+
next = applyXmlTagPatch(next, "videoEncType", intVal);
|
|
2742
|
+
}
|
|
2743
|
+
return `${open}${next}${close}`;
|
|
2744
|
+
});
|
|
2745
|
+
}
|
|
2746
|
+
function normalizeDayNightMode(input) {
|
|
2747
|
+
const stripped = String(input).replace(/&/g, "And");
|
|
2748
|
+
if (!stripped) return stripped;
|
|
2749
|
+
const first = stripped[0];
|
|
2750
|
+
if (first === void 0) return stripped;
|
|
2751
|
+
return first.toLowerCase() + stripped.slice(1);
|
|
2752
|
+
}
|
|
2753
|
+
function normalizeOpenClose(input) {
|
|
2754
|
+
const v = String(input).toLowerCase();
|
|
2755
|
+
if (v === "on" || v === "open" || v === "1" || v === "true") return "open";
|
|
2756
|
+
return "close";
|
|
2757
|
+
}
|
|
2694
2758
|
function buildAbilityInfoExtensionXml(username) {
|
|
2695
2759
|
return `<?xml version="1.0" encoding="UTF-8" ?>
|
|
2696
2760
|
<Extension version="1.1">
|
|
@@ -2698,9 +2762,11 @@ function buildAbilityInfoExtensionXml(username) {
|
|
|
2698
2762
|
<token>system, streaming, PTZ, IO, security, replay, disk, network, alarm, record, video, image</token>
|
|
2699
2763
|
</Extension>`;
|
|
2700
2764
|
}
|
|
2765
|
+
var XML_HEADER;
|
|
2701
2766
|
var init_xml = __esm({
|
|
2702
2767
|
"src/protocol/xml.ts"() {
|
|
2703
2768
|
"use strict";
|
|
2769
|
+
XML_HEADER = `<?xml version="1.0" encoding="UTF-8" ?>`;
|
|
2704
2770
|
}
|
|
2705
2771
|
});
|
|
2706
2772
|
|
|
@@ -3520,6 +3586,137 @@ var init_ReolinkCgiApi = __esm({
|
|
|
3520
3586
|
const param = channel == null ? {} : { channel };
|
|
3521
3587
|
return await this.call("GetPtzPreset", param, 1);
|
|
3522
3588
|
}
|
|
3589
|
+
// ── Isp / Image (colour, flip, day-night, exposure) ──────────────
|
|
3590
|
+
async GetIsp(channel) {
|
|
3591
|
+
const param = channel == null ? {} : { channel };
|
|
3592
|
+
return await this.call("GetIsp", param, 1);
|
|
3593
|
+
}
|
|
3594
|
+
async SetIsp(isp) {
|
|
3595
|
+
return await this.call("SetIsp", isp, 0);
|
|
3596
|
+
}
|
|
3597
|
+
async GetImage(channel) {
|
|
3598
|
+
const param = channel == null ? {} : { channel };
|
|
3599
|
+
return await this.call("GetImage", param, 1);
|
|
3600
|
+
}
|
|
3601
|
+
async SetImage(image) {
|
|
3602
|
+
return await this.call("SetImage", image, 0);
|
|
3603
|
+
}
|
|
3604
|
+
// ── AudioCfg (mute / volume) ─────────────────────────────────────
|
|
3605
|
+
async GetAudioCfg(channel) {
|
|
3606
|
+
const param = channel == null ? {} : { channel };
|
|
3607
|
+
return await this.call("GetAudioCfg", param, 1);
|
|
3608
|
+
}
|
|
3609
|
+
async SetAudioCfg(audio) {
|
|
3610
|
+
return await this.call("SetAudioCfg", audio, 0);
|
|
3611
|
+
}
|
|
3612
|
+
// ── Enc setter (Get already exists above) ────────────────────────
|
|
3613
|
+
async SetEnc(enc) {
|
|
3614
|
+
return await this.call("SetEnc", enc, 0);
|
|
3615
|
+
}
|
|
3616
|
+
// ── MdAlarm (motion detection sensitivity / regions) ─────────────
|
|
3617
|
+
async GetMdAlarm(channel) {
|
|
3618
|
+
const param = channel == null ? {} : { channel };
|
|
3619
|
+
return await this.call("GetMdAlarm", param, 1);
|
|
3620
|
+
}
|
|
3621
|
+
async SetMdAlarm(md) {
|
|
3622
|
+
return await this.call("SetMdAlarm", md, 0);
|
|
3623
|
+
}
|
|
3624
|
+
// ── IrLights ─────────────────────────────────────────────────────
|
|
3625
|
+
async GetIrLights(channel) {
|
|
3626
|
+
const param = channel == null ? {} : { channel };
|
|
3627
|
+
return await this.call("GetIrLights", param, 1);
|
|
3628
|
+
}
|
|
3629
|
+
async SetIrLights(ir) {
|
|
3630
|
+
return await this.call("SetIrLights", ir, 0);
|
|
3631
|
+
}
|
|
3632
|
+
// ── AiCfg (smart-detection enable + class filter) ────────────────
|
|
3633
|
+
async GetAiCfg(channel) {
|
|
3634
|
+
const param = channel == null ? {} : { channel };
|
|
3635
|
+
return await this.call("GetAiCfg", param, 1);
|
|
3636
|
+
}
|
|
3637
|
+
async SetAiCfg(ai) {
|
|
3638
|
+
return await this.call("SetAiCfg", ai, 0);
|
|
3639
|
+
}
|
|
3640
|
+
// ── Mask (privacy-mask zones) ────────────────────────────────────
|
|
3641
|
+
async GetMask(channel) {
|
|
3642
|
+
return await this.call("GetMask", { channel }, 1);
|
|
3643
|
+
}
|
|
3644
|
+
async SetMask(mask) {
|
|
3645
|
+
return await this.call("SetMask", mask, 0);
|
|
3646
|
+
}
|
|
3647
|
+
// ── AudioNoise (input noise reduction) ───────────────────────────
|
|
3648
|
+
async GetAudioNoise(channel) {
|
|
3649
|
+
return await this.call("GetAudioNoise", { channel }, 1);
|
|
3650
|
+
}
|
|
3651
|
+
async SetAudioNoise(noise) {
|
|
3652
|
+
return await this.call("SetAudioNoise", noise, 0);
|
|
3653
|
+
}
|
|
3654
|
+
// ── Rec / RecV20 (recording schedule) ────────────────────────────
|
|
3655
|
+
async GetRec(channel) {
|
|
3656
|
+
return await this.call("GetRec", { channel }, 1);
|
|
3657
|
+
}
|
|
3658
|
+
async SetRec(rec) {
|
|
3659
|
+
return await this.call("SetRec", rec, 0);
|
|
3660
|
+
}
|
|
3661
|
+
/** Newer firmwares advertise `GetRecV20` / `SetRecV20` with the
|
|
3662
|
+
* weekly-schedule `table` field. Same payload shape as `Rec`. */
|
|
3663
|
+
async GetRecV20(channel) {
|
|
3664
|
+
return await this.call("GetRecV20", { channel }, 1);
|
|
3665
|
+
}
|
|
3666
|
+
async SetRecV20(rec) {
|
|
3667
|
+
return await this.call("SetRecV20", rec, 0);
|
|
3668
|
+
}
|
|
3669
|
+
// ── Email (SMTP alert) ───────────────────────────────────────────
|
|
3670
|
+
async GetEmail(channel) {
|
|
3671
|
+
return await this.call("GetEmail", { channel }, 1);
|
|
3672
|
+
}
|
|
3673
|
+
async SetEmail(email) {
|
|
3674
|
+
return await this.call("SetEmail", email, 0);
|
|
3675
|
+
}
|
|
3676
|
+
/** V20 variant on newer firmwares with weekly-schedule `table`. */
|
|
3677
|
+
async GetEmailV20(channel) {
|
|
3678
|
+
return await this.call("GetEmailV20", { channel }, 1);
|
|
3679
|
+
}
|
|
3680
|
+
async SetEmailV20(email) {
|
|
3681
|
+
return await this.call("SetEmailV20", email, 0);
|
|
3682
|
+
}
|
|
3683
|
+
// ── Push (Reolink-cloud push notifications) ──────────────────────
|
|
3684
|
+
async GetPush(channel) {
|
|
3685
|
+
return await this.call("GetPush", { channel }, 1);
|
|
3686
|
+
}
|
|
3687
|
+
async SetPush(push) {
|
|
3688
|
+
return await this.call("SetPush", push, 0);
|
|
3689
|
+
}
|
|
3690
|
+
async GetPushV20(channel) {
|
|
3691
|
+
return await this.call("GetPushV20", { channel }, 1);
|
|
3692
|
+
}
|
|
3693
|
+
async SetPushV20(push) {
|
|
3694
|
+
return await this.call("SetPushV20", push, 0);
|
|
3695
|
+
}
|
|
3696
|
+
// ── AudioAlarm (siren-on-event) ──────────────────────────────────
|
|
3697
|
+
async GetAudioAlarm(channel) {
|
|
3698
|
+
return await this.call("GetAudioAlarm", { channel }, 1);
|
|
3699
|
+
}
|
|
3700
|
+
async SetAudioAlarm(audio) {
|
|
3701
|
+
return await this.call("SetAudioAlarm", audio, 0);
|
|
3702
|
+
}
|
|
3703
|
+
async SetAudioAlarmV20(audio) {
|
|
3704
|
+
return await this.call("SetAudioAlarmV20", audio, 0);
|
|
3705
|
+
}
|
|
3706
|
+
// ── AutoFocus (PTZ AF) ───────────────────────────────────────────
|
|
3707
|
+
async GetAutoFocus(channel) {
|
|
3708
|
+
return await this.call("GetAutoFocus", { channel }, 1);
|
|
3709
|
+
}
|
|
3710
|
+
async SetAutoFocus(af) {
|
|
3711
|
+
return await this.call("SetAutoFocus", af, 0);
|
|
3712
|
+
}
|
|
3713
|
+
// ── AiAlarm (per-class smart-detection thresholds) ───────────────
|
|
3714
|
+
async GetAiAlarm(channel, aiType) {
|
|
3715
|
+
return await this.call("GetAiAlarm", { channel, ai_type: aiType }, 1);
|
|
3716
|
+
}
|
|
3717
|
+
async SetAiAlarm(ai) {
|
|
3718
|
+
return await this.call("SetAiAlarm", ai, 0);
|
|
3719
|
+
}
|
|
3523
3720
|
async GetAudioAlarmV20(channel) {
|
|
3524
3721
|
const param = channel == null ? {} : { channel };
|
|
3525
3722
|
return await this.call("GetAudioAlarmV20", param, 0);
|
|
@@ -7815,19 +8012,34 @@ async function* createNativeStream(api, channel, profile, options) {
|
|
|
7815
8012
|
}
|
|
7816
8013
|
});
|
|
7817
8014
|
streamStarted = true;
|
|
7818
|
-
|
|
8015
|
+
const signal = options?.signal;
|
|
8016
|
+
while (!closed && !signal?.aborted) {
|
|
7819
8017
|
if (frameQueue.length > 0) {
|
|
7820
8018
|
const frame = frameQueue.shift();
|
|
7821
8019
|
yield frame;
|
|
7822
8020
|
} else {
|
|
7823
8021
|
await new Promise((resolve) => {
|
|
7824
8022
|
frameResolve = resolve;
|
|
7825
|
-
setTimeout(() => {
|
|
8023
|
+
const timer = setTimeout(() => {
|
|
7826
8024
|
if (frameResolve === resolve) {
|
|
7827
8025
|
frameResolve = null;
|
|
7828
8026
|
resolve();
|
|
7829
8027
|
}
|
|
7830
8028
|
}, 1e3);
|
|
8029
|
+
if (signal) {
|
|
8030
|
+
const onAbort = () => {
|
|
8031
|
+
clearTimeout(timer);
|
|
8032
|
+
if (frameResolve === resolve) frameResolve = null;
|
|
8033
|
+
resolve();
|
|
8034
|
+
};
|
|
8035
|
+
if (signal.aborted) {
|
|
8036
|
+
clearTimeout(timer);
|
|
8037
|
+
frameResolve = null;
|
|
8038
|
+
resolve();
|
|
8039
|
+
} else {
|
|
8040
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
8041
|
+
}
|
|
8042
|
+
}
|
|
7831
8043
|
});
|
|
7832
8044
|
}
|
|
7833
8045
|
}
|
|
@@ -8000,13 +8212,14 @@ var NativeStreamFanout = class {
|
|
|
8000
8212
|
source = null;
|
|
8001
8213
|
running = false;
|
|
8002
8214
|
pumpPromise = null;
|
|
8215
|
+
abort = new AbortController();
|
|
8003
8216
|
constructor(opts) {
|
|
8004
8217
|
this.opts = opts;
|
|
8005
8218
|
}
|
|
8006
8219
|
start() {
|
|
8007
8220
|
if (this.running) return;
|
|
8008
8221
|
this.running = true;
|
|
8009
|
-
this.source = this.opts.createSource();
|
|
8222
|
+
this.source = this.opts.createSource(this.abort.signal);
|
|
8010
8223
|
this.pumpPromise = (async () => {
|
|
8011
8224
|
try {
|
|
8012
8225
|
for await (const frame of this.source) {
|
|
@@ -8052,6 +8265,7 @@ var NativeStreamFanout = class {
|
|
|
8052
8265
|
this.source = null;
|
|
8053
8266
|
for (const q of this.queues.values()) q.close();
|
|
8054
8267
|
this.queues.clear();
|
|
8268
|
+
this.abort.abort();
|
|
8055
8269
|
try {
|
|
8056
8270
|
await src?.return(void 0);
|
|
8057
8271
|
} catch {
|
|
@@ -8090,9 +8304,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8090
8304
|
requireAuth;
|
|
8091
8305
|
authNonces = /* @__PURE__ */ new Map();
|
|
8092
8306
|
// Track nonces per client
|
|
8093
|
-
AUTH_REALM
|
|
8307
|
+
AUTH_REALM;
|
|
8094
8308
|
NONCE_TIMEOUT_MS = 3e5;
|
|
8095
8309
|
// 5 minutes
|
|
8310
|
+
lazyMetadata;
|
|
8096
8311
|
// Client tracking
|
|
8097
8312
|
connectedClients = /* @__PURE__ */ new Set();
|
|
8098
8313
|
// Set of client IDs (IP:port)
|
|
@@ -8104,8 +8319,15 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8104
8319
|
// Track all client resources for cleanup
|
|
8105
8320
|
clientResources = /* @__PURE__ */ new Map();
|
|
8106
8321
|
isRtspDebugEnabled() {
|
|
8107
|
-
|
|
8108
|
-
|
|
8322
|
+
try {
|
|
8323
|
+
if (this.api.isClosed) {
|
|
8324
|
+
return envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
|
|
8325
|
+
}
|
|
8326
|
+
const dbg = this.api.client.getDebugConfig();
|
|
8327
|
+
return dbg.debugRtsp || envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
|
|
8328
|
+
} catch {
|
|
8329
|
+
return envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
|
|
8330
|
+
}
|
|
8109
8331
|
}
|
|
8110
8332
|
rtspDebugLog(message) {
|
|
8111
8333
|
if (!this.isRtspDebugEnabled()) return;
|
|
@@ -8127,10 +8349,20 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8127
8349
|
// Shared native stream fan-out (single camera stream, multiple RTSP clients)
|
|
8128
8350
|
nativeFanout = null;
|
|
8129
8351
|
noClientAutoStopTimer;
|
|
8352
|
+
/** Fires if camera never sends frames after stream start (sleeping), even with clients connected. */
|
|
8353
|
+
noFrameDeadlineTimer;
|
|
8130
8354
|
/** After last RTSP client; 0 = never auto-stop native stream. */
|
|
8131
8355
|
nativeStreamIdleStopMs;
|
|
8132
8356
|
/** Primed-but-no-PLAY timeout; 0 = disabled. */
|
|
8133
8357
|
nativeStreamPrimeIdleStopMs;
|
|
8358
|
+
/**
|
|
8359
|
+
* Max time to wait for the first camera frame after stream start.
|
|
8360
|
+
* If no frames arrive within this window, the native stream is stopped
|
|
8361
|
+
* (camera is sleeping). Prevents the BaichuanVideoStream watchdog from
|
|
8362
|
+
* firing and waking the camera when no real viewer is watching.
|
|
8363
|
+
* 0 = disabled. Defaults to nativeStreamPrimeIdleStopMs * 2 when > 0.
|
|
8364
|
+
*/
|
|
8365
|
+
nativeStreamNoFrameDeadlineMs;
|
|
8134
8366
|
// Prebuffer: rolling ring of recent video frames for IDR-aligned fast startup.
|
|
8135
8367
|
// When a new client connects while the stream is already running it does not need
|
|
8136
8368
|
// to wait up to one full GOP interval for the next keyframe — we replay frames
|
|
@@ -8256,14 +8488,25 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8256
8488
|
this.logger = options.logger ?? console;
|
|
8257
8489
|
this.tcpRtpFraming = options.tcpRtpFraming ?? "rfc4571";
|
|
8258
8490
|
this.deviceId = options.deviceId;
|
|
8259
|
-
this.externalListener = options.externalListener ?? false;
|
|
8491
|
+
this.externalListener = (options.externalListener ?? false) || (options.muxMode ?? false);
|
|
8260
8492
|
this.nativeStreamIdleStopMs = options.nativeStreamIdleStopMs ?? 3e4;
|
|
8261
8493
|
this.nativeStreamPrimeIdleStopMs = options.nativeStreamPrimeIdleStopMs ?? (this.nativeStreamIdleStopMs > 0 ? 15e3 : 0);
|
|
8262
|
-
this.
|
|
8494
|
+
this.nativeStreamNoFrameDeadlineMs = this.nativeStreamPrimeIdleStopMs > 0 ? Math.min(this.nativeStreamPrimeIdleStopMs * 2, 3e4) : 0;
|
|
8495
|
+
this.authCredentials = (options.credentials ?? []).map((c) => ({
|
|
8496
|
+
username: c.username,
|
|
8497
|
+
...c.password !== void 0 ? { password: c.password } : {},
|
|
8498
|
+
...c.ha1 !== void 0 ? { ha1: c.ha1 } : {}
|
|
8499
|
+
}));
|
|
8263
8500
|
this.requireAuth = options.requireAuth ?? this.authCredentials.length > 0;
|
|
8501
|
+
this.AUTH_REALM = options.authRealm ?? "BaichuanRtspServer";
|
|
8502
|
+
this.lazyMetadata = options.lazyMetadata ?? false;
|
|
8264
8503
|
const transport = this.api.client.getTransport();
|
|
8265
8504
|
this.flow = createRtspFlow(transport, "H264");
|
|
8266
8505
|
}
|
|
8506
|
+
/** Number of currently connected RTSP clients. */
|
|
8507
|
+
get clientCount() {
|
|
8508
|
+
return this.connectedClients.size;
|
|
8509
|
+
}
|
|
8267
8510
|
// --- Authentication helpers ---
|
|
8268
8511
|
/**
|
|
8269
8512
|
* Generate a new nonce for Digest authentication
|
|
@@ -8324,9 +8567,16 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8324
8567
|
this.rtspDebugLog(`Auth failed: nonce mismatch for client ${clientId}`);
|
|
8325
8568
|
return false;
|
|
8326
8569
|
}
|
|
8570
|
+
if (realm !== this.AUTH_REALM) {
|
|
8571
|
+
this.rtspDebugLog(
|
|
8572
|
+
`Auth failed: realm mismatch (client="${realm}", server="${this.AUTH_REALM}")`
|
|
8573
|
+
);
|
|
8574
|
+
return false;
|
|
8575
|
+
}
|
|
8327
8576
|
for (const cred of this.authCredentials) {
|
|
8328
8577
|
if (username !== cred.username) continue;
|
|
8329
|
-
const ha1 = this.md5(`${cred.username}:${
|
|
8578
|
+
const ha1 = cred.ha1 ?? (cred.password !== void 0 ? this.md5(`${cred.username}:${this.AUTH_REALM}:${cred.password}`) : void 0);
|
|
8579
|
+
if (!ha1) continue;
|
|
8330
8580
|
const ha2 = this.md5(`${method}:${authUri || uri}`);
|
|
8331
8581
|
const expectedResponse = this.md5(`${ha1}:${nonce}:${ha2}`);
|
|
8332
8582
|
if (response === expectedResponse) {
|
|
@@ -8355,6 +8605,12 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8355
8605
|
this.noClientAutoStopTimer = void 0;
|
|
8356
8606
|
}
|
|
8357
8607
|
}
|
|
8608
|
+
clearNoFrameDeadlineTimer() {
|
|
8609
|
+
if (this.noFrameDeadlineTimer) {
|
|
8610
|
+
clearTimeout(this.noFrameDeadlineTimer);
|
|
8611
|
+
this.noFrameDeadlineTimer = void 0;
|
|
8612
|
+
}
|
|
8613
|
+
}
|
|
8358
8614
|
setFlowVideoType(videoType, reason) {
|
|
8359
8615
|
if (this.flow.videoType === videoType) return;
|
|
8360
8616
|
const transport = this.api.client.getTransport();
|
|
@@ -8369,25 +8625,31 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8369
8625
|
if (this.active) {
|
|
8370
8626
|
throw new Error("RTSP server is already active");
|
|
8371
8627
|
}
|
|
8372
|
-
|
|
8373
|
-
|
|
8374
|
-
|
|
8375
|
-
if (stream) {
|
|
8376
|
-
this.streamMetadata = {
|
|
8377
|
-
frameRate: stream.frameRate || 25,
|
|
8378
|
-
width: stream.width,
|
|
8379
|
-
height: stream.height
|
|
8380
|
-
};
|
|
8381
|
-
const enc = String(stream.videoEncType ?? "").trim().toLowerCase();
|
|
8382
|
-
const metaVideoType = enc.includes("265") || enc.includes("hevc") ? "H265" : "H264";
|
|
8383
|
-
this.setFlowVideoType(metaVideoType, "metadata");
|
|
8384
|
-
}
|
|
8385
|
-
} catch (error) {
|
|
8386
|
-
this.logger.warn(
|
|
8387
|
-
`[BaichuanRtspServer] Could not get stream metadata: ${error}`
|
|
8628
|
+
if (this.lazyMetadata) {
|
|
8629
|
+
this.logger.info(
|
|
8630
|
+
`[BaichuanRtspServer] lazy metadata: skipping initial getStreamMetadata; will fetch on first DESCRIBE`
|
|
8388
8631
|
);
|
|
8389
|
-
|
|
8390
|
-
|
|
8632
|
+
} else {
|
|
8633
|
+
try {
|
|
8634
|
+
const metadata = await this.api.getStreamMetadata(this.channel);
|
|
8635
|
+
const stream = metadata.streams.find((s) => s.profile === this.profile);
|
|
8636
|
+
if (stream) {
|
|
8637
|
+
this.streamMetadata = {
|
|
8638
|
+
frameRate: stream.frameRate || 25,
|
|
8639
|
+
width: stream.width,
|
|
8640
|
+
height: stream.height
|
|
8641
|
+
};
|
|
8642
|
+
const enc = String(stream.videoEncType ?? "").trim().toLowerCase();
|
|
8643
|
+
const metaVideoType = enc.includes("265") || enc.includes("hevc") ? "H265" : "H264";
|
|
8644
|
+
this.setFlowVideoType(metaVideoType, "metadata");
|
|
8645
|
+
}
|
|
8646
|
+
} catch (error) {
|
|
8647
|
+
this.logger.warn(
|
|
8648
|
+
`[BaichuanRtspServer] Could not get stream metadata: ${error}`
|
|
8649
|
+
);
|
|
8650
|
+
this.streamMetadata = { frameRate: 25 };
|
|
8651
|
+
this.setFlowVideoType("H264", "metadata unavailable");
|
|
8652
|
+
}
|
|
8391
8653
|
}
|
|
8392
8654
|
if (!this.externalListener) {
|
|
8393
8655
|
this.clientConnectionServer = net.createServer((socket) => {
|
|
@@ -8428,6 +8690,30 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8428
8690
|
}
|
|
8429
8691
|
this.handleRtspConnection(socket, initialBuffer);
|
|
8430
8692
|
}
|
|
8693
|
+
/**
|
|
8694
|
+
* Inject an already-accepted client socket from a multiplexer
|
|
8695
|
+
* (e.g. `LocalRtspMux`) that owns the listening port.
|
|
8696
|
+
*
|
|
8697
|
+
* The mux reads the first RTSP request line to determine the target path,
|
|
8698
|
+
* then hands the socket over. Any bytes already consumed during routing
|
|
8699
|
+
* are replayed back onto the socket via `unshift()` so the RTSP parser in
|
|
8700
|
+
* `handleRtspConnection` sees the complete original request.
|
|
8701
|
+
*
|
|
8702
|
+
* @param socket - Client TCP socket, already accepted by the mux.
|
|
8703
|
+
* @param preReadData - Bytes the mux has already pulled off the socket
|
|
8704
|
+
* while parsing the request line. Replayed via `socket.unshift()`
|
|
8705
|
+
* before any further reads.
|
|
8706
|
+
*/
|
|
8707
|
+
injectSocket(socket, preReadData) {
|
|
8708
|
+
if (!this.active) {
|
|
8709
|
+
socket.end("RTSP/1.0 503 Service Unavailable\r\n\r\n");
|
|
8710
|
+
return;
|
|
8711
|
+
}
|
|
8712
|
+
if (preReadData && preReadData.length > 0) {
|
|
8713
|
+
socket.unshift(preReadData);
|
|
8714
|
+
}
|
|
8715
|
+
this.handleRtspConnection(socket);
|
|
8716
|
+
}
|
|
8431
8717
|
/**
|
|
8432
8718
|
* Handle RTSP connection from a client.
|
|
8433
8719
|
*/
|
|
@@ -8592,6 +8878,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8592
8878
|
Public: "DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS"
|
|
8593
8879
|
});
|
|
8594
8880
|
} else if (method === "DESCRIBE") {
|
|
8881
|
+
if (!this.api.isClosed && !this.api.isReady && !this.nativeStreamActive) {
|
|
8882
|
+
void this.api.ensureConnected().catch(() => {
|
|
8883
|
+
});
|
|
8884
|
+
}
|
|
8595
8885
|
if (!this.flow.getFmtp().hasParamSets && this.connectedClients.size === 0) {
|
|
8596
8886
|
try {
|
|
8597
8887
|
if (!this.nativeStreamActive) {
|
|
@@ -8629,6 +8919,27 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8629
8919
|
}
|
|
8630
8920
|
}
|
|
8631
8921
|
}
|
|
8922
|
+
if (!this.hasAudio && this.firstAudioPromise) {
|
|
8923
|
+
const audioPrimingMs = this.api.client.getTransport() === "udp" ? 3e3 : 2e3;
|
|
8924
|
+
const audioPrimingStart = Date.now();
|
|
8925
|
+
try {
|
|
8926
|
+
await Promise.race([
|
|
8927
|
+
this.firstAudioPromise,
|
|
8928
|
+
new Promise((resolve) => setTimeout(resolve, audioPrimingMs))
|
|
8929
|
+
]);
|
|
8930
|
+
} catch {
|
|
8931
|
+
}
|
|
8932
|
+
const audioPrimingElapsed = Date.now() - audioPrimingStart;
|
|
8933
|
+
if (this.hasAudio) {
|
|
8934
|
+
this.logger.info(
|
|
8935
|
+
`[rebroadcast] DESCRIBE audio priming: AAC detected after ${audioPrimingElapsed}ms client=${clientId} path=${this.path}`
|
|
8936
|
+
);
|
|
8937
|
+
} else {
|
|
8938
|
+
this.logger.info(
|
|
8939
|
+
`[rebroadcast] DESCRIBE audio priming: no audio after ${audioPrimingElapsed}ms \u2014 SDP will be video-only client=${clientId} path=${this.path}`
|
|
8940
|
+
);
|
|
8941
|
+
}
|
|
8942
|
+
}
|
|
8632
8943
|
{
|
|
8633
8944
|
const { fmtp, hasParamSets } = this.flow.getFmtp();
|
|
8634
8945
|
const fmtpPreview = fmtp.length > 160 ? `${fmtp.slice(0, 160)}...` : fmtp;
|
|
@@ -8637,12 +8948,13 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8637
8948
|
);
|
|
8638
8949
|
}
|
|
8639
8950
|
const sdp = this.generateSdp();
|
|
8951
|
+
const contentHost = (socket.localAddress && socket.localAddress !== "0.0.0.0" && socket.localAddress !== "::" ? socket.localAddress.replace(/^::ffff:/, "") : null) ?? this.listenHost;
|
|
8640
8952
|
sendResponse(
|
|
8641
8953
|
200,
|
|
8642
8954
|
"OK",
|
|
8643
8955
|
{
|
|
8644
8956
|
"Content-Type": "application/sdp",
|
|
8645
|
-
"Content-Base": `rtsp://${
|
|
8957
|
+
"Content-Base": `rtsp://${contentHost}:${this.listenPort}${this.path}/`
|
|
8646
8958
|
},
|
|
8647
8959
|
sdp
|
|
8648
8960
|
);
|
|
@@ -8665,7 +8977,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8665
8977
|
this.emit("client", clientId);
|
|
8666
8978
|
this.clearNoClientAutoStopTimer();
|
|
8667
8979
|
if (this.connectedClients.size === 1 && !this.nativeStreamActive) {
|
|
8668
|
-
|
|
8980
|
+
void this.startNativeStream();
|
|
8669
8981
|
}
|
|
8670
8982
|
const transportMatch = requestText.match(/Transport:\s*([^\r\n]+)/i);
|
|
8671
8983
|
const transport = (transportMatch?.[1] ?? "").trim();
|
|
@@ -8799,12 +9111,23 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
8799
9111
|
}
|
|
8800
9112
|
}
|
|
8801
9113
|
};
|
|
9114
|
+
const runProcessBuffer = () => {
|
|
9115
|
+
processBuffer().catch((err) => {
|
|
9116
|
+
this.logger.debug(
|
|
9117
|
+
`[BaichuanRtspServer] processBuffer failed for ${clientId}: ${err?.message ?? err}`
|
|
9118
|
+
);
|
|
9119
|
+
try {
|
|
9120
|
+
socket.destroy();
|
|
9121
|
+
} catch {
|
|
9122
|
+
}
|
|
9123
|
+
});
|
|
9124
|
+
};
|
|
8802
9125
|
socket.on("data", (data) => {
|
|
8803
9126
|
buffer = Buffer.concat([buffer, data]);
|
|
8804
|
-
|
|
9127
|
+
runProcessBuffer();
|
|
8805
9128
|
});
|
|
8806
9129
|
if (buffer.includes("\r\n\r\n")) {
|
|
8807
|
-
|
|
9130
|
+
runProcessBuffer();
|
|
8808
9131
|
}
|
|
8809
9132
|
}
|
|
8810
9133
|
/**
|
|
@@ -9672,6 +9995,25 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
9672
9995
|
if (this.nativeStreamActive) {
|
|
9673
9996
|
return;
|
|
9674
9997
|
}
|
|
9998
|
+
if (!this.api.isReady) {
|
|
9999
|
+
if (this.api.isClosed) {
|
|
10000
|
+
this.logger.warn?.(
|
|
10001
|
+
`[rebroadcast] API has been explicitly closed \u2014 stream cannot start profile=${this.profile}`
|
|
10002
|
+
);
|
|
10003
|
+
return;
|
|
10004
|
+
}
|
|
10005
|
+
try {
|
|
10006
|
+
this.logger.info?.(
|
|
10007
|
+
`[rebroadcast] API not ready (idle disconnect?), calling ensureConnected profile=${this.profile}`
|
|
10008
|
+
);
|
|
10009
|
+
await this.api.ensureConnected();
|
|
10010
|
+
} catch (e) {
|
|
10011
|
+
this.logger.warn?.(
|
|
10012
|
+
`[rebroadcast] ensureConnected failed, aborting stream start: ${e}`
|
|
10013
|
+
);
|
|
10014
|
+
return;
|
|
10015
|
+
}
|
|
10016
|
+
}
|
|
9675
10017
|
this.nativeStreamActive = true;
|
|
9676
10018
|
this.firstFrameReceived = false;
|
|
9677
10019
|
this.firstAudioDetected = false;
|
|
@@ -9706,13 +10048,14 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
9706
10048
|
await this.flow.startKeepAlive(this.api);
|
|
9707
10049
|
this.nativeFanout = new NativeStreamFanout({
|
|
9708
10050
|
maxQueueItems: 200,
|
|
9709
|
-
createSource: () => createNativeStream(this.api, this.channel, this.profile, {
|
|
10051
|
+
createSource: (signal) => createNativeStream(this.api, this.channel, this.profile, {
|
|
9710
10052
|
variant: this.variant,
|
|
9711
|
-
...dedicatedClient ? { client: dedicatedClient } : {}
|
|
10053
|
+
...dedicatedClient ? { client: dedicatedClient } : {},
|
|
10054
|
+
signal
|
|
9712
10055
|
}),
|
|
9713
10056
|
onFrame: (frame) => {
|
|
9714
10057
|
if (frame.audio) {
|
|
9715
|
-
if (!this.hasAudio &&
|
|
10058
|
+
if (!this.hasAudio && _BaichuanRtspServer.isAdtsAacFrame(frame.data)) {
|
|
9716
10059
|
const info = _BaichuanRtspServer.parseAdtsSamplingInfo(frame.data);
|
|
9717
10060
|
if (info) {
|
|
9718
10061
|
this.hasAudio = true;
|
|
@@ -9761,6 +10104,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
9761
10104
|
onEnd: () => {
|
|
9762
10105
|
if (!this.nativeStreamActive) return;
|
|
9763
10106
|
this.nativeStreamActive = false;
|
|
10107
|
+
this.clearNoFrameDeadlineTimer();
|
|
10108
|
+
const hadFrames = this.firstFrameReceived;
|
|
9764
10109
|
this.firstFrameReceived = false;
|
|
9765
10110
|
this.firstFramePromise = null;
|
|
9766
10111
|
this.firstFrameResolve = null;
|
|
@@ -9785,7 +10130,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
9785
10130
|
} catch {
|
|
9786
10131
|
}
|
|
9787
10132
|
}
|
|
9788
|
-
if (this.connectedClients.size > 0) {
|
|
10133
|
+
if (this.connectedClients.size > 0 && hadFrames) {
|
|
9789
10134
|
this.logger.info(
|
|
9790
10135
|
`[rebroadcast] restarting native stream for ${this.connectedClients.size} active client(s)`
|
|
9791
10136
|
);
|
|
@@ -9797,6 +10142,19 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
9797
10142
|
}
|
|
9798
10143
|
});
|
|
9799
10144
|
this.nativeFanout.start();
|
|
10145
|
+
this.clearNoFrameDeadlineTimer();
|
|
10146
|
+
if (this.nativeStreamNoFrameDeadlineMs > 0) {
|
|
10147
|
+
this.noFrameDeadlineTimer = setTimeout(() => {
|
|
10148
|
+
this.noFrameDeadlineTimer = void 0;
|
|
10149
|
+
if (!this.firstFrameReceived && this.nativeStreamActive) {
|
|
10150
|
+
this.logger.info(
|
|
10151
|
+
`[rebroadcast] no frames within ${this.nativeStreamNoFrameDeadlineMs}ms \u2014 camera sleeping, stopping stream profile=${this.profile} channel=${this.channel}`
|
|
10152
|
+
);
|
|
10153
|
+
void this.stopNativeStream();
|
|
10154
|
+
}
|
|
10155
|
+
}, this.nativeStreamNoFrameDeadlineMs);
|
|
10156
|
+
this.noFrameDeadlineTimer?.unref?.();
|
|
10157
|
+
}
|
|
9800
10158
|
this.clearNoClientAutoStopTimer();
|
|
9801
10159
|
if (this.nativeStreamPrimeIdleStopMs > 0) {
|
|
9802
10160
|
this.noClientAutoStopTimer = setTimeout(() => {
|
|
@@ -9813,6 +10171,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
9813
10171
|
markFirstFrameReceived() {
|
|
9814
10172
|
if (!this.firstFrameReceived && this.firstFrameResolve) {
|
|
9815
10173
|
this.firstFrameReceived = true;
|
|
10174
|
+
this.clearNoFrameDeadlineTimer();
|
|
9816
10175
|
this.rtspDebugLog(
|
|
9817
10176
|
`First frame received from camera for profile ${this.profile}`
|
|
9818
10177
|
);
|
|
@@ -9839,6 +10198,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events2.E
|
|
|
9839
10198
|
);
|
|
9840
10199
|
this.flow.stopKeepAlive();
|
|
9841
10200
|
this.clearNoClientAutoStopTimer();
|
|
10201
|
+
this.clearNoFrameDeadlineTimer();
|
|
9842
10202
|
this.nativeStreamActive = false;
|
|
9843
10203
|
this.firstFrameReceived = false;
|
|
9844
10204
|
this.firstFramePromise = null;
|
|
@@ -10055,149 +10415,17 @@ init_BcMediaAnnexBDecoder();
|
|
|
10055
10415
|
// src/baichuan/stream/MpegTsMuxer.ts
|
|
10056
10416
|
var TS_PACKET_SIZE = 188;
|
|
10057
10417
|
var TS_SYNC_BYTE = 71;
|
|
10058
|
-
var
|
|
10059
|
-
var
|
|
10060
|
-
var
|
|
10418
|
+
var TS_PAYLOAD_SIZE = TS_PACKET_SIZE - 4;
|
|
10419
|
+
var PID_PAT = 0;
|
|
10420
|
+
var PID_PMT = 4096;
|
|
10421
|
+
var PID_VIDEO = 256;
|
|
10422
|
+
var PID_AUDIO = 257;
|
|
10061
10423
|
var STREAM_TYPE_H264 = 27;
|
|
10062
10424
|
var STREAM_TYPE_H265 = 36;
|
|
10063
|
-
var
|
|
10064
|
-
var
|
|
10065
|
-
var
|
|
10066
|
-
|
|
10067
|
-
const packet = Buffer.alloc(TS_PACKET_SIZE, 255);
|
|
10068
|
-
packet[0] = TS_SYNC_BYTE;
|
|
10069
|
-
packet[1] = 64 | PAT_PID >> 8 & 31;
|
|
10070
|
-
packet[2] = PAT_PID & 255;
|
|
10071
|
-
packet[3] = 16 | patCc & 15;
|
|
10072
|
-
patCc = patCc + 1 & 15;
|
|
10073
|
-
packet[4] = 0;
|
|
10074
|
-
let idx = 5;
|
|
10075
|
-
packet[idx++] = 0;
|
|
10076
|
-
packet[idx++] = 176;
|
|
10077
|
-
packet[idx++] = 13;
|
|
10078
|
-
packet[idx++] = 0;
|
|
10079
|
-
packet[idx++] = 1;
|
|
10080
|
-
packet[idx++] = 193;
|
|
10081
|
-
packet[idx++] = 0;
|
|
10082
|
-
packet[idx++] = 0;
|
|
10083
|
-
packet[idx++] = 0;
|
|
10084
|
-
packet[idx++] = 1;
|
|
10085
|
-
packet[idx++] = 224 | PMT_PID >> 8 & 31;
|
|
10086
|
-
packet[idx++] = PMT_PID & 255;
|
|
10087
|
-
const crc = crc32Mpeg(packet.subarray(5, idx));
|
|
10088
|
-
packet.writeUInt32BE(crc, idx);
|
|
10089
|
-
return packet;
|
|
10090
|
-
}
|
|
10091
|
-
function createPmt(streamType) {
|
|
10092
|
-
const packet = Buffer.alloc(TS_PACKET_SIZE, 255);
|
|
10093
|
-
packet[0] = TS_SYNC_BYTE;
|
|
10094
|
-
packet[1] = 64 | PMT_PID >> 8 & 31;
|
|
10095
|
-
packet[2] = PMT_PID & 255;
|
|
10096
|
-
packet[3] = 16 | pmtCc & 15;
|
|
10097
|
-
pmtCc = pmtCc + 1 & 15;
|
|
10098
|
-
packet[4] = 0;
|
|
10099
|
-
let idx = 5;
|
|
10100
|
-
packet[idx++] = 2;
|
|
10101
|
-
packet[idx++] = 176;
|
|
10102
|
-
packet[idx++] = 18;
|
|
10103
|
-
packet[idx++] = 0;
|
|
10104
|
-
packet[idx++] = 1;
|
|
10105
|
-
packet[idx++] = 193;
|
|
10106
|
-
packet[idx++] = 0;
|
|
10107
|
-
packet[idx++] = 0;
|
|
10108
|
-
packet[idx++] = 224 | VIDEO_PID >> 8 & 31;
|
|
10109
|
-
packet[idx++] = VIDEO_PID & 255;
|
|
10110
|
-
packet[idx++] = 240;
|
|
10111
|
-
packet[idx++] = 0;
|
|
10112
|
-
packet[idx++] = streamType;
|
|
10113
|
-
packet[idx++] = 224 | VIDEO_PID >> 8 & 31;
|
|
10114
|
-
packet[idx++] = VIDEO_PID & 255;
|
|
10115
|
-
packet[idx++] = 240;
|
|
10116
|
-
packet[idx++] = 0;
|
|
10117
|
-
const crc = crc32Mpeg(packet.subarray(5, idx));
|
|
10118
|
-
packet.writeUInt32BE(crc, idx);
|
|
10119
|
-
return packet;
|
|
10120
|
-
}
|
|
10121
|
-
function createVideoPes(data, pts, isKeyframe) {
|
|
10122
|
-
const packets = [];
|
|
10123
|
-
const pts90k = Math.floor(pts * 9e4 / 1e6);
|
|
10124
|
-
const pesHeaderLen = 14;
|
|
10125
|
-
const pesHeader = Buffer.alloc(pesHeaderLen);
|
|
10126
|
-
let idx = 0;
|
|
10127
|
-
pesHeader[idx++] = 0;
|
|
10128
|
-
pesHeader[idx++] = 0;
|
|
10129
|
-
pesHeader[idx++] = 1;
|
|
10130
|
-
pesHeader[idx++] = 224;
|
|
10131
|
-
pesHeader[idx++] = 0;
|
|
10132
|
-
pesHeader[idx++] = 0;
|
|
10133
|
-
pesHeader[idx++] = 128;
|
|
10134
|
-
pesHeader[idx++] = 128;
|
|
10135
|
-
pesHeader[idx++] = 5;
|
|
10136
|
-
pesHeader[idx++] = 33 | pts90k >> 29 & 14;
|
|
10137
|
-
pesHeader[idx++] = pts90k >> 22 & 255;
|
|
10138
|
-
pesHeader[idx++] = 1 | pts90k >> 14 & 254;
|
|
10139
|
-
pesHeader[idx++] = pts90k >> 7 & 255;
|
|
10140
|
-
pesHeader[idx++] = 1 | pts90k << 1 & 254;
|
|
10141
|
-
const pesData = Buffer.concat([pesHeader, data]);
|
|
10142
|
-
let pesOffset = 0;
|
|
10143
|
-
let isFirst = true;
|
|
10144
|
-
while (pesOffset < pesData.length) {
|
|
10145
|
-
const packet = Buffer.alloc(TS_PACKET_SIZE, 255);
|
|
10146
|
-
let pktIdx = 0;
|
|
10147
|
-
packet[pktIdx++] = TS_SYNC_BYTE;
|
|
10148
|
-
packet[pktIdx++] = (isFirst ? 64 : 0) | VIDEO_PID >> 8 & 31;
|
|
10149
|
-
packet[pktIdx++] = VIDEO_PID & 255;
|
|
10150
|
-
const remaining = pesData.length - pesOffset;
|
|
10151
|
-
const maxPayload = TS_PACKET_SIZE - 4;
|
|
10152
|
-
if (remaining >= maxPayload) {
|
|
10153
|
-
packet[pktIdx++] = 16 | videoCc & 15;
|
|
10154
|
-
videoCc = videoCc + 1 & 15;
|
|
10155
|
-
pesData.copy(packet, pktIdx, pesOffset, pesOffset + maxPayload);
|
|
10156
|
-
pesOffset += maxPayload;
|
|
10157
|
-
} else {
|
|
10158
|
-
const adaptLen = maxPayload - remaining - 1;
|
|
10159
|
-
if (adaptLen < 0) {
|
|
10160
|
-
packet[pktIdx++] = 48 | videoCc & 15;
|
|
10161
|
-
videoCc = videoCc + 1 & 15;
|
|
10162
|
-
packet[pktIdx++] = TS_PACKET_SIZE - 4 - 1 - remaining;
|
|
10163
|
-
if (isFirst && isKeyframe) {
|
|
10164
|
-
packet[pktIdx++] = 64;
|
|
10165
|
-
for (let i = pktIdx; i < TS_PACKET_SIZE - remaining; i++) {
|
|
10166
|
-
packet[i] = 255;
|
|
10167
|
-
}
|
|
10168
|
-
} else {
|
|
10169
|
-
packet[pktIdx++] = 0;
|
|
10170
|
-
for (let i = pktIdx; i < TS_PACKET_SIZE - remaining; i++) {
|
|
10171
|
-
packet[i] = 255;
|
|
10172
|
-
}
|
|
10173
|
-
}
|
|
10174
|
-
pesData.copy(packet, TS_PACKET_SIZE - remaining, pesOffset);
|
|
10175
|
-
pesOffset += remaining;
|
|
10176
|
-
} else {
|
|
10177
|
-
packet[pktIdx++] = 48 | videoCc & 15;
|
|
10178
|
-
videoCc = videoCc + 1 & 15;
|
|
10179
|
-
if (adaptLen === 0) {
|
|
10180
|
-
packet[pktIdx++] = 0;
|
|
10181
|
-
} else {
|
|
10182
|
-
packet[pktIdx++] = adaptLen;
|
|
10183
|
-
if (isFirst && isKeyframe) {
|
|
10184
|
-
packet[pktIdx++] = 64;
|
|
10185
|
-
} else {
|
|
10186
|
-
packet[pktIdx++] = 0;
|
|
10187
|
-
}
|
|
10188
|
-
for (let i = 0; i < adaptLen - 1; i++) {
|
|
10189
|
-
packet[pktIdx++] = 255;
|
|
10190
|
-
}
|
|
10191
|
-
}
|
|
10192
|
-
pesData.copy(packet, pktIdx, pesOffset, pesOffset + remaining);
|
|
10193
|
-
pesOffset += remaining;
|
|
10194
|
-
}
|
|
10195
|
-
}
|
|
10196
|
-
packets.push(packet);
|
|
10197
|
-
isFirst = false;
|
|
10198
|
-
}
|
|
10199
|
-
return packets;
|
|
10200
|
-
}
|
|
10425
|
+
var STREAM_TYPE_AAC = 15;
|
|
10426
|
+
var PES_STREAM_ID_VIDEO = 224;
|
|
10427
|
+
var PES_STREAM_ID_AUDIO = 192;
|
|
10428
|
+
var PAT_PMT_INTERVAL = 40;
|
|
10201
10429
|
function crc32Mpeg(data) {
|
|
10202
10430
|
let crc = 4294967295;
|
|
10203
10431
|
for (let i = 0; i < data.length; i++) {
|
|
@@ -10212,45 +10440,218 @@ function crc32Mpeg(data) {
|
|
|
10212
10440
|
}
|
|
10213
10441
|
return crc >>> 0;
|
|
10214
10442
|
}
|
|
10443
|
+
function usToPts(us) {
|
|
10444
|
+
return Math.floor(us * 90 / 1e3) & 8589934591;
|
|
10445
|
+
}
|
|
10446
|
+
function encodePts(buf, offset, pts, prefix) {
|
|
10447
|
+
buf[offset + 0] = prefix << 4 | (pts >>> 30 & 7) << 1 | 1;
|
|
10448
|
+
buf[offset + 1] = pts >>> 22 & 255;
|
|
10449
|
+
buf[offset + 2] = (pts >>> 15 & 127) << 1 | 1;
|
|
10450
|
+
buf[offset + 3] = pts >>> 7 & 255;
|
|
10451
|
+
buf[offset + 4] = (pts & 127) << 1 | 1;
|
|
10452
|
+
}
|
|
10453
|
+
function writeTsHeader(buf, pid, pusi, cc, hasAdapt, hasPayload) {
|
|
10454
|
+
buf[0] = TS_SYNC_BYTE;
|
|
10455
|
+
buf[1] = (pusi ? 64 : 0) | pid >> 8 & 31;
|
|
10456
|
+
buf[2] = pid & 255;
|
|
10457
|
+
buf[3] = (hasAdapt ? 32 : 0) | (hasPayload ? 16 : 0) | cc & 15;
|
|
10458
|
+
}
|
|
10459
|
+
function pesToTsPackets(pesData, pid, ccRef, isKeyframe) {
|
|
10460
|
+
const totalPackets = Math.ceil(pesData.length / TS_PAYLOAD_SIZE);
|
|
10461
|
+
const out = Buffer.allocUnsafe(totalPackets * TS_PACKET_SIZE);
|
|
10462
|
+
let pesOffset = 0;
|
|
10463
|
+
let outOffset = 0;
|
|
10464
|
+
let isFirst = true;
|
|
10465
|
+
while (pesOffset < pesData.length) {
|
|
10466
|
+
const remaining = pesData.length - pesOffset;
|
|
10467
|
+
const packet = out.subarray(outOffset, outOffset + TS_PACKET_SIZE);
|
|
10468
|
+
outOffset += TS_PACKET_SIZE;
|
|
10469
|
+
if (remaining >= TS_PAYLOAD_SIZE) {
|
|
10470
|
+
writeTsHeader(packet, pid, isFirst, ccRef.cc, false, true);
|
|
10471
|
+
ccRef.cc = ccRef.cc + 1 & 15;
|
|
10472
|
+
pesData.copy(packet, 4, pesOffset, pesOffset + TS_PAYLOAD_SIZE);
|
|
10473
|
+
pesOffset += TS_PAYLOAD_SIZE;
|
|
10474
|
+
} else {
|
|
10475
|
+
const paddingNeeded = TS_PAYLOAD_SIZE - remaining;
|
|
10476
|
+
if (paddingNeeded === 1) {
|
|
10477
|
+
writeTsHeader(packet, pid, isFirst, ccRef.cc, true, true);
|
|
10478
|
+
ccRef.cc = ccRef.cc + 1 & 15;
|
|
10479
|
+
packet[4] = 0;
|
|
10480
|
+
pesData.copy(packet, 5, pesOffset, pesOffset + remaining);
|
|
10481
|
+
} else {
|
|
10482
|
+
writeTsHeader(packet, pid, isFirst, ccRef.cc, true, true);
|
|
10483
|
+
ccRef.cc = ccRef.cc + 1 & 15;
|
|
10484
|
+
const adaptLen = paddingNeeded - 1;
|
|
10485
|
+
packet[4] = adaptLen;
|
|
10486
|
+
packet[5] = isFirst && isKeyframe ? 64 : 0;
|
|
10487
|
+
packet.fill(255, 6, 4 + paddingNeeded);
|
|
10488
|
+
pesData.copy(packet, 4 + paddingNeeded, pesOffset, pesOffset + remaining);
|
|
10489
|
+
}
|
|
10490
|
+
pesOffset += remaining;
|
|
10491
|
+
}
|
|
10492
|
+
isFirst = false;
|
|
10493
|
+
}
|
|
10494
|
+
return out;
|
|
10495
|
+
}
|
|
10496
|
+
function buildPat(cc) {
|
|
10497
|
+
const pkt = Buffer.alloc(TS_PACKET_SIZE, 255);
|
|
10498
|
+
pkt[0] = TS_SYNC_BYTE;
|
|
10499
|
+
pkt[1] = 64 | PID_PAT >> 8 & 31;
|
|
10500
|
+
pkt[2] = PID_PAT & 255;
|
|
10501
|
+
pkt[3] = 16 | cc & 15;
|
|
10502
|
+
pkt[4] = 0;
|
|
10503
|
+
const sectionStart = 5;
|
|
10504
|
+
let i = sectionStart;
|
|
10505
|
+
pkt[i++] = 0;
|
|
10506
|
+
pkt[i++] = 176;
|
|
10507
|
+
pkt[i++] = 13;
|
|
10508
|
+
pkt[i++] = 0;
|
|
10509
|
+
pkt[i++] = 1;
|
|
10510
|
+
pkt[i++] = 193;
|
|
10511
|
+
pkt[i++] = 0;
|
|
10512
|
+
pkt[i++] = 0;
|
|
10513
|
+
pkt[i++] = 0;
|
|
10514
|
+
pkt[i++] = 1;
|
|
10515
|
+
pkt[i++] = 224 | PID_PMT >> 8 & 31;
|
|
10516
|
+
pkt[i++] = PID_PMT & 255;
|
|
10517
|
+
const crc = crc32Mpeg(pkt.subarray(sectionStart, i));
|
|
10518
|
+
pkt.writeUInt32BE(crc, i);
|
|
10519
|
+
return pkt;
|
|
10520
|
+
}
|
|
10521
|
+
function buildPmt(videoStreamType, includeAudio, cc) {
|
|
10522
|
+
const pkt = Buffer.alloc(TS_PACKET_SIZE, 255);
|
|
10523
|
+
pkt[0] = TS_SYNC_BYTE;
|
|
10524
|
+
pkt[1] = 64 | PID_PMT >> 8 & 31;
|
|
10525
|
+
pkt[2] = PID_PMT & 255;
|
|
10526
|
+
pkt[3] = 16 | cc & 15;
|
|
10527
|
+
pkt[4] = 0;
|
|
10528
|
+
const sectionStart = 5;
|
|
10529
|
+
let i = sectionStart;
|
|
10530
|
+
pkt[i++] = 2;
|
|
10531
|
+
pkt[i++] = 176;
|
|
10532
|
+
const sectionLenPos = i;
|
|
10533
|
+
i += 1;
|
|
10534
|
+
pkt[i++] = 0;
|
|
10535
|
+
pkt[i++] = 1;
|
|
10536
|
+
pkt[i++] = 193;
|
|
10537
|
+
pkt[i++] = 0;
|
|
10538
|
+
pkt[i++] = 0;
|
|
10539
|
+
pkt[i++] = 224 | PID_VIDEO >> 8 & 31;
|
|
10540
|
+
pkt[i++] = PID_VIDEO & 255;
|
|
10541
|
+
pkt[i++] = 240;
|
|
10542
|
+
pkt[i++] = 0;
|
|
10543
|
+
pkt[i++] = videoStreamType;
|
|
10544
|
+
pkt[i++] = 224 | PID_VIDEO >> 8 & 31;
|
|
10545
|
+
pkt[i++] = PID_VIDEO & 255;
|
|
10546
|
+
pkt[i++] = 240;
|
|
10547
|
+
pkt[i++] = 0;
|
|
10548
|
+
if (includeAudio) {
|
|
10549
|
+
pkt[i++] = STREAM_TYPE_AAC;
|
|
10550
|
+
pkt[i++] = 224 | PID_AUDIO >> 8 & 31;
|
|
10551
|
+
pkt[i++] = PID_AUDIO & 255;
|
|
10552
|
+
pkt[i++] = 240;
|
|
10553
|
+
pkt[i++] = 0;
|
|
10554
|
+
}
|
|
10555
|
+
const sectionLen = i - sectionStart - 3 + 4;
|
|
10556
|
+
pkt[sectionLenPos] = sectionLen;
|
|
10557
|
+
const crc = crc32Mpeg(pkt.subarray(sectionStart, i));
|
|
10558
|
+
pkt.writeUInt32BE(crc, i);
|
|
10559
|
+
return pkt;
|
|
10560
|
+
}
|
|
10561
|
+
function buildVideoPes(annexBData, ptsUs, isKeyframe) {
|
|
10562
|
+
const pts = usToPts(ptsUs);
|
|
10563
|
+
const pesHeader = Buffer.allocUnsafe(14);
|
|
10564
|
+
pesHeader[0] = 0;
|
|
10565
|
+
pesHeader[1] = 0;
|
|
10566
|
+
pesHeader[2] = 1;
|
|
10567
|
+
pesHeader[3] = PES_STREAM_ID_VIDEO;
|
|
10568
|
+
pesHeader[4] = 0;
|
|
10569
|
+
pesHeader[5] = 0;
|
|
10570
|
+
pesHeader[6] = 128 | (isKeyframe ? 4 : 0);
|
|
10571
|
+
pesHeader[7] = 128;
|
|
10572
|
+
pesHeader[8] = 5;
|
|
10573
|
+
encodePts(pesHeader, 9, pts, 2);
|
|
10574
|
+
return Buffer.concat([pesHeader, annexBData]);
|
|
10575
|
+
}
|
|
10576
|
+
function buildAudioPes(adtsData, ptsUs) {
|
|
10577
|
+
const pts = usToPts(ptsUs);
|
|
10578
|
+
const pesPayloadLen = 8 + adtsData.length;
|
|
10579
|
+
const pesHeader = Buffer.allocUnsafe(14);
|
|
10580
|
+
pesHeader[0] = 0;
|
|
10581
|
+
pesHeader[1] = 0;
|
|
10582
|
+
pesHeader[2] = 1;
|
|
10583
|
+
pesHeader[3] = PES_STREAM_ID_AUDIO;
|
|
10584
|
+
pesHeader[4] = pesPayloadLen >> 8 & 255;
|
|
10585
|
+
pesHeader[5] = pesPayloadLen & 255;
|
|
10586
|
+
pesHeader[6] = 128;
|
|
10587
|
+
pesHeader[7] = 128;
|
|
10588
|
+
pesHeader[8] = 5;
|
|
10589
|
+
encodePts(pesHeader, 9, pts, 2);
|
|
10590
|
+
return Buffer.concat([pesHeader, adtsData]);
|
|
10591
|
+
}
|
|
10215
10592
|
var MpegTsMuxer = class {
|
|
10216
|
-
|
|
10217
|
-
|
|
10218
|
-
|
|
10219
|
-
|
|
10220
|
-
|
|
10221
|
-
|
|
10593
|
+
videoStreamType;
|
|
10594
|
+
includeAudio;
|
|
10595
|
+
// Per-instance continuity counters (4-bit, wrap at 16)
|
|
10596
|
+
patCc = 0;
|
|
10597
|
+
pmtCc = 0;
|
|
10598
|
+
videoCc = 0;
|
|
10599
|
+
audioCc = 0;
|
|
10600
|
+
framesSinceTableSend = 0;
|
|
10601
|
+
tablesSent = false;
|
|
10222
10602
|
constructor(options) {
|
|
10223
|
-
this.
|
|
10603
|
+
this.videoStreamType = options.videoType === "H265" ? STREAM_TYPE_H265 : STREAM_TYPE_H264;
|
|
10604
|
+
this.includeAudio = options.includeAudio ?? true;
|
|
10224
10605
|
}
|
|
10225
10606
|
/**
|
|
10226
|
-
*
|
|
10607
|
+
* Mux a video frame (Annex-B H.264 or H.265) into MPEG-TS packets.
|
|
10608
|
+
* PAT and PMT are emitted before keyframes and periodically.
|
|
10609
|
+
*
|
|
10610
|
+
* @param annexBData - Annex-B video data (with start codes)
|
|
10611
|
+
* @param ptsUs - Presentation timestamp in microseconds
|
|
10612
|
+
* @param isKeyframe - Whether this is an IDR / IRAP frame
|
|
10227
10613
|
*/
|
|
10228
|
-
|
|
10229
|
-
|
|
10230
|
-
|
|
10231
|
-
|
|
10232
|
-
|
|
10233
|
-
|
|
10234
|
-
|
|
10614
|
+
muxVideo(annexBData, ptsUs, isKeyframe) {
|
|
10615
|
+
const chunks = [];
|
|
10616
|
+
const needTables = !this.tablesSent || isKeyframe || this.framesSinceTableSend >= PAT_PMT_INTERVAL;
|
|
10617
|
+
if (needTables) {
|
|
10618
|
+
chunks.push(buildPat(this.patCc));
|
|
10619
|
+
this.patCc = this.patCc + 1 & 15;
|
|
10620
|
+
chunks.push(buildPmt(this.videoStreamType, this.includeAudio, this.pmtCc));
|
|
10621
|
+
this.pmtCc = this.pmtCc + 1 & 15;
|
|
10622
|
+
this.tablesSent = true;
|
|
10623
|
+
this.framesSinceTableSend = 0;
|
|
10624
|
+
}
|
|
10625
|
+
this.framesSinceTableSend++;
|
|
10626
|
+
const pes = buildVideoPes(annexBData, ptsUs, isKeyframe);
|
|
10627
|
+
const ccRef = { cc: this.videoCc };
|
|
10628
|
+
chunks.push(pesToTsPackets(pes, PID_VIDEO, ccRef, isKeyframe));
|
|
10629
|
+
this.videoCc = ccRef.cc;
|
|
10630
|
+
return Buffer.concat(chunks);
|
|
10631
|
+
}
|
|
10632
|
+
/**
|
|
10633
|
+
* Mux an audio frame (ADTS AAC) into MPEG-TS packets.
|
|
10634
|
+
* Returns an empty Buffer when includeAudio is false.
|
|
10235
10635
|
*
|
|
10236
|
-
* @param
|
|
10237
|
-
* @param
|
|
10238
|
-
|
|
10239
|
-
|
|
10240
|
-
|
|
10241
|
-
|
|
10242
|
-
const
|
|
10243
|
-
|
|
10244
|
-
|
|
10245
|
-
|
|
10246
|
-
|
|
10247
|
-
|
|
10248
|
-
|
|
10249
|
-
|
|
10250
|
-
this.
|
|
10251
|
-
|
|
10252
|
-
|
|
10253
|
-
|
|
10636
|
+
* @param adtsData - Raw ADTS AAC frame (starting with 0xFF 0xF1/0xF9 syncword)
|
|
10637
|
+
* @param ptsUs - Presentation timestamp in microseconds
|
|
10638
|
+
*/
|
|
10639
|
+
muxAudio(adtsData, ptsUs) {
|
|
10640
|
+
if (!this.includeAudio || adtsData.length === 0) return Buffer.alloc(0);
|
|
10641
|
+
const pes = buildAudioPes(adtsData, ptsUs);
|
|
10642
|
+
const ccRef = { cc: this.audioCc };
|
|
10643
|
+
const result = pesToTsPackets(pes, PID_AUDIO, ccRef, false);
|
|
10644
|
+
this.audioCc = ccRef.cc;
|
|
10645
|
+
return result;
|
|
10646
|
+
}
|
|
10647
|
+
/** Reset all continuity counters and table state (e.g. after stream restart). */
|
|
10648
|
+
reset() {
|
|
10649
|
+
this.patCc = 0;
|
|
10650
|
+
this.pmtCc = 0;
|
|
10651
|
+
this.videoCc = 0;
|
|
10652
|
+
this.audioCc = 0;
|
|
10653
|
+
this.framesSinceTableSend = 0;
|
|
10654
|
+
this.tablesSent = false;
|
|
10254
10655
|
}
|
|
10255
10656
|
};
|
|
10256
10657
|
|
|
@@ -15526,6 +15927,53 @@ var getAiStateViaGetAiAlarm = async (params) => {
|
|
|
15526
15927
|
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr ?? "getAiState failed"));
|
|
15527
15928
|
};
|
|
15528
15929
|
|
|
15930
|
+
// src/reolink/baichuan/utils/sleepInference.ts
|
|
15931
|
+
function decideSleepInferenceTransition(input) {
|
|
15932
|
+
const { inferred, committed, pending, hysteresisPolls } = input;
|
|
15933
|
+
if (committed === void 0) {
|
|
15934
|
+
return {
|
|
15935
|
+
emit: inferred === "sleeping" ? "sleeping" : null,
|
|
15936
|
+
nextCommitted: inferred,
|
|
15937
|
+
nextPending: void 0
|
|
15938
|
+
};
|
|
15939
|
+
}
|
|
15940
|
+
if (inferred === committed) {
|
|
15941
|
+
return {
|
|
15942
|
+
emit: null,
|
|
15943
|
+
nextCommitted: committed,
|
|
15944
|
+
nextPending: void 0
|
|
15945
|
+
};
|
|
15946
|
+
}
|
|
15947
|
+
const effectivePolls = Math.max(1, hysteresisPolls);
|
|
15948
|
+
if (!pending || pending.state !== inferred) {
|
|
15949
|
+
if (effectivePolls <= 1) {
|
|
15950
|
+
return {
|
|
15951
|
+
emit: inferred === "sleeping" ? "sleeping" : "awake",
|
|
15952
|
+
nextCommitted: inferred,
|
|
15953
|
+
nextPending: void 0
|
|
15954
|
+
};
|
|
15955
|
+
}
|
|
15956
|
+
return {
|
|
15957
|
+
emit: null,
|
|
15958
|
+
nextCommitted: committed,
|
|
15959
|
+
nextPending: { state: inferred, count: 1 }
|
|
15960
|
+
};
|
|
15961
|
+
}
|
|
15962
|
+
const nextCount = pending.count + 1;
|
|
15963
|
+
if (nextCount < effectivePolls) {
|
|
15964
|
+
return {
|
|
15965
|
+
emit: null,
|
|
15966
|
+
nextCommitted: committed,
|
|
15967
|
+
nextPending: { state: inferred, count: nextCount }
|
|
15968
|
+
};
|
|
15969
|
+
}
|
|
15970
|
+
return {
|
|
15971
|
+
emit: inferred === "sleeping" ? "sleeping" : "awake",
|
|
15972
|
+
nextCommitted: inferred,
|
|
15973
|
+
nextPending: void 0
|
|
15974
|
+
};
|
|
15975
|
+
}
|
|
15976
|
+
|
|
15529
15977
|
// src/reolink/baichuan/utils/channelInfoPush.ts
|
|
15530
15978
|
init_xml();
|
|
15531
15979
|
var parseOptionalInt = (value) => {
|
|
@@ -17802,7 +18250,17 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
17802
18250
|
statePollingInterval;
|
|
17803
18251
|
udpSleepInferenceInterval;
|
|
17804
18252
|
udpLastInferredSleepStateByChannel = /* @__PURE__ */ new Map();
|
|
18253
|
+
/**
|
|
18254
|
+
* Per-channel pending sleep-state candidate for hysteresis.
|
|
18255
|
+
* When the inference flips to a new state we require N consecutive polls
|
|
18256
|
+
* of that same state before committing it — this filters out transient
|
|
18257
|
+
* flapping caused by non-waking traffic drifting in/out of the 10 s
|
|
18258
|
+
* getSleepStatus() observation window during stream teardown.
|
|
18259
|
+
*/
|
|
18260
|
+
udpPendingSleepStateByChannel = /* @__PURE__ */ new Map();
|
|
17805
18261
|
udpSleepInferenceIntervalMs = 2e3;
|
|
18262
|
+
/** Consecutive inference polls required to commit a new sleeping/awake state. */
|
|
18263
|
+
udpSleepInferenceHysteresisPolls = 2;
|
|
17806
18264
|
lastMotionState;
|
|
17807
18265
|
lastAiState;
|
|
17808
18266
|
aiStatePollingDisabled = false;
|
|
@@ -18269,6 +18727,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18269
18727
|
*/
|
|
18270
18728
|
attachD2cDiscListener(client) {
|
|
18271
18729
|
client.on("d2c_disc", () => this.notifyD2cDisc());
|
|
18730
|
+
client.on("error", () => {
|
|
18731
|
+
});
|
|
18272
18732
|
}
|
|
18273
18733
|
/**
|
|
18274
18734
|
* Acquire a socket from the pool by tag.
|
|
@@ -18387,6 +18847,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18387
18847
|
const clientOpts = log ? { ...this.clientOptions, logger: log } : this.clientOptions;
|
|
18388
18848
|
const newClient = new BaichuanClient(clientOpts);
|
|
18389
18849
|
this.attachD2cDiscListener(newClient);
|
|
18850
|
+
newClient.on("error", (err) => {
|
|
18851
|
+
log?.debug?.(
|
|
18852
|
+
`[SocketPool] tag=${tag} client error: ${err?.message ?? err}`
|
|
18853
|
+
);
|
|
18854
|
+
});
|
|
18390
18855
|
await newClient.login();
|
|
18391
18856
|
const existingCooldown = this.socketPoolCooldowns.get(this.host);
|
|
18392
18857
|
if (existingCooldown) {
|
|
@@ -18902,6 +19367,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18902
19367
|
* Only counts sessions from our own IP address.
|
|
18903
19368
|
*/
|
|
18904
19369
|
async maybeRebootOnTooManySessions() {
|
|
19370
|
+
if (!this.client.isSocketConnected?.()) return;
|
|
18905
19371
|
const threshold = this.maxDedicatedSessionsBeforeReboot ?? 10;
|
|
18906
19372
|
if (this.sessionGuardRebootInFlight) return;
|
|
18907
19373
|
const cooldownMs = 10 * 6e4;
|
|
@@ -19343,6 +19809,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
19343
19809
|
}
|
|
19344
19810
|
async renewSimpleEventSubscription() {
|
|
19345
19811
|
if (this.simpleEventListeners.size === 0) return;
|
|
19812
|
+
if (!this.client.isSocketConnected?.()) return;
|
|
19346
19813
|
if (this.simpleEventResubscribeInFlight)
|
|
19347
19814
|
return await this.simpleEventResubscribeInFlight;
|
|
19348
19815
|
this.simpleEventResubscribeInFlight = (async () => {
|
|
@@ -23110,23 +23577,32 @@ ${stderr}`)
|
|
|
23110
23577
|
return;
|
|
23111
23578
|
}
|
|
23112
23579
|
const channel = this.client.getConfiguredChannel?.() ?? 0;
|
|
23580
|
+
if (!this.client.isSocketConnected?.()) {
|
|
23581
|
+
this.udpPendingSleepStateByChannel.delete(channel);
|
|
23582
|
+
return;
|
|
23583
|
+
}
|
|
23113
23584
|
const status = this.getSleepStatus({ channel });
|
|
23114
23585
|
if (status.state === "unknown") return;
|
|
23115
|
-
const
|
|
23116
|
-
this.
|
|
23117
|
-
|
|
23118
|
-
|
|
23119
|
-
|
|
23120
|
-
|
|
23121
|
-
|
|
23122
|
-
|
|
23123
|
-
|
|
23124
|
-
|
|
23125
|
-
|
|
23586
|
+
const committed = this.udpLastInferredSleepStateByChannel.get(channel);
|
|
23587
|
+
const pending = this.udpPendingSleepStateByChannel.get(channel);
|
|
23588
|
+
const decision = decideSleepInferenceTransition({
|
|
23589
|
+
inferred: status.state,
|
|
23590
|
+
committed,
|
|
23591
|
+
pending,
|
|
23592
|
+
hysteresisPolls: this.udpSleepInferenceHysteresisPolls
|
|
23593
|
+
});
|
|
23594
|
+
this.udpLastInferredSleepStateByChannel.set(
|
|
23595
|
+
channel,
|
|
23596
|
+
decision.nextCommitted
|
|
23597
|
+
);
|
|
23598
|
+
if (decision.nextPending === void 0) {
|
|
23599
|
+
this.udpPendingSleepStateByChannel.delete(channel);
|
|
23600
|
+
} else {
|
|
23601
|
+
this.udpPendingSleepStateByChannel.set(channel, decision.nextPending);
|
|
23126
23602
|
}
|
|
23127
|
-
if (
|
|
23603
|
+
if (decision.emit) {
|
|
23128
23604
|
this.dispatchSimpleEvent({
|
|
23129
|
-
type:
|
|
23605
|
+
type: decision.emit,
|
|
23130
23606
|
channel,
|
|
23131
23607
|
timestamp: Date.now()
|
|
23132
23608
|
});
|
|
@@ -23149,6 +23625,7 @@ ${stderr}`)
|
|
|
23149
23625
|
this.udpSleepInferenceInterval = void 0;
|
|
23150
23626
|
}
|
|
23151
23627
|
this.udpLastInferredSleepStateByChannel.clear();
|
|
23628
|
+
this.udpPendingSleepStateByChannel.clear();
|
|
23152
23629
|
}
|
|
23153
23630
|
/**
|
|
23154
23631
|
* GetEvents via Baichuan (legacy - use subscribeEvents for real-time events).
|
|
@@ -25579,6 +26056,373 @@ ${xml}`
|
|
|
25579
26056
|
await this.cgiApi.login();
|
|
25580
26057
|
return await this.cgiApi.getAllChannelsEvents(options);
|
|
25581
26058
|
}
|
|
26059
|
+
// ====================================================================
|
|
26060
|
+
// Native Baichuan tunable-settings setters
|
|
26061
|
+
//
|
|
26062
|
+
// Replace the CGI passthroughs above with on-wire Baichuan binary
|
|
26063
|
+
// calls. Mirrors the @http_cmd-decorated methods in reolink_aio's
|
|
26064
|
+
// baichuan.py — every command has a documented `cmd_id` (read) and
|
|
26065
|
+
// `cmd_id` (write) pair. The pattern is:
|
|
26066
|
+
//
|
|
26067
|
+
// 1. read XML via `sendXml({ cmdId: GET, channel })`
|
|
26068
|
+
// 2. patch fields via regex (camera firmware is XML-strict; using
|
|
26069
|
+
// the parser would force us to rebuild the document and risk
|
|
26070
|
+
// losing unmodified attributes / element order).
|
|
26071
|
+
// 3. write back via `sendXml({ cmdId: SET, channel, payloadXml })`
|
|
26072
|
+
//
|
|
26073
|
+
// All getters parse via `parseXmlFragmentToJson` so the consumer gets
|
|
26074
|
+
// a clean JSON object instead of XML.
|
|
26075
|
+
// ====================================================================
|
|
26076
|
+
/**
|
|
26077
|
+
* GetEnc via Baichuan (cmdId=56). Returns the `<Compression>` block:
|
|
26078
|
+
* per-stream `mainStream` / `subStream` / `thirdStream` with `audio`
|
|
26079
|
+
* flag, `width`, `height`, `frame` (NOT `frameRate`), `bitRate`,
|
|
26080
|
+
* `videoEncType` (0=h264, 1=h265), `encoderProfile`, `gop`. Mirrors
|
|
26081
|
+
* reolink_aio's `GetEnc` — note the wire payload wraps everything
|
|
26082
|
+
* in `Compression`, not `Enc`.
|
|
26083
|
+
*/
|
|
26084
|
+
async getEnc(channel, options) {
|
|
26085
|
+
const xml = await this.sendPcapDerivedSettingsGetXml({
|
|
26086
|
+
cmdId: BC_CMD_ID_GET_ENC,
|
|
26087
|
+
...channel != null ? { channel } : {},
|
|
26088
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
26089
|
+
});
|
|
26090
|
+
return parseXmlFragmentToJson(xml);
|
|
26091
|
+
}
|
|
26092
|
+
/**
|
|
26093
|
+
* SetEnc via Baichuan (cmdId=57). Read-modify-write — preserves
|
|
26094
|
+
* unspecified fields. Mirrors reolink_aio's `SetEnc`.
|
|
26095
|
+
*
|
|
26096
|
+
* @param channel - Channel number (0-based)
|
|
26097
|
+
* @param patch - Fields to update on `mainStream` and/or `subStream`,
|
|
26098
|
+
* plus a top-level `audio` toggle (0/1). Pass only what you want
|
|
26099
|
+
* to change.
|
|
26100
|
+
*/
|
|
26101
|
+
async setEnc(channel, patch, options) {
|
|
26102
|
+
const ch = this.normalizeChannel(channel);
|
|
26103
|
+
let xml = await this.sendXml({
|
|
26104
|
+
cmdId: BC_CMD_ID_GET_ENC,
|
|
26105
|
+
channel: ch,
|
|
26106
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
26107
|
+
});
|
|
26108
|
+
if (patch.audio !== void 0) {
|
|
26109
|
+
xml = xml.replace(
|
|
26110
|
+
/<audio>[^<]*<\/audio>/g,
|
|
26111
|
+
`<audio>${patch.audio}</audio>`
|
|
26112
|
+
);
|
|
26113
|
+
}
|
|
26114
|
+
xml = applyStreamPatch(xml, "mainStream", patch.mainStream);
|
|
26115
|
+
xml = applyStreamPatch(xml, "subStream", patch.subStream);
|
|
26116
|
+
await this.sendXml({
|
|
26117
|
+
cmdId: BC_CMD_ID_SET_ENC,
|
|
26118
|
+
channel: ch,
|
|
26119
|
+
payloadXml: ensureXmlHeader(xml),
|
|
26120
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
26121
|
+
});
|
|
26122
|
+
}
|
|
26123
|
+
/**
|
|
26124
|
+
* SetImage via Baichuan (cmdId=25, read via cmdId=26). Patches the
|
|
26125
|
+
* `<VideoInput>` block: bright / contrast / saturation / hue /
|
|
26126
|
+
* sharpen. Mirrors reolink_aio's `SetImage`.
|
|
26127
|
+
*/
|
|
26128
|
+
async setImage(channel, patch, options) {
|
|
26129
|
+
const ch = this.normalizeChannel(channel);
|
|
26130
|
+
let xml = await this.sendXml({
|
|
26131
|
+
cmdId: BC_CMD_ID_GET_VIDEO_INPUT,
|
|
26132
|
+
channel: ch,
|
|
26133
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
26134
|
+
});
|
|
26135
|
+
xml = applyXmlTagPatch(xml, "bright", patch.bright);
|
|
26136
|
+
xml = applyXmlTagPatch(xml, "contrast", patch.contrast);
|
|
26137
|
+
xml = applyXmlTagPatch(xml, "saturation", patch.saturation);
|
|
26138
|
+
xml = applyXmlTagPatch(xml, "hue", patch.hue);
|
|
26139
|
+
xml = applyXmlTagPatch(xml, "sharpen", patch.sharpen);
|
|
26140
|
+
await this.sendXml({
|
|
26141
|
+
cmdId: BC_CMD_ID_SET_VIDEO_INPUT,
|
|
26142
|
+
channel: ch,
|
|
26143
|
+
payloadXml: ensureXmlHeader(xml),
|
|
26144
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
26145
|
+
});
|
|
26146
|
+
}
|
|
26147
|
+
/**
|
|
26148
|
+
* SetIsp via Baichuan (cmdId=25 for image side, cmdId=297 for
|
|
26149
|
+
* dayNightThreshold). Patches the `<InputAdvanceCfg>` block:
|
|
26150
|
+
* `DayNight/mode`, `Exposure/mode`, `binning_mode`, `hdrSwitch`.
|
|
26151
|
+
* Mirrors reolink_aio's `SetIsp`.
|
|
26152
|
+
*
|
|
26153
|
+
* @param channel - Channel number (0-based)
|
|
26154
|
+
* @param patch - Fields to update. `dayNight` accepts the camera's
|
|
26155
|
+
* raw enum (`color`, `auto`, `blackAndWhite`, …) — pass it as the
|
|
26156
|
+
* camera reports it (PascalCase / dotted forms get normalized
|
|
26157
|
+
* server-side).
|
|
26158
|
+
*/
|
|
26159
|
+
async setIsp(channel, patch, options) {
|
|
26160
|
+
const ch = this.normalizeChannel(channel);
|
|
26161
|
+
const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
|
|
26162
|
+
const wantsImageWrite = patch.dayNight !== void 0 || patch.exposure !== void 0 || patch.binningMode !== void 0 || patch.hdr !== void 0;
|
|
26163
|
+
if (wantsImageWrite) {
|
|
26164
|
+
let xml = await this.sendXml({
|
|
26165
|
+
cmdId: BC_CMD_ID_GET_VIDEO_INPUT,
|
|
26166
|
+
channel: ch,
|
|
26167
|
+
...timeoutOpts
|
|
26168
|
+
});
|
|
26169
|
+
if (patch.dayNight !== void 0) {
|
|
26170
|
+
const normalized = normalizeDayNightMode(patch.dayNight);
|
|
26171
|
+
xml = patchNestedTag(xml, "DayNight", "mode", normalized);
|
|
26172
|
+
}
|
|
26173
|
+
if (patch.exposure !== void 0) {
|
|
26174
|
+
xml = patchNestedTag(
|
|
26175
|
+
xml,
|
|
26176
|
+
"Exposure",
|
|
26177
|
+
"mode",
|
|
26178
|
+
patch.exposure.toLowerCase()
|
|
26179
|
+
);
|
|
26180
|
+
}
|
|
26181
|
+
if (patch.binningMode !== void 0) {
|
|
26182
|
+
xml = applyXmlTagPatch(xml, "binning_mode", patch.binningMode);
|
|
26183
|
+
}
|
|
26184
|
+
if (patch.hdr !== void 0) {
|
|
26185
|
+
xml = applyXmlTagPatch(xml, "hdrSwitch", patch.hdr);
|
|
26186
|
+
}
|
|
26187
|
+
await this.sendXml({
|
|
26188
|
+
cmdId: BC_CMD_ID_SET_VIDEO_INPUT,
|
|
26189
|
+
channel: ch,
|
|
26190
|
+
payloadXml: ensureXmlHeader(xml),
|
|
26191
|
+
...timeoutOpts
|
|
26192
|
+
});
|
|
26193
|
+
}
|
|
26194
|
+
if (patch.dayNightThreshold !== void 0) {
|
|
26195
|
+
let xml = await this.sendXml({
|
|
26196
|
+
cmdId: BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD,
|
|
26197
|
+
channel: ch,
|
|
26198
|
+
...timeoutOpts
|
|
26199
|
+
});
|
|
26200
|
+
xml = applyXmlTagPatch(xml, "cur", patch.dayNightThreshold);
|
|
26201
|
+
await this.sendXml({
|
|
26202
|
+
cmdId: BC_CMD_ID_SET_DAY_NIGHT_THRESHOLD,
|
|
26203
|
+
channel: ch,
|
|
26204
|
+
payloadXml: ensureXmlHeader(xml),
|
|
26205
|
+
...timeoutOpts
|
|
26206
|
+
});
|
|
26207
|
+
}
|
|
26208
|
+
}
|
|
26209
|
+
/**
|
|
26210
|
+
* GetIsp via Baichuan (cmdId=26). Convenience alias of
|
|
26211
|
+
* `getVideoInput()` so callers that switched from CGI keep the
|
|
26212
|
+
* familiar name. Both return the merged VideoInput +
|
|
26213
|
+
* InputAdvanceCfg blob.
|
|
26214
|
+
*/
|
|
26215
|
+
async getIsp(channel, options) {
|
|
26216
|
+
return this.getVideoInput(channel, options);
|
|
26217
|
+
}
|
|
26218
|
+
/** GetImage via Baichuan (cmdId=26). Same payload as `getIsp` —
|
|
26219
|
+
* Reolink merged VideoInput + InputAdvanceCfg under one cmdId. */
|
|
26220
|
+
async getImage(channel, options) {
|
|
26221
|
+
return this.getVideoInput(channel, options);
|
|
26222
|
+
}
|
|
26223
|
+
/**
|
|
26224
|
+
* GetIrLights via Baichuan (cmdId=208). Returns LedState block:
|
|
26225
|
+
* `IRLedBrightness`, `state` (ir on/off), `lightState` (status LED
|
|
26226
|
+
* open/close), `doorbellLightState`. Mirrors reolink_aio's
|
|
26227
|
+
* `get_status_led`.
|
|
26228
|
+
*/
|
|
26229
|
+
async getIrLights(channel, options) {
|
|
26230
|
+
const xml = await this.sendPcapDerivedSettingsGetXml({
|
|
26231
|
+
cmdId: BC_CMD_ID_GET_LED_STATE,
|
|
26232
|
+
...channel != null ? { channel } : {},
|
|
26233
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
26234
|
+
});
|
|
26235
|
+
return parseXmlFragmentToJson(xml);
|
|
26236
|
+
}
|
|
26237
|
+
/**
|
|
26238
|
+
* SetIrLights via Baichuan (cmdId=209, read via cmdId=208). Patches
|
|
26239
|
+
* IR LED + status LED + doorbell LED + IR brightness. Mirrors
|
|
26240
|
+
* reolink_aio's `set_status_led`.
|
|
26241
|
+
*
|
|
26242
|
+
* @param channel - Channel number (0-based)
|
|
26243
|
+
* @param patch - `irState` ("On" | "Off" | "Auto"), `lightState`
|
|
26244
|
+
* (status LED), `doorbellLightState`, `irBrightness` (0..255).
|
|
26245
|
+
* Camera-side accepts lowercase strings (`open`/`close`); the
|
|
26246
|
+
* helper normalizes from the friendly variants.
|
|
26247
|
+
*/
|
|
26248
|
+
async setIrLights(channel, patch, options) {
|
|
26249
|
+
const ch = this.normalizeChannel(channel);
|
|
26250
|
+
const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
|
|
26251
|
+
let xml = await this.sendXml({
|
|
26252
|
+
cmdId: BC_CMD_ID_GET_LED_STATE,
|
|
26253
|
+
channel: ch,
|
|
26254
|
+
...timeoutOpts
|
|
26255
|
+
});
|
|
26256
|
+
if (patch.lightState !== void 0) {
|
|
26257
|
+
xml = applyXmlTagPatch(
|
|
26258
|
+
xml,
|
|
26259
|
+
"lightState",
|
|
26260
|
+
patch.lightState === "On" ? "open" : "close"
|
|
26261
|
+
);
|
|
26262
|
+
}
|
|
26263
|
+
if (patch.doorbellLightState !== void 0) {
|
|
26264
|
+
xml = applyXmlTagPatch(
|
|
26265
|
+
xml,
|
|
26266
|
+
"doorbellLightState",
|
|
26267
|
+
normalizeOpenClose(patch.doorbellLightState)
|
|
26268
|
+
);
|
|
26269
|
+
}
|
|
26270
|
+
if (patch.irState !== void 0) {
|
|
26271
|
+
const v = String(patch.irState);
|
|
26272
|
+
const out = v === "Off" ? "close" : v.toLowerCase();
|
|
26273
|
+
xml = applyXmlTagPatch(xml, "state", out);
|
|
26274
|
+
}
|
|
26275
|
+
if (patch.irBrightness !== void 0) {
|
|
26276
|
+
xml = applyXmlTagPatch(xml, "IRLedBrightness", patch.irBrightness);
|
|
26277
|
+
}
|
|
26278
|
+
await this.sendXml({
|
|
26279
|
+
cmdId: BC_CMD_ID_SET_LED_STATE,
|
|
26280
|
+
channel: ch,
|
|
26281
|
+
payloadXml: ensureXmlHeader(xml),
|
|
26282
|
+
...timeoutOpts
|
|
26283
|
+
});
|
|
26284
|
+
}
|
|
26285
|
+
/**
|
|
26286
|
+
* SetAudioCfg via Baichuan (cmdId=265, read via cmdId=264). Patches
|
|
26287
|
+
* volume / talk-and-reply / visitor settings. Mirrors reolink_aio's
|
|
26288
|
+
* `SetAudioCfg`.
|
|
26289
|
+
*/
|
|
26290
|
+
async setAudioCfg(channel, patch, options) {
|
|
26291
|
+
const ch = this.normalizeChannel(channel);
|
|
26292
|
+
const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
|
|
26293
|
+
let xml = await this.sendXml({
|
|
26294
|
+
cmdId: BC_CMD_ID_GET_AUDIO_CFG,
|
|
26295
|
+
channel: ch,
|
|
26296
|
+
...timeoutOpts
|
|
26297
|
+
});
|
|
26298
|
+
xml = applyXmlTagPatch(xml, "volume", patch.volume);
|
|
26299
|
+
xml = applyXmlTagPatch(
|
|
26300
|
+
xml,
|
|
26301
|
+
"talkAndReplyVolume",
|
|
26302
|
+
patch.talkAndReplyVolume
|
|
26303
|
+
);
|
|
26304
|
+
xml = applyXmlTagPatch(xml, "visitorVolume", patch.visitorVolume);
|
|
26305
|
+
xml = applyXmlTagPatch(xml, "visitorLoudspeaker", patch.visitorLoudspeaker);
|
|
26306
|
+
await this.sendXml({
|
|
26307
|
+
cmdId: BC_CMD_ID_SET_AUDIO_CFG,
|
|
26308
|
+
channel: ch,
|
|
26309
|
+
payloadXml: ensureXmlHeader(xml),
|
|
26310
|
+
...timeoutOpts
|
|
26311
|
+
});
|
|
26312
|
+
}
|
|
26313
|
+
/**
|
|
26314
|
+
* GetMask (privacy mask) via Baichuan (cmdId=52). Returns the
|
|
26315
|
+
* `<Shelter>` block — `enable` flag + `shelterList`. Mirrors
|
|
26316
|
+
* reolink_aio's `GetMask`.
|
|
26317
|
+
*/
|
|
26318
|
+
async getMask(channel, options) {
|
|
26319
|
+
const xml = await this.sendPcapDerivedSettingsGetXml({
|
|
26320
|
+
cmdId: BC_CMD_ID_GET_PRIVACY_MASK,
|
|
26321
|
+
...channel != null ? { channel } : {},
|
|
26322
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
26323
|
+
});
|
|
26324
|
+
return parseXmlFragmentToJson(xml);
|
|
26325
|
+
}
|
|
26326
|
+
/**
|
|
26327
|
+
* SetMask (privacy mask) via Baichuan (cmdId=53, read via cmdId=52).
|
|
26328
|
+
* Toggles the `<Shelter><enable>` flag. Mirrors reolink_aio's
|
|
26329
|
+
* `SetMask` (which only touches enable too — shelter zone editing
|
|
26330
|
+
* goes through a separate flow).
|
|
26331
|
+
*/
|
|
26332
|
+
async setMask(channel, patch, options) {
|
|
26333
|
+
const ch = this.normalizeChannel(channel);
|
|
26334
|
+
const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
|
|
26335
|
+
let xml = await this.sendXml({
|
|
26336
|
+
cmdId: BC_CMD_ID_GET_PRIVACY_MASK,
|
|
26337
|
+
channel: ch,
|
|
26338
|
+
...timeoutOpts
|
|
26339
|
+
});
|
|
26340
|
+
if (patch.enable !== void 0) {
|
|
26341
|
+
xml = applyXmlTagPatch(xml, "enable", patch.enable ? 1 : 0);
|
|
26342
|
+
}
|
|
26343
|
+
await this.sendXml({
|
|
26344
|
+
cmdId: BC_CMD_ID_SET_PRIVACY_MASK,
|
|
26345
|
+
channel: ch,
|
|
26346
|
+
payloadXml: ensureXmlHeader(xml),
|
|
26347
|
+
...timeoutOpts
|
|
26348
|
+
});
|
|
26349
|
+
}
|
|
26350
|
+
/**
|
|
26351
|
+
* GetAudioNoise via Baichuan (cmdId=439). Reads `enable` + `level`
|
|
26352
|
+
* from the aiDenoise block. Mirrors reolink_aio's `GetAudioNoise`.
|
|
26353
|
+
*
|
|
26354
|
+
* Note: `getAiDenoise` already returns the same payload typed as
|
|
26355
|
+
* `AiDenoiseConfig`. This getter exists for naming parity with
|
|
26356
|
+
* reolink_aio + the reolink CGI.
|
|
26357
|
+
*/
|
|
26358
|
+
async getAudioNoise(channel, options) {
|
|
26359
|
+
const xml = await this.sendPcapDerivedSettingsGetXml({
|
|
26360
|
+
cmdId: BC_CMD_ID_GET_AI_DENOISE,
|
|
26361
|
+
...channel != null ? { channel } : {},
|
|
26362
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
26363
|
+
});
|
|
26364
|
+
return parseXmlFragmentToJson(xml);
|
|
26365
|
+
}
|
|
26366
|
+
/**
|
|
26367
|
+
* SetAudioNoise via Baichuan (cmdId=440, read via cmdId=439).
|
|
26368
|
+
* Mirrors reolink_aio's `SetAudioNoise` — `level <= 0` flips the
|
|
26369
|
+
* enable flag off; positive values turn it on and update the level.
|
|
26370
|
+
*/
|
|
26371
|
+
async setAudioNoise(channel, level, options) {
|
|
26372
|
+
const ch = this.normalizeChannel(channel);
|
|
26373
|
+
const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
|
|
26374
|
+
let xml = await this.sendXml({
|
|
26375
|
+
cmdId: BC_CMD_ID_GET_AI_DENOISE,
|
|
26376
|
+
channel: ch,
|
|
26377
|
+
...timeoutOpts
|
|
26378
|
+
});
|
|
26379
|
+
xml = applyXmlTagPatch(xml, "enable", level > 0 ? 1 : 0);
|
|
26380
|
+
if (level > 0) {
|
|
26381
|
+
xml = applyXmlTagPatch(xml, "level", level);
|
|
26382
|
+
}
|
|
26383
|
+
await this.sendXml({
|
|
26384
|
+
cmdId: BC_CMD_ID_SET_AI_DENOISE,
|
|
26385
|
+
channel: ch,
|
|
26386
|
+
payloadXml: ensureXmlHeader(xml),
|
|
26387
|
+
...timeoutOpts
|
|
26388
|
+
});
|
|
26389
|
+
}
|
|
26390
|
+
/**
|
|
26391
|
+
* GetAutoFocus via Baichuan (cmdId=224). Returns the `<AutoFocus>`
|
|
26392
|
+
* block — only `disable` (0 = AF on, 1 = AF off). Mirrors
|
|
26393
|
+
* reolink_aio's `GetAutoFocus`.
|
|
26394
|
+
*/
|
|
26395
|
+
async getAutoFocus(channel, options) {
|
|
26396
|
+
const ch = this.normalizeChannel(channel);
|
|
26397
|
+
const xml = await this.sendPcapDerivedSettingsGetXml({
|
|
26398
|
+
cmdId: BC_CMD_ID_GET_AUTO_FOCUS,
|
|
26399
|
+
channel: ch,
|
|
26400
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
26401
|
+
});
|
|
26402
|
+
return parseXmlFragmentToJson(xml);
|
|
26403
|
+
}
|
|
26404
|
+
/**
|
|
26405
|
+
* SetAutoFocus via Baichuan (cmdId=225). Mirrors reolink_aio's
|
|
26406
|
+
* `SetAutoFocus`. Note: write-only command — the payload is built
|
|
26407
|
+
* from scratch (no read-modify-write needed).
|
|
26408
|
+
*/
|
|
26409
|
+
async setAutoFocus(channel, disable, options) {
|
|
26410
|
+
const ch = this.normalizeChannel(channel);
|
|
26411
|
+
const disableVal = disable ? 1 : 0;
|
|
26412
|
+
const payloadXml = `<?xml version="1.0" encoding="UTF-8" ?>
|
|
26413
|
+
<body>
|
|
26414
|
+
<AutoFocus version="1.1">
|
|
26415
|
+
<channelId>${ch}</channelId>
|
|
26416
|
+
<disable>${disableVal}</disable>
|
|
26417
|
+
</AutoFocus>
|
|
26418
|
+
</body>`;
|
|
26419
|
+
await this.sendXml({
|
|
26420
|
+
cmdId: BC_CMD_ID_SET_AUTO_FOCUS,
|
|
26421
|
+
channel: ch,
|
|
26422
|
+
payloadXml,
|
|
26423
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
26424
|
+
});
|
|
26425
|
+
}
|
|
25582
26426
|
/**
|
|
25583
26427
|
* Passthrough to ReolinkCgiApi.getAllChannelsBatteryInfo.
|
|
25584
26428
|
* Fetches battery info for all channels via CGI (merged with channel status sleep flag).
|
|
@@ -26747,8 +27591,8 @@ ${scheduleItems}
|
|
|
26747
27591
|
);
|
|
26748
27592
|
let args;
|
|
26749
27593
|
if (useMpegTsMuxer) {
|
|
26750
|
-
MpegTsMuxer
|
|
26751
|
-
tsMuxer
|
|
27594
|
+
tsMuxer = new MpegTsMuxer({ videoType, includeAudio: false });
|
|
27595
|
+
tsMuxer.reset();
|
|
26752
27596
|
args = [
|
|
26753
27597
|
"-hide_banner",
|
|
26754
27598
|
"-loglevel",
|
|
@@ -26912,7 +27756,7 @@ ${scheduleItems}
|
|
|
26912
27756
|
startFfmpeg(videoType);
|
|
26913
27757
|
frameCount++;
|
|
26914
27758
|
if (useMpegTsMuxer && tsMuxer) {
|
|
26915
|
-
const tsData = tsMuxer.
|
|
27759
|
+
const tsData = tsMuxer.muxVideo(data, microseconds, isKeyframe);
|
|
26916
27760
|
input.write(tsData);
|
|
26917
27761
|
} else {
|
|
26918
27762
|
if (videoType === "H264") input.write(H264_AUD);
|
|
@@ -27201,8 +28045,8 @@ ${scheduleItems}
|
|
|
27201
28045
|
logger?.log?.(
|
|
27202
28046
|
`[createRecordingReplayHlsSession] Starting ffmpeg HLS with videoType=${videoType}, transcode=${needsTranscode}, hlsTime=${hlsSegmentDuration}s, fileName=${params.fileName}`
|
|
27203
28047
|
);
|
|
27204
|
-
MpegTsMuxer
|
|
27205
|
-
tsMuxer
|
|
28048
|
+
tsMuxer = new MpegTsMuxer({ videoType, includeAudio: false });
|
|
28049
|
+
tsMuxer.reset();
|
|
27206
28050
|
const args = [
|
|
27207
28051
|
"-hide_banner",
|
|
27208
28052
|
"-loglevel",
|
|
@@ -27367,7 +28211,7 @@ ${scheduleItems}
|
|
|
27367
28211
|
startFfmpeg(videoType);
|
|
27368
28212
|
frameCount++;
|
|
27369
28213
|
if (tsMuxer) {
|
|
27370
|
-
const tsData = tsMuxer.
|
|
28214
|
+
const tsData = tsMuxer.muxVideo(data, microseconds, isKeyframe);
|
|
27371
28215
|
input.write(tsData);
|
|
27372
28216
|
}
|
|
27373
28217
|
if (frameCount === 1) {
|
|
@@ -28202,9 +29046,10 @@ async function autoDetectDeviceType(inputs) {
|
|
|
28202
29046
|
const msg = fmtErr(e);
|
|
28203
29047
|
return msg.includes("Not running") || msg.includes("Baichuan UDP stream closed") || msg.includes("Baichuan socket closed") || msg.includes("ETIMEDOUT") || msg.toLowerCase().includes("timeout");
|
|
28204
29048
|
};
|
|
28205
|
-
const withRetries = async (label, max, op, shouldRetry) => {
|
|
29049
|
+
const withRetries = async (label, max, op, shouldRetry, isAborted) => {
|
|
28206
29050
|
let lastErr;
|
|
28207
29051
|
for (let attempt = 1; attempt <= max; attempt++) {
|
|
29052
|
+
if (isAborted?.()) throw new Error(`${label}: aborted (race won by another method)`);
|
|
28208
29053
|
try {
|
|
28209
29054
|
if (attempt > 1) {
|
|
28210
29055
|
logger?.log?.(`[AutoDetect] ${label}: retry ${attempt}/${max}...`);
|
|
@@ -28213,7 +29058,7 @@ async function autoDetectDeviceType(inputs) {
|
|
|
28213
29058
|
} catch (e) {
|
|
28214
29059
|
lastErr = e;
|
|
28215
29060
|
const msg = fmtErr(e);
|
|
28216
|
-
const retryable = attempt < max && shouldRetry(e);
|
|
29061
|
+
const retryable = attempt < max && !isAborted?.() && shouldRetry(e);
|
|
28217
29062
|
logger?.log?.(
|
|
28218
29063
|
`[AutoDetect] ${label} attempt ${attempt}/${max} failed: ${msg}${retryable ? " (will retry)" : ""}`
|
|
28219
29064
|
);
|
|
@@ -28223,6 +29068,31 @@ async function autoDetectDeviceType(inputs) {
|
|
|
28223
29068
|
}
|
|
28224
29069
|
throw lastErr instanceof Error ? lastErr : new Error(String(lastErr ?? `${label} failed`));
|
|
28225
29070
|
};
|
|
29071
|
+
const runUdpMethodsParallel = async (methods, loginAndDetect, errorPrefix) => {
|
|
29072
|
+
let raceWon = false;
|
|
29073
|
+
const methodErrors = /* @__PURE__ */ new Map();
|
|
29074
|
+
try {
|
|
29075
|
+
return await Promise.any(
|
|
29076
|
+
methods.map(async (m) => {
|
|
29077
|
+
try {
|
|
29078
|
+
const result = await loginAndDetect(m, () => raceWon);
|
|
29079
|
+
raceWon = true;
|
|
29080
|
+
return result;
|
|
29081
|
+
} catch (e) {
|
|
29082
|
+
if (!raceWon) methodErrors.set(m, fmtErr(e));
|
|
29083
|
+
logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${fmtErr(e)}`);
|
|
29084
|
+
throw e;
|
|
29085
|
+
}
|
|
29086
|
+
})
|
|
29087
|
+
);
|
|
29088
|
+
} catch (e) {
|
|
29089
|
+
if (e instanceof AggregateError) {
|
|
29090
|
+
const msgs = methods.map((m) => `${m}: ${methodErrors.get(m) ?? "unknown"}`);
|
|
29091
|
+
throw new Error(`${errorPrefix} ${msgs.join(" | ")}`);
|
|
29092
|
+
}
|
|
29093
|
+
throw e;
|
|
29094
|
+
}
|
|
29095
|
+
};
|
|
28226
29096
|
const effectiveUid = normalizeUid(uid);
|
|
28227
29097
|
logger?.log?.(`[AutoDetect] Pinging ${host}...`);
|
|
28228
29098
|
const isReachable = await pingHost(host);
|
|
@@ -28252,9 +29122,9 @@ async function autoDetectDeviceType(inputs) {
|
|
|
28252
29122
|
normalizedUid = normalizedDiscovered;
|
|
28253
29123
|
}
|
|
28254
29124
|
const methodsToTry = inputs.udpDiscoveryMethod ? [inputs.udpDiscoveryMethod] : ["local-direct", "local-broadcast", "remote", "relay", "map"];
|
|
28255
|
-
|
|
28256
|
-
|
|
28257
|
-
|
|
29125
|
+
return await runUdpMethodsParallel(
|
|
29126
|
+
methodsToTry,
|
|
29127
|
+
async (m, isAborted) => {
|
|
28258
29128
|
logger?.log?.(`[AutoDetect] Trying UDP discovery method: ${m}...`);
|
|
28259
29129
|
const udpApi = await withRetries(
|
|
28260
29130
|
`UDP(${m})`,
|
|
@@ -28277,11 +29147,14 @@ async function autoDetectDeviceType(inputs) {
|
|
|
28277
29147
|
throw e;
|
|
28278
29148
|
}
|
|
28279
29149
|
},
|
|
28280
|
-
shouldRetryUdp
|
|
29150
|
+
shouldRetryUdp,
|
|
29151
|
+
isAborted
|
|
28281
29152
|
);
|
|
28282
|
-
const deviceInfo = await
|
|
28283
|
-
|
|
28284
|
-
|
|
29153
|
+
const [deviceInfo, capabilities, hostNetworkInfo] = await Promise.all([
|
|
29154
|
+
udpApi.getInfo(),
|
|
29155
|
+
udpApi.getDeviceCapabilities(),
|
|
29156
|
+
udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0)
|
|
29157
|
+
]);
|
|
28285
29158
|
const channelNum = capabilities?.support?.channelNum ?? 1;
|
|
28286
29159
|
const model = deviceInfo.type?.trim();
|
|
28287
29160
|
const normalizedModel = model ? model.trim() : void 0;
|
|
@@ -28320,14 +29193,8 @@ async function autoDetectDeviceType(inputs) {
|
|
|
28320
29193
|
channelNum: 1,
|
|
28321
29194
|
api: udpApi
|
|
28322
29195
|
};
|
|
28323
|
-
}
|
|
28324
|
-
|
|
28325
|
-
udpErrors.push(`${m}: ${msg}`);
|
|
28326
|
-
logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${msg}`);
|
|
28327
|
-
}
|
|
28328
|
-
}
|
|
28329
|
-
throw new Error(
|
|
28330
|
-
`Forced UDP autodetect failed for all methods. ${udpErrors.join(" | ")}`
|
|
29196
|
+
},
|
|
29197
|
+
"Forced UDP autodetect failed for all methods."
|
|
28331
29198
|
);
|
|
28332
29199
|
}
|
|
28333
29200
|
let tcpApi;
|
|
@@ -28380,54 +29247,57 @@ async function autoDetectDeviceType(inputs) {
|
|
|
28380
29247
|
}
|
|
28381
29248
|
return void 0;
|
|
28382
29249
|
};
|
|
28383
|
-
const infoProbe = await
|
|
28384
|
-
|
|
28385
|
-
|
|
29250
|
+
const [infoProbe, supportProbe] = await Promise.all([
|
|
29251
|
+
runProbeVariants(
|
|
29252
|
+
"getInfo",
|
|
29253
|
+
[
|
|
29254
|
+
{
|
|
29255
|
+
variant: "cmd80 class=0x6414",
|
|
29256
|
+
op: () => api.getInfo(void 0, {
|
|
29257
|
+
timeoutMs: 2500,
|
|
29258
|
+
messageClass: BC_CLASS_MODERN_24
|
|
29259
|
+
})
|
|
29260
|
+
},
|
|
29261
|
+
{
|
|
29262
|
+
variant: "cmd80 class=0x6614",
|
|
29263
|
+
op: () => api.getInfo(void 0, {
|
|
29264
|
+
timeoutMs: 3e3,
|
|
29265
|
+
messageClass: BC_CLASS_MODERN_20
|
|
29266
|
+
})
|
|
29267
|
+
},
|
|
29268
|
+
{
|
|
29269
|
+
variant: "cmd318(ch0) class=0x6414",
|
|
29270
|
+
op: () => api.getInfo(0, {
|
|
29271
|
+
timeoutMs: 3e3,
|
|
29272
|
+
messageClass: BC_CLASS_MODERN_24
|
|
29273
|
+
})
|
|
29274
|
+
},
|
|
29275
|
+
{
|
|
29276
|
+
variant: "cmd318(ch0) class=0x6614",
|
|
29277
|
+
op: () => api.getInfo(0, {
|
|
29278
|
+
timeoutMs: 3500,
|
|
29279
|
+
messageClass: BC_CLASS_MODERN_20
|
|
29280
|
+
})
|
|
29281
|
+
}
|
|
29282
|
+
]
|
|
29283
|
+
),
|
|
29284
|
+
// Support probes (cmd 199). Some firmwares may not support it or are slow.
|
|
29285
|
+
runProbeVariants("getSupportInfo", [
|
|
28386
29286
|
{
|
|
28387
|
-
variant: "
|
|
28388
|
-
op: () => api.
|
|
29287
|
+
variant: "cmd199 class=0x6414",
|
|
29288
|
+
op: () => api.getSupportInfo({
|
|
28389
29289
|
timeoutMs: 2500,
|
|
28390
29290
|
messageClass: BC_CLASS_MODERN_24
|
|
28391
29291
|
})
|
|
28392
29292
|
},
|
|
28393
29293
|
{
|
|
28394
|
-
variant: "
|
|
28395
|
-
op: () => api.
|
|
28396
|
-
timeoutMs: 3e3,
|
|
28397
|
-
messageClass: BC_CLASS_MODERN_20
|
|
28398
|
-
})
|
|
28399
|
-
},
|
|
28400
|
-
{
|
|
28401
|
-
variant: "cmd318(ch0) class=0x6414",
|
|
28402
|
-
op: () => api.getInfo(0, {
|
|
28403
|
-
timeoutMs: 3e3,
|
|
28404
|
-
messageClass: BC_CLASS_MODERN_24
|
|
28405
|
-
})
|
|
28406
|
-
},
|
|
28407
|
-
{
|
|
28408
|
-
variant: "cmd318(ch0) class=0x6614",
|
|
28409
|
-
op: () => api.getInfo(0, {
|
|
29294
|
+
variant: "cmd199 class=0x6614",
|
|
29295
|
+
op: () => api.getSupportInfo({
|
|
28410
29296
|
timeoutMs: 3500,
|
|
28411
29297
|
messageClass: BC_CLASS_MODERN_20
|
|
28412
29298
|
})
|
|
28413
29299
|
}
|
|
28414
|
-
]
|
|
28415
|
-
);
|
|
28416
|
-
const supportProbe = await runProbeVariants("getSupportInfo", [
|
|
28417
|
-
{
|
|
28418
|
-
variant: "cmd199 class=0x6414",
|
|
28419
|
-
op: () => api.getSupportInfo({
|
|
28420
|
-
timeoutMs: 2500,
|
|
28421
|
-
messageClass: BC_CLASS_MODERN_24
|
|
28422
|
-
})
|
|
28423
|
-
},
|
|
28424
|
-
{
|
|
28425
|
-
variant: "cmd199 class=0x6614",
|
|
28426
|
-
op: () => api.getSupportInfo({
|
|
28427
|
-
timeoutMs: 3500,
|
|
28428
|
-
messageClass: BC_CLASS_MODERN_20
|
|
28429
|
-
})
|
|
28430
|
-
}
|
|
29300
|
+
])
|
|
28431
29301
|
]);
|
|
28432
29302
|
const deviceInfo = infoProbe?.value;
|
|
28433
29303
|
const support = supportProbe?.value;
|
|
@@ -28519,9 +29389,11 @@ async function autoDetectDeviceType(inputs) {
|
|
|
28519
29389
|
}
|
|
28520
29390
|
try {
|
|
28521
29391
|
const detectOverUdpApi = async (udpApi, udpDiscoveryMethod) => {
|
|
28522
|
-
const deviceInfo = await
|
|
28523
|
-
|
|
28524
|
-
|
|
29392
|
+
const [deviceInfo, capabilities, hostNetworkInfo] = await Promise.all([
|
|
29393
|
+
udpApi.getInfo(),
|
|
29394
|
+
udpApi.getDeviceCapabilities(),
|
|
29395
|
+
udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0)
|
|
29396
|
+
]);
|
|
28525
29397
|
const channelNum = capabilities?.support?.channelNum ?? 1;
|
|
28526
29398
|
const model = deviceInfo.type?.trim();
|
|
28527
29399
|
const normalizedModel = model ? model.trim() : void 0;
|
|
@@ -28565,21 +29437,17 @@ async function autoDetectDeviceType(inputs) {
|
|
|
28565
29437
|
};
|
|
28566
29438
|
};
|
|
28567
29439
|
const methodsToTry = ["local-direct", "local-broadcast", "remote", "relay", "map"];
|
|
28568
|
-
const
|
|
28569
|
-
|
|
28570
|
-
|
|
29440
|
+
const viableMethods = normalizedUid ? methodsToTry : methodsToTry.filter((m) => m === "local-direct" || m === "local-broadcast");
|
|
29441
|
+
return await runUdpMethodsParallel(
|
|
29442
|
+
viableMethods,
|
|
29443
|
+
async (m, isAborted) => {
|
|
28571
29444
|
logger?.log?.(`[AutoDetect] Trying UDP discovery method: ${m}...`);
|
|
28572
29445
|
const udpApi = await withRetries(
|
|
28573
29446
|
`UDP(${m})`,
|
|
28574
29447
|
maxRetries,
|
|
28575
29448
|
async (attempt) => {
|
|
28576
|
-
const apiInputs = {
|
|
28577
|
-
|
|
28578
|
-
udpDiscoveryMethod: m
|
|
28579
|
-
};
|
|
28580
|
-
if (normalizedUid) {
|
|
28581
|
-
apiInputs.uid = normalizedUid;
|
|
28582
|
-
}
|
|
29449
|
+
const apiInputs = { ...inputs, udpDiscoveryMethod: m };
|
|
29450
|
+
if (normalizedUid) apiInputs.uid = normalizedUid;
|
|
28583
29451
|
const api = createBaichuanApi(apiInputs, "udp");
|
|
28584
29452
|
try {
|
|
28585
29453
|
await api.login();
|
|
@@ -28594,20 +29462,12 @@ async function autoDetectDeviceType(inputs) {
|
|
|
28594
29462
|
throw e;
|
|
28595
29463
|
}
|
|
28596
29464
|
},
|
|
28597
|
-
shouldRetryUdp
|
|
29465
|
+
shouldRetryUdp,
|
|
29466
|
+
isAborted
|
|
28598
29467
|
);
|
|
28599
|
-
return
|
|
28600
|
-
}
|
|
28601
|
-
|
|
28602
|
-
udpErrors.push(`${m}: ${msg}`);
|
|
28603
|
-
try {
|
|
28604
|
-
} catch {
|
|
28605
|
-
}
|
|
28606
|
-
logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${msg}`);
|
|
28607
|
-
}
|
|
28608
|
-
}
|
|
28609
|
-
throw new Error(
|
|
28610
|
-
`UDP discovery failed for all methods. ${udpErrors.join(" | ")}`
|
|
29468
|
+
return detectOverUdpApi(udpApi, m);
|
|
29469
|
+
},
|
|
29470
|
+
"UDP discovery failed for all methods."
|
|
28611
29471
|
);
|
|
28612
29472
|
} catch (udpError) {
|
|
28613
29473
|
logger?.log?.(
|