@apocaliss92/nodelink-js 0.4.7 → 0.4.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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_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_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;
38
38
  var init_constants = __esm({
39
39
  "src/protocol/constants.ts"() {
40
40
  "use strict";
@@ -137,6 +137,22 @@ var init_constants = __esm({
137
137
  BC_CMD_ID_SET_AI_CFG = 300;
138
138
  BC_CMD_ID_GET_SIREN_STATUS = 547;
139
139
  BC_CMD_ID_SET_AUDIO_TASK = 231;
140
+ BC_CMD_ID_SET_VIDEO_INPUT = 25;
141
+ BC_CMD_ID_SET_DAY_NIGHT_THRESHOLD = 297;
142
+ BC_CMD_ID_GET_ENC = 56;
143
+ BC_CMD_ID_SET_ENC = 57;
144
+ BC_CMD_ID_GET_PRIVACY_MASK = 52;
145
+ BC_CMD_ID_SET_PRIVACY_MASK = 53;
146
+ BC_CMD_ID_SET_AI_DENOISE = 440;
147
+ BC_CMD_ID_SET_LED_STATE = 209;
148
+ BC_CMD_ID_SET_AUDIO_CFG = 265;
149
+ BC_CMD_ID_SET_RECORD = 82;
150
+ BC_CMD_ID_SET_RECORD_CFG = 55;
151
+ BC_CMD_ID_SET_EMAIL_TASK = 216;
152
+ BC_CMD_ID_GET_PUSH_TASK = 219;
153
+ BC_CMD_ID_SET_PUSH_TASK = 218;
154
+ BC_CMD_ID_GET_AUTO_FOCUS = 224;
155
+ BC_CMD_ID_SET_AUTO_FOCUS = 225;
140
156
  BC_CMD_ID_CMD_123 = 123;
141
157
  BC_CMD_ID_CMD_209 = 209;
142
158
  BC_CMD_ID_CMD_265 = 265;
@@ -452,6 +468,59 @@ function buildFloodlightManualXml(channelId, status, durationSeconds = 180) {
452
468
  function buildWhiteLedStateXml(channelId, state) {
453
469
  return buildFloodlightManualXml(channelId, state ? 1 : 0);
454
470
  }
471
+ function ensureXmlHeader(xml) {
472
+ const trimmed = xml.trimStart();
473
+ if (trimmed.startsWith("<?xml")) return xml;
474
+ return `${XML_HEADER}
475
+ ${xml}`;
476
+ }
477
+ function applyXmlTagPatch(xml, tag, value) {
478
+ if (value === void 0) return xml;
479
+ const v = typeof value === "boolean" ? value ? 1 : 0 : value;
480
+ const re = new RegExp(`<${tag}>[^<]*</${tag}>`);
481
+ return xml.replace(re, `<${tag}>${v}</${tag}>`);
482
+ }
483
+ function patchNestedTag(xml, parent, child, value) {
484
+ if (value === void 0) return xml;
485
+ const v = typeof value === "boolean" ? value ? 1 : 0 : value;
486
+ const re = new RegExp(
487
+ `(<${parent}[^>]*>[\\s\\S]*?<${child}>)[^<]*(</${child}>[\\s\\S]*?</${parent}>)`
488
+ );
489
+ return xml.replace(re, `$1${v}$2`);
490
+ }
491
+ function applyStreamPatch(xml, streamTag, patch) {
492
+ if (!patch) return xml;
493
+ const re = new RegExp(
494
+ `(<${streamTag}[^>]*>)([\\s\\S]*?)(</${streamTag}>)`
495
+ );
496
+ return xml.replace(re, (_match, open, body, close) => {
497
+ let next = body;
498
+ if (patch.bitRate !== void 0) {
499
+ next = applyXmlTagPatch(next, "bitRate", patch.bitRate);
500
+ }
501
+ if (patch.frameRate !== void 0) {
502
+ next = applyXmlTagPatch(next, "frameRate", patch.frameRate);
503
+ next = applyXmlTagPatch(next, "frame", patch.frameRate);
504
+ }
505
+ if (patch.videoEncType !== void 0) {
506
+ const intVal = patch.videoEncType === "h265" ? 1 : 0;
507
+ next = applyXmlTagPatch(next, "videoEncType", intVal);
508
+ }
509
+ return `${open}${next}${close}`;
510
+ });
511
+ }
512
+ function normalizeDayNightMode(input) {
513
+ const stripped = String(input).replace(/&/g, "And");
514
+ if (!stripped) return stripped;
515
+ const first = stripped[0];
516
+ if (first === void 0) return stripped;
517
+ return first.toLowerCase() + stripped.slice(1);
518
+ }
519
+ function normalizeOpenClose(input) {
520
+ const v = String(input).toLowerCase();
521
+ if (v === "on" || v === "open" || v === "1" || v === "true") return "open";
522
+ return "close";
523
+ }
455
524
  function buildAbilityInfoExtensionXml(username) {
456
525
  return `<?xml version="1.0" encoding="UTF-8" ?>
457
526
  <Extension version="1.1">
@@ -459,9 +528,11 @@ function buildAbilityInfoExtensionXml(username) {
459
528
  <token>system, streaming, PTZ, IO, security, replay, disk, network, alarm, record, video, image</token>
460
529
  </Extension>`;
461
530
  }
531
+ var XML_HEADER;
462
532
  var init_xml = __esm({
463
533
  "src/protocol/xml.ts"() {
464
534
  "use strict";
535
+ XML_HEADER = `<?xml version="1.0" encoding="UTF-8" ?>`;
465
536
  }
466
537
  });
467
538
 
@@ -2462,7 +2533,7 @@ var init_BaichuanVideoStream = __esm({
2462
2533
  const allowMsgNum0Fallback = this.acceptAnyStreamType && frame.header.msgNum === 0;
2463
2534
  if (!allowMsgNum0Fallback) {
2464
2535
  const frameCount = this._msgNumMismatchCount = (this._msgNumMismatchCount || 0) + 1;
2465
- if (frameCount <= 5) {
2536
+ if (frameCount <= 5 && this.client.getDebugConfig().general) {
2466
2537
  this.logger?.log(
2467
2538
  `[BaichuanVideoStream] Frame msgNum mismatch: received=${frame.header.msgNum}, expected=${this.activeMsgNum}, channel=${this.channel}, profile=${this.profile}, variant=${this.variant} (frame discarded)`
2468
2539
  );
@@ -2472,7 +2543,7 @@ var init_BaichuanVideoStream = __esm({
2472
2543
  }
2473
2544
  if (!this.acceptAnyStreamType && !this.expectedStreamTypes.has(frame.header.streamType)) {
2474
2545
  const frameCount = this._streamTypeMismatchCount = (this._streamTypeMismatchCount || 0) + 1;
2475
- if (frameCount <= 5) {
2546
+ if (frameCount <= 5 && this.client.getDebugConfig().general) {
2476
2547
  this.logger?.log(
2477
2548
  `[BaichuanVideoStream] Frame streamType mismatch: received=${frame.header.streamType}, expectedAny=[${[
2478
2549
  ...this.expectedStreamTypes
@@ -6409,6 +6480,137 @@ var init_ReolinkCgiApi = __esm({
6409
6480
  const param = channel == null ? {} : { channel };
6410
6481
  return await this.call("GetPtzPreset", param, 1);
6411
6482
  }
6483
+ // ── Isp / Image (colour, flip, day-night, exposure) ──────────────
6484
+ async GetIsp(channel) {
6485
+ const param = channel == null ? {} : { channel };
6486
+ return await this.call("GetIsp", param, 1);
6487
+ }
6488
+ async SetIsp(isp) {
6489
+ return await this.call("SetIsp", isp, 0);
6490
+ }
6491
+ async GetImage(channel) {
6492
+ const param = channel == null ? {} : { channel };
6493
+ return await this.call("GetImage", param, 1);
6494
+ }
6495
+ async SetImage(image) {
6496
+ return await this.call("SetImage", image, 0);
6497
+ }
6498
+ // ── AudioCfg (mute / volume) ─────────────────────────────────────
6499
+ async GetAudioCfg(channel) {
6500
+ const param = channel == null ? {} : { channel };
6501
+ return await this.call("GetAudioCfg", param, 1);
6502
+ }
6503
+ async SetAudioCfg(audio) {
6504
+ return await this.call("SetAudioCfg", audio, 0);
6505
+ }
6506
+ // ── Enc setter (Get already exists above) ────────────────────────
6507
+ async SetEnc(enc) {
6508
+ return await this.call("SetEnc", enc, 0);
6509
+ }
6510
+ // ── MdAlarm (motion detection sensitivity / regions) ─────────────
6511
+ async GetMdAlarm(channel) {
6512
+ const param = channel == null ? {} : { channel };
6513
+ return await this.call("GetMdAlarm", param, 1);
6514
+ }
6515
+ async SetMdAlarm(md) {
6516
+ return await this.call("SetMdAlarm", md, 0);
6517
+ }
6518
+ // ── IrLights ─────────────────────────────────────────────────────
6519
+ async GetIrLights(channel) {
6520
+ const param = channel == null ? {} : { channel };
6521
+ return await this.call("GetIrLights", param, 1);
6522
+ }
6523
+ async SetIrLights(ir) {
6524
+ return await this.call("SetIrLights", ir, 0);
6525
+ }
6526
+ // ── AiCfg (smart-detection enable + class filter) ────────────────
6527
+ async GetAiCfg(channel) {
6528
+ const param = channel == null ? {} : { channel };
6529
+ return await this.call("GetAiCfg", param, 1);
6530
+ }
6531
+ async SetAiCfg(ai) {
6532
+ return await this.call("SetAiCfg", ai, 0);
6533
+ }
6534
+ // ── Mask (privacy-mask zones) ────────────────────────────────────
6535
+ async GetMask(channel) {
6536
+ return await this.call("GetMask", { channel }, 1);
6537
+ }
6538
+ async SetMask(mask) {
6539
+ return await this.call("SetMask", mask, 0);
6540
+ }
6541
+ // ── AudioNoise (input noise reduction) ───────────────────────────
6542
+ async GetAudioNoise(channel) {
6543
+ return await this.call("GetAudioNoise", { channel }, 1);
6544
+ }
6545
+ async SetAudioNoise(noise) {
6546
+ return await this.call("SetAudioNoise", noise, 0);
6547
+ }
6548
+ // ── Rec / RecV20 (recording schedule) ────────────────────────────
6549
+ async GetRec(channel) {
6550
+ return await this.call("GetRec", { channel }, 1);
6551
+ }
6552
+ async SetRec(rec) {
6553
+ return await this.call("SetRec", rec, 0);
6554
+ }
6555
+ /** Newer firmwares advertise `GetRecV20` / `SetRecV20` with the
6556
+ * weekly-schedule `table` field. Same payload shape as `Rec`. */
6557
+ async GetRecV20(channel) {
6558
+ return await this.call("GetRecV20", { channel }, 1);
6559
+ }
6560
+ async SetRecV20(rec) {
6561
+ return await this.call("SetRecV20", rec, 0);
6562
+ }
6563
+ // ── Email (SMTP alert) ───────────────────────────────────────────
6564
+ async GetEmail(channel) {
6565
+ return await this.call("GetEmail", { channel }, 1);
6566
+ }
6567
+ async SetEmail(email) {
6568
+ return await this.call("SetEmail", email, 0);
6569
+ }
6570
+ /** V20 variant on newer firmwares with weekly-schedule `table`. */
6571
+ async GetEmailV20(channel) {
6572
+ return await this.call("GetEmailV20", { channel }, 1);
6573
+ }
6574
+ async SetEmailV20(email) {
6575
+ return await this.call("SetEmailV20", email, 0);
6576
+ }
6577
+ // ── Push (Reolink-cloud push notifications) ──────────────────────
6578
+ async GetPush(channel) {
6579
+ return await this.call("GetPush", { channel }, 1);
6580
+ }
6581
+ async SetPush(push) {
6582
+ return await this.call("SetPush", push, 0);
6583
+ }
6584
+ async GetPushV20(channel) {
6585
+ return await this.call("GetPushV20", { channel }, 1);
6586
+ }
6587
+ async SetPushV20(push) {
6588
+ return await this.call("SetPushV20", push, 0);
6589
+ }
6590
+ // ── AudioAlarm (siren-on-event) ──────────────────────────────────
6591
+ async GetAudioAlarm(channel) {
6592
+ return await this.call("GetAudioAlarm", { channel }, 1);
6593
+ }
6594
+ async SetAudioAlarm(audio) {
6595
+ return await this.call("SetAudioAlarm", audio, 0);
6596
+ }
6597
+ async SetAudioAlarmV20(audio) {
6598
+ return await this.call("SetAudioAlarmV20", audio, 0);
6599
+ }
6600
+ // ── AutoFocus (PTZ AF) ───────────────────────────────────────────
6601
+ async GetAutoFocus(channel) {
6602
+ return await this.call("GetAutoFocus", { channel }, 1);
6603
+ }
6604
+ async SetAutoFocus(af) {
6605
+ return await this.call("SetAutoFocus", af, 0);
6606
+ }
6607
+ // ── AiAlarm (per-class smart-detection thresholds) ───────────────
6608
+ async GetAiAlarm(channel, aiType) {
6609
+ return await this.call("GetAiAlarm", { channel, ai_type: aiType }, 1);
6610
+ }
6611
+ async SetAiAlarm(ai) {
6612
+ return await this.call("SetAiAlarm", ai, 0);
6613
+ }
6412
6614
  async GetAudioAlarmV20(channel) {
6413
6615
  const param = channel == null ? {} : { channel };
6414
6616
  return await this.call("GetAudioAlarmV20", param, 0);
@@ -7807,6 +8009,7 @@ __export(index_exports, {
7807
8009
  BC_CMD_ID_GET_AUDIO_ALARM: () => BC_CMD_ID_GET_AUDIO_ALARM,
7808
8010
  BC_CMD_ID_GET_AUDIO_CFG: () => BC_CMD_ID_GET_AUDIO_CFG,
7809
8011
  BC_CMD_ID_GET_AUDIO_TASK: () => BC_CMD_ID_GET_AUDIO_TASK,
8012
+ BC_CMD_ID_GET_AUTO_FOCUS: () => BC_CMD_ID_GET_AUTO_FOCUS,
7810
8013
  BC_CMD_ID_GET_BATTERY_INFO: () => BC_CMD_ID_GET_BATTERY_INFO,
7811
8014
  BC_CMD_ID_GET_BATTERY_INFO_LIST: () => BC_CMD_ID_GET_BATTERY_INFO_LIST,
7812
8015
  BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD: () => BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD,
@@ -7815,6 +8018,7 @@ __export(index_exports, {
7815
8018
  BC_CMD_ID_GET_DING_DONG_LIST: () => BC_CMD_ID_GET_DING_DONG_LIST,
7816
8019
  BC_CMD_ID_GET_DING_DONG_SILENT: () => BC_CMD_ID_GET_DING_DONG_SILENT,
7817
8020
  BC_CMD_ID_GET_EMAIL_TASK: () => BC_CMD_ID_GET_EMAIL_TASK,
8021
+ BC_CMD_ID_GET_ENC: () => BC_CMD_ID_GET_ENC,
7818
8022
  BC_CMD_ID_GET_FTP_TASK: () => BC_CMD_ID_GET_FTP_TASK,
7819
8023
  BC_CMD_ID_GET_HDD_INFO_LIST: () => BC_CMD_ID_GET_HDD_INFO_LIST,
7820
8024
  BC_CMD_ID_GET_KIT_AP_CFG: () => BC_CMD_ID_GET_KIT_AP_CFG,
@@ -7823,8 +8027,10 @@ __export(index_exports, {
7823
8027
  BC_CMD_ID_GET_ONLINE_USER_LIST: () => BC_CMD_ID_GET_ONLINE_USER_LIST,
7824
8028
  BC_CMD_ID_GET_OSD_DATETIME: () => BC_CMD_ID_GET_OSD_DATETIME,
7825
8029
  BC_CMD_ID_GET_PIR_INFO: () => BC_CMD_ID_GET_PIR_INFO,
8030
+ BC_CMD_ID_GET_PRIVACY_MASK: () => BC_CMD_ID_GET_PRIVACY_MASK,
7826
8031
  BC_CMD_ID_GET_PTZ_POSITION: () => BC_CMD_ID_GET_PTZ_POSITION,
7827
8032
  BC_CMD_ID_GET_PTZ_PRESET: () => BC_CMD_ID_GET_PTZ_PRESET,
8033
+ BC_CMD_ID_GET_PUSH_TASK: () => BC_CMD_ID_GET_PUSH_TASK,
7828
8034
  BC_CMD_ID_GET_RECORD: () => BC_CMD_ID_GET_RECORD,
7829
8035
  BC_CMD_ID_GET_RECORD_CFG: () => BC_CMD_ID_GET_RECORD_CFG,
7830
8036
  BC_CMD_ID_GET_REC_ENC_CFG: () => BC_CMD_ID_GET_REC_ENC_CFG,
@@ -7853,11 +8059,23 @@ __export(index_exports, {
7853
8059
  BC_CMD_ID_QUICK_REPLY_PLAY: () => BC_CMD_ID_QUICK_REPLY_PLAY,
7854
8060
  BC_CMD_ID_SET_AI_ALARM: () => BC_CMD_ID_SET_AI_ALARM,
7855
8061
  BC_CMD_ID_SET_AI_CFG: () => BC_CMD_ID_SET_AI_CFG,
8062
+ BC_CMD_ID_SET_AI_DENOISE: () => BC_CMD_ID_SET_AI_DENOISE,
8063
+ BC_CMD_ID_SET_AUDIO_CFG: () => BC_CMD_ID_SET_AUDIO_CFG,
7856
8064
  BC_CMD_ID_SET_AUDIO_TASK: () => BC_CMD_ID_SET_AUDIO_TASK,
8065
+ BC_CMD_ID_SET_AUTO_FOCUS: () => BC_CMD_ID_SET_AUTO_FOCUS,
8066
+ BC_CMD_ID_SET_DAY_NIGHT_THRESHOLD: () => BC_CMD_ID_SET_DAY_NIGHT_THRESHOLD,
7857
8067
  BC_CMD_ID_SET_DING_DONG_CFG: () => BC_CMD_ID_SET_DING_DONG_CFG,
7858
8068
  BC_CMD_ID_SET_DING_DONG_SILENT: () => BC_CMD_ID_SET_DING_DONG_SILENT,
8069
+ BC_CMD_ID_SET_EMAIL_TASK: () => BC_CMD_ID_SET_EMAIL_TASK,
8070
+ BC_CMD_ID_SET_ENC: () => BC_CMD_ID_SET_ENC,
8071
+ BC_CMD_ID_SET_LED_STATE: () => BC_CMD_ID_SET_LED_STATE,
7859
8072
  BC_CMD_ID_SET_MOTION_ALARM: () => BC_CMD_ID_SET_MOTION_ALARM,
7860
8073
  BC_CMD_ID_SET_PIR_INFO: () => BC_CMD_ID_SET_PIR_INFO,
8074
+ BC_CMD_ID_SET_PRIVACY_MASK: () => BC_CMD_ID_SET_PRIVACY_MASK,
8075
+ BC_CMD_ID_SET_PUSH_TASK: () => BC_CMD_ID_SET_PUSH_TASK,
8076
+ BC_CMD_ID_SET_RECORD: () => BC_CMD_ID_SET_RECORD,
8077
+ BC_CMD_ID_SET_RECORD_CFG: () => BC_CMD_ID_SET_RECORD_CFG,
8078
+ BC_CMD_ID_SET_VIDEO_INPUT: () => BC_CMD_ID_SET_VIDEO_INPUT,
7861
8079
  BC_CMD_ID_SET_WHITE_LED_STATE: () => BC_CMD_ID_SET_WHITE_LED_STATE,
7862
8080
  BC_CMD_ID_SET_WHITE_LED_TASK: () => BC_CMD_ID_SET_WHITE_LED_TASK,
7863
8081
  BC_CMD_ID_SET_ZOOM_FOCUS: () => BC_CMD_ID_SET_ZOOM_FOCUS,
@@ -7896,6 +8114,7 @@ __export(index_exports, {
7896
8114
  HlsSessionManager: () => HlsSessionManager,
7897
8115
  Intercom: () => Intercom,
7898
8116
  MjpegTransformer: () => MjpegTransformer,
8117
+ MpegTsMuxer: () => MpegTsMuxer,
7899
8118
  NVR_HUB_EXACT_TYPES: () => NVR_HUB_EXACT_TYPES,
7900
8119
  NVR_HUB_MODEL_PATTERNS: () => NVR_HUB_MODEL_PATTERNS,
7901
8120
  ReolinkBaichuanApi: () => ReolinkBaichuanApi,
@@ -7905,6 +8124,8 @@ __export(index_exports, {
7905
8124
  abilitiesHasAny: () => abilitiesHasAny,
7906
8125
  aesDecrypt: () => aesDecrypt,
7907
8126
  aesEncrypt: () => aesEncrypt,
8127
+ applyStreamPatch: () => applyStreamPatch,
8128
+ applyXmlTagPatch: () => applyXmlTagPatch,
7908
8129
  asLogger: () => asLogger,
7909
8130
  autoDetectDeviceType: () => autoDetectDeviceType,
7910
8131
  bcDecrypt: () => bcDecrypt,
@@ -7954,6 +8175,7 @@ __export(index_exports, {
7954
8175
  createRfc4571TcpServerForReplay: () => createRfc4571TcpServerForReplay,
7955
8176
  createRtspProxyServer: () => createRtspProxyServer,
7956
8177
  createTaggedLogger: () => createTaggedLogger,
8178
+ decideSleepInferenceTransition: () => decideSleepInferenceTransition,
7957
8179
  decideVideoclipTranscodeMode: () => decideVideoclipTranscodeMode,
7958
8180
  decodeHeader: () => decodeHeader,
7959
8181
  deriveAesKey: () => deriveAesKey,
@@ -7968,6 +8190,7 @@ __export(index_exports, {
7968
8190
  discoverViaUdpBroadcast: () => discoverViaUdpBroadcast,
7969
8191
  discoverViaUdpDirect: () => discoverViaUdpDirect,
7970
8192
  encodeHeader: () => encodeHeader,
8193
+ ensureXmlHeader: () => ensureXmlHeader,
7971
8194
  extractH264ParamSetsFromAccessUnit: () => extractH264ParamSetsFromAccessUnit,
7972
8195
  extractH265ParamSetsFromAccessUnit: () => extractH265ParamSetsFromAccessUnit,
7973
8196
  extractPpsFromAnnexB: () => extractPpsFromAnnexB,
@@ -7996,6 +8219,8 @@ __export(index_exports, {
7996
8219
  maskUid: () => maskUid,
7997
8220
  md5HexUpper: () => md5HexUpper,
7998
8221
  md5StrModern: () => md5StrModern,
8222
+ normalizeDayNightMode: () => normalizeDayNightMode,
8223
+ normalizeOpenClose: () => normalizeOpenClose,
7999
8224
  normalizeUid: () => normalizeUid,
8000
8225
  packetizeAacAdtsFrame: () => packetizeAacAdtsFrame,
8001
8226
  packetizeAacRawFrame: () => packetizeAacRawFrame,
@@ -8005,6 +8230,7 @@ __export(index_exports, {
8005
8230
  parseBcMedia: () => parseBcMedia,
8006
8231
  parseRecordingFileName: () => parseRecordingFileName,
8007
8232
  parseSupportXml: () => parseSupportXml,
8233
+ patchNestedTag: () => patchNestedTag,
8008
8234
  printNvrDiagnostics: () => printNvrDiagnostics,
8009
8235
  runAllDiagnosticsConsecutively: () => runAllDiagnosticsConsecutively,
8010
8236
  runMultifocalDiagnosticsConsecutively: () => runMultifocalDiagnosticsConsecutively,
@@ -13234,19 +13460,34 @@ async function* createNativeStream(api, channel, profile, options) {
13234
13460
  }
13235
13461
  });
13236
13462
  streamStarted = true;
13237
- while (!closed) {
13463
+ const signal = options?.signal;
13464
+ while (!closed && !signal?.aborted) {
13238
13465
  if (frameQueue.length > 0) {
13239
13466
  const frame = frameQueue.shift();
13240
13467
  yield frame;
13241
13468
  } else {
13242
13469
  await new Promise((resolve) => {
13243
13470
  frameResolve = resolve;
13244
- setTimeout(() => {
13471
+ const timer = setTimeout(() => {
13245
13472
  if (frameResolve === resolve) {
13246
13473
  frameResolve = null;
13247
13474
  resolve();
13248
13475
  }
13249
13476
  }, 1e3);
13477
+ if (signal) {
13478
+ const onAbort = () => {
13479
+ clearTimeout(timer);
13480
+ if (frameResolve === resolve) frameResolve = null;
13481
+ resolve();
13482
+ };
13483
+ if (signal.aborted) {
13484
+ clearTimeout(timer);
13485
+ frameResolve = null;
13486
+ resolve();
13487
+ } else {
13488
+ signal.addEventListener("abort", onAbort, { once: true });
13489
+ }
13490
+ }
13250
13491
  });
13251
13492
  }
13252
13493
  }
@@ -13419,13 +13660,14 @@ var NativeStreamFanout = class {
13419
13660
  source = null;
13420
13661
  running = false;
13421
13662
  pumpPromise = null;
13663
+ abort = new AbortController();
13422
13664
  constructor(opts) {
13423
13665
  this.opts = opts;
13424
13666
  }
13425
13667
  start() {
13426
13668
  if (this.running) return;
13427
13669
  this.running = true;
13428
- this.source = this.opts.createSource();
13670
+ this.source = this.opts.createSource(this.abort.signal);
13429
13671
  this.pumpPromise = (async () => {
13430
13672
  try {
13431
13673
  for await (const frame of this.source) {
@@ -13471,6 +13713,7 @@ var NativeStreamFanout = class {
13471
13713
  this.source = null;
13472
13714
  for (const q of this.queues.values()) q.close();
13473
13715
  this.queues.clear();
13716
+ this.abort.abort();
13474
13717
  try {
13475
13718
  await src?.return(void 0);
13476
13719
  } catch {
@@ -13509,9 +13752,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
13509
13752
  requireAuth;
13510
13753
  authNonces = /* @__PURE__ */ new Map();
13511
13754
  // Track nonces per client
13512
- AUTH_REALM = "BaichuanRtspServer";
13755
+ AUTH_REALM;
13513
13756
  NONCE_TIMEOUT_MS = 3e5;
13514
13757
  // 5 minutes
13758
+ lazyMetadata;
13515
13759
  // Client tracking
13516
13760
  connectedClients = /* @__PURE__ */ new Set();
13517
13761
  // Set of client IDs (IP:port)
@@ -13523,8 +13767,15 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
13523
13767
  // Track all client resources for cleanup
13524
13768
  clientResources = /* @__PURE__ */ new Map();
13525
13769
  isRtspDebugEnabled() {
13526
- const dbg = this.api.client.getDebugConfig();
13527
- return dbg.debugRtsp || envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
13770
+ try {
13771
+ if (this.api.isClosed) {
13772
+ return envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
13773
+ }
13774
+ const dbg = this.api.client.getDebugConfig();
13775
+ return dbg.debugRtsp || envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
13776
+ } catch {
13777
+ return envBool(process.env.BAICHUAN_DEBUG_RTSP, false);
13778
+ }
13528
13779
  }
13529
13780
  rtspDebugLog(message) {
13530
13781
  if (!this.isRtspDebugEnabled()) return;
@@ -13546,10 +13797,20 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
13546
13797
  // Shared native stream fan-out (single camera stream, multiple RTSP clients)
13547
13798
  nativeFanout = null;
13548
13799
  noClientAutoStopTimer;
13800
+ /** Fires if camera never sends frames after stream start (sleeping), even with clients connected. */
13801
+ noFrameDeadlineTimer;
13549
13802
  /** After last RTSP client; 0 = never auto-stop native stream. */
13550
13803
  nativeStreamIdleStopMs;
13551
13804
  /** Primed-but-no-PLAY timeout; 0 = disabled. */
13552
13805
  nativeStreamPrimeIdleStopMs;
13806
+ /**
13807
+ * Max time to wait for the first camera frame after stream start.
13808
+ * If no frames arrive within this window, the native stream is stopped
13809
+ * (camera is sleeping). Prevents the BaichuanVideoStream watchdog from
13810
+ * firing and waking the camera when no real viewer is watching.
13811
+ * 0 = disabled. Defaults to nativeStreamPrimeIdleStopMs * 2 when > 0.
13812
+ */
13813
+ nativeStreamNoFrameDeadlineMs;
13553
13814
  // Prebuffer: rolling ring of recent video frames for IDR-aligned fast startup.
13554
13815
  // When a new client connects while the stream is already running it does not need
13555
13816
  // to wait up to one full GOP interval for the next keyframe — we replay frames
@@ -13675,14 +13936,25 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
13675
13936
  this.logger = options.logger ?? console;
13676
13937
  this.tcpRtpFraming = options.tcpRtpFraming ?? "rfc4571";
13677
13938
  this.deviceId = options.deviceId;
13678
- this.externalListener = options.externalListener ?? false;
13939
+ this.externalListener = (options.externalListener ?? false) || (options.muxMode ?? false);
13679
13940
  this.nativeStreamIdleStopMs = options.nativeStreamIdleStopMs ?? 3e4;
13680
13941
  this.nativeStreamPrimeIdleStopMs = options.nativeStreamPrimeIdleStopMs ?? (this.nativeStreamIdleStopMs > 0 ? 15e3 : 0);
13681
- this.authCredentials = options.credentials ?? [];
13942
+ this.nativeStreamNoFrameDeadlineMs = this.nativeStreamPrimeIdleStopMs > 0 ? Math.min(this.nativeStreamPrimeIdleStopMs * 2, 3e4) : 0;
13943
+ this.authCredentials = (options.credentials ?? []).map((c) => ({
13944
+ username: c.username,
13945
+ ...c.password !== void 0 ? { password: c.password } : {},
13946
+ ...c.ha1 !== void 0 ? { ha1: c.ha1 } : {}
13947
+ }));
13682
13948
  this.requireAuth = options.requireAuth ?? this.authCredentials.length > 0;
13949
+ this.AUTH_REALM = options.authRealm ?? "BaichuanRtspServer";
13950
+ this.lazyMetadata = options.lazyMetadata ?? false;
13683
13951
  const transport = this.api.client.getTransport();
13684
13952
  this.flow = createRtspFlow(transport, "H264");
13685
13953
  }
13954
+ /** Number of currently connected RTSP clients. */
13955
+ get clientCount() {
13956
+ return this.connectedClients.size;
13957
+ }
13686
13958
  // --- Authentication helpers ---
13687
13959
  /**
13688
13960
  * Generate a new nonce for Digest authentication
@@ -13743,9 +14015,16 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
13743
14015
  this.rtspDebugLog(`Auth failed: nonce mismatch for client ${clientId}`);
13744
14016
  return false;
13745
14017
  }
14018
+ if (realm !== this.AUTH_REALM) {
14019
+ this.rtspDebugLog(
14020
+ `Auth failed: realm mismatch (client="${realm}", server="${this.AUTH_REALM}")`
14021
+ );
14022
+ return false;
14023
+ }
13746
14024
  for (const cred of this.authCredentials) {
13747
14025
  if (username !== cred.username) continue;
13748
- const ha1 = this.md5(`${cred.username}:${realm}:${cred.password}`);
14026
+ const ha1 = cred.ha1 ?? (cred.password !== void 0 ? this.md5(`${cred.username}:${this.AUTH_REALM}:${cred.password}`) : void 0);
14027
+ if (!ha1) continue;
13749
14028
  const ha2 = this.md5(`${method}:${authUri || uri}`);
13750
14029
  const expectedResponse = this.md5(`${ha1}:${nonce}:${ha2}`);
13751
14030
  if (response === expectedResponse) {
@@ -13774,6 +14053,12 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
13774
14053
  this.noClientAutoStopTimer = void 0;
13775
14054
  }
13776
14055
  }
14056
+ clearNoFrameDeadlineTimer() {
14057
+ if (this.noFrameDeadlineTimer) {
14058
+ clearTimeout(this.noFrameDeadlineTimer);
14059
+ this.noFrameDeadlineTimer = void 0;
14060
+ }
14061
+ }
13777
14062
  setFlowVideoType(videoType, reason) {
13778
14063
  if (this.flow.videoType === videoType) return;
13779
14064
  const transport = this.api.client.getTransport();
@@ -13788,25 +14073,31 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
13788
14073
  if (this.active) {
13789
14074
  throw new Error("RTSP server is already active");
13790
14075
  }
13791
- try {
13792
- const metadata = await this.api.getStreamMetadata(this.channel);
13793
- const stream = metadata.streams.find((s) => s.profile === this.profile);
13794
- if (stream) {
13795
- this.streamMetadata = {
13796
- frameRate: stream.frameRate || 25,
13797
- width: stream.width,
13798
- height: stream.height
13799
- };
13800
- const enc = String(stream.videoEncType ?? "").trim().toLowerCase();
13801
- const metaVideoType = enc.includes("265") || enc.includes("hevc") ? "H265" : "H264";
13802
- this.setFlowVideoType(metaVideoType, "metadata");
13803
- }
13804
- } catch (error) {
13805
- this.logger.warn(
13806
- `[BaichuanRtspServer] Could not get stream metadata: ${error}`
14076
+ if (this.lazyMetadata) {
14077
+ this.logger.info(
14078
+ `[BaichuanRtspServer] lazy metadata: skipping initial getStreamMetadata; will fetch on first DESCRIBE`
13807
14079
  );
13808
- this.streamMetadata = { frameRate: 25 };
13809
- this.setFlowVideoType("H264", "metadata unavailable");
14080
+ } else {
14081
+ try {
14082
+ const metadata = await this.api.getStreamMetadata(this.channel);
14083
+ const stream = metadata.streams.find((s) => s.profile === this.profile);
14084
+ if (stream) {
14085
+ this.streamMetadata = {
14086
+ frameRate: stream.frameRate || 25,
14087
+ width: stream.width,
14088
+ height: stream.height
14089
+ };
14090
+ const enc = String(stream.videoEncType ?? "").trim().toLowerCase();
14091
+ const metaVideoType = enc.includes("265") || enc.includes("hevc") ? "H265" : "H264";
14092
+ this.setFlowVideoType(metaVideoType, "metadata");
14093
+ }
14094
+ } catch (error) {
14095
+ this.logger.warn(
14096
+ `[BaichuanRtspServer] Could not get stream metadata: ${error}`
14097
+ );
14098
+ this.streamMetadata = { frameRate: 25 };
14099
+ this.setFlowVideoType("H264", "metadata unavailable");
14100
+ }
13810
14101
  }
13811
14102
  if (!this.externalListener) {
13812
14103
  this.clientConnectionServer = net2.createServer((socket) => {
@@ -13847,6 +14138,30 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
13847
14138
  }
13848
14139
  this.handleRtspConnection(socket, initialBuffer);
13849
14140
  }
14141
+ /**
14142
+ * Inject an already-accepted client socket from a multiplexer
14143
+ * (e.g. `LocalRtspMux`) that owns the listening port.
14144
+ *
14145
+ * The mux reads the first RTSP request line to determine the target path,
14146
+ * then hands the socket over. Any bytes already consumed during routing
14147
+ * are replayed back onto the socket via `unshift()` so the RTSP parser in
14148
+ * `handleRtspConnection` sees the complete original request.
14149
+ *
14150
+ * @param socket - Client TCP socket, already accepted by the mux.
14151
+ * @param preReadData - Bytes the mux has already pulled off the socket
14152
+ * while parsing the request line. Replayed via `socket.unshift()`
14153
+ * before any further reads.
14154
+ */
14155
+ injectSocket(socket, preReadData) {
14156
+ if (!this.active) {
14157
+ socket.end("RTSP/1.0 503 Service Unavailable\r\n\r\n");
14158
+ return;
14159
+ }
14160
+ if (preReadData && preReadData.length > 0) {
14161
+ socket.unshift(preReadData);
14162
+ }
14163
+ this.handleRtspConnection(socket);
14164
+ }
13850
14165
  /**
13851
14166
  * Handle RTSP connection from a client.
13852
14167
  */
@@ -14011,6 +14326,10 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
14011
14326
  Public: "DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS"
14012
14327
  });
14013
14328
  } else if (method === "DESCRIBE") {
14329
+ if (!this.api.isClosed && !this.api.isReady && !this.nativeStreamActive) {
14330
+ void this.api.ensureConnected().catch(() => {
14331
+ });
14332
+ }
14014
14333
  if (!this.flow.getFmtp().hasParamSets && this.connectedClients.size === 0) {
14015
14334
  try {
14016
14335
  if (!this.nativeStreamActive) {
@@ -14048,6 +14367,27 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
14048
14367
  }
14049
14368
  }
14050
14369
  }
14370
+ if (!this.hasAudio && this.firstAudioPromise) {
14371
+ const audioPrimingMs = this.api.client.getTransport() === "udp" ? 3e3 : 2e3;
14372
+ const audioPrimingStart = Date.now();
14373
+ try {
14374
+ await Promise.race([
14375
+ this.firstAudioPromise,
14376
+ new Promise((resolve) => setTimeout(resolve, audioPrimingMs))
14377
+ ]);
14378
+ } catch {
14379
+ }
14380
+ const audioPrimingElapsed = Date.now() - audioPrimingStart;
14381
+ if (this.hasAudio) {
14382
+ this.logger.info(
14383
+ `[rebroadcast] DESCRIBE audio priming: AAC detected after ${audioPrimingElapsed}ms client=${clientId} path=${this.path}`
14384
+ );
14385
+ } else {
14386
+ this.logger.info(
14387
+ `[rebroadcast] DESCRIBE audio priming: no audio after ${audioPrimingElapsed}ms \u2014 SDP will be video-only client=${clientId} path=${this.path}`
14388
+ );
14389
+ }
14390
+ }
14051
14391
  {
14052
14392
  const { fmtp, hasParamSets: hasParamSets2 } = this.flow.getFmtp();
14053
14393
  const fmtpPreview = fmtp.length > 160 ? `${fmtp.slice(0, 160)}...` : fmtp;
@@ -14056,12 +14396,13 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
14056
14396
  );
14057
14397
  }
14058
14398
  const sdp = this.generateSdp();
14399
+ const contentHost = (socket.localAddress && socket.localAddress !== "0.0.0.0" && socket.localAddress !== "::" ? socket.localAddress.replace(/^::ffff:/, "") : null) ?? this.listenHost;
14059
14400
  sendResponse(
14060
14401
  200,
14061
14402
  "OK",
14062
14403
  {
14063
14404
  "Content-Type": "application/sdp",
14064
- "Content-Base": `rtsp://${this.listenHost}:${this.listenPort}${this.path}/`
14405
+ "Content-Base": `rtsp://${contentHost}:${this.listenPort}${this.path}/`
14065
14406
  },
14066
14407
  sdp
14067
14408
  );
@@ -14084,7 +14425,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
14084
14425
  this.emit("client", clientId);
14085
14426
  this.clearNoClientAutoStopTimer();
14086
14427
  if (this.connectedClients.size === 1 && !this.nativeStreamActive) {
14087
- await this.startNativeStream();
14428
+ void this.startNativeStream();
14088
14429
  }
14089
14430
  const transportMatch = requestText.match(/Transport:\s*([^\r\n]+)/i);
14090
14431
  const transport = (transportMatch?.[1] ?? "").trim();
@@ -14218,12 +14559,23 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
14218
14559
  }
14219
14560
  }
14220
14561
  };
14562
+ const runProcessBuffer = () => {
14563
+ processBuffer().catch((err) => {
14564
+ this.logger.debug(
14565
+ `[BaichuanRtspServer] processBuffer failed for ${clientId}: ${err?.message ?? err}`
14566
+ );
14567
+ try {
14568
+ socket.destroy();
14569
+ } catch {
14570
+ }
14571
+ });
14572
+ };
14221
14573
  socket.on("data", (data) => {
14222
14574
  buffer = Buffer.concat([buffer, data]);
14223
- void processBuffer();
14575
+ runProcessBuffer();
14224
14576
  });
14225
14577
  if (buffer.includes("\r\n\r\n")) {
14226
- void processBuffer();
14578
+ runProcessBuffer();
14227
14579
  }
14228
14580
  }
14229
14581
  /**
@@ -15091,6 +15443,25 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
15091
15443
  if (this.nativeStreamActive) {
15092
15444
  return;
15093
15445
  }
15446
+ if (!this.api.isReady) {
15447
+ if (this.api.isClosed) {
15448
+ this.logger.warn?.(
15449
+ `[rebroadcast] API has been explicitly closed \u2014 stream cannot start profile=${this.profile}`
15450
+ );
15451
+ return;
15452
+ }
15453
+ try {
15454
+ this.logger.info?.(
15455
+ `[rebroadcast] API not ready (idle disconnect?), calling ensureConnected profile=${this.profile}`
15456
+ );
15457
+ await this.api.ensureConnected();
15458
+ } catch (e) {
15459
+ this.logger.warn?.(
15460
+ `[rebroadcast] ensureConnected failed, aborting stream start: ${e}`
15461
+ );
15462
+ return;
15463
+ }
15464
+ }
15094
15465
  this.nativeStreamActive = true;
15095
15466
  this.firstFrameReceived = false;
15096
15467
  this.firstAudioDetected = false;
@@ -15125,13 +15496,14 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
15125
15496
  await this.flow.startKeepAlive(this.api);
15126
15497
  this.nativeFanout = new NativeStreamFanout({
15127
15498
  maxQueueItems: 200,
15128
- createSource: () => createNativeStream(this.api, this.channel, this.profile, {
15499
+ createSource: (signal) => createNativeStream(this.api, this.channel, this.profile, {
15129
15500
  variant: this.variant,
15130
- ...dedicatedClient ? { client: dedicatedClient } : {}
15501
+ ...dedicatedClient ? { client: dedicatedClient } : {},
15502
+ signal
15131
15503
  }),
15132
15504
  onFrame: (frame) => {
15133
15505
  if (frame.audio) {
15134
- if (!this.hasAudio && this.api.client.getTransport() === "tcp" && _BaichuanRtspServer.isAdtsAacFrame(frame.data)) {
15506
+ if (!this.hasAudio && _BaichuanRtspServer.isAdtsAacFrame(frame.data)) {
15135
15507
  const info = _BaichuanRtspServer.parseAdtsSamplingInfo(frame.data);
15136
15508
  if (info) {
15137
15509
  this.hasAudio = true;
@@ -15180,6 +15552,8 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
15180
15552
  onEnd: () => {
15181
15553
  if (!this.nativeStreamActive) return;
15182
15554
  this.nativeStreamActive = false;
15555
+ this.clearNoFrameDeadlineTimer();
15556
+ const hadFrames = this.firstFrameReceived;
15183
15557
  this.firstFrameReceived = false;
15184
15558
  this.firstFramePromise = null;
15185
15559
  this.firstFrameResolve = null;
@@ -15204,7 +15578,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
15204
15578
  } catch {
15205
15579
  }
15206
15580
  }
15207
- if (this.connectedClients.size > 0) {
15581
+ if (this.connectedClients.size > 0 && hadFrames) {
15208
15582
  this.logger.info(
15209
15583
  `[rebroadcast] restarting native stream for ${this.connectedClients.size} active client(s)`
15210
15584
  );
@@ -15216,6 +15590,19 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
15216
15590
  }
15217
15591
  });
15218
15592
  this.nativeFanout.start();
15593
+ this.clearNoFrameDeadlineTimer();
15594
+ if (this.nativeStreamNoFrameDeadlineMs > 0) {
15595
+ this.noFrameDeadlineTimer = setTimeout(() => {
15596
+ this.noFrameDeadlineTimer = void 0;
15597
+ if (!this.firstFrameReceived && this.nativeStreamActive) {
15598
+ this.logger.info(
15599
+ `[rebroadcast] no frames within ${this.nativeStreamNoFrameDeadlineMs}ms \u2014 camera sleeping, stopping stream profile=${this.profile} channel=${this.channel}`
15600
+ );
15601
+ void this.stopNativeStream();
15602
+ }
15603
+ }, this.nativeStreamNoFrameDeadlineMs);
15604
+ this.noFrameDeadlineTimer?.unref?.();
15605
+ }
15219
15606
  this.clearNoClientAutoStopTimer();
15220
15607
  if (this.nativeStreamPrimeIdleStopMs > 0) {
15221
15608
  this.noClientAutoStopTimer = setTimeout(() => {
@@ -15232,6 +15619,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
15232
15619
  markFirstFrameReceived() {
15233
15620
  if (!this.firstFrameReceived && this.firstFrameResolve) {
15234
15621
  this.firstFrameReceived = true;
15622
+ this.clearNoFrameDeadlineTimer();
15235
15623
  this.rtspDebugLog(
15236
15624
  `First frame received from camera for profile ${this.profile}`
15237
15625
  );
@@ -15258,6 +15646,7 @@ var BaichuanRtspServer = class _BaichuanRtspServer extends import_node_events4.E
15258
15646
  );
15259
15647
  this.flow.stopKeepAlive();
15260
15648
  this.clearNoClientAutoStopTimer();
15649
+ this.clearNoFrameDeadlineTimer();
15261
15650
  this.nativeStreamActive = false;
15262
15651
  this.firstFrameReceived = false;
15263
15652
  this.firstFramePromise = null;
@@ -15474,149 +15863,17 @@ init_BcMediaAnnexBDecoder();
15474
15863
  // src/baichuan/stream/MpegTsMuxer.ts
15475
15864
  var TS_PACKET_SIZE = 188;
15476
15865
  var TS_SYNC_BYTE = 71;
15477
- var PAT_PID = 0;
15478
- var PMT_PID = 4096;
15479
- var VIDEO_PID = 256;
15866
+ var TS_PAYLOAD_SIZE = TS_PACKET_SIZE - 4;
15867
+ var PID_PAT = 0;
15868
+ var PID_PMT = 4096;
15869
+ var PID_VIDEO = 256;
15870
+ var PID_AUDIO = 257;
15480
15871
  var STREAM_TYPE_H264 = 27;
15481
15872
  var STREAM_TYPE_H265 = 36;
15482
- var patCc = 0;
15483
- var pmtCc = 0;
15484
- var videoCc = 0;
15485
- function createPat() {
15486
- const packet = Buffer.alloc(TS_PACKET_SIZE, 255);
15487
- packet[0] = TS_SYNC_BYTE;
15488
- packet[1] = 64 | PAT_PID >> 8 & 31;
15489
- packet[2] = PAT_PID & 255;
15490
- packet[3] = 16 | patCc & 15;
15491
- patCc = patCc + 1 & 15;
15492
- packet[4] = 0;
15493
- let idx = 5;
15494
- packet[idx++] = 0;
15495
- packet[idx++] = 176;
15496
- packet[idx++] = 13;
15497
- packet[idx++] = 0;
15498
- packet[idx++] = 1;
15499
- packet[idx++] = 193;
15500
- packet[idx++] = 0;
15501
- packet[idx++] = 0;
15502
- packet[idx++] = 0;
15503
- packet[idx++] = 1;
15504
- packet[idx++] = 224 | PMT_PID >> 8 & 31;
15505
- packet[idx++] = PMT_PID & 255;
15506
- const crc = crc32Mpeg(packet.subarray(5, idx));
15507
- packet.writeUInt32BE(crc, idx);
15508
- return packet;
15509
- }
15510
- function createPmt(streamType) {
15511
- const packet = Buffer.alloc(TS_PACKET_SIZE, 255);
15512
- packet[0] = TS_SYNC_BYTE;
15513
- packet[1] = 64 | PMT_PID >> 8 & 31;
15514
- packet[2] = PMT_PID & 255;
15515
- packet[3] = 16 | pmtCc & 15;
15516
- pmtCc = pmtCc + 1 & 15;
15517
- packet[4] = 0;
15518
- let idx = 5;
15519
- packet[idx++] = 2;
15520
- packet[idx++] = 176;
15521
- packet[idx++] = 18;
15522
- packet[idx++] = 0;
15523
- packet[idx++] = 1;
15524
- packet[idx++] = 193;
15525
- packet[idx++] = 0;
15526
- packet[idx++] = 0;
15527
- packet[idx++] = 224 | VIDEO_PID >> 8 & 31;
15528
- packet[idx++] = VIDEO_PID & 255;
15529
- packet[idx++] = 240;
15530
- packet[idx++] = 0;
15531
- packet[idx++] = streamType;
15532
- packet[idx++] = 224 | VIDEO_PID >> 8 & 31;
15533
- packet[idx++] = VIDEO_PID & 255;
15534
- packet[idx++] = 240;
15535
- packet[idx++] = 0;
15536
- const crc = crc32Mpeg(packet.subarray(5, idx));
15537
- packet.writeUInt32BE(crc, idx);
15538
- return packet;
15539
- }
15540
- function createVideoPes(data, pts, isKeyframe) {
15541
- const packets = [];
15542
- const pts90k = Math.floor(pts * 9e4 / 1e6);
15543
- const pesHeaderLen = 14;
15544
- const pesHeader = Buffer.alloc(pesHeaderLen);
15545
- let idx = 0;
15546
- pesHeader[idx++] = 0;
15547
- pesHeader[idx++] = 0;
15548
- pesHeader[idx++] = 1;
15549
- pesHeader[idx++] = 224;
15550
- pesHeader[idx++] = 0;
15551
- pesHeader[idx++] = 0;
15552
- pesHeader[idx++] = 128;
15553
- pesHeader[idx++] = 128;
15554
- pesHeader[idx++] = 5;
15555
- pesHeader[idx++] = 33 | pts90k >> 29 & 14;
15556
- pesHeader[idx++] = pts90k >> 22 & 255;
15557
- pesHeader[idx++] = 1 | pts90k >> 14 & 254;
15558
- pesHeader[idx++] = pts90k >> 7 & 255;
15559
- pesHeader[idx++] = 1 | pts90k << 1 & 254;
15560
- const pesData = Buffer.concat([pesHeader, data]);
15561
- let pesOffset = 0;
15562
- let isFirst = true;
15563
- while (pesOffset < pesData.length) {
15564
- const packet = Buffer.alloc(TS_PACKET_SIZE, 255);
15565
- let pktIdx = 0;
15566
- packet[pktIdx++] = TS_SYNC_BYTE;
15567
- packet[pktIdx++] = (isFirst ? 64 : 0) | VIDEO_PID >> 8 & 31;
15568
- packet[pktIdx++] = VIDEO_PID & 255;
15569
- const remaining = pesData.length - pesOffset;
15570
- const maxPayload = TS_PACKET_SIZE - 4;
15571
- if (remaining >= maxPayload) {
15572
- packet[pktIdx++] = 16 | videoCc & 15;
15573
- videoCc = videoCc + 1 & 15;
15574
- pesData.copy(packet, pktIdx, pesOffset, pesOffset + maxPayload);
15575
- pesOffset += maxPayload;
15576
- } else {
15577
- const adaptLen = maxPayload - remaining - 1;
15578
- if (adaptLen < 0) {
15579
- packet[pktIdx++] = 48 | videoCc & 15;
15580
- videoCc = videoCc + 1 & 15;
15581
- packet[pktIdx++] = TS_PACKET_SIZE - 4 - 1 - remaining;
15582
- if (isFirst && isKeyframe) {
15583
- packet[pktIdx++] = 64;
15584
- for (let i = pktIdx; i < TS_PACKET_SIZE - remaining; i++) {
15585
- packet[i] = 255;
15586
- }
15587
- } else {
15588
- packet[pktIdx++] = 0;
15589
- for (let i = pktIdx; i < TS_PACKET_SIZE - remaining; i++) {
15590
- packet[i] = 255;
15591
- }
15592
- }
15593
- pesData.copy(packet, TS_PACKET_SIZE - remaining, pesOffset);
15594
- pesOffset += remaining;
15595
- } else {
15596
- packet[pktIdx++] = 48 | videoCc & 15;
15597
- videoCc = videoCc + 1 & 15;
15598
- if (adaptLen === 0) {
15599
- packet[pktIdx++] = 0;
15600
- } else {
15601
- packet[pktIdx++] = adaptLen;
15602
- if (isFirst && isKeyframe) {
15603
- packet[pktIdx++] = 64;
15604
- } else {
15605
- packet[pktIdx++] = 0;
15606
- }
15607
- for (let i = 0; i < adaptLen - 1; i++) {
15608
- packet[pktIdx++] = 255;
15609
- }
15610
- }
15611
- pesData.copy(packet, pktIdx, pesOffset, pesOffset + remaining);
15612
- pesOffset += remaining;
15613
- }
15614
- }
15615
- packets.push(packet);
15616
- isFirst = false;
15617
- }
15618
- return packets;
15619
- }
15873
+ var STREAM_TYPE_AAC = 15;
15874
+ var PES_STREAM_ID_VIDEO = 224;
15875
+ var PES_STREAM_ID_AUDIO = 192;
15876
+ var PAT_PMT_INTERVAL = 40;
15620
15877
  function crc32Mpeg(data) {
15621
15878
  let crc = 4294967295;
15622
15879
  for (let i = 0; i < data.length; i++) {
@@ -15631,45 +15888,218 @@ function crc32Mpeg(data) {
15631
15888
  }
15632
15889
  return crc >>> 0;
15633
15890
  }
15891
+ function usToPts(us) {
15892
+ return Math.floor(us * 90 / 1e3) & 8589934591;
15893
+ }
15894
+ function encodePts(buf, offset, pts, prefix) {
15895
+ buf[offset + 0] = prefix << 4 | (pts >>> 30 & 7) << 1 | 1;
15896
+ buf[offset + 1] = pts >>> 22 & 255;
15897
+ buf[offset + 2] = (pts >>> 15 & 127) << 1 | 1;
15898
+ buf[offset + 3] = pts >>> 7 & 255;
15899
+ buf[offset + 4] = (pts & 127) << 1 | 1;
15900
+ }
15901
+ function writeTsHeader(buf, pid, pusi, cc, hasAdapt, hasPayload) {
15902
+ buf[0] = TS_SYNC_BYTE;
15903
+ buf[1] = (pusi ? 64 : 0) | pid >> 8 & 31;
15904
+ buf[2] = pid & 255;
15905
+ buf[3] = (hasAdapt ? 32 : 0) | (hasPayload ? 16 : 0) | cc & 15;
15906
+ }
15907
+ function pesToTsPackets(pesData, pid, ccRef, isKeyframe) {
15908
+ const totalPackets = Math.ceil(pesData.length / TS_PAYLOAD_SIZE);
15909
+ const out = Buffer.allocUnsafe(totalPackets * TS_PACKET_SIZE);
15910
+ let pesOffset = 0;
15911
+ let outOffset = 0;
15912
+ let isFirst = true;
15913
+ while (pesOffset < pesData.length) {
15914
+ const remaining = pesData.length - pesOffset;
15915
+ const packet = out.subarray(outOffset, outOffset + TS_PACKET_SIZE);
15916
+ outOffset += TS_PACKET_SIZE;
15917
+ if (remaining >= TS_PAYLOAD_SIZE) {
15918
+ writeTsHeader(packet, pid, isFirst, ccRef.cc, false, true);
15919
+ ccRef.cc = ccRef.cc + 1 & 15;
15920
+ pesData.copy(packet, 4, pesOffset, pesOffset + TS_PAYLOAD_SIZE);
15921
+ pesOffset += TS_PAYLOAD_SIZE;
15922
+ } else {
15923
+ const paddingNeeded = TS_PAYLOAD_SIZE - remaining;
15924
+ if (paddingNeeded === 1) {
15925
+ writeTsHeader(packet, pid, isFirst, ccRef.cc, true, true);
15926
+ ccRef.cc = ccRef.cc + 1 & 15;
15927
+ packet[4] = 0;
15928
+ pesData.copy(packet, 5, pesOffset, pesOffset + remaining);
15929
+ } else {
15930
+ writeTsHeader(packet, pid, isFirst, ccRef.cc, true, true);
15931
+ ccRef.cc = ccRef.cc + 1 & 15;
15932
+ const adaptLen = paddingNeeded - 1;
15933
+ packet[4] = adaptLen;
15934
+ packet[5] = isFirst && isKeyframe ? 64 : 0;
15935
+ packet.fill(255, 6, 4 + paddingNeeded);
15936
+ pesData.copy(packet, 4 + paddingNeeded, pesOffset, pesOffset + remaining);
15937
+ }
15938
+ pesOffset += remaining;
15939
+ }
15940
+ isFirst = false;
15941
+ }
15942
+ return out;
15943
+ }
15944
+ function buildPat(cc) {
15945
+ const pkt = Buffer.alloc(TS_PACKET_SIZE, 255);
15946
+ pkt[0] = TS_SYNC_BYTE;
15947
+ pkt[1] = 64 | PID_PAT >> 8 & 31;
15948
+ pkt[2] = PID_PAT & 255;
15949
+ pkt[3] = 16 | cc & 15;
15950
+ pkt[4] = 0;
15951
+ const sectionStart = 5;
15952
+ let i = sectionStart;
15953
+ pkt[i++] = 0;
15954
+ pkt[i++] = 176;
15955
+ pkt[i++] = 13;
15956
+ pkt[i++] = 0;
15957
+ pkt[i++] = 1;
15958
+ pkt[i++] = 193;
15959
+ pkt[i++] = 0;
15960
+ pkt[i++] = 0;
15961
+ pkt[i++] = 0;
15962
+ pkt[i++] = 1;
15963
+ pkt[i++] = 224 | PID_PMT >> 8 & 31;
15964
+ pkt[i++] = PID_PMT & 255;
15965
+ const crc = crc32Mpeg(pkt.subarray(sectionStart, i));
15966
+ pkt.writeUInt32BE(crc, i);
15967
+ return pkt;
15968
+ }
15969
+ function buildPmt(videoStreamType, includeAudio, cc) {
15970
+ const pkt = Buffer.alloc(TS_PACKET_SIZE, 255);
15971
+ pkt[0] = TS_SYNC_BYTE;
15972
+ pkt[1] = 64 | PID_PMT >> 8 & 31;
15973
+ pkt[2] = PID_PMT & 255;
15974
+ pkt[3] = 16 | cc & 15;
15975
+ pkt[4] = 0;
15976
+ const sectionStart = 5;
15977
+ let i = sectionStart;
15978
+ pkt[i++] = 2;
15979
+ pkt[i++] = 176;
15980
+ const sectionLenPos = i;
15981
+ i += 1;
15982
+ pkt[i++] = 0;
15983
+ pkt[i++] = 1;
15984
+ pkt[i++] = 193;
15985
+ pkt[i++] = 0;
15986
+ pkt[i++] = 0;
15987
+ pkt[i++] = 224 | PID_VIDEO >> 8 & 31;
15988
+ pkt[i++] = PID_VIDEO & 255;
15989
+ pkt[i++] = 240;
15990
+ pkt[i++] = 0;
15991
+ pkt[i++] = videoStreamType;
15992
+ pkt[i++] = 224 | PID_VIDEO >> 8 & 31;
15993
+ pkt[i++] = PID_VIDEO & 255;
15994
+ pkt[i++] = 240;
15995
+ pkt[i++] = 0;
15996
+ if (includeAudio) {
15997
+ pkt[i++] = STREAM_TYPE_AAC;
15998
+ pkt[i++] = 224 | PID_AUDIO >> 8 & 31;
15999
+ pkt[i++] = PID_AUDIO & 255;
16000
+ pkt[i++] = 240;
16001
+ pkt[i++] = 0;
16002
+ }
16003
+ const sectionLen = i - sectionStart - 3 + 4;
16004
+ pkt[sectionLenPos] = sectionLen;
16005
+ const crc = crc32Mpeg(pkt.subarray(sectionStart, i));
16006
+ pkt.writeUInt32BE(crc, i);
16007
+ return pkt;
16008
+ }
16009
+ function buildVideoPes(annexBData, ptsUs, isKeyframe) {
16010
+ const pts = usToPts(ptsUs);
16011
+ const pesHeader = Buffer.allocUnsafe(14);
16012
+ pesHeader[0] = 0;
16013
+ pesHeader[1] = 0;
16014
+ pesHeader[2] = 1;
16015
+ pesHeader[3] = PES_STREAM_ID_VIDEO;
16016
+ pesHeader[4] = 0;
16017
+ pesHeader[5] = 0;
16018
+ pesHeader[6] = 128 | (isKeyframe ? 4 : 0);
16019
+ pesHeader[7] = 128;
16020
+ pesHeader[8] = 5;
16021
+ encodePts(pesHeader, 9, pts, 2);
16022
+ return Buffer.concat([pesHeader, annexBData]);
16023
+ }
16024
+ function buildAudioPes(adtsData, ptsUs) {
16025
+ const pts = usToPts(ptsUs);
16026
+ const pesPayloadLen = 8 + adtsData.length;
16027
+ const pesHeader = Buffer.allocUnsafe(14);
16028
+ pesHeader[0] = 0;
16029
+ pesHeader[1] = 0;
16030
+ pesHeader[2] = 1;
16031
+ pesHeader[3] = PES_STREAM_ID_AUDIO;
16032
+ pesHeader[4] = pesPayloadLen >> 8 & 255;
16033
+ pesHeader[5] = pesPayloadLen & 255;
16034
+ pesHeader[6] = 128;
16035
+ pesHeader[7] = 128;
16036
+ pesHeader[8] = 5;
16037
+ encodePts(pesHeader, 9, pts, 2);
16038
+ return Buffer.concat([pesHeader, adtsData]);
16039
+ }
15634
16040
  var MpegTsMuxer = class {
15635
- streamType;
15636
- patSent = false;
15637
- pmtSent = false;
15638
- patPmtInterval = 0;
15639
- patPmtIntervalMax = 40;
15640
- // Send PAT/PMT every ~40 frames
16041
+ videoStreamType;
16042
+ includeAudio;
16043
+ // Per-instance continuity counters (4-bit, wrap at 16)
16044
+ patCc = 0;
16045
+ pmtCc = 0;
16046
+ videoCc = 0;
16047
+ audioCc = 0;
16048
+ framesSinceTableSend = 0;
16049
+ tablesSent = false;
15641
16050
  constructor(options) {
15642
- this.streamType = options.videoType === "H265" ? STREAM_TYPE_H265 : STREAM_TYPE_H264;
16051
+ this.videoStreamType = options.videoType === "H265" ? STREAM_TYPE_H265 : STREAM_TYPE_H264;
16052
+ this.includeAudio = options.includeAudio ?? true;
15643
16053
  }
15644
16054
  /**
15645
- * Reset continuity counters (call when starting a new stream).
15646
- */
15647
- static resetCounters() {
15648
- patCc = 0;
15649
- pmtCc = 0;
15650
- videoCc = 0;
15651
- }
15652
- /**
15653
- * Mux a video frame into MPEG-TS packets.
16055
+ * Mux a video frame (Annex-B H.264 or H.265) into MPEG-TS packets.
16056
+ * PAT and PMT are emitted before keyframes and periodically.
15654
16057
  *
15655
- * @param data - Annex-B video data (with start codes)
15656
- * @param microseconds - Frame timestamp in microseconds
15657
- * @param isKeyframe - Whether this is a keyframe
15658
- * @returns Buffer containing all TS packets for this frame
16058
+ * @param annexBData - Annex-B video data (with start codes)
16059
+ * @param ptsUs - Presentation timestamp in microseconds
16060
+ * @param isKeyframe - Whether this is an IDR / IRAP frame
15659
16061
  */
15660
- mux(data, microseconds, isKeyframe) {
15661
- const packets = [];
15662
- if (!this.patSent || !this.pmtSent || isKeyframe || this.patPmtInterval >= this.patPmtIntervalMax) {
15663
- packets.push(createPat());
15664
- packets.push(createPmt(this.streamType));
15665
- this.patSent = true;
15666
- this.pmtSent = true;
15667
- this.patPmtInterval = 0;
15668
- }
15669
- this.patPmtInterval++;
15670
- const videoPackets = createVideoPes(data, microseconds, isKeyframe);
15671
- packets.push(...videoPackets);
15672
- return Buffer.concat(packets);
16062
+ muxVideo(annexBData, ptsUs, isKeyframe) {
16063
+ const chunks = [];
16064
+ const needTables = !this.tablesSent || isKeyframe || this.framesSinceTableSend >= PAT_PMT_INTERVAL;
16065
+ if (needTables) {
16066
+ chunks.push(buildPat(this.patCc));
16067
+ this.patCc = this.patCc + 1 & 15;
16068
+ chunks.push(buildPmt(this.videoStreamType, this.includeAudio, this.pmtCc));
16069
+ this.pmtCc = this.pmtCc + 1 & 15;
16070
+ this.tablesSent = true;
16071
+ this.framesSinceTableSend = 0;
16072
+ }
16073
+ this.framesSinceTableSend++;
16074
+ const pes = buildVideoPes(annexBData, ptsUs, isKeyframe);
16075
+ const ccRef = { cc: this.videoCc };
16076
+ chunks.push(pesToTsPackets(pes, PID_VIDEO, ccRef, isKeyframe));
16077
+ this.videoCc = ccRef.cc;
16078
+ return Buffer.concat(chunks);
16079
+ }
16080
+ /**
16081
+ * Mux an audio frame (ADTS AAC) into MPEG-TS packets.
16082
+ * Returns an empty Buffer when includeAudio is false.
16083
+ *
16084
+ * @param adtsData - Raw ADTS AAC frame (starting with 0xFF 0xF1/0xF9 syncword)
16085
+ * @param ptsUs - Presentation timestamp in microseconds
16086
+ */
16087
+ muxAudio(adtsData, ptsUs) {
16088
+ if (!this.includeAudio || adtsData.length === 0) return Buffer.alloc(0);
16089
+ const pes = buildAudioPes(adtsData, ptsUs);
16090
+ const ccRef = { cc: this.audioCc };
16091
+ const result = pesToTsPackets(pes, PID_AUDIO, ccRef, false);
16092
+ this.audioCc = ccRef.cc;
16093
+ return result;
16094
+ }
16095
+ /** Reset all continuity counters and table state (e.g. after stream restart). */
16096
+ reset() {
16097
+ this.patCc = 0;
16098
+ this.pmtCc = 0;
16099
+ this.videoCc = 0;
16100
+ this.audioCc = 0;
16101
+ this.framesSinceTableSend = 0;
16102
+ this.tablesSent = false;
15673
16103
  }
15674
16104
  };
15675
16105
 
@@ -16103,6 +16533,53 @@ var getAiStateViaGetAiAlarm = async (params) => {
16103
16533
  throw lastErr instanceof Error ? lastErr : new Error(String(lastErr ?? "getAiState failed"));
16104
16534
  };
16105
16535
 
16536
+ // src/reolink/baichuan/utils/sleepInference.ts
16537
+ function decideSleepInferenceTransition(input) {
16538
+ const { inferred, committed, pending, hysteresisPolls } = input;
16539
+ if (committed === void 0) {
16540
+ return {
16541
+ emit: inferred === "sleeping" ? "sleeping" : null,
16542
+ nextCommitted: inferred,
16543
+ nextPending: void 0
16544
+ };
16545
+ }
16546
+ if (inferred === committed) {
16547
+ return {
16548
+ emit: null,
16549
+ nextCommitted: committed,
16550
+ nextPending: void 0
16551
+ };
16552
+ }
16553
+ const effectivePolls = Math.max(1, hysteresisPolls);
16554
+ if (!pending || pending.state !== inferred) {
16555
+ if (effectivePolls <= 1) {
16556
+ return {
16557
+ emit: inferred === "sleeping" ? "sleeping" : "awake",
16558
+ nextCommitted: inferred,
16559
+ nextPending: void 0
16560
+ };
16561
+ }
16562
+ return {
16563
+ emit: null,
16564
+ nextCommitted: committed,
16565
+ nextPending: { state: inferred, count: 1 }
16566
+ };
16567
+ }
16568
+ const nextCount = pending.count + 1;
16569
+ if (nextCount < effectivePolls) {
16570
+ return {
16571
+ emit: null,
16572
+ nextCommitted: committed,
16573
+ nextPending: { state: inferred, count: nextCount }
16574
+ };
16575
+ }
16576
+ return {
16577
+ emit: inferred === "sleeping" ? "sleeping" : "awake",
16578
+ nextCommitted: inferred,
16579
+ nextPending: void 0
16580
+ };
16581
+ }
16582
+
16106
16583
  // src/reolink/baichuan/utils/channelInfoPush.ts
16107
16584
  init_xml();
16108
16585
  var parseOptionalInt = (value) => {
@@ -18379,7 +18856,17 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
18379
18856
  statePollingInterval;
18380
18857
  udpSleepInferenceInterval;
18381
18858
  udpLastInferredSleepStateByChannel = /* @__PURE__ */ new Map();
18859
+ /**
18860
+ * Per-channel pending sleep-state candidate for hysteresis.
18861
+ * When the inference flips to a new state we require N consecutive polls
18862
+ * of that same state before committing it — this filters out transient
18863
+ * flapping caused by non-waking traffic drifting in/out of the 10 s
18864
+ * getSleepStatus() observation window during stream teardown.
18865
+ */
18866
+ udpPendingSleepStateByChannel = /* @__PURE__ */ new Map();
18382
18867
  udpSleepInferenceIntervalMs = 2e3;
18868
+ /** Consecutive inference polls required to commit a new sleeping/awake state. */
18869
+ udpSleepInferenceHysteresisPolls = 2;
18383
18870
  lastMotionState;
18384
18871
  lastAiState;
18385
18872
  aiStatePollingDisabled = false;
@@ -18846,6 +19333,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
18846
19333
  */
18847
19334
  attachD2cDiscListener(client) {
18848
19335
  client.on("d2c_disc", () => this.notifyD2cDisc());
19336
+ client.on("error", () => {
19337
+ });
18849
19338
  }
18850
19339
  /**
18851
19340
  * Acquire a socket from the pool by tag.
@@ -18964,6 +19453,11 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
18964
19453
  const clientOpts = log ? { ...this.clientOptions, logger: log } : this.clientOptions;
18965
19454
  const newClient = new BaichuanClient(clientOpts);
18966
19455
  this.attachD2cDiscListener(newClient);
19456
+ newClient.on("error", (err) => {
19457
+ log?.debug?.(
19458
+ `[SocketPool] tag=${tag} client error: ${err?.message ?? err}`
19459
+ );
19460
+ });
18967
19461
  await newClient.login();
18968
19462
  const existingCooldown = this.socketPoolCooldowns.get(this.host);
18969
19463
  if (existingCooldown) {
@@ -19479,6 +19973,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
19479
19973
  * Only counts sessions from our own IP address.
19480
19974
  */
19481
19975
  async maybeRebootOnTooManySessions() {
19976
+ if (!this.client.isSocketConnected?.()) return;
19482
19977
  const threshold = this.maxDedicatedSessionsBeforeReboot ?? 10;
19483
19978
  if (this.sessionGuardRebootInFlight) return;
19484
19979
  const cooldownMs = 10 * 6e4;
@@ -19920,6 +20415,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
19920
20415
  }
19921
20416
  async renewSimpleEventSubscription() {
19922
20417
  if (this.simpleEventListeners.size === 0) return;
20418
+ if (!this.client.isSocketConnected?.()) return;
19923
20419
  if (this.simpleEventResubscribeInFlight)
19924
20420
  return await this.simpleEventResubscribeInFlight;
19925
20421
  this.simpleEventResubscribeInFlight = (async () => {
@@ -23687,23 +24183,32 @@ ${stderr}`)
23687
24183
  return;
23688
24184
  }
23689
24185
  const channel = this.client.getConfiguredChannel?.() ?? 0;
24186
+ if (!this.client.isSocketConnected?.()) {
24187
+ this.udpPendingSleepStateByChannel.delete(channel);
24188
+ return;
24189
+ }
23690
24190
  const status = this.getSleepStatus({ channel });
23691
24191
  if (status.state === "unknown") return;
23692
- const prev = this.udpLastInferredSleepStateByChannel.get(channel);
23693
- this.udpLastInferredSleepStateByChannel.set(channel, status.state);
23694
- if (prev === void 0) {
23695
- if (status.state === "sleeping") {
23696
- this.dispatchSimpleEvent({
23697
- type: "sleeping",
23698
- channel,
23699
- timestamp: Date.now()
23700
- });
23701
- }
23702
- return;
24192
+ const committed = this.udpLastInferredSleepStateByChannel.get(channel);
24193
+ const pending = this.udpPendingSleepStateByChannel.get(channel);
24194
+ const decision = decideSleepInferenceTransition({
24195
+ inferred: status.state,
24196
+ committed,
24197
+ pending,
24198
+ hysteresisPolls: this.udpSleepInferenceHysteresisPolls
24199
+ });
24200
+ this.udpLastInferredSleepStateByChannel.set(
24201
+ channel,
24202
+ decision.nextCommitted
24203
+ );
24204
+ if (decision.nextPending === void 0) {
24205
+ this.udpPendingSleepStateByChannel.delete(channel);
24206
+ } else {
24207
+ this.udpPendingSleepStateByChannel.set(channel, decision.nextPending);
23703
24208
  }
23704
- if (prev !== status.state) {
24209
+ if (decision.emit) {
23705
24210
  this.dispatchSimpleEvent({
23706
- type: status.state === "sleeping" ? "sleeping" : "awake",
24211
+ type: decision.emit,
23707
24212
  channel,
23708
24213
  timestamp: Date.now()
23709
24214
  });
@@ -23726,6 +24231,7 @@ ${stderr}`)
23726
24231
  this.udpSleepInferenceInterval = void 0;
23727
24232
  }
23728
24233
  this.udpLastInferredSleepStateByChannel.clear();
24234
+ this.udpPendingSleepStateByChannel.clear();
23729
24235
  }
23730
24236
  /**
23731
24237
  * GetEvents via Baichuan (legacy - use subscribeEvents for real-time events).
@@ -26156,6 +26662,373 @@ ${xml}`
26156
26662
  await this.cgiApi.login();
26157
26663
  return await this.cgiApi.getAllChannelsEvents(options);
26158
26664
  }
26665
+ // ====================================================================
26666
+ // Native Baichuan tunable-settings setters
26667
+ //
26668
+ // Replace the CGI passthroughs above with on-wire Baichuan binary
26669
+ // calls. Mirrors the @http_cmd-decorated methods in reolink_aio's
26670
+ // baichuan.py — every command has a documented `cmd_id` (read) and
26671
+ // `cmd_id` (write) pair. The pattern is:
26672
+ //
26673
+ // 1. read XML via `sendXml({ cmdId: GET, channel })`
26674
+ // 2. patch fields via regex (camera firmware is XML-strict; using
26675
+ // the parser would force us to rebuild the document and risk
26676
+ // losing unmodified attributes / element order).
26677
+ // 3. write back via `sendXml({ cmdId: SET, channel, payloadXml })`
26678
+ //
26679
+ // All getters parse via `parseXmlFragmentToJson` so the consumer gets
26680
+ // a clean JSON object instead of XML.
26681
+ // ====================================================================
26682
+ /**
26683
+ * GetEnc via Baichuan (cmdId=56). Returns the `<Compression>` block:
26684
+ * per-stream `mainStream` / `subStream` / `thirdStream` with `audio`
26685
+ * flag, `width`, `height`, `frame` (NOT `frameRate`), `bitRate`,
26686
+ * `videoEncType` (0=h264, 1=h265), `encoderProfile`, `gop`. Mirrors
26687
+ * reolink_aio's `GetEnc` — note the wire payload wraps everything
26688
+ * in `Compression`, not `Enc`.
26689
+ */
26690
+ async getEnc(channel, options) {
26691
+ const xml = await this.sendPcapDerivedSettingsGetXml({
26692
+ cmdId: BC_CMD_ID_GET_ENC,
26693
+ ...channel != null ? { channel } : {},
26694
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
26695
+ });
26696
+ return parseXmlFragmentToJson(xml);
26697
+ }
26698
+ /**
26699
+ * SetEnc via Baichuan (cmdId=57). Read-modify-write — preserves
26700
+ * unspecified fields. Mirrors reolink_aio's `SetEnc`.
26701
+ *
26702
+ * @param channel - Channel number (0-based)
26703
+ * @param patch - Fields to update on `mainStream` and/or `subStream`,
26704
+ * plus a top-level `audio` toggle (0/1). Pass only what you want
26705
+ * to change.
26706
+ */
26707
+ async setEnc(channel, patch, options) {
26708
+ const ch = this.normalizeChannel(channel);
26709
+ let xml = await this.sendXml({
26710
+ cmdId: BC_CMD_ID_GET_ENC,
26711
+ channel: ch,
26712
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
26713
+ });
26714
+ if (patch.audio !== void 0) {
26715
+ xml = xml.replace(
26716
+ /<audio>[^<]*<\/audio>/g,
26717
+ `<audio>${patch.audio}</audio>`
26718
+ );
26719
+ }
26720
+ xml = applyStreamPatch(xml, "mainStream", patch.mainStream);
26721
+ xml = applyStreamPatch(xml, "subStream", patch.subStream);
26722
+ await this.sendXml({
26723
+ cmdId: BC_CMD_ID_SET_ENC,
26724
+ channel: ch,
26725
+ payloadXml: ensureXmlHeader(xml),
26726
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
26727
+ });
26728
+ }
26729
+ /**
26730
+ * SetImage via Baichuan (cmdId=25, read via cmdId=26). Patches the
26731
+ * `<VideoInput>` block: bright / contrast / saturation / hue /
26732
+ * sharpen. Mirrors reolink_aio's `SetImage`.
26733
+ */
26734
+ async setImage(channel, patch, options) {
26735
+ const ch = this.normalizeChannel(channel);
26736
+ let xml = await this.sendXml({
26737
+ cmdId: BC_CMD_ID_GET_VIDEO_INPUT,
26738
+ channel: ch,
26739
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
26740
+ });
26741
+ xml = applyXmlTagPatch(xml, "bright", patch.bright);
26742
+ xml = applyXmlTagPatch(xml, "contrast", patch.contrast);
26743
+ xml = applyXmlTagPatch(xml, "saturation", patch.saturation);
26744
+ xml = applyXmlTagPatch(xml, "hue", patch.hue);
26745
+ xml = applyXmlTagPatch(xml, "sharpen", patch.sharpen);
26746
+ await this.sendXml({
26747
+ cmdId: BC_CMD_ID_SET_VIDEO_INPUT,
26748
+ channel: ch,
26749
+ payloadXml: ensureXmlHeader(xml),
26750
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
26751
+ });
26752
+ }
26753
+ /**
26754
+ * SetIsp via Baichuan (cmdId=25 for image side, cmdId=297 for
26755
+ * dayNightThreshold). Patches the `<InputAdvanceCfg>` block:
26756
+ * `DayNight/mode`, `Exposure/mode`, `binning_mode`, `hdrSwitch`.
26757
+ * Mirrors reolink_aio's `SetIsp`.
26758
+ *
26759
+ * @param channel - Channel number (0-based)
26760
+ * @param patch - Fields to update. `dayNight` accepts the camera's
26761
+ * raw enum (`color`, `auto`, `blackAndWhite`, …) — pass it as the
26762
+ * camera reports it (PascalCase / dotted forms get normalized
26763
+ * server-side).
26764
+ */
26765
+ async setIsp(channel, patch, options) {
26766
+ const ch = this.normalizeChannel(channel);
26767
+ const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
26768
+ const wantsImageWrite = patch.dayNight !== void 0 || patch.exposure !== void 0 || patch.binningMode !== void 0 || patch.hdr !== void 0;
26769
+ if (wantsImageWrite) {
26770
+ let xml = await this.sendXml({
26771
+ cmdId: BC_CMD_ID_GET_VIDEO_INPUT,
26772
+ channel: ch,
26773
+ ...timeoutOpts
26774
+ });
26775
+ if (patch.dayNight !== void 0) {
26776
+ const normalized = normalizeDayNightMode(patch.dayNight);
26777
+ xml = patchNestedTag(xml, "DayNight", "mode", normalized);
26778
+ }
26779
+ if (patch.exposure !== void 0) {
26780
+ xml = patchNestedTag(
26781
+ xml,
26782
+ "Exposure",
26783
+ "mode",
26784
+ patch.exposure.toLowerCase()
26785
+ );
26786
+ }
26787
+ if (patch.binningMode !== void 0) {
26788
+ xml = applyXmlTagPatch(xml, "binning_mode", patch.binningMode);
26789
+ }
26790
+ if (patch.hdr !== void 0) {
26791
+ xml = applyXmlTagPatch(xml, "hdrSwitch", patch.hdr);
26792
+ }
26793
+ await this.sendXml({
26794
+ cmdId: BC_CMD_ID_SET_VIDEO_INPUT,
26795
+ channel: ch,
26796
+ payloadXml: ensureXmlHeader(xml),
26797
+ ...timeoutOpts
26798
+ });
26799
+ }
26800
+ if (patch.dayNightThreshold !== void 0) {
26801
+ let xml = await this.sendXml({
26802
+ cmdId: BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD,
26803
+ channel: ch,
26804
+ ...timeoutOpts
26805
+ });
26806
+ xml = applyXmlTagPatch(xml, "cur", patch.dayNightThreshold);
26807
+ await this.sendXml({
26808
+ cmdId: BC_CMD_ID_SET_DAY_NIGHT_THRESHOLD,
26809
+ channel: ch,
26810
+ payloadXml: ensureXmlHeader(xml),
26811
+ ...timeoutOpts
26812
+ });
26813
+ }
26814
+ }
26815
+ /**
26816
+ * GetIsp via Baichuan (cmdId=26). Convenience alias of
26817
+ * `getVideoInput()` so callers that switched from CGI keep the
26818
+ * familiar name. Both return the merged VideoInput +
26819
+ * InputAdvanceCfg blob.
26820
+ */
26821
+ async getIsp(channel, options) {
26822
+ return this.getVideoInput(channel, options);
26823
+ }
26824
+ /** GetImage via Baichuan (cmdId=26). Same payload as `getIsp` —
26825
+ * Reolink merged VideoInput + InputAdvanceCfg under one cmdId. */
26826
+ async getImage(channel, options) {
26827
+ return this.getVideoInput(channel, options);
26828
+ }
26829
+ /**
26830
+ * GetIrLights via Baichuan (cmdId=208). Returns LedState block:
26831
+ * `IRLedBrightness`, `state` (ir on/off), `lightState` (status LED
26832
+ * open/close), `doorbellLightState`. Mirrors reolink_aio's
26833
+ * `get_status_led`.
26834
+ */
26835
+ async getIrLights(channel, options) {
26836
+ const xml = await this.sendPcapDerivedSettingsGetXml({
26837
+ cmdId: BC_CMD_ID_GET_LED_STATE,
26838
+ ...channel != null ? { channel } : {},
26839
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
26840
+ });
26841
+ return parseXmlFragmentToJson(xml);
26842
+ }
26843
+ /**
26844
+ * SetIrLights via Baichuan (cmdId=209, read via cmdId=208). Patches
26845
+ * IR LED + status LED + doorbell LED + IR brightness. Mirrors
26846
+ * reolink_aio's `set_status_led`.
26847
+ *
26848
+ * @param channel - Channel number (0-based)
26849
+ * @param patch - `irState` ("On" | "Off" | "Auto"), `lightState`
26850
+ * (status LED), `doorbellLightState`, `irBrightness` (0..255).
26851
+ * Camera-side accepts lowercase strings (`open`/`close`); the
26852
+ * helper normalizes from the friendly variants.
26853
+ */
26854
+ async setIrLights(channel, patch, options) {
26855
+ const ch = this.normalizeChannel(channel);
26856
+ const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
26857
+ let xml = await this.sendXml({
26858
+ cmdId: BC_CMD_ID_GET_LED_STATE,
26859
+ channel: ch,
26860
+ ...timeoutOpts
26861
+ });
26862
+ if (patch.lightState !== void 0) {
26863
+ xml = applyXmlTagPatch(
26864
+ xml,
26865
+ "lightState",
26866
+ patch.lightState === "On" ? "open" : "close"
26867
+ );
26868
+ }
26869
+ if (patch.doorbellLightState !== void 0) {
26870
+ xml = applyXmlTagPatch(
26871
+ xml,
26872
+ "doorbellLightState",
26873
+ normalizeOpenClose(patch.doorbellLightState)
26874
+ );
26875
+ }
26876
+ if (patch.irState !== void 0) {
26877
+ const v = String(patch.irState);
26878
+ const out = v === "Off" ? "close" : v.toLowerCase();
26879
+ xml = applyXmlTagPatch(xml, "state", out);
26880
+ }
26881
+ if (patch.irBrightness !== void 0) {
26882
+ xml = applyXmlTagPatch(xml, "IRLedBrightness", patch.irBrightness);
26883
+ }
26884
+ await this.sendXml({
26885
+ cmdId: BC_CMD_ID_SET_LED_STATE,
26886
+ channel: ch,
26887
+ payloadXml: ensureXmlHeader(xml),
26888
+ ...timeoutOpts
26889
+ });
26890
+ }
26891
+ /**
26892
+ * SetAudioCfg via Baichuan (cmdId=265, read via cmdId=264). Patches
26893
+ * volume / talk-and-reply / visitor settings. Mirrors reolink_aio's
26894
+ * `SetAudioCfg`.
26895
+ */
26896
+ async setAudioCfg(channel, patch, options) {
26897
+ const ch = this.normalizeChannel(channel);
26898
+ const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
26899
+ let xml = await this.sendXml({
26900
+ cmdId: BC_CMD_ID_GET_AUDIO_CFG,
26901
+ channel: ch,
26902
+ ...timeoutOpts
26903
+ });
26904
+ xml = applyXmlTagPatch(xml, "volume", patch.volume);
26905
+ xml = applyXmlTagPatch(
26906
+ xml,
26907
+ "talkAndReplyVolume",
26908
+ patch.talkAndReplyVolume
26909
+ );
26910
+ xml = applyXmlTagPatch(xml, "visitorVolume", patch.visitorVolume);
26911
+ xml = applyXmlTagPatch(xml, "visitorLoudspeaker", patch.visitorLoudspeaker);
26912
+ await this.sendXml({
26913
+ cmdId: BC_CMD_ID_SET_AUDIO_CFG,
26914
+ channel: ch,
26915
+ payloadXml: ensureXmlHeader(xml),
26916
+ ...timeoutOpts
26917
+ });
26918
+ }
26919
+ /**
26920
+ * GetMask (privacy mask) via Baichuan (cmdId=52). Returns the
26921
+ * `<Shelter>` block — `enable` flag + `shelterList`. Mirrors
26922
+ * reolink_aio's `GetMask`.
26923
+ */
26924
+ async getMask(channel, options) {
26925
+ const xml = await this.sendPcapDerivedSettingsGetXml({
26926
+ cmdId: BC_CMD_ID_GET_PRIVACY_MASK,
26927
+ ...channel != null ? { channel } : {},
26928
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
26929
+ });
26930
+ return parseXmlFragmentToJson(xml);
26931
+ }
26932
+ /**
26933
+ * SetMask (privacy mask) via Baichuan (cmdId=53, read via cmdId=52).
26934
+ * Toggles the `<Shelter><enable>` flag. Mirrors reolink_aio's
26935
+ * `SetMask` (which only touches enable too — shelter zone editing
26936
+ * goes through a separate flow).
26937
+ */
26938
+ async setMask(channel, patch, options) {
26939
+ const ch = this.normalizeChannel(channel);
26940
+ const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
26941
+ let xml = await this.sendXml({
26942
+ cmdId: BC_CMD_ID_GET_PRIVACY_MASK,
26943
+ channel: ch,
26944
+ ...timeoutOpts
26945
+ });
26946
+ if (patch.enable !== void 0) {
26947
+ xml = applyXmlTagPatch(xml, "enable", patch.enable ? 1 : 0);
26948
+ }
26949
+ await this.sendXml({
26950
+ cmdId: BC_CMD_ID_SET_PRIVACY_MASK,
26951
+ channel: ch,
26952
+ payloadXml: ensureXmlHeader(xml),
26953
+ ...timeoutOpts
26954
+ });
26955
+ }
26956
+ /**
26957
+ * GetAudioNoise via Baichuan (cmdId=439). Reads `enable` + `level`
26958
+ * from the aiDenoise block. Mirrors reolink_aio's `GetAudioNoise`.
26959
+ *
26960
+ * Note: `getAiDenoise` already returns the same payload typed as
26961
+ * `AiDenoiseConfig`. This getter exists for naming parity with
26962
+ * reolink_aio + the reolink CGI.
26963
+ */
26964
+ async getAudioNoise(channel, options) {
26965
+ const xml = await this.sendPcapDerivedSettingsGetXml({
26966
+ cmdId: BC_CMD_ID_GET_AI_DENOISE,
26967
+ ...channel != null ? { channel } : {},
26968
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
26969
+ });
26970
+ return parseXmlFragmentToJson(xml);
26971
+ }
26972
+ /**
26973
+ * SetAudioNoise via Baichuan (cmdId=440, read via cmdId=439).
26974
+ * Mirrors reolink_aio's `SetAudioNoise` — `level <= 0` flips the
26975
+ * enable flag off; positive values turn it on and update the level.
26976
+ */
26977
+ async setAudioNoise(channel, level, options) {
26978
+ const ch = this.normalizeChannel(channel);
26979
+ const timeoutOpts = options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {};
26980
+ let xml = await this.sendXml({
26981
+ cmdId: BC_CMD_ID_GET_AI_DENOISE,
26982
+ channel: ch,
26983
+ ...timeoutOpts
26984
+ });
26985
+ xml = applyXmlTagPatch(xml, "enable", level > 0 ? 1 : 0);
26986
+ if (level > 0) {
26987
+ xml = applyXmlTagPatch(xml, "level", level);
26988
+ }
26989
+ await this.sendXml({
26990
+ cmdId: BC_CMD_ID_SET_AI_DENOISE,
26991
+ channel: ch,
26992
+ payloadXml: ensureXmlHeader(xml),
26993
+ ...timeoutOpts
26994
+ });
26995
+ }
26996
+ /**
26997
+ * GetAutoFocus via Baichuan (cmdId=224). Returns the `<AutoFocus>`
26998
+ * block — only `disable` (0 = AF on, 1 = AF off). Mirrors
26999
+ * reolink_aio's `GetAutoFocus`.
27000
+ */
27001
+ async getAutoFocus(channel, options) {
27002
+ const ch = this.normalizeChannel(channel);
27003
+ const xml = await this.sendPcapDerivedSettingsGetXml({
27004
+ cmdId: BC_CMD_ID_GET_AUTO_FOCUS,
27005
+ channel: ch,
27006
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
27007
+ });
27008
+ return parseXmlFragmentToJson(xml);
27009
+ }
27010
+ /**
27011
+ * SetAutoFocus via Baichuan (cmdId=225). Mirrors reolink_aio's
27012
+ * `SetAutoFocus`. Note: write-only command — the payload is built
27013
+ * from scratch (no read-modify-write needed).
27014
+ */
27015
+ async setAutoFocus(channel, disable, options) {
27016
+ const ch = this.normalizeChannel(channel);
27017
+ const disableVal = disable ? 1 : 0;
27018
+ const payloadXml = `<?xml version="1.0" encoding="UTF-8" ?>
27019
+ <body>
27020
+ <AutoFocus version="1.1">
27021
+ <channelId>${ch}</channelId>
27022
+ <disable>${disableVal}</disable>
27023
+ </AutoFocus>
27024
+ </body>`;
27025
+ await this.sendXml({
27026
+ cmdId: BC_CMD_ID_SET_AUTO_FOCUS,
27027
+ channel: ch,
27028
+ payloadXml,
27029
+ ...options?.timeoutMs != null ? { timeoutMs: options.timeoutMs } : {}
27030
+ });
27031
+ }
26159
27032
  /**
26160
27033
  * Passthrough to ReolinkCgiApi.getAllChannelsBatteryInfo.
26161
27034
  * Fetches battery info for all channels via CGI (merged with channel status sleep flag).
@@ -27324,8 +28197,8 @@ ${scheduleItems}
27324
28197
  );
27325
28198
  let args;
27326
28199
  if (useMpegTsMuxer) {
27327
- MpegTsMuxer.resetCounters();
27328
- tsMuxer = new MpegTsMuxer({ videoType });
28200
+ tsMuxer = new MpegTsMuxer({ videoType, includeAudio: false });
28201
+ tsMuxer.reset();
27329
28202
  args = [
27330
28203
  "-hide_banner",
27331
28204
  "-loglevel",
@@ -27489,7 +28362,7 @@ ${scheduleItems}
27489
28362
  startFfmpeg(videoType);
27490
28363
  frameCount++;
27491
28364
  if (useMpegTsMuxer && tsMuxer) {
27492
- const tsData = tsMuxer.mux(data, microseconds, isKeyframe);
28365
+ const tsData = tsMuxer.muxVideo(data, microseconds, isKeyframe);
27493
28366
  input.write(tsData);
27494
28367
  } else {
27495
28368
  if (videoType === "H264") input.write(H264_AUD);
@@ -27778,8 +28651,8 @@ ${scheduleItems}
27778
28651
  logger?.log?.(
27779
28652
  `[createRecordingReplayHlsSession] Starting ffmpeg HLS with videoType=${videoType}, transcode=${needsTranscode}, hlsTime=${hlsSegmentDuration}s, fileName=${params.fileName}`
27780
28653
  );
27781
- MpegTsMuxer.resetCounters();
27782
- tsMuxer = new MpegTsMuxer({ videoType });
28654
+ tsMuxer = new MpegTsMuxer({ videoType, includeAudio: false });
28655
+ tsMuxer.reset();
27783
28656
  const args = [
27784
28657
  "-hide_banner",
27785
28658
  "-loglevel",
@@ -27944,7 +28817,7 @@ ${scheduleItems}
27944
28817
  startFfmpeg(videoType);
27945
28818
  frameCount++;
27946
28819
  if (tsMuxer) {
27947
- const tsData = tsMuxer.mux(data, microseconds, isKeyframe);
28820
+ const tsData = tsMuxer.muxVideo(data, microseconds, isKeyframe);
27948
28821
  input.write(tsData);
27949
28822
  }
27950
28823
  if (frameCount === 1) {
@@ -33736,6 +34609,9 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
33736
34609
  totalVideoFramesWritten = 0;
33737
34610
  // Prebuffer
33738
34611
  prebuffer = [];
34612
+ // Audio metadata — populated on first valid ADTS AAC frame.
34613
+ // Exposed via getAudioInfo() for the stream-diagnostics feature.
34614
+ audioInfo = null;
33739
34615
  constructor(options) {
33740
34616
  super();
33741
34617
  this.api = options.api;
@@ -33819,6 +34695,45 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
33819
34695
  return this.connectedClients.size;
33820
34696
  }
33821
34697
  // -----------------------------------------------------------------------
34698
+ // Diagnostic subscription API (implements DiagnosticStreamServer)
34699
+ //
34700
+ // Matches the shape of BaichuanRtspServer's diagnostic API so the
34701
+ // stream-diagnostic feature in the Manager app can drive either backend
34702
+ // with identical code.
34703
+ // -----------------------------------------------------------------------
34704
+ /**
34705
+ * Subscribe to the raw native stream for diagnostic purposes.
34706
+ * The subscriber receives the same frames the MPEG-TS muxer consumes
34707
+ * (pre-muxing). Counts as a "consumer" so the native stream is kept alive
34708
+ * for the lifetime of the subscription. If the stream is not already
34709
+ * running (battery camera, prestart=false), this starts it.
34710
+ */
34711
+ async subscribeDiagnostic(id) {
34712
+ this.connectedClients.add(`diag:${id}`);
34713
+ if (!this.nativeStreamActive) {
34714
+ await this.startNativeStream();
34715
+ }
34716
+ if (!this.nativeFanout) {
34717
+ this.connectedClients.delete(`diag:${id}`);
34718
+ throw new Error(
34719
+ "Go2rtcTcpServer: native stream failed to start \u2014 cannot subscribe diagnostic"
34720
+ );
34721
+ }
34722
+ return this.nativeFanout.subscribe(`diag:${id}`);
34723
+ }
34724
+ /** Unsubscribe a diagnostic session and release its consumer slot. */
34725
+ unsubscribeDiagnostic(id) {
34726
+ this.removeClient(`diag:${id}`, "diagnostic unsubscribe");
34727
+ }
34728
+ /**
34729
+ * Returns ADTS AAC audio metadata detected from the native stream, or
34730
+ * null if no audio frame has been observed yet (e.g. video-only cameras
34731
+ * or before the first audio packet arrives).
34732
+ */
34733
+ getAudioInfo() {
34734
+ return this.audioInfo;
34735
+ }
34736
+ // -----------------------------------------------------------------------
33822
34737
  // Client handling
33823
34738
  // -----------------------------------------------------------------------
33824
34739
  handleClient(socket) {
@@ -33863,6 +34778,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
33863
34778
  }
33864
34779
  if (!this.active || !this.nativeFanout) return;
33865
34780
  const subscription = this.nativeFanout.subscribe(clientId);
34781
+ let muxer = null;
33866
34782
  const prebufferSnap = this.prebuffer.slice();
33867
34783
  let lastIdrIdx = -1;
33868
34784
  for (let i = prebufferSnap.length - 1; i >= 0; i--) {
@@ -33876,9 +34792,21 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
33876
34792
  this.logger.info?.(
33877
34793
  `[Go2rtcTcpServer] prebuffer replay client=${clientId} frames=${replay.length}`
33878
34794
  );
34795
+ if (!muxer) {
34796
+ muxer = new MpegTsMuxer({
34797
+ videoType: this.detectedVideoType ?? "H264",
34798
+ includeAudio: true
34799
+ });
34800
+ }
33879
34801
  for (const entry of replay) {
33880
34802
  if (socket.destroyed) return;
33881
- socket.write(entry.data);
34803
+ let ts;
34804
+ if (!entry.audio) {
34805
+ ts = muxer.muxVideo(entry.data, entry.pts, entry.isKeyframe);
34806
+ } else {
34807
+ ts = muxer.muxAudio(entry.data, entry.pts);
34808
+ }
34809
+ if (ts.length > 0) socket.write(ts);
33882
34810
  }
33883
34811
  }
33884
34812
  let seenKeyframe = lastIdrIdx >= 0;
@@ -33897,16 +34825,33 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
33897
34825
  break;
33898
34826
  }
33899
34827
  liveFrameCount++;
33900
- const annexB = this.convertFrame(frame);
34828
+ if (frame.audio) {
34829
+ if (muxer) {
34830
+ const pts2 = frame.microseconds ?? Date.now() * 1e3;
34831
+ const ts2 = muxer.muxAudio(frame.data, pts2);
34832
+ if (ts2.length > 0) socket.write(ts2);
34833
+ }
34834
+ continue;
34835
+ }
34836
+ const annexB = this.convertVideoFrame(frame);
33901
34837
  if (!annexB) continue;
34838
+ const isKf = this.isAnnexBKeyframe(annexB, frame.videoType);
33902
34839
  if (!seenKeyframe) {
33903
- if (!this.isAnnexBKeyframe(annexB, frame.videoType)) continue;
34840
+ if (!isKf) continue;
33904
34841
  seenKeyframe = true;
33905
34842
  this.logger.info?.(
33906
34843
  `[Go2rtcTcpServer] first live keyframe client=${clientId} after ${liveFrameCount} frames`
33907
34844
  );
34845
+ if (!muxer) {
34846
+ muxer = new MpegTsMuxer({
34847
+ videoType: frame.videoType ?? this.detectedVideoType ?? "H264",
34848
+ includeAudio: true
34849
+ });
34850
+ }
33908
34851
  }
33909
- socket.write(annexB);
34852
+ const pts = frame.microseconds ?? Date.now() * 1e3;
34853
+ const ts = muxer.muxVideo(annexB, pts, isKf);
34854
+ socket.write(ts);
33910
34855
  liveVideoWritten++;
33911
34856
  this.totalVideoFramesWritten++;
33912
34857
  if (Date.now() - lastLogAt > 1e4) {
@@ -33935,14 +34880,11 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
33935
34880
  // Frame conversion
33936
34881
  // -----------------------------------------------------------------------
33937
34882
  /**
33938
- * Convert a native frame to wire-ready Annex-B.
33939
- * Audio frames are skipped raw TCP carries only video (Annex-B).
33940
- * go2rtc auto-detects the codec from SPS/PPS/VPS NALUs.
34883
+ * Convert a native video frame to Annex-B.
34884
+ * Returns null for audio frames (handled separately by muxAudio).
33941
34885
  */
33942
- convertFrame(frame) {
33943
- if (frame.audio) {
33944
- return null;
33945
- }
34886
+ convertVideoFrame(frame) {
34887
+ if (frame.audio) return null;
33946
34888
  if (frame.data.length === 0) return null;
33947
34889
  try {
33948
34890
  if (frame.videoType === "H264") {
@@ -34006,10 +34948,71 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
34006
34948
  return nals;
34007
34949
  }
34008
34950
  // -----------------------------------------------------------------------
34951
+ // ADTS AAC parsing (used for audio metadata exposed via getAudioInfo)
34952
+ // -----------------------------------------------------------------------
34953
+ /** True if `b` starts with an ADTS AAC syncword (0xFFF). */
34954
+ static isAdtsAacFrame(b) {
34955
+ return b.length >= 2 && b[0] === 255 && (b[1] & 240) === 240;
34956
+ }
34957
+ /**
34958
+ * Parse an ADTS header into {sampleRate, channels, AudioSpecificConfig hex}.
34959
+ * Returns null when the buffer is not a valid ADTS frame.
34960
+ */
34961
+ static parseAdtsSamplingInfo(b) {
34962
+ if (b.length < 7) return null;
34963
+ if (!_Go2rtcTcpServer.isAdtsAacFrame(b)) return null;
34964
+ const samplingIndex = b[2] >> 2 & 15;
34965
+ const sampleRates = [
34966
+ 96e3,
34967
+ 88200,
34968
+ 64e3,
34969
+ 48e3,
34970
+ 44100,
34971
+ 32e3,
34972
+ 24e3,
34973
+ 22050,
34974
+ 16e3,
34975
+ 12e3,
34976
+ 11025,
34977
+ 8e3,
34978
+ 7350
34979
+ ];
34980
+ const sampleRate = sampleRates[samplingIndex] ?? null;
34981
+ if (!sampleRate) return null;
34982
+ const channelConfig = (b[2] & 1) << 2 | b[3] >> 6 & 3;
34983
+ const channels = channelConfig === 0 ? 1 : channelConfig;
34984
+ const profile = b[2] >> 6 & 3;
34985
+ const audioObjectType = profile + 1;
34986
+ const asc = audioObjectType << 11 | samplingIndex << 7 | channelConfig << 3;
34987
+ const configHex = Buffer.from([asc >> 8 & 255, asc & 255]).toString(
34988
+ "hex"
34989
+ );
34990
+ return { sampleRate, channels, configHex };
34991
+ }
34992
+ // -----------------------------------------------------------------------
34009
34993
  // Native stream management
34010
34994
  // -----------------------------------------------------------------------
34011
34995
  async startNativeStream() {
34012
34996
  if (this.nativeStreamActive) return;
34997
+ if (!this.api.isReady) {
34998
+ if (this.api.isClosed) {
34999
+ this.logger.warn?.(
35000
+ `[Go2rtcTcpServer] API has been explicitly closed \u2014 stream cannot start`
35001
+ );
35002
+ return;
35003
+ }
35004
+ try {
35005
+ this.logger.info?.(
35006
+ `[Go2rtcTcpServer] API not ready (idle disconnect?), calling ensureConnected`
35007
+ );
35008
+ await this.api.ensureConnected();
35009
+ } catch (e) {
35010
+ this.logger.warn?.(
35011
+ `[Go2rtcTcpServer] ensureConnected failed, aborting stream start: ${e}`
35012
+ );
35013
+ return;
35014
+ }
35015
+ }
34013
35016
  this.nativeStreamActive = true;
34014
35017
  let dedicatedClient;
34015
35018
  if (this.deviceId) {
@@ -34028,6 +35031,7 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
34028
35031
  this.logger.info?.(
34029
35032
  `[Go2rtcTcpServer] native stream starting channel=${this.channel} profile=${this.profile} dedicated=${!!dedicatedClient}`
34030
35033
  );
35034
+ let hadFrames = false;
34031
35035
  this.nativeFanout = new NativeStreamFanout2({
34032
35036
  maxQueueItems: 200,
34033
35037
  createSource: () => createNativeStream(this.api, this.channel, this.profile, {
@@ -34035,19 +35039,37 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
34035
35039
  ...dedicatedClient ? { client: dedicatedClient } : {}
34036
35040
  }),
34037
35041
  onFrame: (frame) => {
35042
+ hadFrames = true;
34038
35043
  this.lastFrameAt = Date.now();
34039
35044
  this.totalFramesReceived++;
34040
35045
  if (!frame.audio && (frame.videoType === "H264" || frame.videoType === "H265")) {
34041
35046
  this.detectedVideoType = frame.videoType;
34042
35047
  }
34043
- const wireData = this.convertFrame(frame);
34044
- if (!wireData || wireData.length === 0) return;
34045
- const isKeyframe = !frame.audio && this.isAnnexBKeyframe(wireData, frame.videoType);
35048
+ let prebufData;
35049
+ let isKeyframe;
35050
+ if (frame.audio) {
35051
+ if (frame.data.length === 0) return;
35052
+ if (!this.audioInfo) {
35053
+ const parsed = _Go2rtcTcpServer.parseAdtsSamplingInfo(frame.data);
35054
+ if (parsed) {
35055
+ this.audioInfo = { codec: "aac-adts", ...parsed };
35056
+ }
35057
+ }
35058
+ prebufData = frame.data;
35059
+ isKeyframe = false;
35060
+ } else {
35061
+ const annexB = this.convertVideoFrame(frame);
35062
+ if (!annexB || annexB.length === 0) return;
35063
+ prebufData = annexB;
35064
+ isKeyframe = this.isAnnexBKeyframe(annexB, frame.videoType);
35065
+ }
35066
+ const pts = frame.microseconds ?? Date.now() * 1e3;
34046
35067
  this.prebuffer.push({
34047
- data: Buffer.from(wireData),
35068
+ data: Buffer.from(prebufData),
34048
35069
  time: Date.now(),
34049
35070
  isKeyframe,
34050
- audio: frame.audio
35071
+ audio: frame.audio,
35072
+ pts
34051
35073
  });
34052
35074
  const cutoff = Date.now() - this.prebufferMaxMs;
34053
35075
  let trimIdx = 0;
@@ -34074,7 +35096,23 @@ var Go2rtcTcpServer = class _Go2rtcTcpServer extends import_node_events6.EventEm
34074
35096
  });
34075
35097
  this.dedicatedSessionRelease = void 0;
34076
35098
  }
34077
- if (this.active && (this.connectedClients.size > 0 || this.prestartStream)) {
35099
+ if (!this.prestartStream) {
35100
+ this.logger.info?.(
35101
+ `[Go2rtcTcpServer] battery native stream ended hadFrames=${hadFrames} channel=${this.channel} profile=${this.profile} \u2014 dropping ${this.connectedClients.size} client(s) to prevent wake loop`
35102
+ );
35103
+ for (const [, sock] of this.clientSockets) {
35104
+ sock.destroy();
35105
+ }
35106
+ } else if (this.active) {
35107
+ if (typeof this.api.isStreamProfileRejected === "function" && this.api.isStreamProfileRejected(this.channel, this.profile)) {
35108
+ this.logger.warn?.(
35109
+ `[Go2rtcTcpServer] profile rejected by device channel=${this.channel} profile=${this.profile} \u2014 not restarting`
35110
+ );
35111
+ for (const [, sock] of this.clientSockets) {
35112
+ sock.destroy();
35113
+ }
35114
+ return;
35115
+ }
34078
35116
  this.logger.info?.(
34079
35117
  `[Go2rtcTcpServer] restarting native stream (clients=${this.connectedClients.size}, prestart=${this.prestartStream})`
34080
35118
  );
@@ -36570,9 +37608,10 @@ async function autoDetectDeviceType(inputs) {
36570
37608
  const msg = fmtErr(e);
36571
37609
  return msg.includes("Not running") || msg.includes("Baichuan UDP stream closed") || msg.includes("Baichuan socket closed") || msg.includes("ETIMEDOUT") || msg.toLowerCase().includes("timeout");
36572
37610
  };
36573
- const withRetries = async (label, max, op, shouldRetry) => {
37611
+ const withRetries = async (label, max, op, shouldRetry, isAborted) => {
36574
37612
  let lastErr;
36575
37613
  for (let attempt = 1; attempt <= max; attempt++) {
37614
+ if (isAborted?.()) throw new Error(`${label}: aborted (race won by another method)`);
36576
37615
  try {
36577
37616
  if (attempt > 1) {
36578
37617
  logger?.log?.(`[AutoDetect] ${label}: retry ${attempt}/${max}...`);
@@ -36581,7 +37620,7 @@ async function autoDetectDeviceType(inputs) {
36581
37620
  } catch (e) {
36582
37621
  lastErr = e;
36583
37622
  const msg = fmtErr(e);
36584
- const retryable = attempt < max && shouldRetry(e);
37623
+ const retryable = attempt < max && !isAborted?.() && shouldRetry(e);
36585
37624
  logger?.log?.(
36586
37625
  `[AutoDetect] ${label} attempt ${attempt}/${max} failed: ${msg}${retryable ? " (will retry)" : ""}`
36587
37626
  );
@@ -36591,6 +37630,31 @@ async function autoDetectDeviceType(inputs) {
36591
37630
  }
36592
37631
  throw lastErr instanceof Error ? lastErr : new Error(String(lastErr ?? `${label} failed`));
36593
37632
  };
37633
+ const runUdpMethodsParallel = async (methods, loginAndDetect, errorPrefix) => {
37634
+ let raceWon = false;
37635
+ const methodErrors = /* @__PURE__ */ new Map();
37636
+ try {
37637
+ return await Promise.any(
37638
+ methods.map(async (m) => {
37639
+ try {
37640
+ const result = await loginAndDetect(m, () => raceWon);
37641
+ raceWon = true;
37642
+ return result;
37643
+ } catch (e) {
37644
+ if (!raceWon) methodErrors.set(m, fmtErr(e));
37645
+ logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${fmtErr(e)}`);
37646
+ throw e;
37647
+ }
37648
+ })
37649
+ );
37650
+ } catch (e) {
37651
+ if (e instanceof AggregateError) {
37652
+ const msgs = methods.map((m) => `${m}: ${methodErrors.get(m) ?? "unknown"}`);
37653
+ throw new Error(`${errorPrefix} ${msgs.join(" | ")}`);
37654
+ }
37655
+ throw e;
37656
+ }
37657
+ };
36594
37658
  const effectiveUid = normalizeUid(uid);
36595
37659
  logger?.log?.(`[AutoDetect] Pinging ${host}...`);
36596
37660
  const isReachable = await pingHost(host);
@@ -36620,9 +37684,9 @@ async function autoDetectDeviceType(inputs) {
36620
37684
  normalizedUid = normalizedDiscovered;
36621
37685
  }
36622
37686
  const methodsToTry = inputs.udpDiscoveryMethod ? [inputs.udpDiscoveryMethod] : ["local-direct", "local-broadcast", "remote", "relay", "map"];
36623
- const udpErrors = [];
36624
- for (const m of methodsToTry) {
36625
- try {
37687
+ return await runUdpMethodsParallel(
37688
+ methodsToTry,
37689
+ async (m, isAborted) => {
36626
37690
  logger?.log?.(`[AutoDetect] Trying UDP discovery method: ${m}...`);
36627
37691
  const udpApi = await withRetries(
36628
37692
  `UDP(${m})`,
@@ -36645,11 +37709,14 @@ async function autoDetectDeviceType(inputs) {
36645
37709
  throw e;
36646
37710
  }
36647
37711
  },
36648
- shouldRetryUdp
37712
+ shouldRetryUdp,
37713
+ isAborted
36649
37714
  );
36650
- const deviceInfo = await udpApi.getInfo();
36651
- const capabilities = await udpApi.getDeviceCapabilities();
36652
- const hostNetworkInfo = await udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0);
37715
+ const [deviceInfo, capabilities, hostNetworkInfo] = await Promise.all([
37716
+ udpApi.getInfo(),
37717
+ udpApi.getDeviceCapabilities(),
37718
+ udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0)
37719
+ ]);
36653
37720
  const channelNum = capabilities?.support?.channelNum ?? 1;
36654
37721
  const model = deviceInfo.type?.trim();
36655
37722
  const normalizedModel = model ? model.trim() : void 0;
@@ -36688,14 +37755,8 @@ async function autoDetectDeviceType(inputs) {
36688
37755
  channelNum: 1,
36689
37756
  api: udpApi
36690
37757
  };
36691
- } catch (e) {
36692
- const msg = fmtErr(e);
36693
- udpErrors.push(`${m}: ${msg}`);
36694
- logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${msg}`);
36695
- }
36696
- }
36697
- throw new Error(
36698
- `Forced UDP autodetect failed for all methods. ${udpErrors.join(" | ")}`
37758
+ },
37759
+ "Forced UDP autodetect failed for all methods."
36699
37760
  );
36700
37761
  }
36701
37762
  let tcpApi;
@@ -36748,54 +37809,57 @@ async function autoDetectDeviceType(inputs) {
36748
37809
  }
36749
37810
  return void 0;
36750
37811
  };
36751
- const infoProbe = await runProbeVariants(
36752
- "getInfo",
36753
- [
37812
+ const [infoProbe, supportProbe] = await Promise.all([
37813
+ runProbeVariants(
37814
+ "getInfo",
37815
+ [
37816
+ {
37817
+ variant: "cmd80 class=0x6414",
37818
+ op: () => api.getInfo(void 0, {
37819
+ timeoutMs: 2500,
37820
+ messageClass: BC_CLASS_MODERN_24
37821
+ })
37822
+ },
37823
+ {
37824
+ variant: "cmd80 class=0x6614",
37825
+ op: () => api.getInfo(void 0, {
37826
+ timeoutMs: 3e3,
37827
+ messageClass: BC_CLASS_MODERN_20
37828
+ })
37829
+ },
37830
+ {
37831
+ variant: "cmd318(ch0) class=0x6414",
37832
+ op: () => api.getInfo(0, {
37833
+ timeoutMs: 3e3,
37834
+ messageClass: BC_CLASS_MODERN_24
37835
+ })
37836
+ },
37837
+ {
37838
+ variant: "cmd318(ch0) class=0x6614",
37839
+ op: () => api.getInfo(0, {
37840
+ timeoutMs: 3500,
37841
+ messageClass: BC_CLASS_MODERN_20
37842
+ })
37843
+ }
37844
+ ]
37845
+ ),
37846
+ // Support probes (cmd 199). Some firmwares may not support it or are slow.
37847
+ runProbeVariants("getSupportInfo", [
36754
37848
  {
36755
- variant: "cmd80 class=0x6414",
36756
- op: () => api.getInfo(void 0, {
37849
+ variant: "cmd199 class=0x6414",
37850
+ op: () => api.getSupportInfo({
36757
37851
  timeoutMs: 2500,
36758
37852
  messageClass: BC_CLASS_MODERN_24
36759
37853
  })
36760
37854
  },
36761
37855
  {
36762
- variant: "cmd80 class=0x6614",
36763
- op: () => api.getInfo(void 0, {
36764
- timeoutMs: 3e3,
36765
- messageClass: BC_CLASS_MODERN_20
36766
- })
36767
- },
36768
- {
36769
- variant: "cmd318(ch0) class=0x6414",
36770
- op: () => api.getInfo(0, {
36771
- timeoutMs: 3e3,
36772
- messageClass: BC_CLASS_MODERN_24
36773
- })
36774
- },
36775
- {
36776
- variant: "cmd318(ch0) class=0x6614",
36777
- op: () => api.getInfo(0, {
37856
+ variant: "cmd199 class=0x6614",
37857
+ op: () => api.getSupportInfo({
36778
37858
  timeoutMs: 3500,
36779
37859
  messageClass: BC_CLASS_MODERN_20
36780
37860
  })
36781
37861
  }
36782
- ]
36783
- );
36784
- const supportProbe = await runProbeVariants("getSupportInfo", [
36785
- {
36786
- variant: "cmd199 class=0x6414",
36787
- op: () => api.getSupportInfo({
36788
- timeoutMs: 2500,
36789
- messageClass: BC_CLASS_MODERN_24
36790
- })
36791
- },
36792
- {
36793
- variant: "cmd199 class=0x6614",
36794
- op: () => api.getSupportInfo({
36795
- timeoutMs: 3500,
36796
- messageClass: BC_CLASS_MODERN_20
36797
- })
36798
- }
37862
+ ])
36799
37863
  ]);
36800
37864
  const deviceInfo = infoProbe?.value;
36801
37865
  const support = supportProbe?.value;
@@ -36887,9 +37951,11 @@ async function autoDetectDeviceType(inputs) {
36887
37951
  }
36888
37952
  try {
36889
37953
  const detectOverUdpApi = async (udpApi, udpDiscoveryMethod) => {
36890
- const deviceInfo = await udpApi.getInfo();
36891
- const capabilities = await udpApi.getDeviceCapabilities();
36892
- const hostNetworkInfo = await udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0);
37954
+ const [deviceInfo, capabilities, hostNetworkInfo] = await Promise.all([
37955
+ udpApi.getInfo(),
37956
+ udpApi.getDeviceCapabilities(),
37957
+ udpApi.getNetworkInfo(void 0, { timeoutMs: 1200 }).catch(() => void 0)
37958
+ ]);
36893
37959
  const channelNum = capabilities?.support?.channelNum ?? 1;
36894
37960
  const model = deviceInfo.type?.trim();
36895
37961
  const normalizedModel = model ? model.trim() : void 0;
@@ -36933,21 +37999,17 @@ async function autoDetectDeviceType(inputs) {
36933
37999
  };
36934
38000
  };
36935
38001
  const methodsToTry = ["local-direct", "local-broadcast", "remote", "relay", "map"];
36936
- const udpErrors = [];
36937
- for (const m of methodsToTry) {
36938
- try {
38002
+ const viableMethods = normalizedUid ? methodsToTry : methodsToTry.filter((m) => m === "local-direct" || m === "local-broadcast");
38003
+ return await runUdpMethodsParallel(
38004
+ viableMethods,
38005
+ async (m, isAborted) => {
36939
38006
  logger?.log?.(`[AutoDetect] Trying UDP discovery method: ${m}...`);
36940
38007
  const udpApi = await withRetries(
36941
38008
  `UDP(${m})`,
36942
38009
  maxRetries,
36943
38010
  async (attempt) => {
36944
- const apiInputs = {
36945
- ...inputs,
36946
- udpDiscoveryMethod: m
36947
- };
36948
- if (normalizedUid) {
36949
- apiInputs.uid = normalizedUid;
36950
- }
38011
+ const apiInputs = { ...inputs, udpDiscoveryMethod: m };
38012
+ if (normalizedUid) apiInputs.uid = normalizedUid;
36951
38013
  const api = createBaichuanApi(apiInputs, "udp");
36952
38014
  try {
36953
38015
  await api.login();
@@ -36962,20 +38024,12 @@ async function autoDetectDeviceType(inputs) {
36962
38024
  throw e;
36963
38025
  }
36964
38026
  },
36965
- shouldRetryUdp
38027
+ shouldRetryUdp,
38028
+ isAborted
36966
38029
  );
36967
- return await detectOverUdpApi(udpApi, m);
36968
- } catch (e) {
36969
- const msg = e?.message || e?.toString?.() || String(e);
36970
- udpErrors.push(`${m}: ${msg}`);
36971
- try {
36972
- } catch {
36973
- }
36974
- logger?.log?.(`[AutoDetect] UDP (${m}) failed: ${msg}`);
36975
- }
36976
- }
36977
- throw new Error(
36978
- `UDP discovery failed for all methods. ${udpErrors.join(" | ")}`
38030
+ return detectOverUdpApi(udpApi, m);
38031
+ },
38032
+ "UDP discovery failed for all methods."
36979
38033
  );
36980
38034
  } catch (udpError) {
36981
38035
  logger?.log?.(
@@ -37251,6 +38305,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
37251
38305
  BC_CMD_ID_GET_AUDIO_ALARM,
37252
38306
  BC_CMD_ID_GET_AUDIO_CFG,
37253
38307
  BC_CMD_ID_GET_AUDIO_TASK,
38308
+ BC_CMD_ID_GET_AUTO_FOCUS,
37254
38309
  BC_CMD_ID_GET_BATTERY_INFO,
37255
38310
  BC_CMD_ID_GET_BATTERY_INFO_LIST,
37256
38311
  BC_CMD_ID_GET_DAY_NIGHT_THRESHOLD,
@@ -37259,6 +38314,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
37259
38314
  BC_CMD_ID_GET_DING_DONG_LIST,
37260
38315
  BC_CMD_ID_GET_DING_DONG_SILENT,
37261
38316
  BC_CMD_ID_GET_EMAIL_TASK,
38317
+ BC_CMD_ID_GET_ENC,
37262
38318
  BC_CMD_ID_GET_FTP_TASK,
37263
38319
  BC_CMD_ID_GET_HDD_INFO_LIST,
37264
38320
  BC_CMD_ID_GET_KIT_AP_CFG,
@@ -37267,8 +38323,10 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
37267
38323
  BC_CMD_ID_GET_ONLINE_USER_LIST,
37268
38324
  BC_CMD_ID_GET_OSD_DATETIME,
37269
38325
  BC_CMD_ID_GET_PIR_INFO,
38326
+ BC_CMD_ID_GET_PRIVACY_MASK,
37270
38327
  BC_CMD_ID_GET_PTZ_POSITION,
37271
38328
  BC_CMD_ID_GET_PTZ_PRESET,
38329
+ BC_CMD_ID_GET_PUSH_TASK,
37272
38330
  BC_CMD_ID_GET_RECORD,
37273
38331
  BC_CMD_ID_GET_RECORD_CFG,
37274
38332
  BC_CMD_ID_GET_REC_ENC_CFG,
@@ -37297,11 +38355,23 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
37297
38355
  BC_CMD_ID_QUICK_REPLY_PLAY,
37298
38356
  BC_CMD_ID_SET_AI_ALARM,
37299
38357
  BC_CMD_ID_SET_AI_CFG,
38358
+ BC_CMD_ID_SET_AI_DENOISE,
38359
+ BC_CMD_ID_SET_AUDIO_CFG,
37300
38360
  BC_CMD_ID_SET_AUDIO_TASK,
38361
+ BC_CMD_ID_SET_AUTO_FOCUS,
38362
+ BC_CMD_ID_SET_DAY_NIGHT_THRESHOLD,
37301
38363
  BC_CMD_ID_SET_DING_DONG_CFG,
37302
38364
  BC_CMD_ID_SET_DING_DONG_SILENT,
38365
+ BC_CMD_ID_SET_EMAIL_TASK,
38366
+ BC_CMD_ID_SET_ENC,
38367
+ BC_CMD_ID_SET_LED_STATE,
37303
38368
  BC_CMD_ID_SET_MOTION_ALARM,
37304
38369
  BC_CMD_ID_SET_PIR_INFO,
38370
+ BC_CMD_ID_SET_PRIVACY_MASK,
38371
+ BC_CMD_ID_SET_PUSH_TASK,
38372
+ BC_CMD_ID_SET_RECORD,
38373
+ BC_CMD_ID_SET_RECORD_CFG,
38374
+ BC_CMD_ID_SET_VIDEO_INPUT,
37305
38375
  BC_CMD_ID_SET_WHITE_LED_STATE,
37306
38376
  BC_CMD_ID_SET_WHITE_LED_TASK,
37307
38377
  BC_CMD_ID_SET_ZOOM_FOCUS,
@@ -37340,6 +38410,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
37340
38410
  HlsSessionManager,
37341
38411
  Intercom,
37342
38412
  MjpegTransformer,
38413
+ MpegTsMuxer,
37343
38414
  NVR_HUB_EXACT_TYPES,
37344
38415
  NVR_HUB_MODEL_PATTERNS,
37345
38416
  ReolinkBaichuanApi,
@@ -37349,6 +38420,8 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
37349
38420
  abilitiesHasAny,
37350
38421
  aesDecrypt,
37351
38422
  aesEncrypt,
38423
+ applyStreamPatch,
38424
+ applyXmlTagPatch,
37352
38425
  asLogger,
37353
38426
  autoDetectDeviceType,
37354
38427
  bcDecrypt,
@@ -37398,6 +38471,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
37398
38471
  createRfc4571TcpServerForReplay,
37399
38472
  createRtspProxyServer,
37400
38473
  createTaggedLogger,
38474
+ decideSleepInferenceTransition,
37401
38475
  decideVideoclipTranscodeMode,
37402
38476
  decodeHeader,
37403
38477
  deriveAesKey,
@@ -37412,6 +38486,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
37412
38486
  discoverViaUdpBroadcast,
37413
38487
  discoverViaUdpDirect,
37414
38488
  encodeHeader,
38489
+ ensureXmlHeader,
37415
38490
  extractH264ParamSetsFromAccessUnit,
37416
38491
  extractH265ParamSetsFromAccessUnit,
37417
38492
  extractPpsFromAnnexB,
@@ -37440,6 +38515,8 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
37440
38515
  maskUid,
37441
38516
  md5HexUpper,
37442
38517
  md5StrModern,
38518
+ normalizeDayNightMode,
38519
+ normalizeOpenClose,
37443
38520
  normalizeUid,
37444
38521
  packetizeAacAdtsFrame,
37445
38522
  packetizeAacRawFrame,
@@ -37449,6 +38526,7 @@ var CompositeRtspServer = class extends import_node_events12.EventEmitter {
37449
38526
  parseBcMedia,
37450
38527
  parseRecordingFileName,
37451
38528
  parseSupportXml,
38529
+ patchNestedTag,
37452
38530
  printNvrDiagnostics,
37453
38531
  runAllDiagnosticsConsecutively,
37454
38532
  runMultifocalDiagnosticsConsecutively,