@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/.vscode/settings.json +1 -1
- package/README.md +12 -3
- package/dist/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/package.json +2 -2
- package/src/camera-battery.ts +72 -64
- package/src/camera.ts +11 -13
- package/src/common.ts +90 -85
- package/src/connect.ts +136 -0
- package/src/intercom.ts +1 -1
- package/src/main.ts +133 -80
- package/src/nvr.ts +367 -0
- package/src/presets.ts +6 -6
- package/src/stream-utils.ts +26 -25
- package/src/utils.ts +31 -2
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.
|
|
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.
|
|
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
|
-
|
|
231
|
+
isFromNvr: {
|
|
231
232
|
type: 'boolean',
|
|
232
233
|
hide: true,
|
|
234
|
+
defaultValue: false,
|
|
233
235
|
},
|
|
234
|
-
|
|
235
|
-
|
|
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:
|
|
251
|
+
description: "How often to wake up the camera and update battery status and snapshot (default: 60 minutes).",
|
|
246
252
|
type: "number",
|
|
247
|
-
defaultValue:
|
|
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
|
-
|
|
502
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
992
|
+
const { ipAddress, rtspChannel } = this.storageSettings.values;
|
|
995
993
|
try {
|
|
996
994
|
const api = await this.ensureClient();
|
|
997
|
-
const deviceData = await api.getInfo();
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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).
|