@apocaliss92/nodelink-js 0.4.11 → 0.4.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/BaichuanVideoStream-PHQG4A2L.js +7 -0
- package/dist/{DiagnosticsTools-RNIDFEJK.js → DiagnosticsTools-HGJGVQXZ.js} +3 -2
- package/dist/DiagnosticsTools-HGJGVQXZ.js.map +1 -0
- package/dist/{chunk-EDLMKBG2.js → chunk-IJG45AOT.js} +151 -2733
- package/dist/chunk-IJG45AOT.js.map +1 -0
- package/dist/{chunk-HGQ53FB3.js → chunk-ND73IJIB.js} +765 -45
- package/dist/chunk-ND73IJIB.js.map +1 -0
- package/dist/chunk-W2ANCJVM.js +2703 -0
- package/dist/chunk-W2ANCJVM.js.map +1 -0
- package/dist/cli/rtsp-server.cjs +834 -10
- package/dist/cli/rtsp-server.cjs.map +1 -1
- package/dist/cli/rtsp-server.js +3 -2
- package/dist/cli/rtsp-server.js.map +1 -1
- package/dist/index.cjs +1322 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +708 -142
- package/dist/index.d.ts +601 -20
- package/dist/index.js +527 -85
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
- package/dist/chunk-EDLMKBG2.js.map +0 -1
- package/dist/chunk-HGQ53FB3.js.map +0 -1
- /package/dist/{DiagnosticsTools-RNIDFEJK.js.map → BaichuanVideoStream-PHQG4A2L.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -34,7 +34,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
34
34
|
function bcHeaderHasPayloadOffset(messageClass) {
|
|
35
35
|
return messageClass === BC_CLASS_MODERN_24 || messageClass === BC_CLASS_MODERN_24_ALT || messageClass === BC_CLASS_FILE_DOWNLOAD;
|
|
36
36
|
}
|
|
37
|
-
var BC_TCP_DEFAULT_PORT, BC_MAGIC, BC_MAGIC_REV, BC_XML_KEY, BC_AES_IV, BC_CLASS_LEGACY, BC_CLASS_MODERN_20, BC_CLASS_MODERN_24, BC_CLASS_MODERN_24_ALT, BC_CLASS_FILE_DOWNLOAD, BC_CMD_ID_LOGIN, BC_CMD_ID_LOGOUT, BC_CMD_ID_VIDEO, BC_CMD_ID_VIDEO_STOP, BC_CMD_ID_FILE_INFO_LIST_REPLAY, BC_CMD_ID_FILE_INFO_LIST_STOP, BC_CMD_ID_FILE_INFO_LIST_DL_VIDEO, BC_CMD_ID_FILE_INFO_LIST_DOWNLOAD, BC_CMD_ID_FILE_INFO_LIST_OPEN, BC_CMD_ID_FILE_INFO_LIST_GET, BC_CMD_ID_FILE_INFO_LIST_CLOSE, BC_CMD_ID_FIND_REC_VIDEO_OPEN, BC_CMD_ID_FIND_REC_VIDEO_GET, BC_CMD_ID_FIND_REC_VIDEO_CLOSE, BC_CMD_ID_COVER_PREVIEW, BC_CMD_ID_COVER_STANDALONE_458, BC_CMD_ID_COVER_STANDALONE_459, BC_CMD_ID_COVER_STANDALONE_460, BC_CMD_ID_COVER_STANDALONE_461, BC_CMD_ID_COVER_STANDALONE_462, BC_CMD_ID_COVER_RESPONSE, BC_CMD_ID_TALK_ABILITY, BC_CMD_ID_TALK_RESET, BC_CMD_ID_TALK_CONFIG, BC_CMD_ID_TALK, BC_CMD_ID_PTZ_CONTROL, BC_CMD_ID_PTZ_CONTROL_PRESET, BC_CMD_ID_GET_PTZ_PRESET, BC_CMD_ID_GET_PTZ_POSITION, BC_CMD_ID_GET_ZOOM_FOCUS, BC_CMD_ID_SET_ZOOM_FOCUS, BC_CMD_ID_GET_BATTERY_INFO_LIST, BC_CMD_ID_GET_BATTERY_INFO, BC_CMD_ID_UDP_KEEP_ALIVE, BC_CMD_ID_GET_PIR_INFO, BC_CMD_ID_SET_PIR_INFO, BC_CMD_ID_GET_MOTION_ALARM, BC_CMD_ID_SET_MOTION_ALARM, BC_CMD_ID_ALARM_EVENT_LIST, BC_CMD_ID_GET_AI_ALARM, BC_CMD_ID_SET_AI_ALARM, BC_CMD_ID_GET_AUDIO_ALARM, BC_CMD_ID_AUDIO_ALARM_PLAY, BC_CMD_ID_GET_WHITE_LED, BC_CMD_ID_SET_WHITE_LED_STATE, BC_CMD_ID_SET_WHITE_LED_TASK, BC_CMD_ID_FLOODLIGHT_STATUS_LIST, BC_CMD_ID_ABILITY_INFO, BC_CMD_ID_SUPPORT, BC_CMD_ID_PING, BC_CMD_ID_CHANNEL_INFO_ALL, BC_CMD_ID_GET_OSD_DATETIME, BC_CMD_ID_GET_RECORD_CFG, BC_CMD_ID_GET_ABILITY_SUPPORT, BC_CMD_ID_GET_FTP_TASK, BC_CMD_ID_GET_RECORD, BC_CMD_ID_GET_HDD_INFO_LIST, BC_CMD_ID_GET_WIFI_SIGNAL, BC_CMD_ID_GET_WIFI, BC_CMD_ID_GET_ONLINE_USER_LIST, BC_CMD_ID_GET_DAY_RECORDS, BC_CMD_ID_GET_STREAM_INFO_LIST, BC_CMD_ID_GET_LED_STATE, BC_CMD_ID_GET_EMAIL_TASK, BC_CMD_ID_GET_AUDIO_TASK, BC_CMD_ID_GET_AUDIO_CFG, BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD, BC_CMD_ID_GET_TIMELAPSE_CFG, BC_CMD_ID_GET_AI_DENOISE, BC_CMD_ID_GET_KIT_AP_CFG, BC_CMD_ID_GET_REC_ENC_CFG, BC_CMD_ID_GET_ACCESS_USER_LIST, BC_CMD_ID_GET_SLEEP_STATE, BC_CMD_ID_GET_VIDEO_INPUT, BC_CMD_ID_GET_SYSTEM_GENERAL, BC_CMD_ID_GET_SUPPORT, BC_CMD_ID_GET_AI_CFG, BC_CMD_ID_SET_AI_CFG, BC_CMD_ID_GET_SIREN_STATUS, BC_CMD_ID_SET_AUDIO_TASK, BC_CMD_ID_SET_VIDEO_INPUT, BC_CMD_ID_SET_DAY_NIGHT_THRESHOLD, BC_CMD_ID_GET_ENC, BC_CMD_ID_SET_ENC, BC_CMD_ID_GET_PRIVACY_MASK, BC_CMD_ID_SET_PRIVACY_MASK, BC_CMD_ID_SET_AI_DENOISE, BC_CMD_ID_SET_LED_STATE, BC_CMD_ID_SET_AUDIO_CFG, BC_CMD_ID_SET_RECORD, BC_CMD_ID_SET_RECORD_CFG, BC_CMD_ID_SET_EMAIL_TASK, BC_CMD_ID_GET_PUSH_TASK, BC_CMD_ID_SET_PUSH_TASK, BC_CMD_ID_GET_AUTO_FOCUS, BC_CMD_ID_SET_AUTO_FOCUS, BC_CMD_ID_CMD_123, BC_CMD_ID_CMD_209, BC_CMD_ID_CMD_265, BC_CMD_ID_CMD_440, BC_CMD_ID_PUSH_VIDEO_INPUT, BC_CMD_ID_PUSH_SERIAL, BC_CMD_ID_PUSH_NET_INFO, BC_CMD_ID_PUSH_DINGDONG_LIST, BC_CMD_ID_PUSH_SLEEP_STATUS, BC_CMD_ID_PUSH_COORDINATE_POINT_LIST, BC_CMD_ID_DING_DONG_CTRL, BC_CMD_ID_GET_DING_DONG_LIST, BC_CMD_ID_DING_DONG_OPT, BC_CMD_ID_GET_DING_DONG_CFG, BC_CMD_ID_SET_DING_DONG_CFG, BC_CMD_ID_QUICK_REPLY_PLAY, BC_CMD_ID_GET_DING_DONG_SILENT, BC_CMD_ID_SET_DING_DONG_SILENT;
|
|
37
|
+
var BC_TCP_DEFAULT_PORT, BC_MAGIC, BC_MAGIC_REV, BC_XML_KEY, BC_AES_IV, BC_CLASS_LEGACY, BC_CLASS_MODERN_20, BC_CLASS_MODERN_24, BC_CLASS_MODERN_24_ALT, BC_CLASS_FILE_DOWNLOAD, BC_CMD_ID_LOGIN, BC_CMD_ID_LOGOUT, BC_CMD_ID_VIDEO, BC_CMD_ID_VIDEO_STOP, BC_CMD_ID_FILE_INFO_LIST_REPLAY, BC_CMD_ID_FILE_INFO_LIST_STOP, BC_CMD_ID_FILE_INFO_LIST_DL_VIDEO, BC_CMD_ID_FILE_INFO_LIST_DOWNLOAD, BC_CMD_ID_FILE_INFO_LIST_OPEN, BC_CMD_ID_FILE_INFO_LIST_GET, BC_CMD_ID_FILE_INFO_LIST_CLOSE, BC_CMD_ID_FIND_REC_VIDEO_OPEN, BC_CMD_ID_FIND_REC_VIDEO_GET, BC_CMD_ID_FIND_REC_VIDEO_CLOSE, BC_CMD_ID_COVER_PREVIEW, BC_CMD_ID_COVER_STANDALONE_458, BC_CMD_ID_COVER_STANDALONE_459, BC_CMD_ID_COVER_STANDALONE_460, BC_CMD_ID_COVER_STANDALONE_461, BC_CMD_ID_COVER_STANDALONE_462, BC_CMD_ID_COVER_RESPONSE, BC_CMD_ID_TALK_ABILITY, BC_CMD_ID_TALK_RESET, BC_CMD_ID_TALK_CONFIG, BC_CMD_ID_TALK, BC_CMD_ID_PTZ_CONTROL, BC_CMD_ID_PTZ_CONTROL_PRESET, BC_CMD_ID_GET_PTZ_PRESET, BC_CMD_ID_GET_PTZ_POSITION, BC_CMD_ID_GET_ZOOM_FOCUS, BC_CMD_ID_SET_ZOOM_FOCUS, BC_CMD_ID_GET_BATTERY_INFO_LIST, BC_CMD_ID_GET_BATTERY_INFO, BC_CMD_ID_UDP_KEEP_ALIVE, BC_CMD_ID_GET_PIR_INFO, BC_CMD_ID_SET_PIR_INFO, BC_CMD_ID_GET_MOTION_ALARM, BC_CMD_ID_SET_MOTION_ALARM, BC_CMD_ID_ALARM_EVENT_LIST, BC_CMD_ID_GET_AI_ALARM, BC_CMD_ID_SET_AI_ALARM, BC_CMD_ID_GET_AUDIO_ALARM, BC_CMD_ID_AUDIO_ALARM_PLAY, BC_CMD_ID_GET_WHITE_LED, BC_CMD_ID_SET_WHITE_LED_STATE, BC_CMD_ID_SET_WHITE_LED_TASK, BC_CMD_ID_FLOODLIGHT_STATUS_LIST, BC_CMD_ID_ABILITY_INFO, BC_CMD_ID_SUPPORT, BC_CMD_ID_PING, BC_CMD_ID_CHANNEL_INFO_ALL, BC_CMD_ID_GET_OSD_DATETIME, BC_CMD_ID_SET_OSD_DATETIME, BC_CMD_ID_GET_RECORD_CFG, BC_CMD_ID_GET_ABILITY_SUPPORT, BC_CMD_ID_GET_FTP_TASK, BC_CMD_ID_GET_VERSION_INFO, BC_CMD_ID_GET_RECORD, BC_CMD_ID_GET_HDD_INFO_LIST, BC_CMD_ID_GET_WIFI_SIGNAL, BC_CMD_ID_GET_WIFI, BC_CMD_ID_GET_ONLINE_USER_LIST, BC_CMD_ID_GET_DAY_RECORDS, BC_CMD_ID_GET_STREAM_INFO_LIST, BC_CMD_ID_GET_LED_STATE, BC_CMD_ID_GET_EMAIL_TASK, BC_CMD_ID_GET_AUDIO_TASK, BC_CMD_ID_GET_AUDIO_CFG, BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD, BC_CMD_ID_GET_TIMELAPSE_CFG, BC_CMD_ID_GET_AI_DENOISE, BC_CMD_ID_GET_KIT_AP_CFG, BC_CMD_ID_GET_REC_ENC_CFG, BC_CMD_ID_GET_ACCESS_USER_LIST, BC_CMD_ID_GET_SLEEP_STATE, BC_CMD_ID_GET_VIDEO_INPUT, BC_CMD_ID_GET_SYSTEM_GENERAL, BC_CMD_ID_GET_SUPPORT, BC_CMD_ID_GET_AI_CFG, BC_CMD_ID_SET_AI_CFG, BC_CMD_ID_GET_SIREN_STATUS, BC_CMD_ID_SET_AUDIO_TASK, BC_CMD_ID_SET_VIDEO_INPUT, BC_CMD_ID_SET_DAY_NIGHT_THRESHOLD, BC_CMD_ID_GET_ENC, BC_CMD_ID_SET_ENC, BC_CMD_ID_GET_PRIVACY_MASK, BC_CMD_ID_SET_PRIVACY_MASK, BC_CMD_ID_SET_AI_DENOISE, BC_CMD_ID_SET_LED_STATE, BC_CMD_ID_SET_AUDIO_CFG, BC_CMD_ID_SET_RECORD, BC_CMD_ID_SET_RECORD_CFG, BC_CMD_ID_SET_EMAIL_TASK, BC_CMD_ID_GET_PUSH_TASK, BC_CMD_ID_SET_PUSH_TASK, BC_CMD_ID_GET_AUTO_FOCUS, BC_CMD_ID_SET_AUTO_FOCUS, BC_CMD_ID_CMD_123, BC_CMD_ID_CMD_209, BC_CMD_ID_CMD_265, BC_CMD_ID_CMD_440, BC_CMD_ID_PUSH_VIDEO_INPUT, BC_CMD_ID_PUSH_SERIAL, BC_CMD_ID_PUSH_NET_INFO, BC_CMD_ID_PUSH_DINGDONG_LIST, BC_CMD_ID_PUSH_SLEEP_STATUS, BC_CMD_ID_PUSH_COORDINATE_POINT_LIST, BC_CMD_ID_DING_DONG_CTRL, BC_CMD_ID_GET_DING_DONG_LIST, BC_CMD_ID_DING_DONG_OPT, BC_CMD_ID_GET_DING_DONG_CFG, BC_CMD_ID_SET_DING_DONG_CFG, BC_CMD_ID_QUICK_REPLY_PLAY, BC_CMD_ID_GET_DING_DONG_SILENT, BC_CMD_ID_SET_DING_DONG_SILENT;
|
|
38
38
|
var init_constants = __esm({
|
|
39
39
|
"src/protocol/constants.ts"() {
|
|
40
40
|
"use strict";
|
|
@@ -109,9 +109,11 @@ var init_constants = __esm({
|
|
|
109
109
|
BC_CMD_ID_PING = 93;
|
|
110
110
|
BC_CMD_ID_CHANNEL_INFO_ALL = 145;
|
|
111
111
|
BC_CMD_ID_GET_OSD_DATETIME = 44;
|
|
112
|
+
BC_CMD_ID_SET_OSD_DATETIME = 45;
|
|
112
113
|
BC_CMD_ID_GET_RECORD_CFG = 54;
|
|
113
114
|
BC_CMD_ID_GET_ABILITY_SUPPORT = 58;
|
|
114
115
|
BC_CMD_ID_GET_FTP_TASK = 70;
|
|
116
|
+
BC_CMD_ID_GET_VERSION_INFO = 80;
|
|
115
117
|
BC_CMD_ID_GET_RECORD = 81;
|
|
116
118
|
BC_CMD_ID_GET_HDD_INFO_LIST = 102;
|
|
117
119
|
BC_CMD_ID_GET_WIFI_SIGNAL = 115;
|
|
@@ -480,6 +482,15 @@ function applyXmlTagPatch(xml, tag, value) {
|
|
|
480
482
|
const re = new RegExp(`<${tag}>[^<]*</${tag}>`);
|
|
481
483
|
return xml.replace(re, `<${tag}>${v}</${tag}>`);
|
|
482
484
|
}
|
|
485
|
+
function upsertXmlTag(xml, tag, value) {
|
|
486
|
+
if (value === void 0) return xml;
|
|
487
|
+
const v = typeof value === "boolean" ? value ? 1 : 0 : value;
|
|
488
|
+
const re = new RegExp(`<${tag}>[^<]*</${tag}>`);
|
|
489
|
+
if (re.test(xml)) {
|
|
490
|
+
return xml.replace(re, `<${tag}>${v}</${tag}>`);
|
|
491
|
+
}
|
|
492
|
+
return `${xml}<${tag}>${v}</${tag}>`;
|
|
493
|
+
}
|
|
483
494
|
function patchNestedTag(xml, parent, child, value) {
|
|
484
495
|
if (value === void 0) return xml;
|
|
485
496
|
const v = typeof value === "boolean" ? value ? 1 : 0 : value;
|
|
@@ -495,6 +506,15 @@ function applyStreamPatch(xml, streamTag, patch) {
|
|
|
495
506
|
);
|
|
496
507
|
return xml.replace(re, (_match, open, body, close) => {
|
|
497
508
|
let next = body;
|
|
509
|
+
if (patch.audio !== void 0) {
|
|
510
|
+
next = applyXmlTagPatch(next, "audio", patch.audio);
|
|
511
|
+
}
|
|
512
|
+
if (patch.width !== void 0) {
|
|
513
|
+
next = applyXmlTagPatch(next, "width", patch.width);
|
|
514
|
+
}
|
|
515
|
+
if (patch.height !== void 0) {
|
|
516
|
+
next = applyXmlTagPatch(next, "height", patch.height);
|
|
517
|
+
}
|
|
498
518
|
if (patch.bitRate !== void 0) {
|
|
499
519
|
next = applyXmlTagPatch(next, "bitRate", patch.bitRate);
|
|
500
520
|
}
|
|
@@ -506,6 +526,23 @@ function applyStreamPatch(xml, streamTag, patch) {
|
|
|
506
526
|
const intVal = patch.videoEncType === "h265" ? 1 : 0;
|
|
507
527
|
next = applyXmlTagPatch(next, "videoEncType", intVal);
|
|
508
528
|
}
|
|
529
|
+
if (patch.encoderType !== void 0) {
|
|
530
|
+
next = upsertXmlTag(next, "encoderType", patch.encoderType);
|
|
531
|
+
}
|
|
532
|
+
if (patch.encoderProfile !== void 0) {
|
|
533
|
+
next = upsertXmlTag(next, "encoderProfile", patch.encoderProfile);
|
|
534
|
+
}
|
|
535
|
+
if (patch.gop !== void 0) {
|
|
536
|
+
const gopBlockRe = /(<gop[^>]*>)([\s\S]*?)(<\/gop>)/;
|
|
537
|
+
if (gopBlockRe.test(next)) {
|
|
538
|
+
next = next.replace(
|
|
539
|
+
gopBlockRe,
|
|
540
|
+
(_m, gOpen, gBody, gClose) => `${gOpen}${applyXmlTagPatch(gBody, "cur", patch.gop)}${gClose}`
|
|
541
|
+
);
|
|
542
|
+
} else {
|
|
543
|
+
next = `${next}<gop><cur>${patch.gop}</cur></gop>`;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
509
546
|
return `${open}${next}${close}`;
|
|
510
547
|
});
|
|
511
548
|
}
|
|
@@ -1117,10 +1154,15 @@ var init_BcMediaCodec = __esm({
|
|
|
1117
1154
|
strict;
|
|
1118
1155
|
amountSkipped = 0;
|
|
1119
1156
|
logger;
|
|
1157
|
+
onUnknownChunk;
|
|
1120
1158
|
constructor(strict = false, logger) {
|
|
1121
1159
|
this.strict = strict;
|
|
1122
1160
|
this.logger = logger;
|
|
1123
1161
|
}
|
|
1162
|
+
/** Register a listener that fires for every unknown chunk before recovery. */
|
|
1163
|
+
setUnknownChunkListener(listener) {
|
|
1164
|
+
this.onUnknownChunk = listener;
|
|
1165
|
+
}
|
|
1124
1166
|
/**
|
|
1125
1167
|
* Push data into the codec buffer and try to parse complete BcMedia packets.
|
|
1126
1168
|
* Returns an array of complete BcMedia packets found.
|
|
@@ -1181,6 +1223,13 @@ var init_BcMediaCodec = __esm({
|
|
|
1181
1223
|
}
|
|
1182
1224
|
}
|
|
1183
1225
|
if (next > 0) {
|
|
1226
|
+
if (this.onUnknownChunk) {
|
|
1227
|
+
this.onUnknownChunk({
|
|
1228
|
+
magic,
|
|
1229
|
+
preview: Buffer.from(this.buffer.subarray(0, Math.min(256, next))),
|
|
1230
|
+
skipped: next
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1184
1233
|
this.amountSkipped += next;
|
|
1185
1234
|
this.buffer = this.buffer.subarray(next);
|
|
1186
1235
|
continue;
|
|
@@ -2085,6 +2134,10 @@ var init_BcMediaAnnexBDecoder = __esm({
|
|
|
2085
2134
|
});
|
|
2086
2135
|
|
|
2087
2136
|
// src/baichuan/stream/BaichuanVideoStream.ts
|
|
2137
|
+
var BaichuanVideoStream_exports = {};
|
|
2138
|
+
__export(BaichuanVideoStream_exports, {
|
|
2139
|
+
BaichuanVideoStream: () => BaichuanVideoStream
|
|
2140
|
+
});
|
|
2088
2141
|
function removeEmulationPreventionBytes(rbsp) {
|
|
2089
2142
|
const out = [];
|
|
2090
2143
|
for (let i = 0; i < rbsp.length; i++) {
|
|
@@ -2218,6 +2271,15 @@ var init_BaichuanVideoStream = __esm({
|
|
|
2218
2271
|
acceptAnyStreamType;
|
|
2219
2272
|
lockedChannelId;
|
|
2220
2273
|
bcMediaCodec;
|
|
2274
|
+
/**
|
|
2275
|
+
* Diagnostic-only accessor for the BcMedia codec. Used by tools that need to
|
|
2276
|
+
* inspect unknown chunks (for example to discover undocumented audio
|
|
2277
|
+
* sub-packets the parser currently skips). Not part of the supported public
|
|
2278
|
+
* surface — do not rely on it in application code.
|
|
2279
|
+
*/
|
|
2280
|
+
get _bcMediaCodec() {
|
|
2281
|
+
return this.bcMediaCodec;
|
|
2282
|
+
}
|
|
2221
2283
|
debugH264LogsLeft;
|
|
2222
2284
|
debugSavedSamples;
|
|
2223
2285
|
warnedNonAnnexBOnce = false;
|
|
@@ -2249,6 +2311,14 @@ var init_BaichuanVideoStream = __esm({
|
|
|
2249
2311
|
// Stateful AES decryptor for fragmented BcMedia packets (full_aes mode)
|
|
2250
2312
|
// In CFB mode, continuation frames must use the cipher state from previous frames.
|
|
2251
2313
|
aesStreamDecryptor = null;
|
|
2314
|
+
// Latest frame dimensions reported by BcMedia InfoV1/V2 packets.
|
|
2315
|
+
// Used to attach width/height context to the `additionalHeader` event so
|
|
2316
|
+
// consumers can normalize box coordinates to a fraction of the stream size.
|
|
2317
|
+
latestFrameWidth;
|
|
2318
|
+
latestFrameHeight;
|
|
2319
|
+
// Teardown returned by ReolinkBaichuanApi._registerVideoStreamForDetection.
|
|
2320
|
+
// Called from stop() to detach the detection bridge.
|
|
2321
|
+
detectionTeardown;
|
|
2252
2322
|
/**
|
|
2253
2323
|
* Pending startup error stashed when emitSafeError is called before any
|
|
2254
2324
|
* "error" listener is registered (e.g. camera returns 400 during start()).
|
|
@@ -2911,6 +2981,16 @@ var init_BaichuanVideoStream = __esm({
|
|
|
2911
2981
|
}
|
|
2912
2982
|
videoType = detectedCodec;
|
|
2913
2983
|
}
|
|
2984
|
+
if (media.additionalHeader && media.additionalHeader.length > 0) {
|
|
2985
|
+
this.emit("additionalHeader", {
|
|
2986
|
+
raw: media.additionalHeader,
|
|
2987
|
+
frameType: "Iframe",
|
|
2988
|
+
videoType,
|
|
2989
|
+
microseconds: media.microseconds,
|
|
2990
|
+
...this.latestFrameWidth !== void 0 ? { frameWidth: this.latestFrameWidth } : {},
|
|
2991
|
+
...this.latestFrameHeight !== void 0 ? { frameHeight: this.latestFrameHeight } : {}
|
|
2992
|
+
});
|
|
2993
|
+
}
|
|
2914
2994
|
const annexBData = videoType === "H265" ? convertToAnnexB2(media.data) : convertToAnnexB(media.data);
|
|
2915
2995
|
const isKeyframe = true;
|
|
2916
2996
|
maybeCacheParamSets(annexBData, "Iframe", videoType);
|
|
@@ -2992,6 +3072,18 @@ var init_BaichuanVideoStream = __esm({
|
|
|
2992
3072
|
}
|
|
2993
3073
|
} else if (media.type === "Pframe") {
|
|
2994
3074
|
const chunk = media.data;
|
|
3075
|
+
if (media.additionalHeader && media.additionalHeader.length > 0) {
|
|
3076
|
+
const detected = detectVideoCodecFromNal(chunk);
|
|
3077
|
+
const videoTypeForHeader = detected ?? media.videoType;
|
|
3078
|
+
this.emit("additionalHeader", {
|
|
3079
|
+
raw: media.additionalHeader,
|
|
3080
|
+
frameType: "Pframe",
|
|
3081
|
+
videoType: videoTypeForHeader,
|
|
3082
|
+
microseconds: media.microseconds,
|
|
3083
|
+
...this.latestFrameWidth !== void 0 ? { frameWidth: this.latestFrameWidth } : {},
|
|
3084
|
+
...this.latestFrameHeight !== void 0 ? { frameHeight: this.latestFrameHeight } : {}
|
|
3085
|
+
});
|
|
3086
|
+
}
|
|
2995
3087
|
let videoType = media.videoType;
|
|
2996
3088
|
const detectedCodec = detectVideoCodecFromNal(chunk);
|
|
2997
3089
|
if (detectedCodec && detectedCodec !== videoType) {
|
|
@@ -3034,6 +3126,8 @@ var init_BaichuanVideoStream = __esm({
|
|
|
3034
3126
|
this.emit("audioFrame", media.data);
|
|
3035
3127
|
}
|
|
3036
3128
|
if (media.type === "InfoV1" || media.type === "InfoV2") {
|
|
3129
|
+
if (media.videoWidth > 0) this.latestFrameWidth = media.videoWidth;
|
|
3130
|
+
if (media.videoHeight > 0) this.latestFrameHeight = media.videoHeight;
|
|
3037
3131
|
}
|
|
3038
3132
|
}
|
|
3039
3133
|
if (totalFramesReceived <= 10 || totalFramesReceived % 20 === 0 && (videoFramesEmitted > 0 || audioFramesEmitted > 0)) {
|
|
@@ -3053,6 +3147,12 @@ var init_BaichuanVideoStream = __esm({
|
|
|
3053
3147
|
this.client.on("push", this.videoFrameHandler);
|
|
3054
3148
|
this.active = true;
|
|
3055
3149
|
this.startWatchdog();
|
|
3150
|
+
if (this.api && typeof this.api._registerVideoStreamForDetection === "function") {
|
|
3151
|
+
this.detectionTeardown = this.api._registerVideoStreamForDetection(this, {
|
|
3152
|
+
channel: this.channel,
|
|
3153
|
+
profile: this.profile
|
|
3154
|
+
});
|
|
3155
|
+
}
|
|
3056
3156
|
this.lastMediaAtMs = Date.now();
|
|
3057
3157
|
if (this.api) {
|
|
3058
3158
|
try {
|
|
@@ -3150,6 +3250,13 @@ var init_BaichuanVideoStream = __esm({
|
|
|
3150
3250
|
}
|
|
3151
3251
|
}
|
|
3152
3252
|
this.active = false;
|
|
3253
|
+
if (this.detectionTeardown) {
|
|
3254
|
+
try {
|
|
3255
|
+
this.detectionTeardown();
|
|
3256
|
+
} catch {
|
|
3257
|
+
}
|
|
3258
|
+
this.detectionTeardown = void 0;
|
|
3259
|
+
}
|
|
3153
3260
|
this.emit("close");
|
|
3154
3261
|
}
|
|
3155
3262
|
isActive() {
|
|
@@ -7377,12 +7484,12 @@ var init_ReolinkCgiApi = __esm({
|
|
|
7377
7484
|
"getVideoclipThumbnailJpeg",
|
|
7378
7485
|
`Extracting thumbnail from VOD URL (FLV): ${vodUrl.substring(0, 100)}... (seek=${seekSeconds}s)`
|
|
7379
7486
|
);
|
|
7380
|
-
const { spawn:
|
|
7487
|
+
const { spawn: spawn13 } = await import("child_process");
|
|
7381
7488
|
return new Promise((resolve, reject) => {
|
|
7382
7489
|
const chunks = [];
|
|
7383
7490
|
let stderr = "";
|
|
7384
7491
|
let timedOut = false;
|
|
7385
|
-
const ffmpeg =
|
|
7492
|
+
const ffmpeg = spawn13(ffmpegPath, [
|
|
7386
7493
|
"-y",
|
|
7387
7494
|
"-analyzeduration",
|
|
7388
7495
|
"10000000",
|
|
@@ -8040,6 +8147,7 @@ __export(index_exports, {
|
|
|
8040
8147
|
BC_CMD_ID_GET_SUPPORT: () => BC_CMD_ID_GET_SUPPORT,
|
|
8041
8148
|
BC_CMD_ID_GET_SYSTEM_GENERAL: () => BC_CMD_ID_GET_SYSTEM_GENERAL,
|
|
8042
8149
|
BC_CMD_ID_GET_TIMELAPSE_CFG: () => BC_CMD_ID_GET_TIMELAPSE_CFG,
|
|
8150
|
+
BC_CMD_ID_GET_VERSION_INFO: () => BC_CMD_ID_GET_VERSION_INFO,
|
|
8043
8151
|
BC_CMD_ID_GET_VIDEO_INPUT: () => BC_CMD_ID_GET_VIDEO_INPUT,
|
|
8044
8152
|
BC_CMD_ID_GET_WHITE_LED: () => BC_CMD_ID_GET_WHITE_LED,
|
|
8045
8153
|
BC_CMD_ID_GET_WIFI: () => BC_CMD_ID_GET_WIFI,
|
|
@@ -8070,6 +8178,7 @@ __export(index_exports, {
|
|
|
8070
8178
|
BC_CMD_ID_SET_ENC: () => BC_CMD_ID_SET_ENC,
|
|
8071
8179
|
BC_CMD_ID_SET_LED_STATE: () => BC_CMD_ID_SET_LED_STATE,
|
|
8072
8180
|
BC_CMD_ID_SET_MOTION_ALARM: () => BC_CMD_ID_SET_MOTION_ALARM,
|
|
8181
|
+
BC_CMD_ID_SET_OSD_DATETIME: () => BC_CMD_ID_SET_OSD_DATETIME,
|
|
8073
8182
|
BC_CMD_ID_SET_PIR_INFO: () => BC_CMD_ID_SET_PIR_INFO,
|
|
8074
8183
|
BC_CMD_ID_SET_PRIVACY_MASK: () => BC_CMD_ID_SET_PRIVACY_MASK,
|
|
8075
8184
|
BC_CMD_ID_SET_PUSH_TASK: () => BC_CMD_ID_SET_PUSH_TASK,
|
|
@@ -8178,6 +8287,7 @@ __export(index_exports, {
|
|
|
8178
8287
|
decideSleepInferenceTransition: () => decideSleepInferenceTransition,
|
|
8179
8288
|
decideVideoclipTranscodeMode: () => decideVideoclipTranscodeMode,
|
|
8180
8289
|
decodeHeader: () => decodeHeader,
|
|
8290
|
+
decodeMotionScopeBitmap: () => decodeMotionScopeBitmap,
|
|
8181
8291
|
deriveAesKey: () => deriveAesKey,
|
|
8182
8292
|
detectIosClient: () => detectIosClient,
|
|
8183
8293
|
detectVideoCodecFromNal: () => detectVideoCodecFromNal,
|
|
@@ -8190,6 +8300,7 @@ __export(index_exports, {
|
|
|
8190
8300
|
discoverViaUdpBroadcast: () => discoverViaUdpBroadcast,
|
|
8191
8301
|
discoverViaUdpDirect: () => discoverViaUdpDirect,
|
|
8192
8302
|
encodeHeader: () => encodeHeader,
|
|
8303
|
+
encodeMotionScopeBitmap: () => encodeMotionScopeBitmap,
|
|
8193
8304
|
ensureXmlHeader: () => ensureXmlHeader,
|
|
8194
8305
|
extractH264ParamSetsFromAccessUnit: () => extractH264ParamSetsFromAccessUnit,
|
|
8195
8306
|
extractH265ParamSetsFromAccessUnit: () => extractH265ParamSetsFromAccessUnit,
|
|
@@ -8198,6 +8309,7 @@ __export(index_exports, {
|
|
|
8198
8309
|
extractVpsFromAnnexB: () => extractVpsFromAnnexB,
|
|
8199
8310
|
flattenAbilitiesForChannel: () => flattenAbilitiesForChannel,
|
|
8200
8311
|
formatMjpegFrame: () => formatMjpegFrame,
|
|
8312
|
+
fullCoverageScope: () => fullCoverageScope,
|
|
8201
8313
|
getConstructedVideoStreamOptions: () => getConstructedVideoStreamOptions,
|
|
8202
8314
|
getGlobalLogger: () => getGlobalLogger,
|
|
8203
8315
|
getH265NalType: () => getH265NalType,
|
|
@@ -8241,6 +8353,7 @@ __export(index_exports, {
|
|
|
8241
8353
|
splitAnnexBToNals: () => splitAnnexBToNals,
|
|
8242
8354
|
splitH265AnnexBToNalPayloads: () => splitAnnexBToNalPayloads2,
|
|
8243
8355
|
testChannelStreams: () => testChannelStreams,
|
|
8356
|
+
upsertXmlTag: () => upsertXmlTag,
|
|
8244
8357
|
xmlEscape: () => xmlEscape,
|
|
8245
8358
|
xmlIndicatesFloodlight: () => xmlIndicatesFloodlight,
|
|
8246
8359
|
zipDirectory: () => zipDirectory
|
|
@@ -9969,6 +10082,23 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
9969
10082
|
* even if the current client instance is idle/disconnected.
|
|
9970
10083
|
*/
|
|
9971
10084
|
static streamingRegistry = /* @__PURE__ */ new Map();
|
|
10085
|
+
/**
|
|
10086
|
+
* Per-device set of live BaichuanClient instances.
|
|
10087
|
+
*
|
|
10088
|
+
* Why: when a streaming client unsubscribes (e.g. RTSP grace timer expires
|
|
10089
|
+
* and SocketPool tears the streaming socket down), the global streaming
|
|
10090
|
+
* registry decrements but the GENERAL client of the same device has no
|
|
10091
|
+
* way of knowing — its idle-disconnect timer was last evaluated while
|
|
10092
|
+
* `isDeviceStreamingActive()` was still true (because the streaming socket
|
|
10093
|
+
* was still alive) and wasn't rescheduled. Without this registry the
|
|
10094
|
+
* general socket stays connected, the 60-second session-guard timer keeps
|
|
10095
|
+
* sending getOnlineUserList() to the camera, and a battery camera ends up
|
|
10096
|
+
* waking up every minute (issue #18).
|
|
10097
|
+
*
|
|
10098
|
+
* On streamingRegistry decrement-to-zero we walk this set and kick every
|
|
10099
|
+
* sibling's idle-disconnect timer so it can re-evaluate eligibility.
|
|
10100
|
+
*/
|
|
10101
|
+
static deviceClients = /* @__PURE__ */ new Map();
|
|
9972
10102
|
/**
|
|
9973
10103
|
* Per-host D2C_DISC backoff state that persists across client instance recreation.
|
|
9974
10104
|
*
|
|
@@ -10083,6 +10213,29 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
10083
10213
|
// AlarmEventList (cmdId=33) can be very chatty (often sent every second).
|
|
10084
10214
|
// Track last per-channel alarm state so we only emit on transitions.
|
|
10085
10215
|
alarmEventState = /* @__PURE__ */ new Map();
|
|
10216
|
+
/** Whether this instance is currently in BaichuanClient.deviceClients. */
|
|
10217
|
+
registeredInDeviceClients = false;
|
|
10218
|
+
registerInDeviceClients() {
|
|
10219
|
+
if (this.registeredInDeviceClients) return;
|
|
10220
|
+
const key = this.getDeviceRegistryKey();
|
|
10221
|
+
let set = _BaichuanClient.deviceClients.get(key);
|
|
10222
|
+
if (!set) {
|
|
10223
|
+
set = /* @__PURE__ */ new Set();
|
|
10224
|
+
_BaichuanClient.deviceClients.set(key, set);
|
|
10225
|
+
}
|
|
10226
|
+
set.add(this);
|
|
10227
|
+
this.registeredInDeviceClients = true;
|
|
10228
|
+
}
|
|
10229
|
+
unregisterFromDeviceClients() {
|
|
10230
|
+
if (!this.registeredInDeviceClients) return;
|
|
10231
|
+
const key = this.getDeviceRegistryKey();
|
|
10232
|
+
const set = _BaichuanClient.deviceClients.get(key);
|
|
10233
|
+
if (set) {
|
|
10234
|
+
set.delete(this);
|
|
10235
|
+
if (set.size === 0) _BaichuanClient.deviceClients.delete(key);
|
|
10236
|
+
}
|
|
10237
|
+
this.registeredInDeviceClients = false;
|
|
10238
|
+
}
|
|
10086
10239
|
constructor(options) {
|
|
10087
10240
|
super();
|
|
10088
10241
|
this.opts = options;
|
|
@@ -10097,6 +10250,7 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
10097
10250
|
code: err?.code
|
|
10098
10251
|
});
|
|
10099
10252
|
});
|
|
10253
|
+
this.registerInDeviceClients();
|
|
10100
10254
|
}
|
|
10101
10255
|
newSocketSessionId(transport) {
|
|
10102
10256
|
const short = (0, import_node_crypto2.randomUUID)().split("-")[0] ?? (0, import_node_crypto2.randomUUID)().slice(0, 8);
|
|
@@ -10353,6 +10507,18 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
10353
10507
|
activeStreamClients: nextCount
|
|
10354
10508
|
});
|
|
10355
10509
|
this.contributesToGlobalStreamingRegistry = shouldContribute;
|
|
10510
|
+
if (!shouldContribute && nextCount === 0) {
|
|
10511
|
+
const siblings = _BaichuanClient.deviceClients.get(key);
|
|
10512
|
+
if (siblings) {
|
|
10513
|
+
for (const sib of siblings) {
|
|
10514
|
+
if (sib === this) continue;
|
|
10515
|
+
try {
|
|
10516
|
+
sib.kickIdleDisconnectTimer();
|
|
10517
|
+
} catch {
|
|
10518
|
+
}
|
|
10519
|
+
}
|
|
10520
|
+
}
|
|
10521
|
+
}
|
|
10356
10522
|
}
|
|
10357
10523
|
/**
|
|
10358
10524
|
* True if the device should be considered "awake" due to active streaming.
|
|
@@ -10817,6 +10983,7 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
10817
10983
|
`transport=tcp host=${this.opts.host} port=${port}${sid ? ` sid=${sid}` : ""}${remote ? ` remote=${remote}` : ""}${peer ? ` peer=${peer}` : ""}`
|
|
10818
10984
|
);
|
|
10819
10985
|
this.logSocketState("tcp_connected");
|
|
10986
|
+
this.registerInDeviceClients();
|
|
10820
10987
|
this.startKeepAlive();
|
|
10821
10988
|
this.kickIdleDisconnectTimer();
|
|
10822
10989
|
}
|
|
@@ -11133,6 +11300,7 @@ var BaichuanClient = class _BaichuanClient extends import_node_events2.EventEmit
|
|
|
11133
11300
|
this.logDebug("udp_close_error", e);
|
|
11134
11301
|
}
|
|
11135
11302
|
}
|
|
11303
|
+
this.unregisterFromDeviceClients();
|
|
11136
11304
|
}
|
|
11137
11305
|
handleFrame(frame) {
|
|
11138
11306
|
const now = Date.now();
|
|
@@ -16410,6 +16578,32 @@ var parseAbilityInfoXml = (xml) => {
|
|
|
16410
16578
|
return abilities;
|
|
16411
16579
|
};
|
|
16412
16580
|
|
|
16581
|
+
// src/reolink/baichuan/utils/versionInfo.ts
|
|
16582
|
+
init_xml();
|
|
16583
|
+
function parseVersionInfo(xml) {
|
|
16584
|
+
const out = {};
|
|
16585
|
+
const set = (key) => {
|
|
16586
|
+
const v = getXmlText(xml, key);
|
|
16587
|
+
if (v !== void 0) out[key] = v;
|
|
16588
|
+
};
|
|
16589
|
+
set("name");
|
|
16590
|
+
set("type");
|
|
16591
|
+
set("serialNumber");
|
|
16592
|
+
set("buildDay");
|
|
16593
|
+
set("hardwareVersion");
|
|
16594
|
+
set("cfgVersion");
|
|
16595
|
+
set("firmwareVersion");
|
|
16596
|
+
set("detail");
|
|
16597
|
+
set("IEClient");
|
|
16598
|
+
set("cc3200Version");
|
|
16599
|
+
set("spVersion");
|
|
16600
|
+
set("pakSuffix");
|
|
16601
|
+
set("itemNo");
|
|
16602
|
+
set("aiVersion");
|
|
16603
|
+
set("helpVersion");
|
|
16604
|
+
return out;
|
|
16605
|
+
}
|
|
16606
|
+
|
|
16413
16607
|
// src/reolink/baichuan/utils/aiState.ts
|
|
16414
16608
|
init_constants();
|
|
16415
16609
|
init_xml();
|
|
@@ -16716,6 +16910,204 @@ var buildChannelPushDataLogSnapshot = (channelPushData) => {
|
|
|
16716
16910
|
return { result: resultObj, storedChannels: Object.keys(resultObj) };
|
|
16717
16911
|
};
|
|
16718
16912
|
|
|
16913
|
+
// src/reolink/baichuan/utils/detection.ts
|
|
16914
|
+
var lz4 = __toESM(require("lz4js"), 1);
|
|
16915
|
+
|
|
16916
|
+
// src/reolink/baichuan/utils/aiClassMap.ts
|
|
16917
|
+
function type1ToLabel(type1) {
|
|
16918
|
+
switch (type1) {
|
|
16919
|
+
case 1:
|
|
16920
|
+
return "people";
|
|
16921
|
+
case 2:
|
|
16922
|
+
return "vehicle";
|
|
16923
|
+
case 3:
|
|
16924
|
+
return "animal";
|
|
16925
|
+
case 11259375:
|
|
16926
|
+
return "face";
|
|
16927
|
+
default:
|
|
16928
|
+
return "unknown";
|
|
16929
|
+
}
|
|
16930
|
+
}
|
|
16931
|
+
|
|
16932
|
+
// src/reolink/baichuan/utils/detection.ts
|
|
16933
|
+
var MARKER_LENGTH = 8;
|
|
16934
|
+
var IFRAME_PREFIX_LENGTH = 8;
|
|
16935
|
+
var COUNTER_OFFSET = 8;
|
|
16936
|
+
var BASELINE_SIZE = 128;
|
|
16937
|
+
var FRAME_SIZE_TLV = Buffer.from([3, 4, 0]);
|
|
16938
|
+
var LZ4F_MAGIC = Buffer.from([4, 34, 77, 24]);
|
|
16939
|
+
var CONFIDENCE_DIVISOR = 100;
|
|
16940
|
+
var DEFAULT_AI_FRAME_WIDTH = 896;
|
|
16941
|
+
var DEFAULT_AI_FRAME_HEIGHT = 480;
|
|
16942
|
+
var LZ4_DECOMPRESS_MAX = 256 * 1024;
|
|
16943
|
+
function walkBoxes(buf, start, end, type1, type2, out) {
|
|
16944
|
+
let pos = start;
|
|
16945
|
+
while (pos + 3 <= end) {
|
|
16946
|
+
const t = buf[pos];
|
|
16947
|
+
if (t === 0) return;
|
|
16948
|
+
const length = buf[pos + 1] | buf[pos + 2] << 8;
|
|
16949
|
+
const recordEnd = pos + 3 + length;
|
|
16950
|
+
if (recordEnd > end) return;
|
|
16951
|
+
const isBoxType4 = t === 4 && (length === 10 || length === 13 || length === 14);
|
|
16952
|
+
const isBoxType2 = t === 2 && length === 10;
|
|
16953
|
+
if ((isBoxType4 || isBoxType2) && type1 !== 0 && type2 !== 0) {
|
|
16954
|
+
const x1 = buf.readUInt16LE(pos + 3);
|
|
16955
|
+
const y1 = buf.readUInt16LE(pos + 5);
|
|
16956
|
+
const x2 = buf.readUInt16LE(pos + 7);
|
|
16957
|
+
const y2 = buf.readUInt16LE(pos + 9);
|
|
16958
|
+
const conf = buf.readUInt16LE(pos + 11);
|
|
16959
|
+
if (x2 > x1 && y2 > y1) {
|
|
16960
|
+
out.push({ x1, y1, x2, y2, conf, label: type1ToLabel(type1) });
|
|
16961
|
+
}
|
|
16962
|
+
pos = recordEnd;
|
|
16963
|
+
continue;
|
|
16964
|
+
}
|
|
16965
|
+
if (type1 === 255 && type2 === 2 && t === 2 && length >= LZ4F_MAGIC.length && buf[pos + 3] === LZ4F_MAGIC[0] && buf[pos + 4] === LZ4F_MAGIC[1] && buf[pos + 5] === LZ4F_MAGIC[2] && buf[pos + 6] === LZ4F_MAGIC[3]) {
|
|
16966
|
+
try {
|
|
16967
|
+
const decompressed = lz4.decompress(
|
|
16968
|
+
buf.subarray(pos + 3, recordEnd),
|
|
16969
|
+
LZ4_DECOMPRESS_MAX
|
|
16970
|
+
);
|
|
16971
|
+
const decBuf = Buffer.from(decompressed);
|
|
16972
|
+
walkBoxes(decBuf, 0, decBuf.length, 0, 0, out);
|
|
16973
|
+
} catch {
|
|
16974
|
+
}
|
|
16975
|
+
pos = recordEnd;
|
|
16976
|
+
continue;
|
|
16977
|
+
}
|
|
16978
|
+
if (length > 0) {
|
|
16979
|
+
let nextT1 = type1;
|
|
16980
|
+
let nextT2 = type2;
|
|
16981
|
+
if (type1 === 0) nextT1 = t;
|
|
16982
|
+
else if (type2 === 0) nextT2 = t;
|
|
16983
|
+
walkBoxes(buf, pos + 3, recordEnd, nextT1, nextT2, out);
|
|
16984
|
+
}
|
|
16985
|
+
pos = recordEnd;
|
|
16986
|
+
}
|
|
16987
|
+
}
|
|
16988
|
+
function decodeDetectionHeader(raw, frameType) {
|
|
16989
|
+
const markerOffset = frameType === "Iframe" ? IFRAME_PREFIX_LENGTH : 0;
|
|
16990
|
+
const blockLength = raw.length - markerOffset;
|
|
16991
|
+
const empty = {
|
|
16992
|
+
state: "invalid-marker",
|
|
16993
|
+
markerOffset,
|
|
16994
|
+
blockLength,
|
|
16995
|
+
boxes: []
|
|
16996
|
+
};
|
|
16997
|
+
if (blockLength < MARKER_LENGTH) return empty;
|
|
16998
|
+
if (!hasStandardMarker(raw, markerOffset)) return empty;
|
|
16999
|
+
if (blockLength < COUNTER_OFFSET + 4) return empty;
|
|
17000
|
+
const counter = raw.readUInt32LE(markerOffset + COUNTER_OFFSET);
|
|
17001
|
+
const rawBoxes = [];
|
|
17002
|
+
walkBoxes(raw, markerOffset, raw.length, 0, 0, rawBoxes);
|
|
17003
|
+
let aiFrameWidth = DEFAULT_AI_FRAME_WIDTH;
|
|
17004
|
+
let aiFrameHeight = DEFAULT_AI_FRAME_HEIGHT;
|
|
17005
|
+
let frameSizeFound = false;
|
|
17006
|
+
const searchStart = markerOffset + MARKER_LENGTH;
|
|
17007
|
+
for (let i = searchStart; i + 7 <= raw.length; i++) {
|
|
17008
|
+
if (raw[i] === FRAME_SIZE_TLV[0] && raw[i + 1] === FRAME_SIZE_TLV[1] && raw[i + 2] === FRAME_SIZE_TLV[2]) {
|
|
17009
|
+
const w = raw.readUInt16LE(i + 3);
|
|
17010
|
+
const h = raw.readUInt16LE(i + 5);
|
|
17011
|
+
if (w >= 64 && w <= 8192 && h >= 64 && h <= 8192) {
|
|
17012
|
+
aiFrameWidth = w;
|
|
17013
|
+
aiFrameHeight = h;
|
|
17014
|
+
frameSizeFound = true;
|
|
17015
|
+
break;
|
|
17016
|
+
}
|
|
17017
|
+
}
|
|
17018
|
+
}
|
|
17019
|
+
const specificity = {
|
|
17020
|
+
face: 4,
|
|
17021
|
+
animal: 3,
|
|
17022
|
+
people: 2,
|
|
17023
|
+
vehicle: 1,
|
|
17024
|
+
unknown: 0
|
|
17025
|
+
};
|
|
17026
|
+
const dedup = /* @__PURE__ */ new Map();
|
|
17027
|
+
for (const rb of rawBoxes) {
|
|
17028
|
+
if (rb.x2 > aiFrameWidth || rb.y2 > aiFrameHeight) continue;
|
|
17029
|
+
const key = `${rb.x1}_${rb.y1}_${rb.x2}_${rb.y2}`;
|
|
17030
|
+
const prev = dedup.get(key);
|
|
17031
|
+
if (!prev || (specificity[rb.label] ?? 0) > (specificity[prev.label] ?? 0)) {
|
|
17032
|
+
dedup.set(key, rb);
|
|
17033
|
+
}
|
|
17034
|
+
}
|
|
17035
|
+
const boxes = [];
|
|
17036
|
+
for (const rb of dedup.values()) {
|
|
17037
|
+
boxes.push({
|
|
17038
|
+
x: rb.x1 / aiFrameWidth,
|
|
17039
|
+
y: rb.y1 / aiFrameHeight,
|
|
17040
|
+
width: (rb.x2 - rb.x1) / aiFrameWidth,
|
|
17041
|
+
height: (rb.y2 - rb.y1) / aiFrameHeight,
|
|
17042
|
+
...rb.conf > 0 && rb.conf <= 100 ? { confidence: rb.conf / CONFIDENCE_DIVISOR } : {},
|
|
17043
|
+
...rb.label !== "unknown" ? { label: rb.label } : {}
|
|
17044
|
+
});
|
|
17045
|
+
}
|
|
17046
|
+
let state;
|
|
17047
|
+
if (boxes.length > 0) state = "overlay-decoded";
|
|
17048
|
+
else if (blockLength === BASELINE_SIZE) state = "no-overlay";
|
|
17049
|
+
else state = "overlay-undecoded";
|
|
17050
|
+
return {
|
|
17051
|
+
state,
|
|
17052
|
+
markerOffset,
|
|
17053
|
+
blockLength,
|
|
17054
|
+
counter,
|
|
17055
|
+
...frameSizeFound ? { aiFrameWidth, aiFrameHeight } : {},
|
|
17056
|
+
boxes
|
|
17057
|
+
};
|
|
17058
|
+
}
|
|
17059
|
+
function hasStandardMarker(raw, offset) {
|
|
17060
|
+
if (raw.length < offset + MARKER_LENGTH) return false;
|
|
17061
|
+
return raw[offset] === 255 && raw[offset + 2] === 0 && raw[offset + 3] === 1 && raw[offset + 4] === 11 && raw[offset + 5] === 0 && raw[offset + 6] === 1 && raw[offset + 7] === 8;
|
|
17062
|
+
}
|
|
17063
|
+
|
|
17064
|
+
// src/reolink/baichuan/utils/encOptions.ts
|
|
17065
|
+
function buildEncOptions(list, channel) {
|
|
17066
|
+
const result = { channel };
|
|
17067
|
+
const main = aggregateByType(list, "mainStream");
|
|
17068
|
+
const sub = aggregateByType(list, "subStream");
|
|
17069
|
+
const third = aggregateByType(list, "thirdStream");
|
|
17070
|
+
if (main) result.mainStream = main;
|
|
17071
|
+
if (sub) result.subStream = sub;
|
|
17072
|
+
if (third) result.thirdStream = third;
|
|
17073
|
+
return result;
|
|
17074
|
+
}
|
|
17075
|
+
function aggregateByType(list, type) {
|
|
17076
|
+
const seen = /* @__PURE__ */ new Map();
|
|
17077
|
+
for (const stream of list.streams) {
|
|
17078
|
+
for (const eb of stream.encodeTables) {
|
|
17079
|
+
if (eb.type !== type) continue;
|
|
17080
|
+
const mapped = mapEncodeTable(eb);
|
|
17081
|
+
if (!mapped) continue;
|
|
17082
|
+
const key = `${mapped.width}x${mapped.height}`;
|
|
17083
|
+
if (!seen.has(key)) seen.set(key, mapped);
|
|
17084
|
+
}
|
|
17085
|
+
}
|
|
17086
|
+
if (seen.size === 0) return void 0;
|
|
17087
|
+
return {
|
|
17088
|
+
type,
|
|
17089
|
+
resolutions: [...seen.values()],
|
|
17090
|
+
encoderTypes: ["vbr", "cbr"],
|
|
17091
|
+
encoderProfiles: ["high", "main", "baseline"]
|
|
17092
|
+
};
|
|
17093
|
+
}
|
|
17094
|
+
function mapEncodeTable(eb) {
|
|
17095
|
+
if (eb.width == null || eb.height == null) return void 0;
|
|
17096
|
+
const videoEncTypes = (eb.videoEncTypeList ?? (eb.videoEncType != null ? [eb.videoEncType] : [])).map(
|
|
17097
|
+
(t) => t === 0 ? "h264" : t === 1 ? "h265" : void 0
|
|
17098
|
+
).filter((t) => t !== void 0);
|
|
17099
|
+
return {
|
|
17100
|
+
width: eb.width,
|
|
17101
|
+
height: eb.height,
|
|
17102
|
+
videoEncTypes,
|
|
17103
|
+
...eb.defaultFramerate != null ? { defaultFramerate: eb.defaultFramerate } : {},
|
|
17104
|
+
...eb.defaultBitrate != null ? { defaultBitrate: eb.defaultBitrate } : {},
|
|
17105
|
+
...eb.defaultGop != null ? { defaultGop: eb.defaultGop } : {},
|
|
17106
|
+
framerateOptions: eb.framerateTable ?? [],
|
|
17107
|
+
bitrateOptions: eb.bitrateTable ?? []
|
|
17108
|
+
};
|
|
17109
|
+
}
|
|
17110
|
+
|
|
16719
17111
|
// src/reolink/baichuan/utils/events.ts
|
|
16720
17112
|
var mapToSimpleEvent = (event) => {
|
|
16721
17113
|
const timestamp = event.timestamp ?? Date.now();
|
|
@@ -18839,6 +19231,21 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
18839
19231
|
sessionGuardIntervalTimer;
|
|
18840
19232
|
simpleEventListeners = /* @__PURE__ */ new Set();
|
|
18841
19233
|
simpleEventSubscribed = false;
|
|
19234
|
+
// Detection events are sourced from BcMedia additionalHeader on active video
|
|
19235
|
+
// streams. Unlike simpleEvent, no Baichuan subscribe command is needed — the
|
|
19236
|
+
// data flows whenever a stream is open. Active streams register themselves via
|
|
19237
|
+
// _registerVideoStreamForDetection (called from BaichuanVideoStream.start).
|
|
19238
|
+
detectionEventListeners = /* @__PURE__ */ new Set();
|
|
19239
|
+
detectionEventStreamHooks = /* @__PURE__ */ new Map();
|
|
19240
|
+
// Auto-managed substream for `onObjectDetections` listeners. Reference-counted
|
|
19241
|
+
// by the listener set: the substream is opened on the first listener and torn
|
|
19242
|
+
// down with the last one. Mirrors `onSimpleEvent`'s subscribe/unsubscribe
|
|
19243
|
+
// lifecycle so a caller never has to manage a video stream just to read AI
|
|
19244
|
+
// detections.
|
|
19245
|
+
objectDetectionListeners = /* @__PURE__ */ new Set();
|
|
19246
|
+
objectDetectionStream;
|
|
19247
|
+
objectDetectionStreamStartInFlight;
|
|
19248
|
+
objectDetectionInternalListener;
|
|
18842
19249
|
simpleEventSubscribeInFlight;
|
|
18843
19250
|
simpleEventUnsubscribeInFlight;
|
|
18844
19251
|
simpleEventResubscribeTimer;
|
|
@@ -20274,6 +20681,205 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
20274
20681
|
}
|
|
20275
20682
|
}
|
|
20276
20683
|
}
|
|
20684
|
+
/**
|
|
20685
|
+
* Subscribe to per-frame detection events sourced from the BcMedia
|
|
20686
|
+
* `additionalHeader` block on active video streams.
|
|
20687
|
+
*
|
|
20688
|
+
* Mirrors {@link onSimpleEvent} but is fed by the streaming side-channel:
|
|
20689
|
+
* one event fires for every I-frame / P-frame that carries an overlay block.
|
|
20690
|
+
* Coordinates are reported in normalized [0, 1] fractions of the source
|
|
20691
|
+
* frame, so the same box renders correctly on mainStream, subStream, and
|
|
20692
|
+
* externStream.
|
|
20693
|
+
*
|
|
20694
|
+
* Unlike `onSimpleEvent`, no Baichuan subscribe command is involved — events
|
|
20695
|
+
* only flow while a video stream is open. The library hooks every
|
|
20696
|
+
* `BaichuanVideoStream` created via this API for the listener's lifetime.
|
|
20697
|
+
*/
|
|
20698
|
+
onDetection(callback) {
|
|
20699
|
+
this.detectionEventListeners.add(callback);
|
|
20700
|
+
}
|
|
20701
|
+
/**
|
|
20702
|
+
* Remove a single detection callback, or all of them if `callback` is omitted.
|
|
20703
|
+
*/
|
|
20704
|
+
offDetection(callback) {
|
|
20705
|
+
if (callback) {
|
|
20706
|
+
this.detectionEventListeners.delete(callback);
|
|
20707
|
+
} else {
|
|
20708
|
+
this.detectionEventListeners.clear();
|
|
20709
|
+
}
|
|
20710
|
+
}
|
|
20711
|
+
/**
|
|
20712
|
+
* Subscribe to AI object detections (people / vehicle / animal / face boxes
|
|
20713
|
+
* with class label and confidence) without managing a video stream yourself.
|
|
20714
|
+
*
|
|
20715
|
+
* Mirrors {@link onSimpleEvent} end-to-end: the API opens a dedicated
|
|
20716
|
+
* substream behind the scenes on the first listener, forwards every box-bearing
|
|
20717
|
+
* `additionalHeader` to your callback, and tears the stream down when the last
|
|
20718
|
+
* listener unsubscribes. The substream is the lightest profile (typically
|
|
20719
|
+
* 640×360) so the additional bandwidth/CPU overhead is minimal.
|
|
20720
|
+
*
|
|
20721
|
+
* Each event carries normalized `[0, 1]` box coordinates, a class label, and
|
|
20722
|
+
* a confidence score — render-ready without further conversion.
|
|
20723
|
+
*/
|
|
20724
|
+
async onObjectDetections(callback) {
|
|
20725
|
+
this.objectDetectionListeners.add(callback);
|
|
20726
|
+
this.logger.debug?.(
|
|
20727
|
+
`[ReolinkBaichuanApi] onObjectDetections: registering listener (total=${this.objectDetectionListeners.size})`
|
|
20728
|
+
);
|
|
20729
|
+
await this.ensureObjectDetectionStream();
|
|
20730
|
+
}
|
|
20731
|
+
/**
|
|
20732
|
+
* Remove one detection callback, or all of them if `callback` is omitted.
|
|
20733
|
+
* When the last listener is removed the auto-managed substream is closed.
|
|
20734
|
+
*/
|
|
20735
|
+
async offObjectDetections(callback) {
|
|
20736
|
+
if (callback) {
|
|
20737
|
+
this.objectDetectionListeners.delete(callback);
|
|
20738
|
+
} else {
|
|
20739
|
+
this.objectDetectionListeners.clear();
|
|
20740
|
+
}
|
|
20741
|
+
if (this.objectDetectionListeners.size === 0) {
|
|
20742
|
+
await this.tearDownObjectDetectionStream();
|
|
20743
|
+
}
|
|
20744
|
+
}
|
|
20745
|
+
async ensureObjectDetectionStream() {
|
|
20746
|
+
if (this.objectDetectionStream) return;
|
|
20747
|
+
if (this.objectDetectionStreamStartInFlight) {
|
|
20748
|
+
await this.objectDetectionStreamStartInFlight;
|
|
20749
|
+
return;
|
|
20750
|
+
}
|
|
20751
|
+
this.objectDetectionStreamStartInFlight = (async () => {
|
|
20752
|
+
const { BaichuanVideoStream: BaichuanVideoStream2 } = await Promise.resolve().then(() => (init_BaichuanVideoStream(), BaichuanVideoStream_exports));
|
|
20753
|
+
const sessionKey = `live:object-detections:ch0:sub`;
|
|
20754
|
+
const dedicated = await this.createDedicatedSession(sessionKey);
|
|
20755
|
+
const stream = new BaichuanVideoStream2({
|
|
20756
|
+
client: dedicated.client,
|
|
20757
|
+
api: this,
|
|
20758
|
+
channel: 0,
|
|
20759
|
+
profile: "sub",
|
|
20760
|
+
logger: this.logger
|
|
20761
|
+
});
|
|
20762
|
+
this.objectDetectionInternalListener = (event) => {
|
|
20763
|
+
for (const cb of this.objectDetectionListeners) {
|
|
20764
|
+
try {
|
|
20765
|
+
void Promise.resolve(cb(event)).catch((e) => {
|
|
20766
|
+
(this.logger.warn ?? this.logger.error).call(
|
|
20767
|
+
this.logger,
|
|
20768
|
+
"[ReolinkBaichuanApi] onObjectDetections handler error",
|
|
20769
|
+
formatErrorForLog(e)
|
|
20770
|
+
);
|
|
20771
|
+
});
|
|
20772
|
+
} catch (e) {
|
|
20773
|
+
(this.logger.warn ?? this.logger.error).call(
|
|
20774
|
+
this.logger,
|
|
20775
|
+
"[ReolinkBaichuanApi] onObjectDetections handler error",
|
|
20776
|
+
formatErrorForLog(e)
|
|
20777
|
+
);
|
|
20778
|
+
}
|
|
20779
|
+
}
|
|
20780
|
+
};
|
|
20781
|
+
this.detectionEventListeners.add(this.objectDetectionInternalListener);
|
|
20782
|
+
try {
|
|
20783
|
+
await stream.start();
|
|
20784
|
+
} catch (e) {
|
|
20785
|
+
if (this.objectDetectionInternalListener) {
|
|
20786
|
+
this.detectionEventListeners.delete(
|
|
20787
|
+
this.objectDetectionInternalListener
|
|
20788
|
+
);
|
|
20789
|
+
this.objectDetectionInternalListener = void 0;
|
|
20790
|
+
}
|
|
20791
|
+
await dedicated.release().catch(() => {
|
|
20792
|
+
});
|
|
20793
|
+
throw e;
|
|
20794
|
+
}
|
|
20795
|
+
this.objectDetectionStream = {
|
|
20796
|
+
stop: () => stream.stop(),
|
|
20797
|
+
release: () => dedicated.release()
|
|
20798
|
+
};
|
|
20799
|
+
this.logger.debug?.(
|
|
20800
|
+
`[ReolinkBaichuanApi] onObjectDetections: substream started (key=${sessionKey})`
|
|
20801
|
+
);
|
|
20802
|
+
})();
|
|
20803
|
+
try {
|
|
20804
|
+
await this.objectDetectionStreamStartInFlight;
|
|
20805
|
+
} finally {
|
|
20806
|
+
this.objectDetectionStreamStartInFlight = void 0;
|
|
20807
|
+
}
|
|
20808
|
+
}
|
|
20809
|
+
async tearDownObjectDetectionStream() {
|
|
20810
|
+
const handle = this.objectDetectionStream;
|
|
20811
|
+
this.objectDetectionStream = void 0;
|
|
20812
|
+
if (this.objectDetectionInternalListener) {
|
|
20813
|
+
this.detectionEventListeners.delete(this.objectDetectionInternalListener);
|
|
20814
|
+
this.objectDetectionInternalListener = void 0;
|
|
20815
|
+
}
|
|
20816
|
+
if (!handle) return;
|
|
20817
|
+
try {
|
|
20818
|
+
await handle.stop();
|
|
20819
|
+
} catch (e) {
|
|
20820
|
+
this.logger.debug?.(
|
|
20821
|
+
`[ReolinkBaichuanApi] onObjectDetections: stream stop error: ${formatErrorForLog(e)}`
|
|
20822
|
+
);
|
|
20823
|
+
}
|
|
20824
|
+
try {
|
|
20825
|
+
await handle.release();
|
|
20826
|
+
} catch (e) {
|
|
20827
|
+
this.logger.debug?.(
|
|
20828
|
+
`[ReolinkBaichuanApi] onObjectDetections: session release error: ${formatErrorForLog(e)}`
|
|
20829
|
+
);
|
|
20830
|
+
}
|
|
20831
|
+
this.logger.debug?.(
|
|
20832
|
+
`[ReolinkBaichuanApi] onObjectDetections: substream torn down`
|
|
20833
|
+
);
|
|
20834
|
+
}
|
|
20835
|
+
/**
|
|
20836
|
+
* Internal: invoked by BaichuanVideoStream when it starts so the API can hook
|
|
20837
|
+
* its `additionalHeader` event. Returns a teardown function the stream calls
|
|
20838
|
+
* on stop. Not intended for direct use by consumers.
|
|
20839
|
+
*/
|
|
20840
|
+
_registerVideoStreamForDetection(stream, context) {
|
|
20841
|
+
const listener = (info) => {
|
|
20842
|
+
if (this.detectionEventListeners.size === 0) return;
|
|
20843
|
+
const decoded = decodeDetectionHeader(info.raw, info.frameType);
|
|
20844
|
+
const event = {
|
|
20845
|
+
channel: context.channel,
|
|
20846
|
+
microseconds: info.microseconds,
|
|
20847
|
+
profile: context.profile,
|
|
20848
|
+
boxes: decoded.boxes,
|
|
20849
|
+
...info.frameWidth !== void 0 ? { frameWidth: info.frameWidth } : {},
|
|
20850
|
+
...info.frameHeight !== void 0 ? { frameHeight: info.frameHeight } : {},
|
|
20851
|
+
decodeState: decoded.state,
|
|
20852
|
+
rawHeader: info.raw
|
|
20853
|
+
};
|
|
20854
|
+
this.dispatchDetectionEvent(event);
|
|
20855
|
+
};
|
|
20856
|
+
stream.on("additionalHeader", listener);
|
|
20857
|
+
const teardown = () => {
|
|
20858
|
+
stream.off("additionalHeader", listener);
|
|
20859
|
+
this.detectionEventStreamHooks.delete(stream);
|
|
20860
|
+
};
|
|
20861
|
+
this.detectionEventStreamHooks.set(stream, teardown);
|
|
20862
|
+
return teardown;
|
|
20863
|
+
}
|
|
20864
|
+
dispatchDetectionEvent(evt) {
|
|
20865
|
+
for (const cb of this.detectionEventListeners) {
|
|
20866
|
+
try {
|
|
20867
|
+
void Promise.resolve(cb(evt)).catch((e) => {
|
|
20868
|
+
(this.logger.warn ?? this.logger.error).call(
|
|
20869
|
+
this.logger,
|
|
20870
|
+
"[ReolinkBaichuanApi] onDetection handler error",
|
|
20871
|
+
formatErrorForLog(e)
|
|
20872
|
+
);
|
|
20873
|
+
});
|
|
20874
|
+
} catch (e) {
|
|
20875
|
+
(this.logger.warn ?? this.logger.error).call(
|
|
20876
|
+
this.logger,
|
|
20877
|
+
"[ReolinkBaichuanApi] onDetection handler error",
|
|
20878
|
+
formatErrorForLog(e)
|
|
20879
|
+
);
|
|
20880
|
+
}
|
|
20881
|
+
}
|
|
20882
|
+
}
|
|
20277
20883
|
startSimpleEventResubscribeTimer() {
|
|
20278
20884
|
if (this.simpleEventResubscribeTimer) return;
|
|
20279
20885
|
if (this.simpleEventListeners.size === 0) return;
|
|
@@ -20656,6 +21262,9 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
20656
21262
|
this.stopUdpSleepInference();
|
|
20657
21263
|
this.stopSimpleEventWatchdog();
|
|
20658
21264
|
this.stopSimpleEventResubscribeTimer();
|
|
21265
|
+
this.objectDetectionListeners.clear();
|
|
21266
|
+
await this.tearDownObjectDetectionStream().catch(() => {
|
|
21267
|
+
});
|
|
20659
21268
|
await this.cleanup();
|
|
20660
21269
|
await this.stopAllActiveStreams();
|
|
20661
21270
|
await this.cleanupSocketPool();
|
|
@@ -21003,6 +21612,53 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
|
|
|
21003
21612
|
const xml = `<?xml version="1.0" encoding="UTF-8" ?><body><${tag} version="1.1"><enable>${params.enable ? 1 : 0}</enable></${tag}></body>`;
|
|
21004
21613
|
await this.sendXml({ cmdId: 36, payloadXml: xml });
|
|
21005
21614
|
}
|
|
21615
|
+
/**
|
|
21616
|
+
* Full port-config setter (cmd_id 36). Patches one or more of the six
|
|
21617
|
+
* service ports the camera serves — Server (Baichuan), HTTP, HTTPS,
|
|
21618
|
+
* RTSP, RTMP, ONVIF. Each entry takes an optional `port` (number) and
|
|
21619
|
+
* `enable` (boolean); fields the caller doesn't pass are left alone.
|
|
21620
|
+
*
|
|
21621
|
+
* Sends one block per port that has any field set, then issues a
|
|
21622
|
+
* single cmd_36 with the merged body. The camera accepts multiple
|
|
21623
|
+
* `<XxxPort>` siblings in the same payload.
|
|
21624
|
+
*
|
|
21625
|
+
* Wire format observed on E1 Zoom:
|
|
21626
|
+
*
|
|
21627
|
+
* <body>
|
|
21628
|
+
* <RtspPort version="1.1">
|
|
21629
|
+
* <rtspPort>554</rtspPort>
|
|
21630
|
+
* <enable>1</enable>
|
|
21631
|
+
* </RtspPort>
|
|
21632
|
+
* <HttpsPort version="1.1">
|
|
21633
|
+
* <enable>0</enable>
|
|
21634
|
+
* </HttpsPort>
|
|
21635
|
+
* ...
|
|
21636
|
+
* </body>
|
|
21637
|
+
*/
|
|
21638
|
+
async setPortConfig(patch) {
|
|
21639
|
+
const blocks = [];
|
|
21640
|
+
const append = (tag, portField, cfg) => {
|
|
21641
|
+
if (!cfg) return;
|
|
21642
|
+
if (cfg.port === void 0 && cfg.enable === void 0) return;
|
|
21643
|
+
const inner = [];
|
|
21644
|
+
if (cfg.port !== void 0) {
|
|
21645
|
+
inner.push(`<${portField}>${cfg.port}</${portField}>`);
|
|
21646
|
+
}
|
|
21647
|
+
if (cfg.enable !== void 0) {
|
|
21648
|
+
inner.push(`<enable>${cfg.enable ? 1 : 0}</enable>`);
|
|
21649
|
+
}
|
|
21650
|
+
blocks.push(`<${tag} version="1.1">${inner.join("")}</${tag}>`);
|
|
21651
|
+
};
|
|
21652
|
+
append("ServerPort", "serverPort", patch.server);
|
|
21653
|
+
append("HttpPort", "httpPort", patch.http);
|
|
21654
|
+
append("HttpsPort", "httpsPort", patch.https);
|
|
21655
|
+
append("RtspPort", "rtspPort", patch.rtsp);
|
|
21656
|
+
append("RtmpPort", "rtmpPort", patch.rtmp);
|
|
21657
|
+
append("OnvifPort", "onvifPort", patch.onvif);
|
|
21658
|
+
if (blocks.length === 0) return;
|
|
21659
|
+
const xml = `<?xml version="1.0" encoding="UTF-8" ?><body>${blocks.join("")}</body>`;
|
|
21660
|
+
await this.sendXml({ cmdId: 36, payloadXml: xml });
|
|
21661
|
+
}
|
|
21006
21662
|
/** GetDevInfo via Baichuan: host cmd_id 80, channel cmd_id 318 */
|
|
21007
21663
|
async getInfo(channel, options) {
|
|
21008
21664
|
const req = { cmdId: channel == null ? 80 : 318 };
|
|
@@ -23663,7 +24319,7 @@ ${stderr}`)
|
|
|
23663
24319
|
* Convert a raw video keyframe to JPEG using ffmpeg.
|
|
23664
24320
|
*/
|
|
23665
24321
|
async convertFrameToJpeg(params) {
|
|
23666
|
-
const { spawn:
|
|
24322
|
+
const { spawn: spawn13 } = await import("child_process");
|
|
23667
24323
|
const ffmpeg = params.ffmpegPath ?? "ffmpeg";
|
|
23668
24324
|
const inputFormat = params.videoCodec === "H265" ? "hevc" : "h264";
|
|
23669
24325
|
return new Promise((resolve, reject) => {
|
|
@@ -23685,7 +24341,7 @@ ${stderr}`)
|
|
|
23685
24341
|
"2",
|
|
23686
24342
|
"pipe:1"
|
|
23687
24343
|
];
|
|
23688
|
-
const proc =
|
|
24344
|
+
const proc = spawn13(ffmpeg, args, {
|
|
23689
24345
|
stdio: ["pipe", "pipe", "pipe"]
|
|
23690
24346
|
});
|
|
23691
24347
|
const chunks = [];
|
|
@@ -23828,7 +24484,7 @@ ${stderr}`)
|
|
|
23828
24484
|
* Internal helper to mux video+audio into MP4 using ffmpeg.
|
|
23829
24485
|
*/
|
|
23830
24486
|
async muxToMp4(params) {
|
|
23831
|
-
const { spawn:
|
|
24487
|
+
const { spawn: spawn13 } = await import("child_process");
|
|
23832
24488
|
const { randomUUID: randomUUID3 } = await import("crypto");
|
|
23833
24489
|
const fs6 = await import("fs/promises");
|
|
23834
24490
|
const os2 = await import("os");
|
|
@@ -23880,7 +24536,7 @@ ${stderr}`)
|
|
|
23880
24536
|
outputPath
|
|
23881
24537
|
);
|
|
23882
24538
|
await new Promise((resolve, reject) => {
|
|
23883
|
-
const p =
|
|
24539
|
+
const p = spawn13(ffmpeg, args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
23884
24540
|
let stderr = "";
|
|
23885
24541
|
p.stderr.on("data", (d) => {
|
|
23886
24542
|
stderr += d.toString();
|
|
@@ -24687,6 +25343,27 @@ ${stderr}`)
|
|
|
24687
25343
|
);
|
|
24688
25344
|
}
|
|
24689
25345
|
}
|
|
25346
|
+
async gotoPtzPreset(arg1, arg2) {
|
|
25347
|
+
const ch = arg2 === void 0 ? this.normalizeChannel(void 0) : this.normalizeChannel(arg1);
|
|
25348
|
+
const presetId = arg2 === void 0 ? arg1 : arg2;
|
|
25349
|
+
const channelId = ch;
|
|
25350
|
+
const payloadXml = buildPtzPresetXmlV2(channelId, presetId, "toPos");
|
|
25351
|
+
const extensionXml = buildChannelExtensionXml(channelId);
|
|
25352
|
+
const frame = await this.client.sendFrame({
|
|
25353
|
+
cmdId: BC_CMD_ID_PTZ_CONTROL_PRESET,
|
|
25354
|
+
channel: ch,
|
|
25355
|
+
channelIdOverride: channelId,
|
|
25356
|
+
extensionXml,
|
|
25357
|
+
payloadXml,
|
|
25358
|
+
messageClass: BC_CLASS_MODERN_24,
|
|
25359
|
+
streamType: 0
|
|
25360
|
+
});
|
|
25361
|
+
if (frame.header.responseCode !== 200) {
|
|
25362
|
+
throw new Error(
|
|
25363
|
+
`PTZ goto preset rejected (response_code ${frame.header.responseCode})`
|
|
25364
|
+
);
|
|
25365
|
+
}
|
|
25366
|
+
}
|
|
24690
25367
|
async deletePtzPreset(arg1, arg2) {
|
|
24691
25368
|
const ch = arg2 === void 0 ? this.normalizeChannel(void 0) : this.normalizeChannel(arg1);
|
|
24692
25369
|
const presetId = arg2 === void 0 ? arg1 : arg2;
|
|
@@ -25338,22 +26015,62 @@ ${stderr}`)
|
|
|
25338
26015
|
const channel = typeof arg1 === "number" ? arg1 : arg3;
|
|
25339
26016
|
const enabled = typeof arg1 === "number" ? arg2 : arg1;
|
|
25340
26017
|
const sensitivity = typeof arg1 === "number" ? arg3 : arg2;
|
|
25341
|
-
|
|
26018
|
+
return await this.setMotionAlarmFull({
|
|
26019
|
+
...channel !== void 0 ? { channel } : {},
|
|
26020
|
+
enabled,
|
|
26021
|
+
...sensitivity !== void 0 ? { sensitivity } : {}
|
|
26022
|
+
});
|
|
26023
|
+
}
|
|
26024
|
+
/**
|
|
26025
|
+
* Set motion alarm with full control, including the detection-zone grid.
|
|
26026
|
+
*
|
|
26027
|
+
* Wire format observed on E1 Zoom (cmd_id=47 SetMdAlarm body):
|
|
26028
|
+
*
|
|
26029
|
+
* <MD version="1.1">
|
|
26030
|
+
* <channelId>0</channelId>
|
|
26031
|
+
* <enable>1</enable>
|
|
26032
|
+
* <usepir>0</usepir>
|
|
26033
|
+
* <width>60</width> <height>33</height>
|
|
26034
|
+
* <scope>
|
|
26035
|
+
* <columns>96</columns> <rows>64</rows>
|
|
26036
|
+
* <valueTable>{base64 6144-bit bitmap}</valueTable>
|
|
26037
|
+
* </scope>
|
|
26038
|
+
* ... other camera-specific fields ...
|
|
26039
|
+
* </MD>
|
|
26040
|
+
*
|
|
26041
|
+
* We do a read-modify-write of the GET response so any camera-specific
|
|
26042
|
+
* extension fields are preserved untouched. Pass `valueTable` to update
|
|
26043
|
+
* the detection zone — see `encodeMotionScopeBitmap` for the bitmap layout.
|
|
26044
|
+
*
|
|
26045
|
+
* @param channel - 0-based channel
|
|
26046
|
+
* @param enabled - toggle motion detection on/off (optional)
|
|
26047
|
+
* @param sensitivity - 0-50, higher = more sensitive (optional)
|
|
26048
|
+
* @param valueTable - base64-encoded grid bitmap; size must match
|
|
26049
|
+
* `<scope><columns>×<rows></scope>` from the GET (optional)
|
|
26050
|
+
*/
|
|
26051
|
+
async setMotionAlarmFull(opts) {
|
|
26052
|
+
const ch = this.normalizeChannel(opts.channel);
|
|
25342
26053
|
const currentXml = await this.sendXml({
|
|
25343
26054
|
cmdId: BC_CMD_ID_GET_MOTION_ALARM,
|
|
25344
26055
|
channel: ch
|
|
25345
26056
|
});
|
|
25346
26057
|
let modifiedXml = currentXml;
|
|
25347
|
-
if (enabled !== void 0) {
|
|
26058
|
+
if (opts.enabled !== void 0) {
|
|
25348
26059
|
modifiedXml = modifiedXml.replace(
|
|
25349
26060
|
/<enable>[^<]*<\/enable>/,
|
|
25350
|
-
`<enable>${enabled ? "1" : "0"}</enable>`
|
|
26061
|
+
`<enable>${opts.enabled ? "1" : "0"}</enable>`
|
|
25351
26062
|
);
|
|
25352
26063
|
}
|
|
25353
|
-
if (sensitivity !== void 0) {
|
|
26064
|
+
if (opts.sensitivity !== void 0) {
|
|
25354
26065
|
modifiedXml = modifiedXml.replace(
|
|
25355
26066
|
/<sensitivityDefault>[^<]*<\/sensitivityDefault>/,
|
|
25356
|
-
`<sensitivityDefault>${sensitivity}</sensitivityDefault>`
|
|
26067
|
+
`<sensitivityDefault>${opts.sensitivity}</sensitivityDefault>`
|
|
26068
|
+
);
|
|
26069
|
+
}
|
|
26070
|
+
if (opts.valueTable !== void 0) {
|
|
26071
|
+
modifiedXml = modifiedXml.replace(
|
|
26072
|
+
/<valueTable>[^<]*<\/valueTable>/,
|
|
26073
|
+
`<valueTable>${opts.valueTable}</valueTable>`
|
|
25357
26074
|
);
|
|
25358
26075
|
}
|
|
25359
26076
|
await this.sendXml({
|
|
@@ -26697,12 +27414,24 @@ ${xml}`
|
|
|
26697
27414
|
}
|
|
26698
27415
|
/**
|
|
26699
27416
|
* SetEnc via Baichuan (cmdId=57). Read-modify-write — preserves
|
|
26700
|
-
* unspecified fields. Mirrors reolink_aio's `SetEnc
|
|
27417
|
+
* unspecified fields. Mirrors reolink_aio's `SetEnc` plus the additional
|
|
27418
|
+
* `width`/`height`/`encoderType`/`encoderProfile`/`gop`/`thirdStream`
|
|
27419
|
+
* fields observed in the official mobile app (see `pcap/resolution.pcapng`).
|
|
27420
|
+
*
|
|
27421
|
+
* Field meaning per stream:
|
|
27422
|
+
* - `audio` — 0/1 toggle
|
|
27423
|
+
* - `width`/`height` — resolution in pixels. Must be one of the
|
|
27424
|
+
* resolutions returned by {@link getStreamInfoList}.
|
|
27425
|
+
* - `bitRate` — kbps. Must match the table from `getStreamInfoList`.
|
|
27426
|
+
* - `frameRate` — fps. Must match the table from `getStreamInfoList`.
|
|
27427
|
+
* - `videoEncType` — `"h264"` or `"h265"`
|
|
27428
|
+
* - `encoderType` — `"vbr"` or `"cbr"`
|
|
27429
|
+
* - `encoderProfile` — `"high"`, `"main"`, or `"baseline"`
|
|
27430
|
+
* - `gop` — keyframe interval in seconds (sets `<gop><cur>`)
|
|
26701
27431
|
*
|
|
26702
27432
|
* @param channel - Channel number (0-based)
|
|
26703
|
-
* @param patch - Fields to update
|
|
26704
|
-
*
|
|
26705
|
-
* to change.
|
|
27433
|
+
* @param patch - Fields to update. Pass only the fields you want to change;
|
|
27434
|
+
* everything else is preserved from the device's current configuration.
|
|
26706
27435
|
*/
|
|
26707
27436
|
async setEnc(channel, patch, options) {
|
|
26708
27437
|
const ch = this.normalizeChannel(channel);
|
|
@@ -26719,6 +27448,7 @@ ${xml}`
|
|
|
26719
27448
|
}
|
|
26720
27449
|
xml = applyStreamPatch(xml, "mainStream", patch.mainStream);
|
|
26721
27450
|
xml = applyStreamPatch(xml, "subStream", patch.subStream);
|
|
27451
|
+
xml = applyStreamPatch(xml, "thirdStream", patch.thirdStream);
|
|
26722
27452
|
await this.sendXml({
|
|
26723
27453
|
cmdId: BC_CMD_ID_SET_ENC,
|
|
26724
27454
|
channel: ch,
|
|
@@ -27326,6 +28056,71 @@ ${xml}`
|
|
|
27326
28056
|
`PCAP-derived settings GET failed for cmdId=${params.cmdId}: ${String(lastErr)}`
|
|
27327
28057
|
);
|
|
27328
28058
|
}
|
|
28059
|
+
/**
|
|
28060
|
+
* Update the OSD timestamp + channel-name overlay via cmd_id=45
|
|
28061
|
+
* (SetOsdDatetime). The schema is the same `<body><OsdDatetime>` +
|
|
28062
|
+
* `<OsdChannelName>` block returned by `getOsdDatetime` — we
|
|
28063
|
+
* read-modify-write so any extension fields the camera sent are
|
|
28064
|
+
* preserved.
|
|
28065
|
+
*
|
|
28066
|
+
* Position is in **camera pixel coordinates** (e.g. (1,1) for top-left,
|
|
28067
|
+
* not preset strings). Set `enable=0` to hide the overlay; the camera
|
|
28068
|
+
* keeps the stored position so re-enabling later restores it.
|
|
28069
|
+
*/
|
|
28070
|
+
async setOsdDatetime(channel, patch, options) {
|
|
28071
|
+
const ch = this.normalizeChannel(channel);
|
|
28072
|
+
const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
|
|
28073
|
+
let xml = await this.sendPcapDerivedSettingsGetXml({
|
|
28074
|
+
cmdId: BC_CMD_ID_GET_OSD_DATETIME,
|
|
28075
|
+
channel: ch,
|
|
28076
|
+
...timeoutOpts
|
|
28077
|
+
});
|
|
28078
|
+
const patchBlock = (block, fields) => {
|
|
28079
|
+
const start = xml.indexOf(`<${block}`);
|
|
28080
|
+
if (start < 0) return;
|
|
28081
|
+
const end = xml.indexOf(`</${block}>`, start);
|
|
28082
|
+
if (end < 0) return;
|
|
28083
|
+
let body = xml.slice(start, end);
|
|
28084
|
+
for (const [tag, value] of Object.entries(fields)) {
|
|
28085
|
+
if (value === void 0) continue;
|
|
28086
|
+
const raw = typeof value === "boolean" ? value ? "1" : "0" : String(value);
|
|
28087
|
+
const escaped = raw.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
28088
|
+
if (body.includes(`<${tag}>`)) {
|
|
28089
|
+
body = body.replace(
|
|
28090
|
+
new RegExp(`<${tag}>[^<]*<\\/${tag}>`),
|
|
28091
|
+
`<${tag}>${escaped}</${tag}>`
|
|
28092
|
+
);
|
|
28093
|
+
} else {
|
|
28094
|
+
body += `<${tag}>${escaped}</${tag}>`;
|
|
28095
|
+
}
|
|
28096
|
+
}
|
|
28097
|
+
xml = xml.slice(0, start) + body + xml.slice(end);
|
|
28098
|
+
};
|
|
28099
|
+
if (patch.datetime) {
|
|
28100
|
+
patchBlock("OsdDatetime", {
|
|
28101
|
+
enable: patch.datetime.enable,
|
|
28102
|
+
topLeftX: patch.datetime.topLeftX,
|
|
28103
|
+
topLeftY: patch.datetime.topLeftY,
|
|
28104
|
+
language: patch.datetime.language
|
|
28105
|
+
});
|
|
28106
|
+
}
|
|
28107
|
+
if (patch.channelName) {
|
|
28108
|
+
patchBlock("OsdChannelName", {
|
|
28109
|
+
name: patch.channelName.name,
|
|
28110
|
+
enable: patch.channelName.enable,
|
|
28111
|
+
topLeftX: patch.channelName.topLeftX,
|
|
28112
|
+
topLeftY: patch.channelName.topLeftY,
|
|
28113
|
+
enWatermark: patch.channelName.enWatermark,
|
|
28114
|
+
enBgcolor: patch.channelName.enBgcolor
|
|
28115
|
+
});
|
|
28116
|
+
}
|
|
28117
|
+
await this.sendXml({
|
|
28118
|
+
cmdId: BC_CMD_ID_SET_OSD_DATETIME,
|
|
28119
|
+
channel: ch,
|
|
28120
|
+
payloadXml: ensureXmlHeader(xml),
|
|
28121
|
+
...timeoutOpts
|
|
28122
|
+
});
|
|
28123
|
+
}
|
|
27329
28124
|
async getOsdDatetime(channel, options) {
|
|
27330
28125
|
const rawXml = await this.sendPcapDerivedSettingsGetXml({
|
|
27331
28126
|
cmdId: BC_CMD_ID_GET_OSD_DATETIME,
|
|
@@ -27518,6 +28313,41 @@ ${xml}`
|
|
|
27518
28313
|
});
|
|
27519
28314
|
return { streams };
|
|
27520
28315
|
}
|
|
28316
|
+
/**
|
|
28317
|
+
* Return the set of values `setEnc` will accept on each stream of `channel`.
|
|
28318
|
+
* Aggregates `getStreamInfoList` (cmd_146) into a UI-friendly shape:
|
|
28319
|
+
* per-stream resolutions with their allowed codecs/framerates/bitrates plus
|
|
28320
|
+
* the enumerated encoder modes/profiles Reolink exposes.
|
|
28321
|
+
*
|
|
28322
|
+
* Useful for populating selectors and validating user input before calling
|
|
28323
|
+
* `setEnc` — picking an unsupported combination causes the camera to reject
|
|
28324
|
+
* the SET_ENC command (responseCode != 200).
|
|
28325
|
+
*/
|
|
28326
|
+
async getEncOptions(channel, options) {
|
|
28327
|
+
const list = await this.getStreamInfoList(channel, options);
|
|
28328
|
+
return buildEncOptions(list, channel);
|
|
28329
|
+
}
|
|
28330
|
+
/**
|
|
28331
|
+
* Read the camera's `<VersionInfo>` block (cmd_id=80). Returns the
|
|
28332
|
+
* friendly name, model code (e.g. `"E1 Zoom"`), serial number, firmware
|
|
28333
|
+
* version, hardware revision, build day, AI model bundle version, etc.
|
|
28334
|
+
*
|
|
28335
|
+
* This is the same info the Reolink mobile app shows in "About this
|
|
28336
|
+
* device" — distinct from `getSystemGeneral` (cmd_104) which carries
|
|
28337
|
+
* time/locale.
|
|
28338
|
+
*
|
|
28339
|
+
* No channel parameter: this command is device-global on NVRs/Hubs and
|
|
28340
|
+
* camera-global on standalone cameras. Pass an explicit channel via the
|
|
28341
|
+
* underlying `sendXml` only if a specific firmware demands it (none we've
|
|
28342
|
+
* tested do).
|
|
28343
|
+
*/
|
|
28344
|
+
async getVersionInfo(options) {
|
|
28345
|
+
const xml = await this.sendXml({
|
|
28346
|
+
cmdId: BC_CMD_ID_GET_VERSION_INFO,
|
|
28347
|
+
...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
|
|
28348
|
+
});
|
|
28349
|
+
return parseVersionInfo(xml);
|
|
28350
|
+
}
|
|
27521
28351
|
async getLedState(channel, options) {
|
|
27522
28352
|
const rawXml = await this.sendPcapDerivedSettingsGetXml({
|
|
27523
28353
|
cmdId: BC_CMD_ID_GET_LED_STATE,
|
|
@@ -34596,6 +35426,17 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
34596
35426
|
// Native stream
|
|
34597
35427
|
nativeFanout = null;
|
|
34598
35428
|
nativeStreamActive = false;
|
|
35429
|
+
// Set only by stopNativeStream() (explicit teardown) so the fanout's onEnd
|
|
35430
|
+
// callback can short-circuit cleanup/restart logic. NOT set by the inactivity-
|
|
35431
|
+
// timeout force-restart path — that flow wants onEnd to run and decide
|
|
35432
|
+
// whether to restart based on prestartStream / connected clients.
|
|
35433
|
+
nativeStreamStopping = false;
|
|
35434
|
+
// Pending retry timer for the unbounded auto-restart loop. When a stream
|
|
35435
|
+
// start fails transiently (camera in maintenance reboot, idle-disconnect
|
|
35436
|
+
// race, etc.) we keep trying with exponential backoff until either the
|
|
35437
|
+
// server is stopped or a frame finally arrives.
|
|
35438
|
+
nativeStreamRetryTimer;
|
|
35439
|
+
nativeStreamRetryDelayMs = 0;
|
|
34599
35440
|
dedicatedSessionRelease;
|
|
34600
35441
|
detectedVideoType;
|
|
34601
35442
|
// Client tracking
|
|
@@ -34664,6 +35505,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
34664
35505
|
if (!this.active) return;
|
|
34665
35506
|
this.active = false;
|
|
34666
35507
|
clearTimeout(this.stopGraceTimer);
|
|
35508
|
+
this.clearNativeStreamRetry();
|
|
34667
35509
|
this.stopStreamHealthMonitor();
|
|
34668
35510
|
for (const [id, sock] of this.clientSockets) {
|
|
34669
35511
|
sock.destroy();
|
|
@@ -34992,6 +35834,46 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
34992
35834
|
// -----------------------------------------------------------------------
|
|
34993
35835
|
// Native stream management
|
|
34994
35836
|
// -----------------------------------------------------------------------
|
|
35837
|
+
/**
|
|
35838
|
+
* Schedule another startNativeStream() attempt after the given delay.
|
|
35839
|
+
* Idempotent: a no-op if a retry is already scheduled, the server is no
|
|
35840
|
+
* longer active, or an explicit stop is in progress. Implements unbounded
|
|
35841
|
+
* exponential backoff (5s → 60s) so a camera that stays unreachable for
|
|
35842
|
+
* minutes (e.g. nightly maintenance reboot) eventually recovers without
|
|
35843
|
+
* manual intervention — see issue #16.
|
|
35844
|
+
*/
|
|
35845
|
+
scheduleNativeStreamRetry(reason) {
|
|
35846
|
+
if (!this.active) return;
|
|
35847
|
+
if (this.nativeStreamStopping) return;
|
|
35848
|
+
if (this.nativeStreamRetryTimer) return;
|
|
35849
|
+
const delay = this.nativeStreamRetryDelayMs > 0 ? this.nativeStreamRetryDelayMs : 5e3;
|
|
35850
|
+
this.logger.info?.(
|
|
35851
|
+
`[Go2rtcTcpServer] scheduling native stream retry in ${(delay / 1e3).toFixed(0)}s (reason=${reason})`
|
|
35852
|
+
);
|
|
35853
|
+
this.nativeStreamRetryTimer = setTimeout(() => {
|
|
35854
|
+
this.nativeStreamRetryTimer = void 0;
|
|
35855
|
+
if (!this.active) return;
|
|
35856
|
+
if (this.nativeStreamStopping) return;
|
|
35857
|
+
this.startNativeStream().catch((err) => {
|
|
35858
|
+
this.logger.warn?.(
|
|
35859
|
+
`[Go2rtcTcpServer] retry of startNativeStream threw: ${err instanceof Error ? err.message : err}`
|
|
35860
|
+
);
|
|
35861
|
+
});
|
|
35862
|
+
}, delay);
|
|
35863
|
+
this.nativeStreamRetryDelayMs = Math.min(delay * 2, 6e4);
|
|
35864
|
+
}
|
|
35865
|
+
/**
|
|
35866
|
+
* Cancel any pending retry timer and reset the backoff. Called on explicit
|
|
35867
|
+
* stop and on first-frame-received so the next failure starts the backoff
|
|
35868
|
+
* window from scratch.
|
|
35869
|
+
*/
|
|
35870
|
+
clearNativeStreamRetry() {
|
|
35871
|
+
if (this.nativeStreamRetryTimer) {
|
|
35872
|
+
clearTimeout(this.nativeStreamRetryTimer);
|
|
35873
|
+
this.nativeStreamRetryTimer = void 0;
|
|
35874
|
+
}
|
|
35875
|
+
this.nativeStreamRetryDelayMs = 0;
|
|
35876
|
+
}
|
|
34995
35877
|
async startNativeStream() {
|
|
34996
35878
|
if (this.nativeStreamActive) return;
|
|
34997
35879
|
if (!this.api.isReady) {
|
|
@@ -35008,8 +35890,9 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
35008
35890
|
await this.api.ensureConnected();
|
|
35009
35891
|
} catch (e) {
|
|
35010
35892
|
this.logger.warn?.(
|
|
35011
|
-
`[Go2rtcTcpServer] ensureConnected failed
|
|
35893
|
+
`[Go2rtcTcpServer] ensureConnected failed: ${e}`
|
|
35012
35894
|
);
|
|
35895
|
+
this.scheduleNativeStreamRetry("ensureConnected failed");
|
|
35013
35896
|
return;
|
|
35014
35897
|
}
|
|
35015
35898
|
}
|
|
@@ -35039,6 +35922,9 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
35039
35922
|
...dedicatedClient ? { client: dedicatedClient } : {}
|
|
35040
35923
|
}),
|
|
35041
35924
|
onFrame: (frame) => {
|
|
35925
|
+
if (!hadFrames) {
|
|
35926
|
+
this.clearNativeStreamRetry();
|
|
35927
|
+
}
|
|
35042
35928
|
hadFrames = true;
|
|
35043
35929
|
this.lastFrameAt = Date.now();
|
|
35044
35930
|
this.totalFramesReceived++;
|
|
@@ -35082,7 +35968,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
35082
35968
|
this.logger.warn?.(`[Go2rtcTcpServer] native stream error: ${error}`);
|
|
35083
35969
|
},
|
|
35084
35970
|
onEnd: () => {
|
|
35085
|
-
if (
|
|
35971
|
+
if (this.nativeStreamStopping) return;
|
|
35086
35972
|
this.nativeStreamActive = false;
|
|
35087
35973
|
this.nativeFanout = null;
|
|
35088
35974
|
this.stopStreamHealthMonitor();
|
|
@@ -35124,18 +36010,24 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
|
|
|
35124
36010
|
this.startStreamHealthMonitor();
|
|
35125
36011
|
}
|
|
35126
36012
|
async stopNativeStream() {
|
|
36013
|
+
this.nativeStreamStopping = true;
|
|
35127
36014
|
this.nativeStreamActive = false;
|
|
36015
|
+
this.clearNativeStreamRetry();
|
|
35128
36016
|
this.stopStreamHealthMonitor();
|
|
35129
36017
|
const fanout = this.nativeFanout;
|
|
35130
36018
|
this.nativeFanout = null;
|
|
35131
|
-
|
|
35132
|
-
|
|
35133
|
-
|
|
35134
|
-
|
|
35135
|
-
|
|
35136
|
-
|
|
35137
|
-
|
|
35138
|
-
|
|
36019
|
+
try {
|
|
36020
|
+
if (fanout) {
|
|
36021
|
+
await fanout.stop();
|
|
36022
|
+
}
|
|
36023
|
+
this.prebuffer = [];
|
|
36024
|
+
if (this.dedicatedSessionRelease) {
|
|
36025
|
+
await this.dedicatedSessionRelease().catch(() => {
|
|
36026
|
+
});
|
|
36027
|
+
this.dedicatedSessionRelease = void 0;
|
|
36028
|
+
}
|
|
36029
|
+
} finally {
|
|
36030
|
+
this.nativeStreamStopping = false;
|
|
35139
36031
|
}
|
|
35140
36032
|
}
|
|
35141
36033
|
// -----------------------------------------------------------------------
|
|
@@ -35325,10 +36217,15 @@ var BaichuanHttpStreamServer = class extends import_node_events7.EventEmitter {
|
|
|
35325
36217
|
// Force a known frame rate on raw H.264 input so the muxer gets valid PTS/DTS.
|
|
35326
36218
|
"-r",
|
|
35327
36219
|
String(this.inputFps),
|
|
36220
|
+
// `+genpts` generates uniform PTS from `-r` for raw Annex-B input. We
|
|
36221
|
+
// deliberately do NOT pass `-use_wallclock_as_timestamps 1`: that
|
|
36222
|
+
// overrides the generated PTS with the host wallclock at frame ARRIVAL
|
|
36223
|
+
// time, and the network stream is bursty so the resulting PTS is
|
|
36224
|
+
// uneven. With `-r` forcing a target rate downstream, ffmpeg then
|
|
36225
|
+
// drops/duplicates frames to match — visible as the periodic stutter
|
|
36226
|
+
// reported on local-restreamer recordings (issue #11).
|
|
35328
36227
|
"-fflags",
|
|
35329
36228
|
"+genpts",
|
|
35330
|
-
"-use_wallclock_as_timestamps",
|
|
35331
|
-
"1",
|
|
35332
36229
|
"-f",
|
|
35333
36230
|
"h264",
|
|
35334
36231
|
// Input format (H.264 Annex-B)
|
|
@@ -36007,8 +36904,195 @@ var BaichuanMjpegServer = class extends import_node_events9.EventEmitter {
|
|
|
36007
36904
|
};
|
|
36008
36905
|
|
|
36009
36906
|
// src/baichuan/stream/BaichuanWebRTCServer.ts
|
|
36010
|
-
var
|
|
36907
|
+
var import_node_events11 = require("events");
|
|
36011
36908
|
init_BcMediaAnnexBDecoder();
|
|
36909
|
+
|
|
36910
|
+
// src/baichuan/stream/AacToOpusTranscoder.ts
|
|
36911
|
+
var import_node_child_process11 = require("child_process");
|
|
36912
|
+
var import_node_dgram3 = require("dgram");
|
|
36913
|
+
var import_node_events10 = require("events");
|
|
36914
|
+
var AacToOpusTranscoder = class extends import_node_events10.EventEmitter {
|
|
36915
|
+
opts;
|
|
36916
|
+
socket = null;
|
|
36917
|
+
ffmpeg = null;
|
|
36918
|
+
port = 0;
|
|
36919
|
+
starting = null;
|
|
36920
|
+
stopped = false;
|
|
36921
|
+
constructor(options = {}) {
|
|
36922
|
+
super();
|
|
36923
|
+
this.opts = {
|
|
36924
|
+
ffmpegPath: options.ffmpegPath ?? "ffmpeg",
|
|
36925
|
+
opusSampleRate: options.opusSampleRate ?? 48e3,
|
|
36926
|
+
opusChannels: options.opusChannels ?? 2,
|
|
36927
|
+
opusFrameMs: options.opusFrameMs ?? 20,
|
|
36928
|
+
opusBitrate: options.opusBitrate ?? 64e3,
|
|
36929
|
+
...options.logger !== void 0 ? { logger: options.logger } : {}
|
|
36930
|
+
};
|
|
36931
|
+
}
|
|
36932
|
+
log(level, message) {
|
|
36933
|
+
this.opts.logger?.(level, `[AacToOpusTranscoder] ${message}`);
|
|
36934
|
+
}
|
|
36935
|
+
/**
|
|
36936
|
+
* Allocate the UDP loopback socket and spawn ffmpeg. Must be awaited before
|
|
36937
|
+
* the first call to `feedAac`.
|
|
36938
|
+
*/
|
|
36939
|
+
async start() {
|
|
36940
|
+
if (this.starting) return this.starting;
|
|
36941
|
+
this.starting = this._start();
|
|
36942
|
+
return this.starting;
|
|
36943
|
+
}
|
|
36944
|
+
async _start() {
|
|
36945
|
+
if (this.stopped) throw new Error("transcoder stopped");
|
|
36946
|
+
this.socket = (0, import_node_dgram3.createSocket)("udp4");
|
|
36947
|
+
await new Promise((resolve, reject) => {
|
|
36948
|
+
this.socket.once("error", reject);
|
|
36949
|
+
this.socket.bind({ address: "127.0.0.1", port: 0 }, () => {
|
|
36950
|
+
this.socket.removeListener("error", reject);
|
|
36951
|
+
this.port = this.socket.address().port;
|
|
36952
|
+
resolve();
|
|
36953
|
+
});
|
|
36954
|
+
});
|
|
36955
|
+
this.log("info", `UDP loopback bound on 127.0.0.1:${this.port}`);
|
|
36956
|
+
this.socket.on("message", (rtpPacket) => this.handleRtp(rtpPacket));
|
|
36957
|
+
this.socket.on("error", (err) => {
|
|
36958
|
+
this.log("error", `socket error: ${err.message}`);
|
|
36959
|
+
this.emit("error", err);
|
|
36960
|
+
});
|
|
36961
|
+
const args = [
|
|
36962
|
+
"-loglevel",
|
|
36963
|
+
"warning",
|
|
36964
|
+
"-fflags",
|
|
36965
|
+
"nobuffer",
|
|
36966
|
+
"-f",
|
|
36967
|
+
"aac",
|
|
36968
|
+
"-i",
|
|
36969
|
+
"pipe:0",
|
|
36970
|
+
"-c:a",
|
|
36971
|
+
"libopus",
|
|
36972
|
+
"-ar",
|
|
36973
|
+
String(this.opts.opusSampleRate),
|
|
36974
|
+
"-ac",
|
|
36975
|
+
String(this.opts.opusChannels),
|
|
36976
|
+
"-application",
|
|
36977
|
+
"audio",
|
|
36978
|
+
"-frame_duration",
|
|
36979
|
+
String(this.opts.opusFrameMs),
|
|
36980
|
+
"-b:a",
|
|
36981
|
+
String(this.opts.opusBitrate),
|
|
36982
|
+
// Important: disable VBR so the output rate matches the configured
|
|
36983
|
+
// bitrate consistently — easier on the browser jitter buffer.
|
|
36984
|
+
"-vbr",
|
|
36985
|
+
"off",
|
|
36986
|
+
"-f",
|
|
36987
|
+
"rtp",
|
|
36988
|
+
`rtp://127.0.0.1:${this.port}`
|
|
36989
|
+
];
|
|
36990
|
+
this.log("info", `spawning ffmpeg with: ${this.opts.ffmpegPath} ${args.join(" ")}`);
|
|
36991
|
+
this.ffmpeg = (0, import_node_child_process11.spawn)(this.opts.ffmpegPath, args, {
|
|
36992
|
+
stdio: ["pipe", "ignore", "pipe"]
|
|
36993
|
+
});
|
|
36994
|
+
this.ffmpeg.on("error", (err) => {
|
|
36995
|
+
this.log("error", `ffmpeg spawn error: ${err.message}`);
|
|
36996
|
+
this.emit("error", err);
|
|
36997
|
+
});
|
|
36998
|
+
this.ffmpeg.stderr?.on("data", (chunk) => {
|
|
36999
|
+
this.log("debug", `ffmpeg stderr: ${chunk.toString().trim()}`);
|
|
37000
|
+
});
|
|
37001
|
+
this.ffmpeg.on("exit", (code) => {
|
|
37002
|
+
this.log(code === 0 ? "info" : "warn", `ffmpeg exited code=${code}`);
|
|
37003
|
+
this.emit("exit", code);
|
|
37004
|
+
});
|
|
37005
|
+
}
|
|
37006
|
+
/**
|
|
37007
|
+
* Feed one or more concatenated ADTS AAC frames to ffmpeg. Returns the
|
|
37008
|
+
* number of bytes written. Safe to call before a frame is fully buffered.
|
|
37009
|
+
*/
|
|
37010
|
+
feedAac(buf) {
|
|
37011
|
+
if (this.stopped) return false;
|
|
37012
|
+
if (!this.ffmpeg?.stdin || !this.ffmpeg.stdin.writable) {
|
|
37013
|
+
this.log("debug", "drop AAC frame \u2014 ffmpeg stdin not writable");
|
|
37014
|
+
return false;
|
|
37015
|
+
}
|
|
37016
|
+
return this.ffmpeg.stdin.write(buf);
|
|
37017
|
+
}
|
|
37018
|
+
/**
|
|
37019
|
+
* Close stdin (ffmpeg exits cleanly on EOF) and tear down the UDP socket.
|
|
37020
|
+
*/
|
|
37021
|
+
async stop() {
|
|
37022
|
+
if (this.stopped) return;
|
|
37023
|
+
this.stopped = true;
|
|
37024
|
+
try {
|
|
37025
|
+
this.ffmpeg?.stdin?.end();
|
|
37026
|
+
} catch {
|
|
37027
|
+
}
|
|
37028
|
+
if (this.ffmpeg && this.ffmpeg.exitCode === null) {
|
|
37029
|
+
await new Promise((resolve) => {
|
|
37030
|
+
const t = setTimeout(() => {
|
|
37031
|
+
this.ffmpeg?.kill("SIGKILL");
|
|
37032
|
+
resolve();
|
|
37033
|
+
}, 500);
|
|
37034
|
+
this.ffmpeg?.once("exit", () => {
|
|
37035
|
+
clearTimeout(t);
|
|
37036
|
+
resolve();
|
|
37037
|
+
});
|
|
37038
|
+
});
|
|
37039
|
+
}
|
|
37040
|
+
this.ffmpeg = null;
|
|
37041
|
+
if (this.socket) {
|
|
37042
|
+
try {
|
|
37043
|
+
this.socket.close();
|
|
37044
|
+
} catch {
|
|
37045
|
+
}
|
|
37046
|
+
this.socket = null;
|
|
37047
|
+
}
|
|
37048
|
+
this.log("info", "stopped");
|
|
37049
|
+
}
|
|
37050
|
+
/**
|
|
37051
|
+
* Parse an RTP packet ffmpeg emitted on the loopback socket and surface
|
|
37052
|
+
* its Opus payload. RTP header layout (RFC 3550):
|
|
37053
|
+
*
|
|
37054
|
+
* 0 1 2 3
|
|
37055
|
+
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
37056
|
+
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
37057
|
+
* |V=2|P|X| CC |M| PT | sequence number |
|
|
37058
|
+
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
37059
|
+
* | timestamp |
|
|
37060
|
+
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
37061
|
+
* | synchronization source (SSRC) identifier |
|
|
37062
|
+
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
37063
|
+
*
|
|
37064
|
+
* We honor the CC, X (extension) and P (padding) bits to compute the
|
|
37065
|
+
* payload offset / length correctly even if ffmpeg ever emits those.
|
|
37066
|
+
*/
|
|
37067
|
+
handleRtp(packet) {
|
|
37068
|
+
if (packet.length < 12) return;
|
|
37069
|
+
const b0 = packet[0];
|
|
37070
|
+
const b1 = packet[1];
|
|
37071
|
+
const version = b0 >> 6;
|
|
37072
|
+
if (version !== 2) return;
|
|
37073
|
+
const padding = (b0 & 32) !== 0;
|
|
37074
|
+
const extension = (b0 & 16) !== 0;
|
|
37075
|
+
const csrcCount = b0 & 15;
|
|
37076
|
+
const marker = (b1 & 128) !== 0;
|
|
37077
|
+
let offset = 12 + csrcCount * 4;
|
|
37078
|
+
if (extension) {
|
|
37079
|
+
if (packet.length < offset + 4) return;
|
|
37080
|
+
const extLen = packet.readUInt16BE(offset + 2);
|
|
37081
|
+
offset += 4 + extLen * 4;
|
|
37082
|
+
}
|
|
37083
|
+
let end = packet.length;
|
|
37084
|
+
if (padding) {
|
|
37085
|
+
const pad = packet[packet.length - 1];
|
|
37086
|
+
end -= pad;
|
|
37087
|
+
}
|
|
37088
|
+
if (offset >= end) return;
|
|
37089
|
+
const timestamp = packet.readUInt32BE(4);
|
|
37090
|
+
const payload = Buffer.from(packet.subarray(offset, end));
|
|
37091
|
+
this.emit("packet", { payload, timestamp, marker });
|
|
37092
|
+
}
|
|
37093
|
+
};
|
|
37094
|
+
|
|
37095
|
+
// src/baichuan/stream/BaichuanWebRTCServer.ts
|
|
36012
37096
|
init_H264Converter();
|
|
36013
37097
|
function parseAnnexBNalUnits(annexB) {
|
|
36014
37098
|
const nalUnits = [];
|
|
@@ -36044,7 +37128,7 @@ function getH264NalType(nalUnit) {
|
|
|
36044
37128
|
function getH265NalType2(nalUnit) {
|
|
36045
37129
|
return nalUnit[0] >> 1 & 63;
|
|
36046
37130
|
}
|
|
36047
|
-
var BaichuanWebRTCServer = class extends
|
|
37131
|
+
var BaichuanWebRTCServer = class extends import_node_events11.EventEmitter {
|
|
36048
37132
|
options;
|
|
36049
37133
|
sessions = /* @__PURE__ */ new Map();
|
|
36050
37134
|
sessionIdCounter = 0;
|
|
@@ -36282,6 +37366,14 @@ Error: ${err}`
|
|
|
36282
37366
|
}
|
|
36283
37367
|
session.dataChannel = null;
|
|
36284
37368
|
}
|
|
37369
|
+
if (session.audioTranscoder) {
|
|
37370
|
+
try {
|
|
37371
|
+
await session.audioTranscoder.stop();
|
|
37372
|
+
} catch (err) {
|
|
37373
|
+
this.log("debug", `Error stopping audio transcoder: ${err}`);
|
|
37374
|
+
}
|
|
37375
|
+
session.audioTranscoder = null;
|
|
37376
|
+
}
|
|
36285
37377
|
if (session.cleanup) {
|
|
36286
37378
|
session.cleanup();
|
|
36287
37379
|
}
|
|
@@ -36408,6 +37500,17 @@ Error: ${err}`
|
|
|
36408
37500
|
}
|
|
36409
37501
|
if (frame.audio) {
|
|
36410
37502
|
session.stats.audioFrames++;
|
|
37503
|
+
if (session.stats.audioFrames === 1) {
|
|
37504
|
+
const head = frame.data && frame.data.length > 0 ? frame.data.subarray(0, Math.min(8, frame.data.length)).toString("hex") : "(empty)";
|
|
37505
|
+
this.log(
|
|
37506
|
+
"info",
|
|
37507
|
+
`First audio frame for ${session.id}: codec=${frame.codec ?? "?"} bytes=${frame.data?.length ?? 0} head=${head}`
|
|
37508
|
+
);
|
|
37509
|
+
}
|
|
37510
|
+
if (this.options.ffmpegPath !== "" && frame.data && frame.data.length > 0) {
|
|
37511
|
+
await this.ensureAudioTranscoder(session, werift);
|
|
37512
|
+
session.audioTranscoder?.feedAac(frame.data);
|
|
37513
|
+
}
|
|
36411
37514
|
} else {
|
|
36412
37515
|
if (frame.data) {
|
|
36413
37516
|
if (!session.videoCodec && frame.videoType) {
|
|
@@ -36478,7 +37581,7 @@ Error: ${err}`
|
|
|
36478
37581
|
if (now - lastLogTime >= 5e3) {
|
|
36479
37582
|
this.log(
|
|
36480
37583
|
"debug",
|
|
36481
|
-
`WebRTC session ${session.id} [${session.videoCodec}]: sent ${session.stats.videoFrames} frames, ${packetsSentSinceLastLog} packets, ${Math.round(session.stats.bytesSent / 1024)} KB`
|
|
37584
|
+
`WebRTC session ${session.id} [${session.videoCodec}]: sent ${session.stats.videoFrames} video frames, ${packetsSentSinceLastLog} packets, ${Math.round(session.stats.bytesSent / 1024)} KB | audio frames=${session.stats.audioFrames}`
|
|
36482
37585
|
);
|
|
36483
37586
|
lastLogTime = now;
|
|
36484
37587
|
packetsSentSinceLastLog = 0;
|
|
@@ -36494,6 +37597,77 @@ Error: ${err}`
|
|
|
36494
37597
|
}
|
|
36495
37598
|
this.log("info", `Native stream ended for session ${session.id}`);
|
|
36496
37599
|
}
|
|
37600
|
+
/**
|
|
37601
|
+
* Lazily start the AAC → Opus transcoder for `session` and wire it to the
|
|
37602
|
+
* audio RTP track. ffmpeg writes RTP-formatted Opus packets back to a UDP
|
|
37603
|
+
* loopback the transcoder owns; we strip the RTP header and rewrap the
|
|
37604
|
+
* Opus payload with our audioTrack's SSRC so the browser receives a
|
|
37605
|
+
* coherent stream.
|
|
37606
|
+
*/
|
|
37607
|
+
async ensureAudioTranscoder(session, werift) {
|
|
37608
|
+
if (session.audioTranscoder !== void 0) return;
|
|
37609
|
+
const { RtpPacket, RtpHeader } = werift;
|
|
37610
|
+
const ssrc = session.audioTrack?.ssrc ?? Math.floor(Math.random() * 4294967295);
|
|
37611
|
+
const transcoder = new AacToOpusTranscoder({
|
|
37612
|
+
...this.options.ffmpegPath ? { ffmpegPath: this.options.ffmpegPath } : {},
|
|
37613
|
+
logger: (level, msg) => this.log(level, msg)
|
|
37614
|
+
});
|
|
37615
|
+
session.audioRtpSequence = Math.floor(Math.random() * 65535);
|
|
37616
|
+
session.audioRtpTimestampBase = 0;
|
|
37617
|
+
transcoder.on("packet", ({ payload, timestamp, marker }) => {
|
|
37618
|
+
try {
|
|
37619
|
+
if (session.audioRtpTimestampBase === void 0 || session.audioRtpTimestampBase === 0) {
|
|
37620
|
+
session.audioRtpTimestampBase = timestamp;
|
|
37621
|
+
}
|
|
37622
|
+
const localTs = timestamp - session.audioRtpTimestampBase >>> 0;
|
|
37623
|
+
const seq = session.audioRtpSequence;
|
|
37624
|
+
session.audioRtpSequence = seq + 1 & 65535;
|
|
37625
|
+
const header = new RtpHeader({
|
|
37626
|
+
version: 2,
|
|
37627
|
+
padding: false,
|
|
37628
|
+
extension: false,
|
|
37629
|
+
marker,
|
|
37630
|
+
// Werift assigns 111 to Opus by default in the receiver SDP, but it
|
|
37631
|
+
// also accepts other PTs. Use 111 for compatibility with the offer
|
|
37632
|
+
// we generated earlier.
|
|
37633
|
+
payloadType: 111,
|
|
37634
|
+
sequenceNumber: seq,
|
|
37635
|
+
timestamp: localTs,
|
|
37636
|
+
ssrc
|
|
37637
|
+
});
|
|
37638
|
+
const rtp = new RtpPacket(header, payload);
|
|
37639
|
+
session.audioTrack?.writeRtp(rtp);
|
|
37640
|
+
if (!session.audioStartedLogged) {
|
|
37641
|
+
session.audioStartedLogged = true;
|
|
37642
|
+
this.log(
|
|
37643
|
+
"info",
|
|
37644
|
+
`Audio RTP started for ${session.id} (PT=111, ssrc=${ssrc})`
|
|
37645
|
+
);
|
|
37646
|
+
}
|
|
37647
|
+
} catch (err) {
|
|
37648
|
+
this.log(
|
|
37649
|
+
"warn",
|
|
37650
|
+
`audio writeRtp failed for ${session.id}: ${err.message}`
|
|
37651
|
+
);
|
|
37652
|
+
}
|
|
37653
|
+
});
|
|
37654
|
+
transcoder.on("error", (err) => {
|
|
37655
|
+
this.log("error", `audio transcoder error for ${session.id}: ${err.message}`);
|
|
37656
|
+
});
|
|
37657
|
+
transcoder.on("exit", (code) => {
|
|
37658
|
+
this.log("info", `audio transcoder exited (${code}) for ${session.id}`);
|
|
37659
|
+
});
|
|
37660
|
+
try {
|
|
37661
|
+
await transcoder.start();
|
|
37662
|
+
session.audioTranscoder = transcoder;
|
|
37663
|
+
} catch (err) {
|
|
37664
|
+
this.log(
|
|
37665
|
+
"error",
|
|
37666
|
+
`failed to start audio transcoder for ${session.id}: ${err.message}`
|
|
37667
|
+
);
|
|
37668
|
+
session.audioTranscoder = null;
|
|
37669
|
+
}
|
|
37670
|
+
}
|
|
36497
37671
|
/**
|
|
36498
37672
|
* Send H.264 frame via RTP media track
|
|
36499
37673
|
* Returns the number of RTP packets sent
|
|
@@ -36781,21 +37955,21 @@ Error: ${err}`
|
|
|
36781
37955
|
`Sending ${codec} frame ${frameNumber}: ${packet.length} bytes, keyframe=${isKeyframe}`
|
|
36782
37956
|
);
|
|
36783
37957
|
}
|
|
36784
|
-
const
|
|
37958
|
+
const CHUNK_HEADER_LEN = 4;
|
|
37959
|
+
const MAX_PAYLOAD_PER_CHUNK = 16e3 - CHUNK_HEADER_LEN;
|
|
36785
37960
|
try {
|
|
36786
|
-
|
|
36787
|
-
|
|
36788
|
-
|
|
36789
|
-
|
|
36790
|
-
|
|
36791
|
-
|
|
36792
|
-
|
|
36793
|
-
|
|
36794
|
-
|
|
36795
|
-
|
|
36796
|
-
|
|
36797
|
-
|
|
36798
|
-
}
|
|
37961
|
+
const totalChunks = Math.max(
|
|
37962
|
+
1,
|
|
37963
|
+
Math.ceil(packet.length / MAX_PAYLOAD_PER_CHUNK)
|
|
37964
|
+
);
|
|
37965
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
37966
|
+
const start = i * MAX_PAYLOAD_PER_CHUNK;
|
|
37967
|
+
const end = Math.min(start + MAX_PAYLOAD_PER_CHUNK, packet.length);
|
|
37968
|
+
const chunk = packet.subarray(start, end);
|
|
37969
|
+
const chunkHeader = Buffer.alloc(CHUNK_HEADER_LEN);
|
|
37970
|
+
chunkHeader.writeUInt16BE(i, 0);
|
|
37971
|
+
chunkHeader.writeUInt16BE(totalChunks, 2);
|
|
37972
|
+
session.videoDataChannel.send(Buffer.concat([chunkHeader, chunk]));
|
|
36799
37973
|
}
|
|
36800
37974
|
return true;
|
|
36801
37975
|
} catch (err) {
|
|
@@ -36946,12 +38120,12 @@ Error: ${err}`
|
|
|
36946
38120
|
};
|
|
36947
38121
|
|
|
36948
38122
|
// src/baichuan/stream/BaichuanHlsServer.ts
|
|
36949
|
-
var
|
|
38123
|
+
var import_node_events12 = require("events");
|
|
36950
38124
|
var import_node_fs = __toESM(require("fs"), 1);
|
|
36951
38125
|
var import_promises3 = __toESM(require("fs/promises"), 1);
|
|
36952
38126
|
var import_node_os3 = __toESM(require("os"), 1);
|
|
36953
38127
|
var import_node_path3 = __toESM(require("path"), 1);
|
|
36954
|
-
var
|
|
38128
|
+
var import_node_child_process12 = require("child_process");
|
|
36955
38129
|
init_BcMediaAnnexBDecoder();
|
|
36956
38130
|
init_H264Converter();
|
|
36957
38131
|
init_H265Converter();
|
|
@@ -37026,7 +38200,7 @@ function getNalTypes(codec, annexB) {
|
|
|
37026
38200
|
}
|
|
37027
38201
|
});
|
|
37028
38202
|
}
|
|
37029
|
-
var BaichuanHlsServer = class extends
|
|
38203
|
+
var BaichuanHlsServer = class extends import_node_events12.EventEmitter {
|
|
37030
38204
|
api;
|
|
37031
38205
|
channel;
|
|
37032
38206
|
profile;
|
|
@@ -37383,10 +38557,17 @@ var BaichuanHlsServer = class extends import_node_events11.EventEmitter {
|
|
|
37383
38557
|
"-hide_banner",
|
|
37384
38558
|
"-loglevel",
|
|
37385
38559
|
"warning",
|
|
38560
|
+
// `+genpts` makes ffmpeg generate uniform PTS from the declared `-r`
|
|
38561
|
+
// when the raw H.264/H.265 input has none. We deliberately do NOT use
|
|
38562
|
+
// `-use_wallclock_as_timestamps 1` here: it replaces the generated
|
|
38563
|
+
// PTS with the host wallclock at FRAME ARRIVAL time, and because the
|
|
38564
|
+
// camera ships frames in bursty network reads, the resulting PTS
|
|
38565
|
+
// sequence is uneven. With `-r 25` (or anything else) forcing a
|
|
38566
|
+
// target rate downstream, ffmpeg then drops/duplicates frames to
|
|
38567
|
+
// match — visible as the periodic stutter / pulsing reported on
|
|
38568
|
+
// local-restreamer recordings (issue #11).
|
|
37386
38569
|
"-fflags",
|
|
37387
38570
|
"+genpts",
|
|
37388
|
-
"-use_wallclock_as_timestamps",
|
|
37389
|
-
"1",
|
|
37390
38571
|
"-r",
|
|
37391
38572
|
"25",
|
|
37392
38573
|
"-f",
|
|
@@ -37421,7 +38602,7 @@ var BaichuanHlsServer = class extends import_node_events11.EventEmitter {
|
|
|
37421
38602
|
this.segmentPattern,
|
|
37422
38603
|
this.playlistPath
|
|
37423
38604
|
);
|
|
37424
|
-
const p = (0,
|
|
38605
|
+
const p = (0, import_node_child_process12.spawn)(this.ffmpegPath, args, {
|
|
37425
38606
|
stdio: ["pipe", "ignore", "pipe"]
|
|
37426
38607
|
});
|
|
37427
38608
|
p.on("error", (err) => {
|
|
@@ -38043,10 +39224,10 @@ async function autoDetectDeviceType(inputs) {
|
|
|
38043
39224
|
}
|
|
38044
39225
|
|
|
38045
39226
|
// src/multifocal/compositeRtspServer.ts
|
|
38046
|
-
var
|
|
38047
|
-
var
|
|
39227
|
+
var import_node_events13 = require("events");
|
|
39228
|
+
var import_node_child_process13 = require("child_process");
|
|
38048
39229
|
var net5 = __toESM(require("net"), 1);
|
|
38049
|
-
var CompositeRtspServer = class extends
|
|
39230
|
+
var CompositeRtspServer = class extends import_node_events13.EventEmitter {
|
|
38050
39231
|
options;
|
|
38051
39232
|
compositeStream = null;
|
|
38052
39233
|
rtspServer = null;
|
|
@@ -38151,7 +39332,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
|
|
|
38151
39332
|
this.logger.log?.(
|
|
38152
39333
|
`[CompositeRtspServer] Starting ffmpeg RTSP server: ${ffmpegArgs.join(" ")}`
|
|
38153
39334
|
);
|
|
38154
|
-
this.ffmpegProcess = (0,
|
|
39335
|
+
this.ffmpegProcess = (0, import_node_child_process13.spawn)("ffmpeg", ffmpegArgs, {
|
|
38155
39336
|
stdio: ["pipe", "pipe", "pipe"]
|
|
38156
39337
|
});
|
|
38157
39338
|
this.ffmpegProcess.on("error", (error) => {
|
|
@@ -38259,6 +39440,86 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
|
|
|
38259
39440
|
return this.connectedClients.size;
|
|
38260
39441
|
}
|
|
38261
39442
|
};
|
|
39443
|
+
|
|
39444
|
+
// src/reolink/baichuan/utils/motionZone.ts
|
|
39445
|
+
function decodeMotionScopeBitmap(valueTable, columns, rows, width = columns, height = rows) {
|
|
39446
|
+
const trimmed = valueTable.trim().replace(/[^A-Za-z0-9+/=]/g, "");
|
|
39447
|
+
const bytes = base64DecodeToBytes(trimmed);
|
|
39448
|
+
const totalBits = columns * rows;
|
|
39449
|
+
if (bytes.length * 8 < totalBits) {
|
|
39450
|
+
throw new Error(
|
|
39451
|
+
`valueTable too short: have ${bytes.length * 8} bits, need ${totalBits}`
|
|
39452
|
+
);
|
|
39453
|
+
}
|
|
39454
|
+
const w = Math.min(width, columns);
|
|
39455
|
+
const h = Math.min(height, rows);
|
|
39456
|
+
const cells = new Array(w * h);
|
|
39457
|
+
for (let r = 0; r < h; r++) {
|
|
39458
|
+
for (let c = 0; c < w; c++) {
|
|
39459
|
+
const bitIndex = r * columns + c;
|
|
39460
|
+
const byteIdx = bitIndex >> 3;
|
|
39461
|
+
const bitIdx = 7 - (bitIndex & 7);
|
|
39462
|
+
cells[r * w + c] = (bytes[byteIdx] ?? 0) >> bitIdx & 1 ? true : false;
|
|
39463
|
+
}
|
|
39464
|
+
}
|
|
39465
|
+
return { width: w, height: h, columns, rows, cells };
|
|
39466
|
+
}
|
|
39467
|
+
function encodeMotionScopeBitmap(scope) {
|
|
39468
|
+
const bytes = new Uint8Array(Math.ceil(scope.columns * scope.rows / 8));
|
|
39469
|
+
for (let r = 0; r < scope.height; r++) {
|
|
39470
|
+
for (let c = 0; c < scope.width; c++) {
|
|
39471
|
+
const on = scope.cells[r * scope.width + c];
|
|
39472
|
+
if (!on) continue;
|
|
39473
|
+
const bitIndex = r * scope.columns + c;
|
|
39474
|
+
const byteIdx = bitIndex >> 3;
|
|
39475
|
+
const bitIdx = 7 - (bitIndex & 7);
|
|
39476
|
+
bytes[byteIdx] = (bytes[byteIdx] ?? 0) | 1 << bitIdx;
|
|
39477
|
+
}
|
|
39478
|
+
}
|
|
39479
|
+
return base64EncodeBytes(bytes);
|
|
39480
|
+
}
|
|
39481
|
+
function fullCoverageScope(columns, rows, width = columns, height = rows) {
|
|
39482
|
+
return {
|
|
39483
|
+
width,
|
|
39484
|
+
height,
|
|
39485
|
+
columns,
|
|
39486
|
+
rows,
|
|
39487
|
+
cells: new Array(width * height).fill(true)
|
|
39488
|
+
};
|
|
39489
|
+
}
|
|
39490
|
+
var B64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
39491
|
+
function base64EncodeBytes(bytes) {
|
|
39492
|
+
let out = "";
|
|
39493
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
39494
|
+
const b0 = bytes[i] ?? 0;
|
|
39495
|
+
const b1 = bytes[i + 1] ?? 0;
|
|
39496
|
+
const b2 = bytes[i + 2] ?? 0;
|
|
39497
|
+
out += B64_ALPHABET[b0 >> 2];
|
|
39498
|
+
out += B64_ALPHABET[(b0 & 3) << 4 | b1 >> 4];
|
|
39499
|
+
out += i + 1 < bytes.length ? B64_ALPHABET[(b1 & 15) << 2 | b2 >> 6] : "=";
|
|
39500
|
+
out += i + 2 < bytes.length ? B64_ALPHABET[b2 & 63] : "=";
|
|
39501
|
+
}
|
|
39502
|
+
return out;
|
|
39503
|
+
}
|
|
39504
|
+
function base64DecodeToBytes(b64) {
|
|
39505
|
+
const padded = b64.padEnd(Math.ceil(b64.length / 4) * 4, "=");
|
|
39506
|
+
const stripped = padded.replace(/=+$/, "");
|
|
39507
|
+
const out = new Uint8Array(Math.floor(stripped.length * 6 / 8));
|
|
39508
|
+
let bits = 0;
|
|
39509
|
+
let value = 0;
|
|
39510
|
+
let outIdx = 0;
|
|
39511
|
+
for (const ch of stripped) {
|
|
39512
|
+
const idx = B64_ALPHABET.indexOf(ch);
|
|
39513
|
+
if (idx < 0) continue;
|
|
39514
|
+
value = value << 6 | idx;
|
|
39515
|
+
bits += 6;
|
|
39516
|
+
if (bits >= 8) {
|
|
39517
|
+
bits -= 8;
|
|
39518
|
+
out[outIdx++] = value >> bits & 255;
|
|
39519
|
+
}
|
|
39520
|
+
}
|
|
39521
|
+
return out.subarray(0, outIdx);
|
|
39522
|
+
}
|
|
38262
39523
|
// Annotate the CommonJS export names for ESM import in node:
|
|
38263
39524
|
0 && (module.exports = {
|
|
38264
39525
|
AesStreamDecryptor,
|
|
@@ -38336,6 +39597,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
|
|
|
38336
39597
|
BC_CMD_ID_GET_SUPPORT,
|
|
38337
39598
|
BC_CMD_ID_GET_SYSTEM_GENERAL,
|
|
38338
39599
|
BC_CMD_ID_GET_TIMELAPSE_CFG,
|
|
39600
|
+
BC_CMD_ID_GET_VERSION_INFO,
|
|
38339
39601
|
BC_CMD_ID_GET_VIDEO_INPUT,
|
|
38340
39602
|
BC_CMD_ID_GET_WHITE_LED,
|
|
38341
39603
|
BC_CMD_ID_GET_WIFI,
|
|
@@ -38366,6 +39628,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
|
|
|
38366
39628
|
BC_CMD_ID_SET_ENC,
|
|
38367
39629
|
BC_CMD_ID_SET_LED_STATE,
|
|
38368
39630
|
BC_CMD_ID_SET_MOTION_ALARM,
|
|
39631
|
+
BC_CMD_ID_SET_OSD_DATETIME,
|
|
38369
39632
|
BC_CMD_ID_SET_PIR_INFO,
|
|
38370
39633
|
BC_CMD_ID_SET_PRIVACY_MASK,
|
|
38371
39634
|
BC_CMD_ID_SET_PUSH_TASK,
|
|
@@ -38474,6 +39737,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
|
|
|
38474
39737
|
decideSleepInferenceTransition,
|
|
38475
39738
|
decideVideoclipTranscodeMode,
|
|
38476
39739
|
decodeHeader,
|
|
39740
|
+
decodeMotionScopeBitmap,
|
|
38477
39741
|
deriveAesKey,
|
|
38478
39742
|
detectIosClient,
|
|
38479
39743
|
detectVideoCodecFromNal,
|
|
@@ -38486,6 +39750,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
|
|
|
38486
39750
|
discoverViaUdpBroadcast,
|
|
38487
39751
|
discoverViaUdpDirect,
|
|
38488
39752
|
encodeHeader,
|
|
39753
|
+
encodeMotionScopeBitmap,
|
|
38489
39754
|
ensureXmlHeader,
|
|
38490
39755
|
extractH264ParamSetsFromAccessUnit,
|
|
38491
39756
|
extractH265ParamSetsFromAccessUnit,
|
|
@@ -38494,6 +39759,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
|
|
|
38494
39759
|
extractVpsFromAnnexB,
|
|
38495
39760
|
flattenAbilitiesForChannel,
|
|
38496
39761
|
formatMjpegFrame,
|
|
39762
|
+
fullCoverageScope,
|
|
38497
39763
|
getConstructedVideoStreamOptions,
|
|
38498
39764
|
getGlobalLogger,
|
|
38499
39765
|
getH265NalType,
|
|
@@ -38537,6 +39803,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
|
|
|
38537
39803
|
splitAnnexBToNals,
|
|
38538
39804
|
splitH265AnnexBToNalPayloads,
|
|
38539
39805
|
testChannelStreams,
|
|
39806
|
+
upsertXmlTag,
|
|
38540
39807
|
xmlEscape,
|
|
38541
39808
|
xmlIndicatesFloodlight,
|
|
38542
39809
|
zipDirectory
|