@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/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: CommonCameraMixin) {
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?: CommonCameraMixin;
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?: CommonCameraMixin;
26
+ device?: ReolinkCamera;
28
27
  }
29
28
 
30
29
  class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider, DeviceCreator {
31
30
  devices = new Map<string, BaseBaichuanClass>();
32
- mixinsMap = new Map<string, CommonCameraMixin>();
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 = `${serialNumber}${isBattery ? batteryMultifocalSuffix : multifocalSuffix}`;
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
- const deviceInfo = detection.deviceInfo || {};
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 = `${serialNumber}${batteryCameraSuffix}`;
158
+ nativeId = `${identifier}${batteryCameraSuffix}`;
168
159
  } else {
169
- nativeId = `${serialNumber}${cameraSuffix}`;
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 CommonCameraMixin;
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 ReolinkNativeBatteryCamera(nativeId, this);
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 ReolinkNativeCamera(nativeId, this);
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.mixinsMap.get(deviceId);
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, Setting, Settings, SettingValue } from "@scrypted/sdk";
3
- import { ReolinkNativeCamera } from "./camera";
4
- import { ReolinkNativeBatteryCamera } from "./camera-battery";
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 { batteryCameraSuffix, cameraSuffix, getDeviceInterfaces, updateDeviceInfo } from "./utils";
6
+ import { ReolinkNativeNvrDevice } from "./nvr";
7
+ import { batteryCameraSuffix, cameraSuffix, getDeviceInterfaces } from "./utils";
8
8
 
9
- export class ReolinkNativeMultiFocalDevice extends CommonCameraMixin implements Settings, DeviceProvider, Reboot {
9
+ export class ReolinkNativeMultiFocalDevice extends ReolinkCamera implements Settings, DeviceProvider, Reboot {
10
10
  plugin: ReolinkNativePlugin;
11
- cameraNativeMap = new Map<string, ReolinkNativeCamera | ReolinkNativeBatteryCamera>();
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
- getAbilities(): DeviceCapabilities {
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
- 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
- }
28
+ getInterfaces(lensType?: NativeVideoStreamVariant) {
29
+ const logger = this.getBaichuanLogger();
30
+ const { capabilities: caps, multifocalInfo } = this.storageSettings.values;
54
31
 
55
- private scheduleInit(isReinit: boolean = false): void {
56
- // Cancel any pending init/reinit
57
- if (this.initReinitTimeout) {
58
- clearTimeout(this.initReinitTimeout);
59
- }
32
+ let capabilities: DeviceCapabilities = { ...caps };
60
33
 
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
- }
34
+ if (lensType) {
35
+ const channelInfo = (multifocalInfo as DualLensChannelAnalysis).channels.find(c => c.variantType === lensType);
71
36
 
72
- async init(): Promise<void> {
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
- await this.ensureBaichuanClient();
78
- await this.reportDevices();
79
- await this.subscribeToEvents();
80
- } 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
- }
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
- const api = await this.ensureBaichuanClient();
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
- const camera = await this.getDevice(nativeId);
63
+ const logger = this.getBaichuanLogger();
160
64
 
161
- if (!camera) {
162
- logger.error(`Failed to get device ${nativeId}`);
163
- continue;
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
- camera.storageSettings.values.rtspChannel = channel;
167
- camera.classes = objects;
168
- camera.presets = presets;
169
- camera.storageSettings.values.username = username;
170
- camera.storageSettings.values.password = password;
171
- camera.storageSettings.values.ipAddress = ipAddress;
172
- camera.storageSettings.values.capabilities = deviceCapabilities;
173
- if (this.isBattery) {
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.cameraNativeMap.get(nativeId);
136
+ let device = this.lensDevicesMap.get(nativeId);
184
137
  if (!device) {
185
138
  if (nativeId.endsWith(batteryCameraSuffix)) {
186
- device = new ReolinkNativeBatteryCamera(nativeId, this.plugin, undefined, this);
139
+ device = new ReolinkCamera(nativeId, this.plugin, { type: 'battery', multiFocalDevice: this });
187
140
  } else {
188
- device = new ReolinkNativeCamera(nativeId, this.plugin, undefined, this);
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.cameraNativeMap.delete(nativeId);
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.ensureBaichuanClient();
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.log(JSON.stringify(multifocalDiagnostics));
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