@apocaliss92/scrypted-reolink-native 0.1.42 → 0.2.1
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/build-lib.sh +31 -0
- package/dist/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/package.json +2 -1
- package/src/baichuan-base.ts +149 -30
- package/src/camera.ts +3004 -89
- package/src/intercom.ts +5 -7
- package/src/main.ts +18 -27
- package/src/multiFocal.ts +194 -172
- package/src/nvr.ts +96 -238
- package/src/presets.ts +2 -2
- package/src/stream-utils.ts +232 -101
- package/src/utils.ts +22 -23
- package/src/camera-battery.ts +0 -336
- package/src/common.ts +0 -2551
package/src/intercom.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import type { ReolinkBaichuanApi } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
|
|
2
2
|
import sdk, { FFmpegInput, MediaObject, ScryptedMimeTypes } from "@scrypted/sdk";
|
|
3
3
|
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
4
|
-
|
|
5
|
-
import type { ReolinkNativeCamera } from "./camera";
|
|
6
|
-
import { CommonCameraMixin } from "./common";
|
|
4
|
+
import { ReolinkCamera } from "./camera";
|
|
7
5
|
|
|
8
6
|
// Keep this low: Reolink blocks are ~64ms at 16kHz (1025 samples).
|
|
9
7
|
// A small backlog avoids multi-second latency when the pipeline stalls.
|
|
@@ -23,7 +21,7 @@ export class ReolinkBaichuanIntercom {
|
|
|
23
21
|
private sendChain: Promise<void> = Promise.resolve();
|
|
24
22
|
private pcmBuffer: Buffer = Buffer.alloc(0);
|
|
25
23
|
|
|
26
|
-
constructor(private camera:
|
|
24
|
+
constructor(private camera: ReolinkCamera) {
|
|
27
25
|
}
|
|
28
26
|
|
|
29
27
|
get blocksPerPayload(): number {
|
|
@@ -63,7 +61,7 @@ export class ReolinkBaichuanIntercom {
|
|
|
63
61
|
});
|
|
64
62
|
}
|
|
65
63
|
catch (e) {
|
|
66
|
-
logger.warn("Intercom: unable to fetch TalkAbility", e);
|
|
64
|
+
logger.warn("Intercom: unable to fetch TalkAbility", e?.message || String(e));
|
|
67
65
|
}
|
|
68
66
|
}
|
|
69
67
|
|
|
@@ -81,7 +79,7 @@ export class ReolinkBaichuanIntercom {
|
|
|
81
79
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
82
80
|
}
|
|
83
81
|
} catch (e) {
|
|
84
|
-
logger.debug('Failed to check/wake camera for intercom, proceeding anyway', e);
|
|
82
|
+
logger.debug('Failed to check/wake camera for intercom, proceeding anyway', e?.message || String(e));
|
|
85
83
|
}
|
|
86
84
|
}
|
|
87
85
|
|
|
@@ -221,7 +219,7 @@ export class ReolinkBaichuanIntercom {
|
|
|
221
219
|
await Promise.race([session.stop(), sleepMs(2000)]);
|
|
222
220
|
}
|
|
223
221
|
catch (e) {
|
|
224
|
-
logger.warn("Intercom session stop error", e);
|
|
222
|
+
logger.warn("Intercom session stop error", e?.message || String(e));
|
|
225
223
|
}
|
|
226
224
|
}
|
|
227
225
|
})().finally(() => {
|
package/src/main.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import sdk, { DeviceCreator, DeviceCreatorSettings, DeviceProvider, HttpRequest, HttpResponse, MediaObject, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, ScryptedNativeId, Setting, VideoClips } from "@scrypted/sdk";
|
|
2
2
|
import { BaseBaichuanClass } from "./baichuan-base";
|
|
3
|
-
import { ReolinkNativeCamera } from "./camera";
|
|
4
|
-
import { ReolinkNativeBatteryCamera } from "./camera-battery";
|
|
5
|
-
import { CommonCameraMixin } from "./common";
|
|
6
3
|
import { ReolinkNativeMultiFocalDevice } from "./multiFocal";
|
|
7
4
|
import { ReolinkNativeNvrDevice } from "./nvr";
|
|
8
5
|
import { batteryCameraSuffix, batteryMultifocalSuffix, cameraSuffix, extractThumbnailFromVideo, getDeviceInterfaces, handleVideoClipRequest, multifocalSuffix, nvrSuffix } from "./utils";
|
|
6
|
+
import { randomBytes } from "crypto";
|
|
7
|
+
import { ReolinkCamera } from "./camera";
|
|
9
8
|
|
|
10
9
|
interface ThumbnailRequest {
|
|
11
10
|
deviceId: string;
|
|
@@ -13,7 +12,7 @@ interface ThumbnailRequest {
|
|
|
13
12
|
rtmpUrl?: string;
|
|
14
13
|
filePath?: string;
|
|
15
14
|
logger?: Console;
|
|
16
|
-
device?:
|
|
15
|
+
device?: ReolinkCamera;
|
|
17
16
|
resolve: (mo: MediaObject) => void;
|
|
18
17
|
reject: (error: Error) => void;
|
|
19
18
|
}
|
|
@@ -24,12 +23,12 @@ interface ThumbnailRequestInput {
|
|
|
24
23
|
rtmpUrl?: string;
|
|
25
24
|
filePath?: string;
|
|
26
25
|
logger?: Console;
|
|
27
|
-
device?:
|
|
26
|
+
device?: ReolinkCamera;
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider, DeviceCreator {
|
|
31
30
|
devices = new Map<string, BaseBaichuanClass>();
|
|
32
|
-
|
|
31
|
+
camerasMap = new Map<string, ReolinkCamera>();
|
|
33
32
|
nvrDeviceId: string;
|
|
34
33
|
private thumbnailQueue: ThumbnailRequest[] = [];
|
|
35
34
|
private thumbnailProcessing = false;
|
|
@@ -65,7 +64,6 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
65
64
|
throw new Error('IP address, username, and password are required');
|
|
66
65
|
}
|
|
67
66
|
|
|
68
|
-
// Auto-detect device type (camera, battery-cam, or nvr)
|
|
69
67
|
this.console.log(`[AutoDetect] Starting device type detection for ${ipAddress}...`);
|
|
70
68
|
const { autoDetectDeviceType } = await import("@apocaliss92/reolink-baichuan-js");
|
|
71
69
|
|
|
@@ -78,19 +76,20 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
78
76
|
logger: this.console,
|
|
79
77
|
},
|
|
80
78
|
);
|
|
79
|
+
const { ip, mac } = detection.hostNetworkInfo ?? {}
|
|
81
80
|
|
|
82
81
|
this.console.log(`[AutoDetect] Detected device type: ${detection.type} (transport: ${detection.transport}). Device info: ${JSON.stringify(detection.deviceInfo)}`);
|
|
83
82
|
|
|
84
83
|
// Use the API that was successfully used for detection
|
|
85
84
|
const detectedApi = detection.api;
|
|
85
|
+
const deviceInfo = detection.deviceInfo || {};
|
|
86
|
+
const name = deviceInfo?.name || `Reolink ${detection.type}`;
|
|
87
|
+
const identifier = uid || mac || ip || name || randomBytes(4).toString('hex');
|
|
86
88
|
|
|
87
89
|
// Handle multi-focal device case
|
|
88
90
|
if (detection.type === 'multifocal') {
|
|
89
|
-
const deviceInfo = detection.deviceInfo || {};
|
|
90
|
-
const name = deviceInfo.name || 'Reolink Multi-Focal';
|
|
91
|
-
const serialNumber = deviceInfo.serialNumber || deviceInfo.itemNo || `multifocal-${Date.now()}`;
|
|
92
91
|
const isBattery = detection.transport === 'udp';
|
|
93
|
-
nativeId = `${
|
|
92
|
+
nativeId = `${identifier}${isBattery ? batteryMultifocalSuffix : multifocalSuffix}`;
|
|
94
93
|
|
|
95
94
|
settings.newCamera ||= name;
|
|
96
95
|
|
|
@@ -126,10 +125,7 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
126
125
|
|
|
127
126
|
// Handle NVR case
|
|
128
127
|
if (detection.type === 'nvr') {
|
|
129
|
-
|
|
130
|
-
const name = deviceInfo?.name || 'Reolink NVR';
|
|
131
|
-
const serialNumber = deviceInfo?.serialNumber || deviceInfo?.itemNo || `nvr-${Date.now()}`;
|
|
132
|
-
nativeId = `${serialNumber}${nvrSuffix}`;
|
|
128
|
+
nativeId = `${identifier}${nvrSuffix}`;
|
|
133
129
|
|
|
134
130
|
settings.newCamera ||= name;
|
|
135
131
|
|
|
@@ -157,16 +153,11 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
157
153
|
return nativeId;
|
|
158
154
|
}
|
|
159
155
|
|
|
160
|
-
// For camera and battery-cam, create the device
|
|
161
|
-
const deviceInfo = detection.deviceInfo || {};
|
|
162
|
-
const name = deviceInfo?.name || 'Reolink Camera';
|
|
163
|
-
const serialNumber = deviceInfo?.serialNumber || deviceInfo?.itemNo || `unknown-${Date.now()}`;
|
|
164
|
-
|
|
165
156
|
// Create nativeId based on device type
|
|
166
157
|
if (detection.type === 'battery-cam') {
|
|
167
|
-
nativeId = `${
|
|
158
|
+
nativeId = `${identifier}${batteryCameraSuffix}`;
|
|
168
159
|
} else {
|
|
169
|
-
nativeId = `${
|
|
160
|
+
nativeId = `${identifier}${cameraSuffix}`;
|
|
170
161
|
}
|
|
171
162
|
|
|
172
163
|
settings.newCamera ||= name;
|
|
@@ -189,7 +180,7 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
189
180
|
providerNativeId: this.nativeId,
|
|
190
181
|
});
|
|
191
182
|
|
|
192
|
-
const device = await this.getDevice(nativeId) as
|
|
183
|
+
const device = await this.getDevice(nativeId) as ReolinkCamera;
|
|
193
184
|
|
|
194
185
|
device.info = deviceInfo;
|
|
195
186
|
device.classes = objects;
|
|
@@ -205,7 +196,7 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
205
196
|
return nativeId;
|
|
206
197
|
}
|
|
207
198
|
catch (e) {
|
|
208
|
-
this.console.error('Error adding Reolink device', e);
|
|
199
|
+
this.console.error('Error adding Reolink device', e?.message || String(e));
|
|
209
200
|
throw e;
|
|
210
201
|
}
|
|
211
202
|
}
|
|
@@ -248,7 +239,7 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
248
239
|
|
|
249
240
|
createCamera(nativeId: string) {
|
|
250
241
|
if (nativeId.endsWith(batteryCameraSuffix)) {
|
|
251
|
-
return new
|
|
242
|
+
return new ReolinkCamera(nativeId, this, { type: 'battery' });
|
|
252
243
|
} else if (nativeId.endsWith(nvrSuffix)) {
|
|
253
244
|
return new ReolinkNativeNvrDevice(nativeId, this);
|
|
254
245
|
} else if (nativeId.endsWith(batteryMultifocalSuffix)) {
|
|
@@ -256,7 +247,7 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
256
247
|
} else if (nativeId.endsWith(multifocalSuffix)) {
|
|
257
248
|
return new ReolinkNativeMultiFocalDevice(nativeId, this, "multi-focal");
|
|
258
249
|
} else {
|
|
259
|
-
return new
|
|
250
|
+
return new ReolinkCamera(nativeId, this, { type: 'regular' });
|
|
260
251
|
}
|
|
261
252
|
}
|
|
262
253
|
|
|
@@ -296,7 +287,7 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
296
287
|
// logger.log(`Webhook request: type=${type}, deviceId=${deviceId}, fileId=${fileId}`);
|
|
297
288
|
|
|
298
289
|
// Get the device
|
|
299
|
-
const device = this.
|
|
290
|
+
const device = this.camerasMap.get(deviceId);
|
|
300
291
|
if (!device) {
|
|
301
292
|
response.send('Device not found', { code: 404 });
|
|
302
293
|
return;
|
package/src/multiFocal.ts
CHANGED
|
@@ -1,36 +1,20 @@
|
|
|
1
|
-
import type { DeviceCapabilities, DualLensChannelAnalysis, ReolinkSimpleEvent } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
|
|
2
|
-
import sdk, { Device, DeviceProvider, Reboot, ScryptedDeviceType,
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { CameraType, CommonCameraMixin } from "./common";
|
|
1
|
+
import type { BatteryInfo, DeviceCapabilities, DualLensChannelAnalysis, NativeVideoStreamVariant, ReolinkBaichuanApi, ReolinkSimpleEvent, SleepStatus } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
|
|
2
|
+
import sdk, { Device, DeviceProvider, Reboot, ScryptedDeviceType, Settings } from "@scrypted/sdk";
|
|
3
|
+
import type { BaichuanConnectionConfig } from "./baichuan-base";
|
|
4
|
+
import { CameraType, ReolinkCamera } from "./camera";
|
|
6
5
|
import ReolinkNativePlugin from "./main";
|
|
7
|
-
import {
|
|
6
|
+
import { ReolinkNativeNvrDevice } from "./nvr";
|
|
7
|
+
import { batteryCameraSuffix, cameraSuffix, getDeviceInterfaces } from "./utils";
|
|
8
8
|
|
|
9
|
-
export class ReolinkNativeMultiFocalDevice extends
|
|
9
|
+
export class ReolinkNativeMultiFocalDevice extends ReolinkCamera implements Settings, DeviceProvider, Reboot {
|
|
10
10
|
plugin: ReolinkNativePlugin;
|
|
11
|
-
|
|
11
|
+
lensDevicesMap = new Map<string, ReolinkCamera>();
|
|
12
12
|
private channelToNativeIdMap = new Map<number, string>();
|
|
13
|
-
private initReinitTimeout: NodeJS.Timeout | undefined;
|
|
14
|
-
isBattery: boolean;
|
|
15
13
|
|
|
16
|
-
constructor(nativeId: string, plugin: ReolinkNativePlugin, type: CameraType) {
|
|
17
|
-
super(nativeId, plugin, { type });
|
|
18
|
-
this.plugin = plugin;
|
|
19
|
-
|
|
20
|
-
this.scheduleInit();
|
|
21
|
-
}
|
|
14
|
+
constructor(nativeId: string, plugin: ReolinkNativePlugin, type: CameraType, nvrDevice?: ReolinkNativeNvrDevice) {
|
|
15
|
+
super(nativeId, plugin, { type, nvrDevice });
|
|
22
16
|
|
|
23
|
-
|
|
24
|
-
const { capabilities } = this.storageSettings.values;
|
|
25
|
-
|
|
26
|
-
return {
|
|
27
|
-
...capabilities,
|
|
28
|
-
hasPan: false,
|
|
29
|
-
hasTilt: false,
|
|
30
|
-
hasZoom: false,
|
|
31
|
-
hasPresets: false,
|
|
32
|
-
hasIntercom: false,
|
|
33
|
-
}
|
|
17
|
+
this.plugin = plugin;
|
|
34
18
|
}
|
|
35
19
|
|
|
36
20
|
protected async onBeforeCleanup(): Promise<void> {
|
|
@@ -41,153 +25,127 @@ export class ReolinkNativeMultiFocalDevice extends CommonCameraMixin implements
|
|
|
41
25
|
return this.name || 'Multi-Focal Device';
|
|
42
26
|
}
|
|
43
27
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
clearTimeout(this.initReinitTimeout);
|
|
48
|
-
this.initReinitTimeout = undefined;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Schedule reinit with debounce
|
|
52
|
-
this.scheduleInit(true);
|
|
53
|
-
}
|
|
28
|
+
getInterfaces(lensType?: NativeVideoStreamVariant) {
|
|
29
|
+
const logger = this.getBaichuanLogger();
|
|
30
|
+
const { capabilities: caps, multifocalInfo } = this.storageSettings.values;
|
|
54
31
|
|
|
55
|
-
|
|
56
|
-
// Cancel any pending init/reinit
|
|
57
|
-
if (this.initReinitTimeout) {
|
|
58
|
-
clearTimeout(this.initReinitTimeout);
|
|
59
|
-
}
|
|
32
|
+
let capabilities: DeviceCapabilities = { ...caps };
|
|
60
33
|
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
if (isReinit) {
|
|
64
|
-
logger.log('Reinitializing multi-focal device...');
|
|
65
|
-
await this.cleanupBaichuanApi();
|
|
66
|
-
}
|
|
67
|
-
await this.init();
|
|
68
|
-
this.initReinitTimeout = undefined;
|
|
69
|
-
}, isReinit ? 500 : 2000);
|
|
70
|
-
}
|
|
34
|
+
if (lensType) {
|
|
35
|
+
const channelInfo = (multifocalInfo as DualLensChannelAnalysis).channels.find(c => c.variantType === lensType);
|
|
71
36
|
|
|
72
|
-
|
|
73
|
-
const logger = this.getBaichuanLogger();
|
|
74
|
-
try {
|
|
75
|
-
this.storageSettings.settings.uid.hide = !this.isBattery;
|
|
37
|
+
const hasPtz = channelInfo?.hasPan || channelInfo?.hasTilt || channelInfo?.hasZoom;
|
|
76
38
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
logger.error(`Error details: ${JSON.stringify(e)}`);
|
|
87
|
-
}
|
|
39
|
+
capabilities = {
|
|
40
|
+
...capabilities,
|
|
41
|
+
hasPan: channelInfo.hasPan,
|
|
42
|
+
hasTilt: channelInfo.hasTilt,
|
|
43
|
+
hasZoom: channelInfo?.hasZoom,
|
|
44
|
+
hasPresets: channelInfo?.hasPresets || hasPtz,
|
|
45
|
+
hasIntercom: channelInfo?.hasIntercom,
|
|
46
|
+
hasPtz,
|
|
47
|
+
};
|
|
88
48
|
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
getInterfaces(channel: number) {
|
|
92
|
-
const logger = this.getBaichuanLogger();
|
|
93
|
-
const { capabilities: caps, multifocalInfo } = this.storageSettings.values;
|
|
94
|
-
const channelInfo = (multifocalInfo as DualLensChannelAnalysis).channels.find(c => c.channel === channel);
|
|
95
|
-
|
|
96
|
-
const capabilities: DeviceCapabilities = {
|
|
97
|
-
...caps,
|
|
98
|
-
hasPan: channelInfo.hasPan,
|
|
99
|
-
hasTilt: channelInfo.hasTilt,
|
|
100
|
-
hasZoom: channelInfo.hasZoom,
|
|
101
|
-
hasPresets: channelInfo.hasPresets,
|
|
102
|
-
hasIntercom: channelInfo.hasIntercom,
|
|
103
|
-
};
|
|
104
49
|
|
|
105
50
|
const { interfaces } = getDeviceInterfaces({
|
|
106
51
|
capabilities,
|
|
107
52
|
logger,
|
|
108
53
|
});
|
|
109
54
|
|
|
55
|
+
// logger.debug(`Interfaces found for lens ${lensType}: ${JSON.stringify({ interfaces, capabilities, multifocalInfo })}`);
|
|
56
|
+
|
|
110
57
|
return { interfaces, capabilities };
|
|
111
58
|
}
|
|
112
59
|
|
|
113
60
|
async reportDevices(): Promise<void> {
|
|
114
|
-
|
|
115
|
-
const logger = this.getBaichuanLogger();
|
|
116
|
-
const { username, password, ipAddress, uid } = this.storageSettings.values;
|
|
117
|
-
|
|
118
|
-
const { capabilities, support, abilities, features, objects, presets } = await api.getDeviceCapabilities();
|
|
119
|
-
|
|
120
|
-
const multifocalInfo = await api.getDualLensChannelInfo();
|
|
121
|
-
logger.log(`Sync entities from remote for ${multifocalInfo.channels.length} channels`);
|
|
122
|
-
|
|
123
|
-
this.storageSettings.values.multifocalInfo = multifocalInfo;
|
|
124
|
-
this.storageSettings.values.capabilities = capabilities;
|
|
125
|
-
|
|
126
|
-
// TODO: Remove this after debugging
|
|
127
|
-
logger.log(`Multichannel info: ${JSON.stringify({ multifocalInfo, capabilities, support, abilities, features, objects, presets })}`);
|
|
128
|
-
// logger.debug(`Multichannel info: ${JSON.stringify({ multifocalInfo, capabilities, support, abilities, features, objects, presets })}`);
|
|
129
|
-
|
|
130
|
-
for (const channelInfo of multifocalInfo?.channels ?? []) {
|
|
131
|
-
const { channel, lensType } = channelInfo;
|
|
132
|
-
|
|
133
|
-
const name = `${this.name} - ${lensType}`;
|
|
134
|
-
const nativeId = `${this.nativeId}-channel${channel}${this.isBattery ? batteryCameraSuffix : cameraSuffix}`;
|
|
135
|
-
|
|
136
|
-
this.channelToNativeIdMap.set(channel, nativeId);
|
|
137
|
-
const { interfaces, capabilities: deviceCapabilities } = this.getInterfaces(channel);
|
|
138
|
-
|
|
139
|
-
const device: Device = {
|
|
140
|
-
providerNativeId: this.nativeId,
|
|
141
|
-
name,
|
|
142
|
-
nativeId,
|
|
143
|
-
info: {
|
|
144
|
-
...this.info,
|
|
145
|
-
metadata: {
|
|
146
|
-
channel,
|
|
147
|
-
lensType
|
|
148
|
-
}
|
|
149
|
-
},
|
|
150
|
-
interfaces,
|
|
151
|
-
type: ScryptedDeviceType.Camera,
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
await sdk.deviceManager.onDeviceDiscovered(device);
|
|
155
|
-
|
|
156
|
-
// TODO: Remove this after debugging
|
|
157
|
-
logger.log(`Discovering lens device ${nativeId}: ${JSON.stringify({ interfaces, deviceCapabilities })}`);
|
|
61
|
+
await super.reportDevices();
|
|
158
62
|
|
|
159
|
-
|
|
63
|
+
const logger = this.getBaichuanLogger();
|
|
160
64
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
65
|
+
try {
|
|
66
|
+
const api = await this.ensureClient();
|
|
67
|
+
const { username, password, ipAddress, uid, rtspChannel } = this.storageSettings.values;
|
|
68
|
+
|
|
69
|
+
const { capabilities, objects, presets } = await api.getDeviceCapabilities(rtspChannel, {
|
|
70
|
+
mergeDualLensOnSameChannel: true,
|
|
71
|
+
});
|
|
72
|
+
const multifocalInfo = await api.getDualLensChannelInfo(rtspChannel, {
|
|
73
|
+
onNvr: !!this.nvrDevice
|
|
74
|
+
});
|
|
75
|
+
logger.log(`Discovering ${multifocalInfo.channels.length} lenses`);
|
|
76
|
+
logger.debug({ multifocalInfo, capabilities });
|
|
77
|
+
|
|
78
|
+
this.storageSettings.values.multifocalInfo = multifocalInfo;
|
|
79
|
+
this.storageSettings.values.capabilities = capabilities;
|
|
80
|
+
|
|
81
|
+
for (const channelInfo of multifocalInfo?.channels ?? []) {
|
|
82
|
+
const { channel, lensType, variantType } = channelInfo;
|
|
83
|
+
|
|
84
|
+
const name = `${this.name} - ${lensType}`;
|
|
85
|
+
const nativeId = `${this.nativeId}-${lensType}${this.isBattery ? batteryCameraSuffix : cameraSuffix}`;
|
|
86
|
+
|
|
87
|
+
this.channelToNativeIdMap.set(channel, nativeId);
|
|
88
|
+
const { interfaces, capabilities: deviceCapabilities } = this.getInterfaces();
|
|
89
|
+
|
|
90
|
+
const device: Device = {
|
|
91
|
+
providerNativeId: this.nativeId,
|
|
92
|
+
name,
|
|
93
|
+
nativeId,
|
|
94
|
+
info: {
|
|
95
|
+
...this.info,
|
|
96
|
+
metadata: {
|
|
97
|
+
channel,
|
|
98
|
+
lensType
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
interfaces,
|
|
102
|
+
type: ScryptedDeviceType.Camera,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
await sdk.deviceManager.onDeviceDiscovered(device);
|
|
106
|
+
|
|
107
|
+
logger.log(`Discovering lens ${lensType}`);
|
|
108
|
+
logger.debug(`${JSON.stringify({ interfaces, deviceCapabilities })}`)
|
|
109
|
+
|
|
110
|
+
const camera = await this.getDevice(nativeId);
|
|
111
|
+
|
|
112
|
+
if (!camera) {
|
|
113
|
+
logger.error(`Failed to get device ${nativeId}`);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
165
116
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
117
|
+
camera.storageSettings.values.rtspChannel = channel;
|
|
118
|
+
camera.classes = objects;
|
|
119
|
+
camera.presets = presets;
|
|
120
|
+
camera.storageSettings.values.username = username;
|
|
121
|
+
camera.storageSettings.values.password = password;
|
|
122
|
+
camera.storageSettings.values.ipAddress = ipAddress;
|
|
123
|
+
camera.storageSettings.values.variantType = variantType;
|
|
124
|
+
camera.storageSettings.values.rtspChannel = channel;
|
|
125
|
+
camera.storageSettings.values.capabilities = deviceCapabilities;
|
|
174
126
|
camera.storageSettings.values.uid = uid;
|
|
175
127
|
}
|
|
128
|
+
} catch (e) {
|
|
129
|
+
logger.error('Failed to report devices', e?.message || String(e));
|
|
130
|
+
throw e;
|
|
176
131
|
}
|
|
177
|
-
|
|
178
|
-
await super.reportDevices();
|
|
179
132
|
}
|
|
180
133
|
|
|
181
134
|
async getDevice(nativeId: string) {
|
|
182
135
|
if (nativeId.endsWith(cameraSuffix) || nativeId.endsWith(batteryCameraSuffix)) {
|
|
183
|
-
let device = this.
|
|
136
|
+
let device = this.lensDevicesMap.get(nativeId);
|
|
184
137
|
if (!device) {
|
|
185
138
|
if (nativeId.endsWith(batteryCameraSuffix)) {
|
|
186
|
-
device = new
|
|
139
|
+
device = new ReolinkCamera(nativeId, this.plugin, { type: 'battery', multiFocalDevice: this });
|
|
187
140
|
} else {
|
|
188
|
-
device = new
|
|
141
|
+
device = new ReolinkCamera(nativeId, this.plugin, { type: 'regular', multiFocalDevice: this });
|
|
189
142
|
}
|
|
190
143
|
}
|
|
144
|
+
|
|
145
|
+
if (device) {
|
|
146
|
+
this.lensDevicesMap.set(nativeId, device);
|
|
147
|
+
}
|
|
148
|
+
|
|
191
149
|
return device;
|
|
192
150
|
} else {
|
|
193
151
|
return super.getDevice(nativeId);
|
|
@@ -195,34 +153,10 @@ export class ReolinkNativeMultiFocalDevice extends CommonCameraMixin implements
|
|
|
195
153
|
}
|
|
196
154
|
|
|
197
155
|
async releaseDevice(id: string, nativeId: string) {
|
|
198
|
-
this.
|
|
156
|
+
this.lensDevicesMap.delete(nativeId);
|
|
199
157
|
super.releaseDevice(id, nativeId);
|
|
200
158
|
}
|
|
201
159
|
|
|
202
|
-
forwardNativeEvent(ev: ReolinkSimpleEvent): void {
|
|
203
|
-
const logger = this.getBaichuanLogger();
|
|
204
|
-
const channel = ev?.channel;
|
|
205
|
-
|
|
206
|
-
if (channel === undefined) {
|
|
207
|
-
logger.debug('Event missing channel, ignoring');
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const nativeId = this.channelToNativeIdMap.get(channel);
|
|
212
|
-
if (!nativeId) {
|
|
213
|
-
logger.debug(`No camera found for channel ${channel}, ignoring event`);
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const camera = this.cameraNativeMap.get(nativeId);
|
|
218
|
-
if (!camera) {
|
|
219
|
-
logger.debug(`Camera ${nativeId} not yet initialized, ignoring event`);
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
camera.onSimpleEvent(ev);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
160
|
async unsubscribeFromAllEvents(): Promise<void> {
|
|
227
161
|
await super.unsubscribeFromEvents();
|
|
228
162
|
}
|
|
@@ -237,16 +171,104 @@ export class ReolinkNativeMultiFocalDevice extends CommonCameraMixin implements
|
|
|
237
171
|
throw new Error('Missing device credentials');
|
|
238
172
|
}
|
|
239
173
|
|
|
240
|
-
const api = await this.
|
|
174
|
+
const api = await this.ensureClient();
|
|
241
175
|
|
|
242
176
|
const multifocalDiagnostics = await api.collectMultifocalDiagnostics(logger);
|
|
243
177
|
|
|
244
178
|
logger.log(`NVR diagnostics completed successfully.`);
|
|
245
|
-
logger.
|
|
179
|
+
logger.debug(JSON.stringify(multifocalDiagnostics));
|
|
246
180
|
} catch (e) {
|
|
247
|
-
logger.error('Failed to run NVR diagnostics', e);
|
|
181
|
+
logger.error('Failed to run NVR diagnostics', e?.message || String(e));
|
|
248
182
|
throw e;
|
|
249
183
|
}
|
|
250
184
|
}
|
|
185
|
+
|
|
186
|
+
async ensureClient(): Promise<ReolinkBaichuanApi> {
|
|
187
|
+
if (this.nvrDevice) {
|
|
188
|
+
return await this.nvrDevice.ensureBaichuanClient();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Use base class implementation
|
|
192
|
+
return await this.ensureBaichuanClient();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
protected getStreamClientInputs(): BaichuanConnectionConfig {
|
|
196
|
+
const { ipAddress, username, password } = this.storageSettings.values;
|
|
197
|
+
const debugOptions = this.getBaichuanDebugOptions();
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
host: ipAddress,
|
|
201
|
+
username,
|
|
202
|
+
password,
|
|
203
|
+
transport: this.transport,
|
|
204
|
+
debugOptions,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Create a dedicated Baichuan API session for streaming (used by StreamManager).
|
|
210
|
+
* MultiFocal creates its own socket for stream clients, or delegates to NVR if on NVR.
|
|
211
|
+
*/
|
|
212
|
+
async createStreamClient(streamKey: string): Promise<ReolinkBaichuanApi> {
|
|
213
|
+
// If on NVR, delegate to NVR to create the socket
|
|
214
|
+
if (this.nvrDevice) {
|
|
215
|
+
return await this.nvrDevice.createStreamClient(streamKey);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Otherwise, use base class createStreamClient which manages stream clients per streamKey
|
|
219
|
+
return await super.createStreamClient(streamKey);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
getLensDevices() {
|
|
223
|
+
const devices = Array.from(this.lensDevicesMap.values());
|
|
224
|
+
// const logger = this.getBaichuanLogger();
|
|
225
|
+
// logger.debug(`Found ${devices.length} lens devices: ${devices.map(d => d.nativeId).join(', ')}`);
|
|
226
|
+
|
|
227
|
+
return devices;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async updateBatteryInfo() {
|
|
231
|
+
const batteryInfo = await super.updateBatteryInfo();
|
|
232
|
+
const lensDevices = this.getLensDevices();
|
|
233
|
+
|
|
234
|
+
for (const camera of lensDevices) {
|
|
235
|
+
await camera.updateBatteryInfo(batteryInfo);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return batteryInfo;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
onSimpleEvent(ev: ReolinkSimpleEvent) {
|
|
242
|
+
super.onSimpleEvent(ev);
|
|
243
|
+
const logger = this.getBaichuanLogger();
|
|
244
|
+
const lensDevices = this.getLensDevices();
|
|
245
|
+
|
|
246
|
+
for (const camera of lensDevices) {
|
|
247
|
+
logger.debug(`Forward ${ev.type} event to lens device ${camera.nativeId}`);
|
|
248
|
+
camera.onSimpleEvent(ev);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async updateSleepingState(sleepStatus: SleepStatus) {
|
|
253
|
+
const logger = this.getBaichuanLogger();
|
|
254
|
+
await super.updateSleepingState(sleepStatus);
|
|
255
|
+
const lensDevices = this.getLensDevices();
|
|
256
|
+
|
|
257
|
+
for (const camera of lensDevices) {
|
|
258
|
+
logger.debug(`Forward ${JSON.stringify(sleepStatus)} sleeping state to lens device ${camera.nativeId}`);
|
|
259
|
+
await camera.updateSleepingState(sleepStatus);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async updateOnlineState(isOnline: boolean) {
|
|
264
|
+
const logger = this.getBaichuanLogger();
|
|
265
|
+
await super.updateOnlineState(isOnline);
|
|
266
|
+
const lensDevices = this.getLensDevices();
|
|
267
|
+
|
|
268
|
+
for (const camera of lensDevices) {
|
|
269
|
+
logger.debug(`Forward ${isOnline ? 'online' : 'offline'} state to lens device ${camera.nativeId}`);
|
|
270
|
+
await camera.updateOnlineState(isOnline);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
251
273
|
}
|
|
252
274
|
|