@apocaliss92/scrypted-reolink-native 0.1.2 → 0.1.4
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 +82 -65
- package/src/connect.ts +136 -2
- package/src/intercom.ts +1 -1
- package/src/main.ts +135 -80
- package/src/nvr.ts +364 -0
- package/src/presets.ts +6 -6
- package/src/stream-utils.ts +26 -25
package/src/connect.ts
CHANGED
|
@@ -86,8 +86,6 @@ export async function createBaichuanApi(props: {
|
|
|
86
86
|
}
|
|
87
87
|
};
|
|
88
88
|
|
|
89
|
-
logger.log('Connecting with options:', JSON.stringify(base, null, 2));
|
|
90
|
-
|
|
91
89
|
if (transport === "tcp") {
|
|
92
90
|
const api = new ReolinkBaichuanApi({
|
|
93
91
|
...base,
|
|
@@ -119,6 +117,142 @@ export type UdpFallbackInfo = {
|
|
|
119
117
|
tcpError: unknown;
|
|
120
118
|
};
|
|
121
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
|
+
|
|
122
256
|
// export async function connectBaichuanWithTcpUdpFallback(
|
|
123
257
|
// inputs: BaichuanConnectInputs,
|
|
124
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).
|
package/src/main.ts
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import sdk, { DeviceCreator, DeviceCreatorSettings, DeviceInformation, DeviceProvider, ScryptedDeviceBase, ScryptedDeviceType, ScryptedNativeId, Setting } from "@scrypted/sdk";
|
|
1
|
+
import sdk, { DeviceCreator, DeviceCreatorSettings, DeviceInformation, DeviceProvider, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting } from "@scrypted/sdk";
|
|
2
2
|
import { ReolinkNativeCamera } from "./camera";
|
|
3
3
|
import { ReolinkNativeBatteryCamera } from "./camera-battery";
|
|
4
|
-
import {
|
|
4
|
+
import { ReolinkNativeNvrDevice } from "./nvr";
|
|
5
|
+
import { autoDetectDeviceType, createBaichuanApi } from "./connect";
|
|
5
6
|
import { getDeviceInterfaces } from "./utils";
|
|
6
7
|
|
|
7
8
|
class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider, DeviceCreator {
|
|
8
|
-
devices = new Map<string, ReolinkNativeCamera | ReolinkNativeBatteryCamera>();
|
|
9
|
+
devices = new Map<string, ReolinkNativeCamera | ReolinkNativeBatteryCamera | ReolinkNativeNvrDevice>();
|
|
9
10
|
|
|
10
11
|
getScryptedDeviceCreator(): string {
|
|
11
12
|
return 'Reolink Native camera';
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
async getDevice(nativeId: ScryptedNativeId): Promise<ReolinkNativeCamera | ReolinkNativeBatteryCamera> {
|
|
15
|
+
async getDevice(nativeId: ScryptedNativeId): Promise<ReolinkNativeCamera | ReolinkNativeBatteryCamera | ReolinkNativeNvrDevice> {
|
|
15
16
|
if (this.devices.has(nativeId)) {
|
|
16
|
-
return this.devices.get(nativeId)
|
|
17
|
+
return this.devices.get(nativeId)!;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
const newCamera = this.createCamera(nativeId);
|
|
@@ -23,88 +24,145 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
23
24
|
|
|
24
25
|
async createDevice(settings: DeviceCreatorSettings, nativeId?: string): Promise<string> {
|
|
25
26
|
const ipAddress = settings.ip?.toString();
|
|
26
|
-
let info: DeviceInformation = {};
|
|
27
|
-
|
|
28
27
|
const username = settings.username?.toString();
|
|
29
28
|
const password = settings.password?.toString();
|
|
30
29
|
const uid = settings.uid?.toString();
|
|
31
|
-
const isBatteryCam = settings.isBatteryCam === true || settings.isBatteryCam?.toString() === 'true';
|
|
32
30
|
|
|
33
|
-
if (
|
|
34
|
-
throw new Error('
|
|
31
|
+
if (!ipAddress || !username || !password) {
|
|
32
|
+
throw new Error('IP address, username, and password are required');
|
|
35
33
|
}
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
35
|
+
// Auto-detect device type (camera, battery-cam, or nvr)
|
|
36
|
+
this.console.log(`[AutoDetect] Starting device type detection for ${ipAddress}...`);
|
|
37
|
+
const detection = await autoDetectDeviceType(
|
|
38
|
+
{
|
|
39
|
+
host: ipAddress,
|
|
40
|
+
username,
|
|
41
|
+
password,
|
|
42
|
+
uid,
|
|
43
|
+
logger: this.console,
|
|
44
|
+
},
|
|
45
|
+
this.console
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
this.console.log(`[AutoDetect] Detected device type: ${detection.type} (transport: ${detection.transport})`);
|
|
49
|
+
|
|
50
|
+
// Handle NVR case
|
|
51
|
+
if (detection.type === 'nvr') {
|
|
52
|
+
const deviceInfo = detection.deviceInfo || {};
|
|
53
|
+
const name = deviceInfo?.name || 'Reolink NVR';
|
|
54
|
+
const serialNumber = deviceInfo?.serialNumber || deviceInfo?.itemNo || `nvr-${Date.now()}`;
|
|
55
|
+
nativeId = `${serialNumber}-nvr`;
|
|
56
|
+
|
|
57
|
+
settings.newCamera ||= name;
|
|
58
|
+
|
|
59
|
+
await sdk.deviceManager.onDeviceDiscovered({
|
|
60
|
+
nativeId,
|
|
61
|
+
name,
|
|
62
|
+
interfaces: [
|
|
63
|
+
ScryptedInterface.Settings,
|
|
64
|
+
ScryptedInterface.DeviceDiscovery,
|
|
65
|
+
ScryptedInterface.DeviceProvider,
|
|
66
|
+
ScryptedInterface.Reboot,
|
|
67
|
+
],
|
|
68
|
+
type: ScryptedDeviceType.Builtin,
|
|
69
|
+
providerNativeId: this.nativeId,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const device = await this.getDevice(nativeId);
|
|
73
|
+
if (!(device instanceof ReolinkNativeNvrDevice)) {
|
|
74
|
+
throw new Error('Expected NVR device but got different type');
|
|
75
|
+
}
|
|
76
|
+
device.storageSettings.values.ipAddress = ipAddress;
|
|
77
|
+
device.storageSettings.values.username = username;
|
|
78
|
+
device.storageSettings.values.password = password;
|
|
79
|
+
device.updateDeviceInfo(deviceInfo);
|
|
51
80
|
|
|
52
|
-
|
|
81
|
+
return nativeId;
|
|
82
|
+
}
|
|
53
83
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
84
|
+
// For camera and battery-cam, create the device
|
|
85
|
+
const deviceInfo = detection.deviceInfo || {};
|
|
86
|
+
const name = deviceInfo?.name || 'Reolink Camera';
|
|
87
|
+
const serialNumber = deviceInfo?.serialNumber || deviceInfo?.itemNo || `unknown-${Date.now()}`;
|
|
88
|
+
|
|
89
|
+
// Create nativeId based on device type
|
|
90
|
+
if (detection.type === 'battery-cam') {
|
|
91
|
+
nativeId = `${serialNumber}-battery-cam`;
|
|
92
|
+
} else {
|
|
93
|
+
nativeId = `${serialNumber}-cam`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
settings.newCamera ||= name;
|
|
97
|
+
|
|
98
|
+
// Create API connection to get capabilities
|
|
99
|
+
const api = await createBaichuanApi({
|
|
100
|
+
inputs: {
|
|
101
|
+
host: ipAddress,
|
|
102
|
+
username,
|
|
103
|
+
password,
|
|
104
|
+
uid: detection.uid,
|
|
105
|
+
logger: this.console,
|
|
106
|
+
},
|
|
107
|
+
transport: detection.transport,
|
|
108
|
+
logger: this.console,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
await api.login();
|
|
113
|
+
const rtspChannel = 0;
|
|
114
|
+
const { abilities, capabilities, objects, presets } = await api.getDeviceCapabilities(rtspChannel);
|
|
115
|
+
|
|
116
|
+
this.console.log(JSON.stringify({ abilities, capabilities, deviceInfo }));
|
|
117
|
+
|
|
118
|
+
const { interfaces, type } = getDeviceInterfaces({
|
|
119
|
+
capabilities,
|
|
120
|
+
logger: this.console,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await sdk.deviceManager.onDeviceDiscovered({
|
|
124
|
+
nativeId,
|
|
125
|
+
name,
|
|
126
|
+
interfaces,
|
|
127
|
+
type,
|
|
128
|
+
providerNativeId: this.nativeId,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const device = await this.getDevice(nativeId);
|
|
132
|
+
if (device instanceof ReolinkNativeNvrDevice) {
|
|
133
|
+
// NVR devices are handled separately above
|
|
134
|
+
throw new Error('NVR device should not reach this code path');
|
|
100
135
|
}
|
|
136
|
+
|
|
137
|
+
// Type guard: device is either ReolinkNativeCamera or ReolinkNativeBatteryCamera
|
|
138
|
+
device.info = deviceInfo;
|
|
139
|
+
device.classes = objects;
|
|
140
|
+
device.presets = presets;
|
|
141
|
+
device.storageSettings.values.username = username;
|
|
142
|
+
device.storageSettings.values.password = password;
|
|
143
|
+
device.storageSettings.values.rtspChannel = rtspChannel;
|
|
144
|
+
device.storageSettings.values.ipAddress = ipAddress;
|
|
145
|
+
device.storageSettings.values.capabilities = capabilities;
|
|
146
|
+
device.storageSettings.values.uid = detection.uid;
|
|
147
|
+
device.updateDeviceInfo();
|
|
148
|
+
|
|
149
|
+
return nativeId;
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
this.console.error('Error adding Reolink device', e);
|
|
153
|
+
throw e;
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
await api.close();
|
|
101
157
|
}
|
|
102
158
|
}
|
|
103
159
|
|
|
104
160
|
async releaseDevice(id: string, nativeId: ScryptedNativeId): Promise<void> {
|
|
105
161
|
if (this.devices.has(nativeId)) {
|
|
106
162
|
const device = this.devices.get(nativeId);
|
|
107
|
-
|
|
163
|
+
if (device && 'release' in device && typeof device.release === 'function') {
|
|
164
|
+
await device.release();
|
|
165
|
+
}
|
|
108
166
|
this.devices.delete(nativeId);
|
|
109
167
|
}
|
|
110
168
|
}
|
|
@@ -116,12 +174,6 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
116
174
|
title: 'IP Address',
|
|
117
175
|
placeholder: '192.168.2.222',
|
|
118
176
|
},
|
|
119
|
-
{
|
|
120
|
-
key: 'isBatteryCam',
|
|
121
|
-
title: 'Is Battery Camera',
|
|
122
|
-
description: 'Enable for Reolink battery cameras. Uses UDP/BCUDP for discovery and streaming. Requires UID.',
|
|
123
|
-
type: 'boolean',
|
|
124
|
-
},
|
|
125
177
|
{
|
|
126
178
|
key: 'username',
|
|
127
179
|
title: 'Username',
|
|
@@ -134,7 +186,7 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
134
186
|
{
|
|
135
187
|
key: 'uid',
|
|
136
188
|
title: 'UID',
|
|
137
|
-
description: 'Reolink UID (required for battery cameras)',
|
|
189
|
+
description: 'Reolink UID (optional, required for battery cameras if TCP connection fails)',
|
|
138
190
|
}
|
|
139
191
|
]
|
|
140
192
|
}
|
|
@@ -142,8 +194,11 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
142
194
|
createCamera(nativeId: string) {
|
|
143
195
|
if (nativeId.endsWith('-battery-cam')) {
|
|
144
196
|
return new ReolinkNativeBatteryCamera(nativeId, this);
|
|
197
|
+
} else if (nativeId.endsWith('-nvr')) {
|
|
198
|
+
return new ReolinkNativeNvrDevice(nativeId, this);
|
|
199
|
+
} else {
|
|
200
|
+
return new ReolinkNativeCamera(nativeId, this);
|
|
145
201
|
}
|
|
146
|
-
return new ReolinkNativeCamera(nativeId, this);
|
|
147
202
|
}
|
|
148
203
|
}
|
|
149
204
|
|