@apocaliss92/scrypted-reolink-native 0.1.19 → 0.1.21
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/dist/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/package.json +3 -2
- package/src/camera.ts +0 -6
- package/src/common.ts +45 -91
- package/src/connect.ts +1 -199
- package/src/main.ts +9 -16
- package/src/multifocal.ts +66 -71
- package/src/nvr.ts +39 -12
- package/src/stream-utils.ts +0 -141
- package/src/utils.ts +19 -0
package/dist/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apocaliss92/scrypted-reolink-native",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"description": "Use any reolink camera with Scrypted, even older/unsupported models without HTTP protocol support",
|
|
5
5
|
"author": "@apocaliss92",
|
|
6
6
|
"license": "Apache",
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"scrypted",
|
|
22
22
|
"plugin",
|
|
23
23
|
"reolink",
|
|
24
|
-
"
|
|
24
|
+
"native",
|
|
25
|
+
"native",
|
|
25
26
|
"camera"
|
|
26
27
|
],
|
|
27
28
|
"scrypted": {
|
package/src/camera.ts
CHANGED
|
@@ -70,17 +70,11 @@ export class ReolinkNativeCamera extends CommonCameraMixin {
|
|
|
70
70
|
|
|
71
71
|
// Reset client and clear cache on recoverable error
|
|
72
72
|
await this.resetBaichuanClient(e);
|
|
73
|
-
this.cachedNetPort = undefined;
|
|
74
73
|
|
|
75
74
|
// Important: callers must re-acquire the client inside fn.
|
|
76
75
|
try {
|
|
77
76
|
return await fn();
|
|
78
77
|
} catch (retryError) {
|
|
79
|
-
// If retry also fails with recoverable error, don't spam logs
|
|
80
|
-
if (this.isRecoverableBaichuanError(retryError)) {
|
|
81
|
-
// Silently fail to avoid spam, but still throw to caller
|
|
82
|
-
throw retryError;
|
|
83
|
-
}
|
|
84
78
|
throw retryError;
|
|
85
79
|
}
|
|
86
80
|
}
|
package/src/common.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DeviceCapabilities, PtzCommand, PtzPreset, ReolinkBaichuanApi, ReolinkSimpleEvent, StreamSamplingSelection } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
|
|
1
|
+
import type { DeviceCapabilities, PtzCommand, PtzPreset, ReolinkBaichuanApi, ReolinkSimpleEvent, ReolinkSupportedStream, StreamSamplingSelection } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
|
|
2
2
|
import sdk, { BinarySensor, Brightness, Camera, Device, DeviceProvider, Intercom, MediaObject, MediaStreamUrl, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, OnOff, PanTiltZoom, PanTiltZoomCommand, RequestMediaStreamOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera, VideoTextOverlay, VideoTextOverlays } from "@scrypted/sdk";
|
|
3
3
|
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
|
4
4
|
import type { UrlMediaStreamOptions } from "../../scrypted/plugins/rtsp/src/rtsp";
|
|
@@ -11,10 +11,8 @@ import { ReolinkNativeNvrDevice } from "./nvr";
|
|
|
11
11
|
import { ReolinkNativeMultiFocalDevice } from "./multifocal";
|
|
12
12
|
import { ReolinkPtzPresets } from "./presets";
|
|
13
13
|
import {
|
|
14
|
-
buildVideoStreamOptions,
|
|
15
14
|
createRfc4571MediaObjectFromStreamManager,
|
|
16
15
|
expectedVideoTypeFromUrlMediaStreamOptions,
|
|
17
|
-
isNativeStreamId,
|
|
18
16
|
parseStreamProfileFromId,
|
|
19
17
|
selectStreamOption,
|
|
20
18
|
StreamManager
|
|
@@ -222,6 +220,10 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
222
220
|
json: true,
|
|
223
221
|
hide: true,
|
|
224
222
|
},
|
|
223
|
+
operationChannels: {
|
|
224
|
+
json: true,
|
|
225
|
+
hide: true,
|
|
226
|
+
},
|
|
225
227
|
// Battery camera specific
|
|
226
228
|
uid: {
|
|
227
229
|
title: 'UID',
|
|
@@ -503,7 +505,6 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
503
505
|
// Video stream properties
|
|
504
506
|
protected cachedVideoStreamOptions?: UrlMediaStreamOptions[];
|
|
505
507
|
protected fetchingStreams = false;
|
|
506
|
-
protected cachedNetPort?: { rtsp?: { port?: number; enable?: number }; rtmp?: { port?: number; enable?: number } };
|
|
507
508
|
protected lastNetPortCacheAttempt: number = 0;
|
|
508
509
|
protected netPortCacheBackoffMs: number = 5000; // 5 seconds backoff on failure
|
|
509
510
|
|
|
@@ -752,7 +753,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
752
753
|
const selection = Array.from(this.getDispatchEventsSelection?.() ?? new Set()).sort();
|
|
753
754
|
const enabled = selection.length > 0;
|
|
754
755
|
|
|
755
|
-
logger.
|
|
756
|
+
logger.debug(`subscribeToEvents called: enabled=${enabled}, selection=[${selection.join(', ')}], protocol=${this.protocol}`);
|
|
756
757
|
|
|
757
758
|
this.unsubscribedToEvents();
|
|
758
759
|
|
|
@@ -1288,54 +1289,6 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1288
1289
|
}
|
|
1289
1290
|
}
|
|
1290
1291
|
|
|
1291
|
-
protected getRtspAddress(): string {
|
|
1292
|
-
const { ipAddress } = this.storageSettings.values;
|
|
1293
|
-
const rtspPort = this.cachedNetPort?.rtsp?.port ?? 554;
|
|
1294
|
-
return `${ipAddress}:${rtspPort}`;
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
protected getRtmpAddress(): string {
|
|
1298
|
-
const { ipAddress } = this.storageSettings.values;
|
|
1299
|
-
const rtmpPort = this.cachedNetPort?.rtmp?.port ?? 1935;
|
|
1300
|
-
return `${ipAddress}:${rtmpPort}`;
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
protected async ensureNetPortCache(): Promise<void> {
|
|
1304
|
-
const logger = this.getBaichuanLogger();
|
|
1305
|
-
|
|
1306
|
-
if (this.cachedNetPort) {
|
|
1307
|
-
return;
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
// Implement backoff to avoid spam when socket is closed
|
|
1311
|
-
const now = Date.now();
|
|
1312
|
-
if (now - this.lastNetPortCacheAttempt < this.netPortCacheBackoffMs) {
|
|
1313
|
-
// Use defaults if we're in backoff period
|
|
1314
|
-
this.cachedNetPort = {
|
|
1315
|
-
rtsp: { port: 554, enable: 1 },
|
|
1316
|
-
rtmp: { port: 1935, enable: 1 },
|
|
1317
|
-
};
|
|
1318
|
-
return;
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
this.lastNetPortCacheAttempt = now;
|
|
1322
|
-
|
|
1323
|
-
try {
|
|
1324
|
-
const client = await this.ensureClient();
|
|
1325
|
-
this.cachedNetPort = await client.getNetPort();
|
|
1326
|
-
} catch (e) {
|
|
1327
|
-
// Only log if it's not a recoverable error to avoid spam
|
|
1328
|
-
if (!this.isRecoverableBaichuanError?.(e)) {
|
|
1329
|
-
logger.warn('Failed to get net port, using defaults', e);
|
|
1330
|
-
}
|
|
1331
|
-
// Use defaults if we can't get the ports
|
|
1332
|
-
this.cachedNetPort = {
|
|
1333
|
-
rtsp: { port: 554, enable: 1 },
|
|
1334
|
-
rtmp: { port: 1935, enable: 1 },
|
|
1335
|
-
};
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
1292
|
async getVideoStreamOptions(): Promise<UrlMediaStreamOptions[]> {
|
|
1340
1293
|
const logger = this.getBaichuanLogger();
|
|
1341
1294
|
|
|
@@ -1353,36 +1306,44 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1353
1306
|
|
|
1354
1307
|
const client = await this.ensureClient();
|
|
1355
1308
|
|
|
1356
|
-
const {
|
|
1309
|
+
const { rtspChannel } = this.storageSettings.values;
|
|
1357
1310
|
|
|
1358
1311
|
try {
|
|
1359
|
-
await
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1312
|
+
const { nativeStreams, rtmpStreams, rtspStreams } = await client.buildVideoStreamOptions(rtspChannel);
|
|
1313
|
+
|
|
1314
|
+
let supportedStreams: ReolinkSupportedStream[] = [];
|
|
1315
|
+
if (this.nvrDevice && this.nvrDevice.info.model === 'HOMEHUB') {
|
|
1316
|
+
supportedStreams = [...nativeStreams, ...rtspStreams, ...rtmpStreams];
|
|
1317
|
+
} else {
|
|
1318
|
+
supportedStreams = [...rtspStreams, ...rtmpStreams, ...nativeStreams];
|
|
1363
1319
|
}
|
|
1364
|
-
}
|
|
1365
1320
|
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1321
|
+
for (const supportedStream of supportedStreams) {
|
|
1322
|
+
const { id, metadata, url, name, container } = supportedStream;
|
|
1323
|
+
|
|
1324
|
+
const codec = String(metadata.videoEncType || "").includes("264")
|
|
1325
|
+
? "h264"
|
|
1326
|
+
: String(metadata.videoEncType || "").includes("265")
|
|
1327
|
+
? "h265"
|
|
1328
|
+
: String(metadata.videoEncType || "").toLowerCase();
|
|
1329
|
+
|
|
1330
|
+
streams.push({
|
|
1331
|
+
id,
|
|
1332
|
+
name,
|
|
1333
|
+
url,
|
|
1334
|
+
container,
|
|
1335
|
+
video: { codec, width: metadata.width, height: metadata.height },
|
|
1336
|
+
// audio: { codec: metadata.audioCodec }
|
|
1337
|
+
})
|
|
1338
|
+
}
|
|
1377
1339
|
} catch (e) {
|
|
1378
1340
|
if (!this.isRecoverableBaichuanError?.(e)) {
|
|
1379
1341
|
logger.warn('Failed to build RTSP/RTMP stream options, falling back to Native', e);
|
|
1380
1342
|
}
|
|
1381
|
-
this.cachedNetPort = undefined;
|
|
1382
1343
|
}
|
|
1383
1344
|
|
|
1384
1345
|
if (streams.length) {
|
|
1385
|
-
logger.log('Fetched video stream options', { streams
|
|
1346
|
+
logger.log('Fetched video stream options', { streams });
|
|
1386
1347
|
this.cachedVideoStreamOptions = streams;
|
|
1387
1348
|
return streams;
|
|
1388
1349
|
}
|
|
@@ -1397,13 +1358,13 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1397
1358
|
const selected = selectStreamOption(vsos, vso);
|
|
1398
1359
|
|
|
1399
1360
|
// Check if this is a native stream (prefixed with "native_")
|
|
1400
|
-
const isNative = isNativeStreamId(selected.id);
|
|
1401
1361
|
|
|
1402
1362
|
// If stream has RTSP/RTMP URL (not native), add credentials and create MediaStreamUrl
|
|
1403
|
-
if (
|
|
1363
|
+
if (selected.url && (selected.container === 'rtsp' || selected.container === 'rtmp')) {
|
|
1404
1364
|
const urlWithCredentials = this.addRtspCredentials(selected.url);
|
|
1405
1365
|
const ret: MediaStreamUrl = {
|
|
1406
1366
|
container: selected.container,
|
|
1367
|
+
// url: selected.url,
|
|
1407
1368
|
url: urlWithCredentials,
|
|
1408
1369
|
mediaStreamOptions: selected,
|
|
1409
1370
|
};
|
|
@@ -1429,22 +1390,16 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1429
1390
|
expectedVideoType,
|
|
1430
1391
|
selected,
|
|
1431
1392
|
sourceId: this.id,
|
|
1432
|
-
onDetectedCodec: (detectedCodec) => {
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
container: 'rtp',
|
|
1443
|
-
video: { codec: detectedCodec },
|
|
1444
|
-
url: ``
|
|
1445
|
-
});
|
|
1446
|
-
this.cachedVideoStreamOptions = next;
|
|
1447
|
-
},
|
|
1393
|
+
// onDetectedCodec: (detectedCodec) => {
|
|
1394
|
+
// const prev = this.cachedVideoStreamOptions ?? [];
|
|
1395
|
+
// const next = prev.filter((s) => s.id !== nativeId);
|
|
1396
|
+
// next.push({
|
|
1397
|
+
// container: 'rtp',
|
|
1398
|
+
// video: { codec: detectedCodec },
|
|
1399
|
+
// url: ``
|
|
1400
|
+
// });
|
|
1401
|
+
// this.cachedVideoStreamOptions = next;
|
|
1402
|
+
// },
|
|
1448
1403
|
});
|
|
1449
1404
|
};
|
|
1450
1405
|
|
|
@@ -1467,7 +1422,6 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1467
1422
|
|
|
1468
1423
|
async credentialsChanged(): Promise<void> {
|
|
1469
1424
|
this.cachedVideoStreamOptions = undefined;
|
|
1470
|
-
this.cachedNetPort = undefined;
|
|
1471
1425
|
}
|
|
1472
1426
|
|
|
1473
1427
|
// PTZ Presets methods
|
package/src/connect.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BaichuanClientOptions, ReolinkBaichuanApi
|
|
1
|
+
import type { BaichuanClientOptions, ReolinkBaichuanApi } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
|
|
2
2
|
|
|
3
3
|
export type BaichuanTransport = "tcp" | "udp";
|
|
4
4
|
|
|
@@ -16,29 +16,6 @@ export function normalizeUid(uid?: string): string | undefined {
|
|
|
16
16
|
return v ? v : undefined;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export function maskUid(uid: string): string {
|
|
20
|
-
const v = uid.trim();
|
|
21
|
-
if (v.length <= 8) return v;
|
|
22
|
-
return `${v.slice(0, 4)}…${v.slice(-4)}`;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function isTcpFailureThatShouldFallbackToUdp(e: unknown): boolean {
|
|
26
|
-
const message = (e as any)?.message || (e as any)?.toString?.() || "";
|
|
27
|
-
if (typeof message !== "string") return false;
|
|
28
|
-
|
|
29
|
-
// Fallback only on transport/connection style failures.
|
|
30
|
-
// Wrong credentials won't be fixed by switching to UDP.
|
|
31
|
-
return (
|
|
32
|
-
message.includes("ECONNREFUSED") ||
|
|
33
|
-
message.includes("ETIMEDOUT") ||
|
|
34
|
-
message.includes("EHOSTUNREACH") ||
|
|
35
|
-
message.includes("ENETUNREACH") ||
|
|
36
|
-
message.includes("socket hang up") ||
|
|
37
|
-
message.includes("TCP connection timeout") ||
|
|
38
|
-
message.includes("Baichuan socket closed")
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
19
|
export async function createBaichuanApi(props: {
|
|
43
20
|
inputs: BaichuanConnectInputs,
|
|
44
21
|
transport: BaichuanTransport,
|
|
@@ -108,178 +85,3 @@ export async function createBaichuanApi(props: {
|
|
|
108
85
|
attachErrorHandler(api);
|
|
109
86
|
return api;
|
|
110
87
|
}
|
|
111
|
-
|
|
112
|
-
export type UdpFallbackInfo = {
|
|
113
|
-
host: string;
|
|
114
|
-
uid?: string;
|
|
115
|
-
uidMissing: boolean;
|
|
116
|
-
tcpError: unknown;
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
export type DeviceType = 'camera' | 'battery-cam' | 'nvr' | 'multifocal';
|
|
120
|
-
|
|
121
|
-
export type AutoDetectResult = {
|
|
122
|
-
type: DeviceType;
|
|
123
|
-
transport: BaichuanTransport;
|
|
124
|
-
uid: string;
|
|
125
|
-
deviceInfo?: ReolinkDeviceInfo;
|
|
126
|
-
channelNum?: number;
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Simple ping check to verify IP is reachable
|
|
131
|
-
*/
|
|
132
|
-
async function pingHost(host: string, timeoutMs: number = 3000): Promise<boolean> {
|
|
133
|
-
return new Promise((resolve) => {
|
|
134
|
-
const { exec } = require('child_process');
|
|
135
|
-
const platform = process.platform;
|
|
136
|
-
const pingCmd = platform === 'win32' ? `ping -n 1 -w ${timeoutMs} ${host}` : `ping -c 1 -W ${Math.floor(timeoutMs / 1000)} ${host}`;
|
|
137
|
-
|
|
138
|
-
exec(pingCmd, (error: any) => {
|
|
139
|
-
resolve(!error);
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Auto-detect device type by trying TCP first, then UDP if needed.
|
|
146
|
-
* - First: Ping the IP to verify it's reachable
|
|
147
|
-
* - TCP success: Check if NVR (multiple channels) or regular camera
|
|
148
|
-
* - TCP failure: Try UDP (always battery camera)
|
|
149
|
-
*/
|
|
150
|
-
export async function autoDetectDeviceType(
|
|
151
|
-
inputs: BaichuanConnectInputs,
|
|
152
|
-
logger: Console,
|
|
153
|
-
): Promise<AutoDetectResult> {
|
|
154
|
-
const { host, username, password, uid } = inputs;
|
|
155
|
-
|
|
156
|
-
// Ping the host first to verify it's reachable
|
|
157
|
-
logger.log(`[AutoDetect] Pinging ${host}...`);
|
|
158
|
-
const isReachable = await pingHost(host);
|
|
159
|
-
if (!isReachable) {
|
|
160
|
-
logger.warn(`[AutoDetect] Host ${host} is not reachable via ping, but continuing with connection attempt...`);
|
|
161
|
-
} else {
|
|
162
|
-
logger.log(`[AutoDetect] Host ${host} is reachable`);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Try TCP first
|
|
166
|
-
let tcpApi: ReolinkBaichuanApi | undefined;
|
|
167
|
-
try {
|
|
168
|
-
logger.log(`[AutoDetect] Trying TCP connection to ${host}...`);
|
|
169
|
-
tcpApi = await createBaichuanApi({
|
|
170
|
-
inputs: { host, username, password, logger },
|
|
171
|
-
transport: 'tcp',
|
|
172
|
-
logger,
|
|
173
|
-
});
|
|
174
|
-
await tcpApi.login();
|
|
175
|
-
|
|
176
|
-
// Get device info to check device type
|
|
177
|
-
const deviceInfo = await tcpApi.getInfo();
|
|
178
|
-
const { support } = await tcpApi.getDeviceCapabilities();
|
|
179
|
-
const channelNum = support?.channelNum ?? 1;
|
|
180
|
-
|
|
181
|
-
logger.log(`[AutoDetect] TCP connection successful. channelNum=${channelNum}`);
|
|
182
|
-
|
|
183
|
-
// Multi-focal devices have 2 or 3 channels
|
|
184
|
-
if (channelNum === 2 || channelNum === 3) {
|
|
185
|
-
logger.log(`[AutoDetect] Detected multi-focal device (${channelNum} channels, channelNum=${channelNum})`);
|
|
186
|
-
await tcpApi.close();
|
|
187
|
-
return {
|
|
188
|
-
type: 'multifocal',
|
|
189
|
-
transport: 'tcp',
|
|
190
|
-
uid: uid || '',
|
|
191
|
-
deviceInfo,
|
|
192
|
-
channelNum,
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// If channelNum > 1, it's likely an NVR
|
|
197
|
-
if (channelNum > 1) {
|
|
198
|
-
logger.log(`[AutoDetect] Detected NVR (${channelNum} channels)`);
|
|
199
|
-
await tcpApi.close();
|
|
200
|
-
return {
|
|
201
|
-
type: 'nvr',
|
|
202
|
-
transport: 'tcp',
|
|
203
|
-
uid: uid || '',
|
|
204
|
-
deviceInfo,
|
|
205
|
-
channelNum,
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Single channel device - regular camera
|
|
210
|
-
logger.log(`[AutoDetect] Detected regular camera (single channel)`);
|
|
211
|
-
await tcpApi.close();
|
|
212
|
-
return {
|
|
213
|
-
type: 'camera',
|
|
214
|
-
transport: 'tcp',
|
|
215
|
-
uid: uid || '',
|
|
216
|
-
deviceInfo,
|
|
217
|
-
channelNum: 1,
|
|
218
|
-
};
|
|
219
|
-
} catch (tcpError) {
|
|
220
|
-
// TCP failed, try UDP (battery camera)
|
|
221
|
-
if (tcpApi) {
|
|
222
|
-
try {
|
|
223
|
-
await tcpApi.close();
|
|
224
|
-
} catch {
|
|
225
|
-
// ignore
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (!isTcpFailureThatShouldFallbackToUdp(tcpError)) {
|
|
230
|
-
// Not a transport error, rethrow
|
|
231
|
-
throw tcpError;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
logger.log(`[AutoDetect] TCP failed, trying UDP (battery camera)...`);
|
|
235
|
-
const normalizedUid = normalizeUid(uid);
|
|
236
|
-
if (!normalizedUid) {
|
|
237
|
-
throw new Error(
|
|
238
|
-
`TCP connection failed and device likely requires UDP/BCUDP. UID is required for battery cameras (ip=${host}).`
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
try {
|
|
243
|
-
const udpApi = await createBaichuanApi({
|
|
244
|
-
inputs: { host, username, password, uid: normalizedUid, logger },
|
|
245
|
-
transport: 'udp',
|
|
246
|
-
logger,
|
|
247
|
-
});
|
|
248
|
-
await udpApi.login();
|
|
249
|
-
|
|
250
|
-
const deviceInfo = await udpApi.getInfo();
|
|
251
|
-
const { support } = await udpApi.getDeviceCapabilities();
|
|
252
|
-
const channelNum = support?.channelNum ?? 1;
|
|
253
|
-
|
|
254
|
-
// Multi-focal devices can also be UDP (battery multi-focal cameras)
|
|
255
|
-
if (channelNum === 2 || channelNum === 3) {
|
|
256
|
-
logger.log(`[AutoDetect] UDP connection successful. Detected multi-focal device (${channelNum} channels).`);
|
|
257
|
-
await udpApi.close();
|
|
258
|
-
return {
|
|
259
|
-
type: 'multifocal',
|
|
260
|
-
transport: 'udp',
|
|
261
|
-
uid: normalizedUid,
|
|
262
|
-
deviceInfo,
|
|
263
|
-
channelNum,
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Regular battery camera
|
|
268
|
-
logger.log(`[AutoDetect] UDP connection successful. Detected battery camera.`);
|
|
269
|
-
await udpApi.close();
|
|
270
|
-
|
|
271
|
-
return {
|
|
272
|
-
type: 'battery-cam',
|
|
273
|
-
transport: 'udp',
|
|
274
|
-
uid: normalizedUid,
|
|
275
|
-
deviceInfo,
|
|
276
|
-
channelNum: 1,
|
|
277
|
-
};
|
|
278
|
-
} catch (udpError) {
|
|
279
|
-
logger.error(`[AutoDetect] Both TCP and UDP failed. TCP error: ${tcpError}, UDP error: ${udpError}`);
|
|
280
|
-
throw new Error(
|
|
281
|
-
`Failed to connect via both TCP and UDP. TCP: ${(tcpError as any)?.message || tcpError}, UDP: ${(udpError as any)?.message || udpError}`
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
package/src/main.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import sdk, { DeviceCreator, DeviceCreatorSettings,
|
|
1
|
+
import sdk, { DeviceCreator, DeviceCreatorSettings, DeviceProvider, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedNativeId, Setting } from "@scrypted/sdk";
|
|
2
|
+
import { BaseBaichuanClass } from "./baichuan-base";
|
|
2
3
|
import { ReolinkNativeCamera } from "./camera";
|
|
3
4
|
import { ReolinkNativeBatteryCamera } from "./camera-battery";
|
|
4
|
-
import {
|
|
5
|
+
import { CommonCameraMixin } from "./common";
|
|
6
|
+
import { createBaichuanApi } from "./connect";
|
|
5
7
|
import { ReolinkNativeMultiFocalDevice } from "./multifocal";
|
|
6
|
-
import {
|
|
8
|
+
import { ReolinkNativeNvrDevice } from "./nvr";
|
|
7
9
|
import { batteryCameraSuffix, cameraSuffix, getDeviceInterfaces, multifocalSuffix, nvrSuffix } from "./utils";
|
|
8
|
-
import { BaseBaichuanClass } from "./baichuan-base";
|
|
9
|
-
import { CommonCameraMixin } from "./common";
|
|
10
10
|
|
|
11
11
|
class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider, DeviceCreator {
|
|
12
12
|
devices = new Map<string, BaseBaichuanClass>();
|
|
@@ -45,6 +45,8 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
45
45
|
|
|
46
46
|
// Auto-detect device type (camera, battery-cam, or nvr)
|
|
47
47
|
this.console.log(`[AutoDetect] Starting device type detection for ${ipAddress}...`);
|
|
48
|
+
const { autoDetectDeviceType } = await import("@apocaliss92/reolink-baichuan-js");
|
|
49
|
+
|
|
48
50
|
const detection = await autoDetectDeviceType(
|
|
49
51
|
{
|
|
50
52
|
host: ipAddress,
|
|
@@ -53,7 +55,6 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
53
55
|
uid,
|
|
54
56
|
logger: this.console,
|
|
55
57
|
},
|
|
56
|
-
this.console
|
|
57
58
|
);
|
|
58
59
|
|
|
59
60
|
this.console.log(`[AutoDetect] Detected device type: ${detection.type} (transport: ${detection.transport})`);
|
|
@@ -88,11 +89,6 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
88
89
|
device.storageSettings.values.username = username;
|
|
89
90
|
device.storageSettings.values.password = password;
|
|
90
91
|
device.storageSettings.values.uid = detection.uid || '';
|
|
91
|
-
|
|
92
|
-
// Update the protocol based on detection result
|
|
93
|
-
// Note: This requires updating the protocol property, but it's readonly
|
|
94
|
-
// The transport is already set in the constructor during createDevice
|
|
95
|
-
// For now, we'll rely on the constructor parameter
|
|
96
92
|
|
|
97
93
|
return nativeId;
|
|
98
94
|
}
|
|
@@ -162,7 +158,7 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
162
158
|
const rtspChannel = 0;
|
|
163
159
|
const { abilities, capabilities, objects, presets } = await api.getDeviceCapabilities(rtspChannel);
|
|
164
160
|
|
|
165
|
-
this.console.log(JSON.stringify({ abilities, capabilities, deviceInfo }));
|
|
161
|
+
this.console.log(nativeId, JSON.stringify({ abilities, capabilities, deviceInfo }));
|
|
166
162
|
|
|
167
163
|
const { interfaces, type } = getDeviceInterfaces({
|
|
168
164
|
capabilities,
|
|
@@ -178,6 +174,7 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
178
174
|
});
|
|
179
175
|
|
|
180
176
|
const device = await this.getDevice(nativeId) as CommonCameraMixin;
|
|
177
|
+
this.console.log(name, interfaces, type, device);
|
|
181
178
|
|
|
182
179
|
device.info = deviceInfo;
|
|
183
180
|
device.classes = objects;
|
|
@@ -240,10 +237,6 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
240
237
|
} else if (nativeId.endsWith(nvrSuffix)) {
|
|
241
238
|
return new ReolinkNativeNvrDevice(nativeId, this);
|
|
242
239
|
} else if (nativeId.endsWith(multifocalSuffix)) {
|
|
243
|
-
// Get transport from device settings if available, otherwise default to TCP
|
|
244
|
-
// The transport is determined during autoDetect and should be stored
|
|
245
|
-
// For now, we'll try to infer from UID presence (if UID is set, likely UDP)
|
|
246
|
-
// Default to TCP for now - the transport will be set correctly during createDevice
|
|
247
240
|
return new ReolinkNativeMultiFocalDevice(nativeId, this, 'tcp');
|
|
248
241
|
} else {
|
|
249
242
|
return new ReolinkNativeCamera(nativeId, this);
|