@apocaliss92/scrypted-reolink-native 0.1.3 → 0.1.5

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/src/common.ts CHANGED
@@ -6,23 +6,24 @@ import { createBaichuanApi, normalizeUid, type BaichuanTransport } from "./conne
6
6
  import { convertDebugLogsToApiOptions, DebugLogOption, getApiRelevantDebugLogs, getDebugLogChoices } from "./debug-options";
7
7
  import { ReolinkBaichuanIntercom } from "./intercom";
8
8
  import ReolinkNativePlugin from "./main";
9
+ import { ReolinkNativeNvrDevice } from "./nvr";
9
10
  import { ReolinkPtzPresets } from "./presets";
10
11
  import {
11
12
  buildVideoStreamOptionsFromRtspRtmp,
12
13
  createRfc4571MediaObjectFromStreamManager,
13
14
  expectedVideoTypeFromUrlMediaStreamOptions,
14
- fetchVideoStreamOptionsFromApi,
15
15
  isNativeStreamId,
16
16
  parseStreamProfileFromId,
17
17
  selectStreamOption,
18
- StreamManager,
18
+ StreamManager
19
19
  } from "./stream-utils";
20
- import { getDeviceInterfaces } from "./utils";
20
+ import { getDeviceInterfaces, updateDeviceInfo } from "./utils";
21
21
 
22
22
  export type CameraType = 'battery' | 'regular';
23
23
 
24
24
  export interface CommonCameraMixinOptions {
25
25
  type: CameraType;
26
+ nvrDevice?: ReolinkNativeNvrDevice; // Optional reference to NVR device
26
27
  }
27
28
 
28
29
  class ReolinkCameraSiren extends ScryptedDeviceBase implements OnOff {
@@ -138,7 +139,7 @@ class ReolinkCameraPirSensor extends ScryptedDeviceBase implements OnOff, Settin
138
139
  await this.storageSettings.putSetting(key, value);
139
140
 
140
141
  // Apply the new settings to the camera
141
- const channel = this.camera.getRtspChannel();
142
+ const channel = this.camera.storageSettings.values.rtspChannel;
142
143
  const enabled = this.on ? 1 : 0;
143
144
  const sensitive = this.storageSettings.values.sensitive;
144
145
  const reduceAlarm = this.storageSettings.values.reduceAlarm ? 1 : 0;
@@ -166,7 +167,7 @@ class ReolinkCameraPirSensor extends ScryptedDeviceBase implements OnOff, Settin
166
167
  }
167
168
 
168
169
  private async updatePirSettings(): Promise<void> {
169
- const channel = this.camera.getRtspChannel();
170
+ const channel = this.camera.storageSettings.values.rtspChannel;
170
171
  const enabled = this.on ? 1 : 0;
171
172
  const sensitive = this.storageSettings.values.sensitive;
172
173
  const reduceAlarm = this.storageSettings.values.reduceAlarm ? 1 : 0;
@@ -227,24 +228,29 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
227
228
  await this.credentialsChanged();
228
229
  }
229
230
  },
230
- mixinsSetup: {
231
+ isFromNvr: {
231
232
  type: 'boolean',
232
233
  hide: true,
234
+ defaultValue: false,
233
235
  },
234
- snapshotCacheMinutes: {
235
- title: "Snapshot Cache Minutes",
236
- subgroup: 'Advanced',
237
- description: "Return a cached snapshot if taken within the last N minutes.",
238
- type: "number",
239
- defaultValue: 5,
236
+ mixinsSetup: {
237
+ type: 'boolean',
240
238
  hide: true,
241
239
  },
240
+ // snapshotCacheMinutes: {
241
+ // title: "Snapshot Cache Minutes",
242
+ // subgroup: 'Advanced',
243
+ // description: "Return a cached snapshot if taken within the last N minutes.",
244
+ // type: "number",
245
+ // defaultValue: 60,
246
+ // hide: true,
247
+ // },
242
248
  batteryUpdateIntervalMinutes: {
243
249
  title: "Battery Update Interval (minutes)",
244
250
  subgroup: 'Advanced',
245
- description: "How often to wake up the camera and update battery status and snapshot (default: 10 minutes).",
251
+ description: "How often to wake up the camera and update battery status and snapshot (default: 60 minutes).",
246
252
  type: "number",
247
- defaultValue: 10,
253
+ defaultValue: 60,
248
254
  hide: true,
249
255
  },
250
256
  // Regular camera specific
@@ -493,21 +499,15 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
493
499
  initComplete?: boolean;
494
500
  resetBaichuanClient?(reason?: any): Promise<void>;
495
501
 
502
+ protected nvrDevice?: any; // Optional reference to NVR device
503
+
496
504
  constructor(nativeId: string, public plugin: ReolinkNativePlugin, public options: CommonCameraMixinOptions) {
497
505
  super(nativeId);
498
506
  // Set protocol based on camera type
499
- this.protocol = options.type === 'battery' ? 'udp' : 'tcp';
507
+ this.protocol = !options.nvrDevice && options.type === 'battery' ? 'udp' : 'tcp';
500
508
 
501
- this.streamManager = new StreamManager({
502
- createStreamClient: () => this.createStreamClient(),
503
- getLogger: () => this.getLogger(),
504
- credentials: {
505
- username: this.storageSettings.values.username || '',
506
- password: this.storageSettings.values.password || '',
507
- },
508
- // For battery cameras, we use a shared connection
509
- sharedConnection: options.type === 'battery',
510
- });
509
+ // Store NVR device reference if provided
510
+ this.nvrDevice = options.nvrDevice;
511
511
 
512
512
  setTimeout(async () => {
513
513
  await this.parentInit();
@@ -517,12 +517,6 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
517
517
  throw new Error("Method not implemented.");
518
518
  }
519
519
 
520
- // Common method implementations
521
- public getRtspChannel(): number {
522
- const channel = this.storageSettings.values.rtspChannel;
523
- return channel !== undefined ? Number(channel) : 0;
524
- }
525
-
526
520
  public getAbilities(): DeviceCapabilities {
527
521
  return this.storageSettings.values.capabilities;
528
522
  }
@@ -589,7 +583,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
589
583
  return;
590
584
  }
591
585
 
592
- const channel = this.getRtspChannel();
586
+ const channel = this.storageSettings.values.rtspChannel;
593
587
  if (ev?.channel !== undefined && ev.channel !== channel) {
594
588
  if (this.isEventLogsEnabled()) {
595
589
  logger.debug(`Event channel ${ev.channel} does not match camera channel ${channel}, ignoring`);
@@ -637,6 +631,10 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
637
631
  }
638
632
 
639
633
  async subscribeToEvents(): Promise<void> {
634
+ if (this.nvrDevice) {
635
+ return;
636
+ }
637
+
640
638
  const logger = this.getLogger();
641
639
  const selection = Array.from(this.getDispatchEventsSelection?.() ?? new Set()).sort();
642
640
  const enabled = selection.length > 0;
@@ -676,7 +674,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
676
674
  // VideoTextOverlays interface implementation
677
675
  async getVideoTextOverlays(): Promise<Record<string, VideoTextOverlay>> {
678
676
  const client = await this.ensureClient();
679
- const channel = this.getRtspChannel();
677
+ const channel = this.storageSettings.values.rtspChannel;
680
678
 
681
679
  let osd = this.storageSettings.values.cachedOsd;
682
680
 
@@ -698,7 +696,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
698
696
 
699
697
  async setVideoTextOverlay(id: 'osdChannel' | 'osdTime', value: VideoTextOverlay): Promise<void> {
700
698
  const client = await this.ensureClient();
701
- const channel = this.getRtspChannel();
699
+ const channel = this.storageSettings.values.rtspChannel;
702
700
 
703
701
  const osd = await client.getOsd(channel);
704
702
 
@@ -728,7 +726,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
728
726
  return;
729
727
  }
730
728
 
731
- const channel = this.getRtspChannel();
729
+ const channel = this.storageSettings.values.rtspChannel;
732
730
 
733
731
  // Preset navigation.
734
732
  const preset = command.preset;
@@ -959,9 +957,10 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
959
957
  });
960
958
  }
961
959
 
962
- // Settings methods
963
960
  async getSettings(): Promise<Setting[]> {
964
- return await this.storageSettings.getSettings();
961
+ const settings = await this.storageSettings.getSettings();
962
+
963
+ return settings;
965
964
  }
966
965
 
967
966
  async putSetting(key: string, value: string): Promise<void> {
@@ -989,29 +988,18 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
989
988
  }
990
989
  }
991
990
 
992
- // Device info update
993
991
  async updateDeviceInfo(): Promise<void> {
994
- const ip = this.storageSettings.values.ipAddress;
992
+ const { ipAddress, rtspChannel } = this.storageSettings.values;
995
993
  try {
996
994
  const api = await this.ensureClient();
997
- const deviceData = await api.getInfo();
998
- const info = this.info || {};
999
- info.ip = ip;
1000
-
1001
- info.serialNumber = deviceData?.serialNumber || deviceData?.itemNo;
1002
- info.firmware = deviceData?.firmwareVersion || deviceData?.firmVer;
1003
- info.version = deviceData?.hardwareVersion || deviceData?.boardInfo;
1004
- info.model = deviceData?.type || deviceData?.typeInfo;
1005
- info.manufacturer = 'Reolink native';
1006
- info.managementUrl = `http://${ip}`;
1007
- this.info = info;
995
+ const deviceData = await api.getInfo(this.nvrDevice ? rtspChannel : undefined);
996
+
997
+ await updateDeviceInfo({
998
+ device: this,
999
+ ipAddress,
1000
+ deviceData,
1001
+ });
1008
1002
  } catch (e) {
1009
- // If API call fails, at least set basic info
1010
- const info = this.info || {};
1011
- info.ip = ip;
1012
- info.manufacturer = 'Reolink native';
1013
- info.managementUrl = `http://${ip}`;
1014
- this.info = info;
1015
1003
  this.getLogger().warn('Failed to fetch device info', e);
1016
1004
  }
1017
1005
  }
@@ -1041,7 +1029,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1041
1029
  }
1042
1030
 
1043
1031
  async setSirenEnabled(enabled: boolean): Promise<void> {
1044
- const channel = this.getRtspChannel();
1032
+ const channel = this.storageSettings.values.rtspChannel;
1045
1033
 
1046
1034
  await this.withBaichuanRetry(async () => {
1047
1035
  const api = await this.ensureClient();
@@ -1050,7 +1038,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1050
1038
  }
1051
1039
 
1052
1040
  async setFloodlightState(on?: boolean, brightness?: number): Promise<void> {
1053
- const channel = this.getRtspChannel();
1041
+ const channel = this.storageSettings.values.rtspChannel;
1054
1042
 
1055
1043
  await this.withBaichuanRetry(async () => {
1056
1044
  const api = await this.ensureClient();
@@ -1059,7 +1047,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1059
1047
  }
1060
1048
 
1061
1049
  async setPirEnabled(enabled: boolean): Promise<void> {
1062
- const channel = this.getRtspChannel();
1050
+ const channel = this.storageSettings.values.rtspChannel;
1063
1051
 
1064
1052
  // Get current PIR settings from the sensor if available
1065
1053
  let sensitive: number | undefined;
@@ -1091,7 +1079,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1091
1079
  const api = this.baichuanApi;
1092
1080
  if (!api) return;
1093
1081
 
1094
- const channel = this.getRtspChannel();
1082
+ const channel = this.storageSettings.values.rtspChannel;
1095
1083
  const { hasSiren, hasFloodlight, hasPir } = this.getAbilities();
1096
1084
 
1097
1085
  try {
@@ -1234,16 +1222,13 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1234
1222
  return [];
1235
1223
  }
1236
1224
 
1237
- // while (this.fetchingStreams) {
1238
- // await new Promise((resolve) => setTimeout(resolve, 500));
1239
- // }
1240
1225
  this.fetchingStreams = true;
1241
1226
 
1242
1227
  let streams: UrlMediaStreamOptions[] = [];
1243
1228
 
1244
1229
  const client = await this.ensureClient();
1245
1230
 
1246
- const { ipAddress, username, password, rtspChannel } = this.storageSettings.values;
1231
+ const { ipAddress, rtspChannel, isFromNvr } = this.storageSettings.values;
1247
1232
 
1248
1233
  try {
1249
1234
  await this.ensureNetPortCache();
@@ -1255,12 +1240,14 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1255
1240
 
1256
1241
  try {
1257
1242
  streams = await buildVideoStreamOptionsFromRtspRtmp(
1258
- client,
1259
- rtspChannel,
1260
- ipAddress,
1261
- username,
1262
- password,
1263
- this.cachedNetPort,
1243
+ {
1244
+ client,
1245
+ ipAddress,
1246
+ cachedNetPort: this.cachedNetPort,
1247
+ isFromNvr,
1248
+ rtspChannel,
1249
+ logger,
1250
+ },
1264
1251
  );
1265
1252
  } catch (e) {
1266
1253
  if (!this.isRecoverableBaichuanError?.(e)) {
@@ -1269,12 +1256,8 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1269
1256
  this.cachedNetPort = undefined;
1270
1257
  }
1271
1258
 
1272
-
1273
- const nativeStreams = await fetchVideoStreamOptionsFromApi(client, rtspChannel, this.getLogger());
1274
- streams = [...streams, ...nativeStreams];
1275
-
1276
1259
  if (streams.length) {
1277
- logger.log('Fetched video stream options', streams);
1260
+ logger.log('Fetched video stream options', { streams, netPort: this.cachedNetPort });
1278
1261
  this.cachedVideoStreamOptions = streams;
1279
1262
  return streams;
1280
1263
  }
@@ -1308,7 +1291,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1308
1291
  }
1309
1292
 
1310
1293
  const profile = parseStreamProfileFromId(selected.id) || 'main';
1311
- const channel = this.getRtspChannel();
1294
+ const channel = this.storageSettings.values.rtspChannel;
1312
1295
  const streamKey = `${channel}_${profile}`;
1313
1296
  const expectedVideoType = expectedVideoTypeFromUrlMediaStreamOptions(selected);
1314
1297
 
@@ -1356,10 +1339,6 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1356
1339
  this.ensureClientPromise = (async () => {
1357
1340
  const { ipAddress, username, password, uid } = this.storageSettings.values;
1358
1341
 
1359
- if (!ipAddress || !username || !password) {
1360
- throw new Error('Missing camera credentials');
1361
- }
1362
-
1363
1342
  // Only tear down previous session if it exists and is not connected
1364
1343
  if (this.baichuanApi) {
1365
1344
  const isConnected = this.baichuanApi.client.isSocketConnected();
@@ -1409,8 +1388,8 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1409
1388
  {
1410
1389
  inputs: {
1411
1390
  host: ipAddress,
1412
- username,
1413
- password,
1391
+ username: username,
1392
+ password: password,
1414
1393
  uid: normalizedUid,
1415
1394
  logger: this.console,
1416
1395
  debugOptions,
@@ -1472,7 +1451,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1472
1451
  this.refreshingState = true;
1473
1452
 
1474
1453
  const logger = this.getLogger();
1475
- const channel = this.getRtspChannel();
1454
+ const channel = this.storageSettings.values.rtspChannel;
1476
1455
 
1477
1456
  try {
1478
1457
  const { capabilities, abilities, support, presets, objects } = await this.withBaichuanRetry(async () => {
@@ -1492,7 +1471,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1492
1471
 
1493
1472
  const device: Device = {
1494
1473
  nativeId: this.nativeId,
1495
- providerNativeId: this.plugin?.nativeId,
1474
+ providerNativeId: this.nvrDevice?.nativeId ?? this.plugin?.nativeId,
1496
1475
  name: this.name,
1497
1476
  interfaces,
1498
1477
  type,
@@ -1564,8 +1543,21 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1564
1543
  }
1565
1544
 
1566
1545
  const isBattery = this.options.type === 'battery';
1546
+ const { username, password } = this.storageSettings.values;
1567
1547
 
1568
- this.storageSettings.settings.snapshotCacheMinutes.hide = !isBattery;
1548
+ this.streamManager = new StreamManager({
1549
+ createStreamClient: () => this.createStreamClient(),
1550
+ getLogger: () => this.getLogger(),
1551
+ credentials: {
1552
+ username,
1553
+ password
1554
+ },
1555
+ // For battery cameras, we use a shared connection
1556
+ sharedConnection: isBattery,
1557
+ });
1558
+
1559
+
1560
+ // this.storageSettings.settings.snapshotCacheMinutes.hide = !isBattery;
1569
1561
  this.storageSettings.settings.uid.hide = !isBattery;
1570
1562
  this.storageSettings.settings.batteryUpdateIntervalMinutes.hide = !isBattery;
1571
1563
 
@@ -1591,6 +1583,19 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1591
1583
  logger.warn('Failed to subscribe to Baichuan events', e);
1592
1584
  }
1593
1585
 
1586
+ const { isFromNvr } = this.storageSettings.values;
1587
+
1588
+ if (isFromNvr && this.nvrDevice) {
1589
+ this.storageSettings.settings.username.hide = true;
1590
+ this.storageSettings.settings.password.hide = true;
1591
+ this.storageSettings.settings.ipAddress.hide = true;
1592
+ this.storageSettings.settings.uid.hide = true;
1593
+
1594
+ this.storageSettings.settings.username.defaultValue = this.nvrDevice.storageSettings.values.username;
1595
+ this.storageSettings.settings.password.defaultValue = this.nvrDevice.storageSettings.values.password;
1596
+ this.storageSettings.settings.ipAddress.defaultValue = this.nvrDevice.storageSettings.values.ipAddress;
1597
+ }
1598
+
1594
1599
  await this.init();
1595
1600
  this.initComplete = true;
1596
1601
  }
package/src/connect.ts CHANGED
@@ -117,6 +117,142 @@ export type UdpFallbackInfo = {
117
117
  tcpError: unknown;
118
118
  };
119
119
 
120
+ export type DeviceType = 'camera' | 'battery-cam' | 'nvr';
121
+
122
+ export type AutoDetectResult = {
123
+ type: DeviceType;
124
+ transport: BaichuanTransport;
125
+ uid?: string;
126
+ deviceInfo?: Record<string, string>;
127
+ channelNum?: number;
128
+ };
129
+
130
+ /**
131
+ * Simple ping check to verify IP is reachable
132
+ */
133
+ async function pingHost(host: string, timeoutMs: number = 3000): Promise<boolean> {
134
+ return new Promise((resolve) => {
135
+ const { exec } = require('child_process');
136
+ const platform = process.platform;
137
+ const pingCmd = platform === 'win32' ? `ping -n 1 -w ${timeoutMs} ${host}` : `ping -c 1 -W ${Math.floor(timeoutMs / 1000)} ${host}`;
138
+
139
+ exec(pingCmd, (error: any) => {
140
+ resolve(!error);
141
+ });
142
+ });
143
+ }
144
+
145
+ /**
146
+ * Auto-detect device type by trying TCP first, then UDP if needed.
147
+ * - First: Ping the IP to verify it's reachable
148
+ * - TCP success: Check if NVR (multiple channels) or regular camera
149
+ * - TCP failure: Try UDP (always battery camera)
150
+ */
151
+ export async function autoDetectDeviceType(
152
+ inputs: BaichuanConnectInputs,
153
+ logger: Console,
154
+ ): Promise<AutoDetectResult> {
155
+ const { host, username, password, uid } = inputs;
156
+
157
+ // Ping the host first to verify it's reachable
158
+ logger.log(`[AutoDetect] Pinging ${host}...`);
159
+ const isReachable = await pingHost(host);
160
+ if (!isReachable) {
161
+ logger.warn(`[AutoDetect] Host ${host} is not reachable via ping, but continuing with connection attempt...`);
162
+ } else {
163
+ logger.log(`[AutoDetect] Host ${host} is reachable`);
164
+ }
165
+
166
+ // Try TCP first
167
+ let tcpApi: ReolinkBaichuanApi | undefined;
168
+ try {
169
+ logger.log(`[AutoDetect] Trying TCP connection to ${host}...`);
170
+ tcpApi = await createBaichuanApi({
171
+ inputs: { host, username, password, logger },
172
+ transport: 'tcp',
173
+ logger,
174
+ });
175
+ await tcpApi.login();
176
+
177
+ // Get device info to check if it's an NVR
178
+ const deviceInfo = await tcpApi.getInfo();
179
+ const { support } = await tcpApi.getDeviceCapabilities(0);
180
+ const channelNum = support?.channelNum ?? 1;
181
+
182
+ logger.log(`[AutoDetect] TCP connection successful. channelNum=${channelNum}`);
183
+
184
+ // If channelNum > 1, it's likely an NVR
185
+ if (channelNum > 1) {
186
+ logger.log(`[AutoDetect] Detected NVR (${channelNum} channels)`);
187
+ await tcpApi.close();
188
+ return {
189
+ type: 'nvr',
190
+ transport: 'tcp',
191
+ deviceInfo,
192
+ channelNum,
193
+ };
194
+ }
195
+
196
+ // Single channel device - regular camera
197
+ logger.log(`[AutoDetect] Detected regular camera (single channel)`);
198
+ await tcpApi.close();
199
+ return {
200
+ type: 'camera',
201
+ transport: 'tcp',
202
+ deviceInfo,
203
+ channelNum: 1,
204
+ };
205
+ } catch (tcpError) {
206
+ // TCP failed, try UDP (battery camera)
207
+ if (tcpApi) {
208
+ try {
209
+ await tcpApi.close();
210
+ } catch {
211
+ // ignore
212
+ }
213
+ }
214
+
215
+ if (!isTcpFailureThatShouldFallbackToUdp(tcpError)) {
216
+ // Not a transport error, rethrow
217
+ throw tcpError;
218
+ }
219
+
220
+ logger.log(`[AutoDetect] TCP failed, trying UDP (battery camera)...`);
221
+ const normalizedUid = normalizeUid(uid);
222
+ if (!normalizedUid) {
223
+ throw new Error(
224
+ `TCP connection failed and device likely requires UDP/BCUDP. UID is required for battery cameras (ip=${host}).`
225
+ );
226
+ }
227
+
228
+ try {
229
+ const udpApi = await createBaichuanApi({
230
+ inputs: { host, username, password, uid: normalizedUid, logger },
231
+ transport: 'udp',
232
+ logger,
233
+ });
234
+ await udpApi.login();
235
+
236
+ const deviceInfo = await udpApi.getInfo();
237
+ logger.log(`[AutoDetect] UDP connection successful. Detected battery camera.`);
238
+ await udpApi.close();
239
+
240
+ return {
241
+ type: 'battery-cam',
242
+ transport: 'udp',
243
+ uid: normalizedUid,
244
+ deviceInfo,
245
+ channelNum: 1,
246
+ };
247
+ } catch (udpError) {
248
+ logger.error(`[AutoDetect] Both TCP and UDP failed. TCP error: ${tcpError}, UDP error: ${udpError}`);
249
+ throw new Error(
250
+ `Failed to connect via both TCP and UDP. TCP: ${(tcpError as any)?.message || tcpError}, UDP: ${(udpError as any)?.message || udpError}`
251
+ );
252
+ }
253
+ }
254
+ }
255
+
120
256
  // export async function connectBaichuanWithTcpUdpFallback(
121
257
  // inputs: BaichuanConnectInputs,
122
258
  // onUdpFallback?: (info: UdpFallbackInfo) => void,
package/src/intercom.ts CHANGED
@@ -39,7 +39,7 @@ export class ReolinkBaichuanIntercom {
39
39
  );
40
40
 
41
41
  await this.stop();
42
- const channel = this.camera.getRtspChannel();
42
+ const channel = this.camera.storageSettings.values.rtspChannel;
43
43
 
44
44
  // Best-effort: log codec requirements exposed by the camera.
45
45
  // This mirrors neolink's source of truth: TalkAbility (cmd_id=10).