@apocaliss92/scrypted-reolink-native 0.1.42 → 0.2.0
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-battery.ts +5 -58
- package/src/camera.ts +5 -2
- package/src/common.ts +471 -240
- package/src/intercom.ts +3 -3
- package/src/main.ts +38 -21
- package/src/multiFocal.ts +238 -144
- package/src/nvr.ts +194 -160
- package/src/stream-utils.ts +232 -101
- package/src/utils.ts +19 -19
package/src/intercom.ts
CHANGED
|
@@ -63,7 +63,7 @@ export class ReolinkBaichuanIntercom {
|
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
65
|
catch (e) {
|
|
66
|
-
logger.warn("Intercom: unable to fetch TalkAbility", e);
|
|
66
|
+
logger.warn("Intercom: unable to fetch TalkAbility", e?.message || String(e));
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -81,7 +81,7 @@ export class ReolinkBaichuanIntercom {
|
|
|
81
81
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
82
82
|
}
|
|
83
83
|
} catch (e) {
|
|
84
|
-
logger.debug('Failed to check/wake camera for intercom, proceeding anyway', e);
|
|
84
|
+
logger.debug('Failed to check/wake camera for intercom, proceeding anyway', e?.message || String(e));
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
@@ -221,7 +221,7 @@ export class ReolinkBaichuanIntercom {
|
|
|
221
221
|
await Promise.race([session.stop(), sleepMs(2000)]);
|
|
222
222
|
}
|
|
223
223
|
catch (e) {
|
|
224
|
-
logger.warn("Intercom session stop error", e);
|
|
224
|
+
logger.warn("Intercom session stop error", e?.message || String(e));
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
227
|
})().finally(() => {
|
package/src/main.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { CommonCameraMixin } from "./common";
|
|
|
6
6
|
import { ReolinkNativeMultiFocalDevice } from "./multiFocal";
|
|
7
7
|
import { ReolinkNativeNvrDevice } from "./nvr";
|
|
8
8
|
import { batteryCameraSuffix, batteryMultifocalSuffix, cameraSuffix, extractThumbnailFromVideo, getDeviceInterfaces, handleVideoClipRequest, multifocalSuffix, nvrSuffix } from "./utils";
|
|
9
|
+
import { randomBytes } from "crypto";
|
|
9
10
|
|
|
10
11
|
interface ThumbnailRequest {
|
|
11
12
|
deviceId: string;
|
|
@@ -29,6 +30,7 @@ interface ThumbnailRequestInput {
|
|
|
29
30
|
|
|
30
31
|
class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider, DeviceCreator {
|
|
31
32
|
devices = new Map<string, BaseBaichuanClass>();
|
|
33
|
+
private deviceCreationPromises = new Map<string, Promise<BaseBaichuanClass>>();
|
|
32
34
|
mixinsMap = new Map<string, CommonCameraMixin>();
|
|
33
35
|
nvrDeviceId: string;
|
|
34
36
|
private thumbnailQueue: ThumbnailRequest[] = [];
|
|
@@ -46,13 +48,36 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
async getDevice(nativeId: ScryptedNativeId): Promise<BaseBaichuanClass> {
|
|
51
|
+
// Return existing device if available
|
|
49
52
|
if (this.devices.has(nativeId)) {
|
|
50
53
|
return this.devices.get(nativeId)!;
|
|
51
54
|
}
|
|
52
55
|
|
|
53
|
-
|
|
54
|
-
this.
|
|
55
|
-
|
|
56
|
+
// Check if device creation is already in progress to prevent race conditions
|
|
57
|
+
const existingPromise = this.deviceCreationPromises.get(nativeId);
|
|
58
|
+
if (existingPromise) {
|
|
59
|
+
return existingPromise;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Create device creation promise to handle concurrent requests
|
|
63
|
+
const creationPromise = (async () => {
|
|
64
|
+
try {
|
|
65
|
+
// Double-check after async operation
|
|
66
|
+
if (this.devices.has(nativeId)) {
|
|
67
|
+
return this.devices.get(nativeId)!;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const newCamera = this.createCamera(nativeId);
|
|
71
|
+
this.devices.set(nativeId, newCamera);
|
|
72
|
+
return newCamera;
|
|
73
|
+
} finally {
|
|
74
|
+
// Clean up the promise after creation completes
|
|
75
|
+
this.deviceCreationPromises.delete(nativeId);
|
|
76
|
+
}
|
|
77
|
+
})();
|
|
78
|
+
|
|
79
|
+
this.deviceCreationPromises.set(nativeId, creationPromise);
|
|
80
|
+
return creationPromise;
|
|
56
81
|
}
|
|
57
82
|
|
|
58
83
|
async createDevice(settings: DeviceCreatorSettings, nativeId?: string): Promise<string> {
|
|
@@ -65,7 +90,6 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
65
90
|
throw new Error('IP address, username, and password are required');
|
|
66
91
|
}
|
|
67
92
|
|
|
68
|
-
// Auto-detect device type (camera, battery-cam, or nvr)
|
|
69
93
|
this.console.log(`[AutoDetect] Starting device type detection for ${ipAddress}...`);
|
|
70
94
|
const { autoDetectDeviceType } = await import("@apocaliss92/reolink-baichuan-js");
|
|
71
95
|
|
|
@@ -78,19 +102,20 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
78
102
|
logger: this.console,
|
|
79
103
|
},
|
|
80
104
|
);
|
|
105
|
+
const { ip, mac } = detection.hostNetworkInfo ?? {}
|
|
81
106
|
|
|
82
107
|
this.console.log(`[AutoDetect] Detected device type: ${detection.type} (transport: ${detection.transport}). Device info: ${JSON.stringify(detection.deviceInfo)}`);
|
|
83
108
|
|
|
84
109
|
// Use the API that was successfully used for detection
|
|
85
110
|
const detectedApi = detection.api;
|
|
111
|
+
const deviceInfo = detection.deviceInfo || {};
|
|
112
|
+
const name = deviceInfo?.name || `Reolink ${detection.type}`;
|
|
113
|
+
const identifier = uid || mac || ip || name || randomBytes(4).toString('hex');
|
|
86
114
|
|
|
87
115
|
// Handle multi-focal device case
|
|
88
116
|
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
117
|
const isBattery = detection.transport === 'udp';
|
|
93
|
-
nativeId = `${
|
|
118
|
+
nativeId = `${identifier}${isBattery ? batteryMultifocalSuffix : multifocalSuffix}`;
|
|
94
119
|
|
|
95
120
|
settings.newCamera ||= name;
|
|
96
121
|
|
|
@@ -126,10 +151,7 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
126
151
|
|
|
127
152
|
// Handle NVR case
|
|
128
153
|
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}`;
|
|
154
|
+
nativeId = `${identifier}${nvrSuffix}`;
|
|
133
155
|
|
|
134
156
|
settings.newCamera ||= name;
|
|
135
157
|
|
|
@@ -157,16 +179,11 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
157
179
|
return nativeId;
|
|
158
180
|
}
|
|
159
181
|
|
|
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
182
|
// Create nativeId based on device type
|
|
166
183
|
if (detection.type === 'battery-cam') {
|
|
167
|
-
nativeId = `${
|
|
184
|
+
nativeId = `${identifier}${batteryCameraSuffix}`;
|
|
168
185
|
} else {
|
|
169
|
-
nativeId = `${
|
|
186
|
+
nativeId = `${identifier}${cameraSuffix}`;
|
|
170
187
|
}
|
|
171
188
|
|
|
172
189
|
settings.newCamera ||= name;
|
|
@@ -205,8 +222,8 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
205
222
|
return nativeId;
|
|
206
223
|
}
|
|
207
224
|
catch (e) {
|
|
208
|
-
this.console.error('Error adding Reolink device', e);
|
|
209
|
-
throw e;
|
|
225
|
+
this.console.error('Error adding Reolink device', e?.message || String(e));
|
|
226
|
+
throw e;
|
|
210
227
|
}
|
|
211
228
|
}
|
|
212
229
|
|
package/src/multiFocal.ts
CHANGED
|
@@ -1,36 +1,24 @@
|
|
|
1
|
-
import type { DeviceCapabilities, DualLensChannelAnalysis, ReolinkSimpleEvent } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
|
|
2
|
-
import
|
|
1
|
+
import type { DeviceCapabilities, DualLensChannelAnalysis, NativeVideoStreamVariant, ReolinkBaichuanApi, ReolinkSimpleEvent, SleepStatus, StreamProfile } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
|
|
2
|
+
import type { BaichuanConnectionConfig } from "./baichuan-base";
|
|
3
|
+
import sdk, { Device, DeviceProvider, Reboot, ScryptedDeviceType, Settings } from "@scrypted/sdk";
|
|
3
4
|
import { ReolinkNativeCamera } from "./camera";
|
|
4
5
|
import { ReolinkNativeBatteryCamera } from "./camera-battery";
|
|
5
6
|
import { CameraType, CommonCameraMixin } from "./common";
|
|
6
7
|
import ReolinkNativePlugin from "./main";
|
|
7
|
-
import { batteryCameraSuffix, cameraSuffix, getDeviceInterfaces
|
|
8
|
+
import { batteryCameraSuffix, cameraSuffix, getDeviceInterfaces } from "./utils";
|
|
9
|
+
import { ReolinkNativeNvrDevice } from "./nvr";
|
|
10
|
+
import { createBaichuanApi } from "./connect";
|
|
8
11
|
|
|
9
12
|
export class ReolinkNativeMultiFocalDevice extends CommonCameraMixin implements Settings, DeviceProvider, Reboot {
|
|
10
13
|
plugin: ReolinkNativePlugin;
|
|
11
14
|
cameraNativeMap = new Map<string, ReolinkNativeCamera | ReolinkNativeBatteryCamera>();
|
|
12
15
|
private channelToNativeIdMap = new Map<number, string>();
|
|
13
|
-
private initReinitTimeout: NodeJS.Timeout | undefined;
|
|
14
16
|
isBattery: boolean;
|
|
15
17
|
|
|
16
|
-
constructor(nativeId: string, plugin: ReolinkNativePlugin, type: CameraType) {
|
|
17
|
-
super(nativeId, plugin, { type });
|
|
18
|
-
this.plugin = plugin;
|
|
19
|
-
|
|
20
|
-
this.scheduleInit();
|
|
21
|
-
}
|
|
18
|
+
constructor(nativeId: string, plugin: ReolinkNativePlugin, type: CameraType, nvrDevice?: ReolinkNativeNvrDevice) {
|
|
19
|
+
super(nativeId, plugin, { type, nvrDevice });
|
|
22
20
|
|
|
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
|
-
}
|
|
21
|
+
this.plugin = plugin;
|
|
34
22
|
}
|
|
35
23
|
|
|
36
24
|
protected async onBeforeCleanup(): Promise<void> {
|
|
@@ -41,141 +29,123 @@ export class ReolinkNativeMultiFocalDevice extends CommonCameraMixin implements
|
|
|
41
29
|
return this.name || 'Multi-Focal Device';
|
|
42
30
|
}
|
|
43
31
|
|
|
44
|
-
async reinit(): Promise<void> {
|
|
45
|
-
// Cancel any pending init/reinit
|
|
46
|
-
if (this.initReinitTimeout) {
|
|
47
|
-
clearTimeout(this.initReinitTimeout);
|
|
48
|
-
this.initReinitTimeout = undefined;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Schedule reinit with debounce
|
|
52
|
-
this.scheduleInit(true);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
private scheduleInit(isReinit: boolean = false): void {
|
|
56
|
-
// Cancel any pending init/reinit
|
|
57
|
-
if (this.initReinitTimeout) {
|
|
58
|
-
clearTimeout(this.initReinitTimeout);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
this.initReinitTimeout = setTimeout(async () => {
|
|
62
|
-
const logger = this.getBaichuanLogger();
|
|
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
|
-
}
|
|
71
|
-
|
|
72
32
|
async init(): Promise<void> {
|
|
73
33
|
const logger = this.getBaichuanLogger();
|
|
34
|
+
|
|
74
35
|
try {
|
|
75
36
|
this.storageSettings.settings.uid.hide = !this.isBattery;
|
|
76
37
|
|
|
77
|
-
await this.
|
|
78
|
-
|
|
38
|
+
await this.ensureClient();
|
|
39
|
+
// subscribeToEvents in common.ts will check if this device has a parent (nvrDevice)
|
|
40
|
+
// and skip subscription if needed - events will be forwarded from parent
|
|
79
41
|
await this.subscribeToEvents();
|
|
80
42
|
} catch (e) {
|
|
81
|
-
logger.error('Failed to initialize multi-focal device', e);
|
|
82
|
-
if (e instanceof Error) {
|
|
83
|
-
logger.error(`Error message: ${e.message}`);
|
|
84
|
-
logger.error(`Error stack: ${e.stack}`);
|
|
85
|
-
} else {
|
|
86
|
-
logger.error(`Error details: ${JSON.stringify(e)}`);
|
|
87
|
-
}
|
|
43
|
+
logger.error('Failed to initialize multi-focal device', e?.message || String(e));
|
|
88
44
|
}
|
|
89
45
|
}
|
|
90
46
|
|
|
91
|
-
getInterfaces(
|
|
47
|
+
getInterfaces(lensType?: NativeVideoStreamVariant) {
|
|
92
48
|
const logger = this.getBaichuanLogger();
|
|
93
49
|
const { capabilities: caps, multifocalInfo } = this.storageSettings.values;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
50
|
+
|
|
51
|
+
let capabilities: DeviceCapabilities = { ...caps };
|
|
52
|
+
|
|
53
|
+
if (lensType) {
|
|
54
|
+
const channelInfo = (multifocalInfo as DualLensChannelAnalysis).channels.find(c => c.variantType === lensType);
|
|
55
|
+
|
|
56
|
+
const hasPtz = channelInfo?.hasPan || channelInfo?.hasTilt || channelInfo?.hasZoom;
|
|
57
|
+
|
|
58
|
+
capabilities = {
|
|
59
|
+
...capabilities,
|
|
60
|
+
hasPan: channelInfo.hasPan,
|
|
61
|
+
hasTilt: channelInfo.hasTilt,
|
|
62
|
+
hasZoom: channelInfo?.hasZoom,
|
|
63
|
+
hasPresets: channelInfo?.hasPresets || hasPtz,
|
|
64
|
+
hasIntercom: channelInfo?.hasIntercom,
|
|
65
|
+
hasPtz,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
104
68
|
|
|
105
69
|
const { interfaces } = getDeviceInterfaces({
|
|
106
70
|
capabilities,
|
|
107
71
|
logger,
|
|
108
72
|
});
|
|
109
73
|
|
|
74
|
+
logger.debug(`Interfaces found for lens ${lensType}: ${JSON.stringify({ interfaces, capabilities, multifocalInfo })}`);
|
|
75
|
+
|
|
110
76
|
return { interfaces, capabilities };
|
|
111
77
|
}
|
|
112
78
|
|
|
113
79
|
async reportDevices(): Promise<void> {
|
|
114
|
-
const api = await this.ensureBaichuanClient();
|
|
115
80
|
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
81
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
82
|
+
try {
|
|
83
|
+
const api = await this.ensureClient();
|
|
84
|
+
const { username, password, ipAddress, uid, rtspChannel } = this.storageSettings.values;
|
|
85
|
+
|
|
86
|
+
const { capabilities, objects, presets } = await api.getDeviceCapabilities(rtspChannel, {
|
|
87
|
+
mergeDualLensOnSameChannel: true,
|
|
88
|
+
});
|
|
89
|
+
const multifocalInfo = await api.getDualLensChannelInfo(rtspChannel, {
|
|
90
|
+
onNvr: !!this.nvrDevice
|
|
91
|
+
});
|
|
92
|
+
logger.log(`Discovering ${multifocalInfo.channels.length} lenses`);
|
|
93
|
+
logger.debug({ multifocalInfo, capabilities });
|
|
94
|
+
|
|
95
|
+
this.storageSettings.values.multifocalInfo = multifocalInfo;
|
|
96
|
+
this.storageSettings.values.capabilities = capabilities;
|
|
97
|
+
|
|
98
|
+
for (const channelInfo of multifocalInfo?.channels ?? []) {
|
|
99
|
+
const { channel, lensType, variantType } = channelInfo;
|
|
100
|
+
|
|
101
|
+
const name = `${this.name} - ${lensType}`;
|
|
102
|
+
const nativeId = `${this.nativeId}-${lensType}${this.isBattery ? batteryCameraSuffix : cameraSuffix}`;
|
|
103
|
+
|
|
104
|
+
this.channelToNativeIdMap.set(channel, nativeId);
|
|
105
|
+
const { interfaces, capabilities: deviceCapabilities } = this.getInterfaces();
|
|
106
|
+
|
|
107
|
+
const device: Device = {
|
|
108
|
+
providerNativeId: this.nativeId,
|
|
109
|
+
name,
|
|
110
|
+
nativeId,
|
|
111
|
+
info: {
|
|
112
|
+
...this.info,
|
|
113
|
+
metadata: {
|
|
114
|
+
channel,
|
|
115
|
+
lensType
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
interfaces,
|
|
119
|
+
type: ScryptedDeviceType.Camera,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
await sdk.deviceManager.onDeviceDiscovered(device);
|
|
123
|
+
|
|
124
|
+
logger.log(`Discovering lens ${lensType}`);
|
|
125
|
+
logger.debug(`${JSON.stringify({ interfaces, deviceCapabilities })}`)
|
|
126
|
+
|
|
127
|
+
const camera = await this.getDevice(nativeId);
|
|
128
|
+
|
|
129
|
+
if (!camera) {
|
|
130
|
+
logger.error(`Failed to get device ${nativeId}`);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
165
133
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
134
|
+
camera.storageSettings.values.rtspChannel = channel;
|
|
135
|
+
camera.classes = objects;
|
|
136
|
+
camera.presets = presets;
|
|
137
|
+
camera.storageSettings.values.username = username;
|
|
138
|
+
camera.storageSettings.values.password = password;
|
|
139
|
+
camera.storageSettings.values.ipAddress = ipAddress;
|
|
140
|
+
camera.storageSettings.values.variantType = variantType;
|
|
141
|
+
camera.storageSettings.values.rtspChannel = channel;
|
|
142
|
+
camera.storageSettings.values.capabilities = deviceCapabilities;
|
|
174
143
|
camera.storageSettings.values.uid = uid;
|
|
175
144
|
}
|
|
145
|
+
} catch (e) {
|
|
146
|
+
logger.error('Failed to report devices', e?.message || String(e));
|
|
147
|
+
throw e;
|
|
176
148
|
}
|
|
177
|
-
|
|
178
|
-
await super.reportDevices();
|
|
179
149
|
}
|
|
180
150
|
|
|
181
151
|
async getDevice(nativeId: string) {
|
|
@@ -199,34 +169,122 @@ export class ReolinkNativeMultiFocalDevice extends CommonCameraMixin implements
|
|
|
199
169
|
super.releaseDevice(id, nativeId);
|
|
200
170
|
}
|
|
201
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Forward events received from parent (NVR if child, or directly from Baichuan if standalone)
|
|
174
|
+
* to the MultiFocal device itself AND to ALL lens devices (camera children) of this MultiFocal.
|
|
175
|
+
* This ensures that:
|
|
176
|
+
* 1. The MultiFocal device itself receives events (it can have event handling capabilities)
|
|
177
|
+
* 2. All lenses receive the events, even if they share the same channel
|
|
178
|
+
* (e.g., wide and tele on the same channel on NVR).
|
|
179
|
+
* Only the root device (NVR or standalone MultiFocal) subscribes to events,
|
|
180
|
+
* and events are forwarded down the hierarchy.
|
|
181
|
+
*/
|
|
202
182
|
forwardNativeEvent(ev: ReolinkSimpleEvent): void {
|
|
203
183
|
const logger = this.getBaichuanLogger();
|
|
204
|
-
const
|
|
184
|
+
const eventChannel = ev?.channel;
|
|
205
185
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
186
|
+
// First, forward event to the MultiFocal device itself
|
|
187
|
+
try {
|
|
188
|
+
this.onSimpleEvent(ev);
|
|
189
|
+
} catch (e) {
|
|
190
|
+
logger.warn(`Error forwarding event to MultiFocal device itself:`, e?.message || String(e));
|
|
209
191
|
}
|
|
210
192
|
|
|
211
|
-
|
|
212
|
-
if
|
|
213
|
-
|
|
193
|
+
// Then, forward event to all lens devices (camera children) of this MultiFocal
|
|
194
|
+
// Even if event has a specific channel, we forward to all lenses because:
|
|
195
|
+
// 1. On NVR, wide and tele lenses can share the same channel
|
|
196
|
+
// 2. Events might be relevant to all lenses of the MultiFocal device
|
|
197
|
+
const lensDevices = Array.from(this.cameraNativeMap.values());
|
|
198
|
+
const forwardedCount = lensDevices.length;
|
|
199
|
+
|
|
200
|
+
if (forwardedCount === 0) {
|
|
201
|
+
logger.debug(`No lens devices found for MultiFocal, event forwarded only to MultiFocal itself`);
|
|
214
202
|
return;
|
|
215
203
|
}
|
|
216
204
|
|
|
217
|
-
|
|
218
|
-
if (!camera) {
|
|
219
|
-
logger.debug(`Camera ${nativeId} not yet initialized, ignoring event`);
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
205
|
+
logger.debug(`Forwarding event (channel=${eventChannel}) to MultiFocal itself and ${forwardedCount} lens device(s)`);
|
|
222
206
|
|
|
223
|
-
camera
|
|
207
|
+
// Forward event to all camera children (lens devices)
|
|
208
|
+
for (const camera of lensDevices) {
|
|
209
|
+
try {
|
|
210
|
+
// Each lens device will filter events based on its own channel if needed
|
|
211
|
+
camera.onSimpleEvent(ev);
|
|
212
|
+
} catch (e) {
|
|
213
|
+
logger.warn(`Error forwarding event to lens device ${camera.nativeId}:`, e?.message || String(e));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
224
216
|
}
|
|
225
217
|
|
|
226
218
|
async unsubscribeFromAllEvents(): Promise<void> {
|
|
227
219
|
await super.unsubscribeFromEvents();
|
|
228
220
|
}
|
|
229
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Update sleeping state for the MultiFocal device itself and propagate to all lens devices.
|
|
224
|
+
* This ensures that when the MultiFocal receives a sleeping/awake state update (from events or API calls),
|
|
225
|
+
* the state is synchronized across the MultiFocal and all its lens children.
|
|
226
|
+
*/
|
|
227
|
+
async updateSleepingState(sleepStatus: SleepStatus): Promise<void> {
|
|
228
|
+
const logger = this.getBaichuanLogger();
|
|
229
|
+
|
|
230
|
+
// First, update the MultiFocal device's own sleeping state
|
|
231
|
+
await super.updateSleepingState(sleepStatus);
|
|
232
|
+
|
|
233
|
+
// Then, propagate the state to all lens devices (camera children)
|
|
234
|
+
const lensDevices = Array.from(this.cameraNativeMap.values());
|
|
235
|
+
|
|
236
|
+
if (lensDevices.length === 0) {
|
|
237
|
+
logger.debug(`No lens devices found for MultiFocal, sleeping state updated only for MultiFocal itself`);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
logger.debug(`Propagating sleeping state (state=${sleepStatus.state}) to ${lensDevices.length} lens device(s)`);
|
|
242
|
+
|
|
243
|
+
// Propagate sleeping state to all lens devices
|
|
244
|
+
await Promise.allSettled(
|
|
245
|
+
lensDevices.map(async (camera) => {
|
|
246
|
+
try {
|
|
247
|
+
await camera.updateSleepingState(sleepStatus);
|
|
248
|
+
} catch (e) {
|
|
249
|
+
logger.warn(`Error propagating sleeping state to lens device ${camera.nativeId}:`, e?.message || String(e));
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Update online state for the MultiFocal device itself and propagate to all lens devices.
|
|
257
|
+
* This ensures that when the MultiFocal receives an online/offline state update (from events or API calls),
|
|
258
|
+
* the state is synchronized across the MultiFocal and all its lens children.
|
|
259
|
+
*/
|
|
260
|
+
async updateOnlineState(isOnline: boolean): Promise<void> {
|
|
261
|
+
const logger = this.getBaichuanLogger();
|
|
262
|
+
|
|
263
|
+
// First, update the MultiFocal device's own online state
|
|
264
|
+
await super.updateOnlineState(isOnline);
|
|
265
|
+
|
|
266
|
+
// Then, propagate the state to all lens devices (camera children)
|
|
267
|
+
const lensDevices = Array.from(this.cameraNativeMap.values());
|
|
268
|
+
|
|
269
|
+
if (lensDevices.length === 0) {
|
|
270
|
+
logger.debug(`No lens devices found for MultiFocal, online state updated only for MultiFocal itself`);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
logger.debug(`Propagating online state (isOnline=${isOnline}) to ${lensDevices.length} lens device(s)`);
|
|
275
|
+
|
|
276
|
+
// Propagate online state to all lens devices
|
|
277
|
+
await Promise.allSettled(
|
|
278
|
+
lensDevices.map(async (camera) => {
|
|
279
|
+
try {
|
|
280
|
+
await camera.updateOnlineState(isOnline);
|
|
281
|
+
} catch (e) {
|
|
282
|
+
logger.warn(`Error propagating online state to lens device ${camera.nativeId}:`, e?.message || String(e));
|
|
283
|
+
}
|
|
284
|
+
})
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
230
288
|
public async runDiagnostics(): Promise<void> {
|
|
231
289
|
const logger = this.getBaichuanLogger();
|
|
232
290
|
logger.log(`Starting Multifocal diagnostics...`);
|
|
@@ -237,16 +295,52 @@ export class ReolinkNativeMultiFocalDevice extends CommonCameraMixin implements
|
|
|
237
295
|
throw new Error('Missing device credentials');
|
|
238
296
|
}
|
|
239
297
|
|
|
240
|
-
const api = await this.
|
|
298
|
+
const api = await this.ensureClient();
|
|
241
299
|
|
|
242
300
|
const multifocalDiagnostics = await api.collectMultifocalDiagnostics(logger);
|
|
243
301
|
|
|
244
302
|
logger.log(`NVR diagnostics completed successfully.`);
|
|
245
|
-
logger.
|
|
303
|
+
logger.debug(JSON.stringify(multifocalDiagnostics));
|
|
246
304
|
} catch (e) {
|
|
247
|
-
logger.error('Failed to run NVR diagnostics', e);
|
|
305
|
+
logger.error('Failed to run NVR diagnostics', e?.message || String(e));
|
|
248
306
|
throw e;
|
|
249
307
|
}
|
|
250
308
|
}
|
|
309
|
+
|
|
310
|
+
async ensureClient(): Promise<ReolinkBaichuanApi> {
|
|
311
|
+
if (this.nvrDevice) {
|
|
312
|
+
return await this.nvrDevice.ensureBaichuanClient();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Use base class implementation
|
|
316
|
+
return await this.ensureBaichuanClient();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
protected getStreamClientInputs(): BaichuanConnectionConfig {
|
|
320
|
+
const { ipAddress, username, password } = this.storageSettings.values;
|
|
321
|
+
const debugOptions = this.getBaichuanDebugOptions();
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
host: ipAddress,
|
|
325
|
+
username,
|
|
326
|
+
password,
|
|
327
|
+
transport: this.transport,
|
|
328
|
+
debugOptions,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Create a dedicated Baichuan API session for streaming (used by StreamManager).
|
|
334
|
+
* MultiFocal creates its own socket for stream clients, or delegates to NVR if on NVR.
|
|
335
|
+
*/
|
|
336
|
+
async createStreamClient(streamKey: string): Promise<ReolinkBaichuanApi> {
|
|
337
|
+
// If on NVR, delegate to NVR to create the socket
|
|
338
|
+
if (this.nvrDevice) {
|
|
339
|
+
return await this.nvrDevice.createStreamClient(streamKey);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Otherwise, use base class createStreamClient which manages stream clients per streamKey
|
|
343
|
+
return await super.createStreamClient(streamKey);
|
|
344
|
+
}
|
|
251
345
|
}
|
|
252
346
|
|