@apocaliss92/scrypted-reolink-native 0.2.0 → 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 {
package/src/main.ts CHANGED
@@ -1,12 +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";
9
6
  import { randomBytes } from "crypto";
7
+ import { ReolinkCamera } from "./camera";
10
8
 
11
9
  interface ThumbnailRequest {
12
10
  deviceId: string;
@@ -14,7 +12,7 @@ interface ThumbnailRequest {
14
12
  rtmpUrl?: string;
15
13
  filePath?: string;
16
14
  logger?: Console;
17
- device?: CommonCameraMixin;
15
+ device?: ReolinkCamera;
18
16
  resolve: (mo: MediaObject) => void;
19
17
  reject: (error: Error) => void;
20
18
  }
@@ -25,13 +23,12 @@ interface ThumbnailRequestInput {
25
23
  rtmpUrl?: string;
26
24
  filePath?: string;
27
25
  logger?: Console;
28
- device?: CommonCameraMixin;
26
+ device?: ReolinkCamera;
29
27
  }
30
28
 
31
29
  class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider, DeviceCreator {
32
30
  devices = new Map<string, BaseBaichuanClass>();
33
- private deviceCreationPromises = new Map<string, Promise<BaseBaichuanClass>>();
34
- mixinsMap = new Map<string, CommonCameraMixin>();
31
+ camerasMap = new Map<string, ReolinkCamera>();
35
32
  nvrDeviceId: string;
36
33
  private thumbnailQueue: ThumbnailRequest[] = [];
37
34
  private thumbnailProcessing = false;
@@ -48,36 +45,13 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
48
45
  }
49
46
 
50
47
  async getDevice(nativeId: ScryptedNativeId): Promise<BaseBaichuanClass> {
51
- // Return existing device if available
52
48
  if (this.devices.has(nativeId)) {
53
49
  return this.devices.get(nativeId)!;
54
50
  }
55
51
 
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;
52
+ const newCamera = this.createCamera(nativeId);
53
+ this.devices.set(nativeId, newCamera);
54
+ return newCamera;
81
55
  }
82
56
 
83
57
  async createDevice(settings: DeviceCreatorSettings, nativeId?: string): Promise<string> {
@@ -206,7 +180,7 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
206
180
  providerNativeId: this.nativeId,
207
181
  });
208
182
 
209
- const device = await this.getDevice(nativeId) as CommonCameraMixin;
183
+ const device = await this.getDevice(nativeId) as ReolinkCamera;
210
184
 
211
185
  device.info = deviceInfo;
212
186
  device.classes = objects;
@@ -223,7 +197,7 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
223
197
  }
224
198
  catch (e) {
225
199
  this.console.error('Error adding Reolink device', e?.message || String(e));
226
- throw e;
200
+ throw e;
227
201
  }
228
202
  }
229
203
 
@@ -265,7 +239,7 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
265
239
 
266
240
  createCamera(nativeId: string) {
267
241
  if (nativeId.endsWith(batteryCameraSuffix)) {
268
- return new ReolinkNativeBatteryCamera(nativeId, this);
242
+ return new ReolinkCamera(nativeId, this, { type: 'battery' });
269
243
  } else if (nativeId.endsWith(nvrSuffix)) {
270
244
  return new ReolinkNativeNvrDevice(nativeId, this);
271
245
  } else if (nativeId.endsWith(batteryMultifocalSuffix)) {
@@ -273,7 +247,7 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
273
247
  } else if (nativeId.endsWith(multifocalSuffix)) {
274
248
  return new ReolinkNativeMultiFocalDevice(nativeId, this, "multi-focal");
275
249
  } else {
276
- return new ReolinkNativeCamera(nativeId, this);
250
+ return new ReolinkCamera(nativeId, this, { type: 'regular' });
277
251
  }
278
252
  }
279
253
 
@@ -313,7 +287,7 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
313
287
  // logger.log(`Webhook request: type=${type}, deviceId=${deviceId}, fileId=${fileId}`);
314
288
 
315
289
  // Get the device
316
- const device = this.mixinsMap.get(deviceId);
290
+ const device = this.camerasMap.get(deviceId);
317
291
  if (!device) {
318
292
  response.send('Device not found', { code: 404 });
319
293
  return;
package/src/multiFocal.ts CHANGED
@@ -1,19 +1,15 @@
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";
1
+ import type { BatteryInfo, DeviceCapabilities, DualLensChannelAnalysis, NativeVideoStreamVariant, ReolinkBaichuanApi, ReolinkSimpleEvent, SleepStatus } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
3
2
  import sdk, { Device, DeviceProvider, Reboot, ScryptedDeviceType, Settings } from "@scrypted/sdk";
4
- import { ReolinkNativeCamera } from "./camera";
5
- import { ReolinkNativeBatteryCamera } from "./camera-battery";
6
- import { CameraType, CommonCameraMixin } from "./common";
3
+ import type { BaichuanConnectionConfig } from "./baichuan-base";
4
+ import { CameraType, ReolinkCamera } from "./camera";
7
5
  import ReolinkNativePlugin from "./main";
8
- import { batteryCameraSuffix, cameraSuffix, getDeviceInterfaces } from "./utils";
9
6
  import { ReolinkNativeNvrDevice } from "./nvr";
10
- import { createBaichuanApi } from "./connect";
7
+ import { batteryCameraSuffix, cameraSuffix, getDeviceInterfaces } from "./utils";
11
8
 
12
- export class ReolinkNativeMultiFocalDevice extends CommonCameraMixin implements Settings, DeviceProvider, Reboot {
9
+ export class ReolinkNativeMultiFocalDevice extends ReolinkCamera implements Settings, DeviceProvider, Reboot {
13
10
  plugin: ReolinkNativePlugin;
14
- cameraNativeMap = new Map<string, ReolinkNativeCamera | ReolinkNativeBatteryCamera>();
11
+ lensDevicesMap = new Map<string, ReolinkCamera>();
15
12
  private channelToNativeIdMap = new Map<number, string>();
16
- isBattery: boolean;
17
13
 
18
14
  constructor(nativeId: string, plugin: ReolinkNativePlugin, type: CameraType, nvrDevice?: ReolinkNativeNvrDevice) {
19
15
  super(nativeId, plugin, { type, nvrDevice });
@@ -29,21 +25,6 @@ export class ReolinkNativeMultiFocalDevice extends CommonCameraMixin implements
29
25
  return this.name || 'Multi-Focal Device';
30
26
  }
31
27
 
32
- async init(): Promise<void> {
33
- const logger = this.getBaichuanLogger();
34
-
35
- try {
36
- this.storageSettings.settings.uid.hide = !this.isBattery;
37
-
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
41
- await this.subscribeToEvents();
42
- } catch (e) {
43
- logger.error('Failed to initialize multi-focal device', e?.message || String(e));
44
- }
45
- }
46
-
47
28
  getInterfaces(lensType?: NativeVideoStreamVariant) {
48
29
  const logger = this.getBaichuanLogger();
49
30
  const { capabilities: caps, multifocalInfo } = this.storageSettings.values;
@@ -71,12 +52,14 @@ export class ReolinkNativeMultiFocalDevice extends CommonCameraMixin implements
71
52
  logger,
72
53
  });
73
54
 
74
- logger.debug(`Interfaces found for lens ${lensType}: ${JSON.stringify({ interfaces, capabilities, multifocalInfo })}`);
55
+ // logger.debug(`Interfaces found for lens ${lensType}: ${JSON.stringify({ interfaces, capabilities, multifocalInfo })}`);
75
56
 
76
57
  return { interfaces, capabilities };
77
58
  }
78
59
 
79
60
  async reportDevices(): Promise<void> {
61
+ await super.reportDevices();
62
+
80
63
  const logger = this.getBaichuanLogger();
81
64
 
82
65
  try {
@@ -150,14 +133,19 @@ export class ReolinkNativeMultiFocalDevice extends CommonCameraMixin implements
150
133
 
151
134
  async getDevice(nativeId: string) {
152
135
  if (nativeId.endsWith(cameraSuffix) || nativeId.endsWith(batteryCameraSuffix)) {
153
- let device = this.cameraNativeMap.get(nativeId);
136
+ let device = this.lensDevicesMap.get(nativeId);
154
137
  if (!device) {
155
138
  if (nativeId.endsWith(batteryCameraSuffix)) {
156
- device = new ReolinkNativeBatteryCamera(nativeId, this.plugin, undefined, this);
139
+ device = new ReolinkCamera(nativeId, this.plugin, { type: 'battery', multiFocalDevice: this });
157
140
  } else {
158
- device = new ReolinkNativeCamera(nativeId, this.plugin, undefined, this);
141
+ device = new ReolinkCamera(nativeId, this.plugin, { type: 'regular', multiFocalDevice: this });
159
142
  }
160
143
  }
144
+
145
+ if (device) {
146
+ this.lensDevicesMap.set(nativeId, device);
147
+ }
148
+
161
149
  return device;
162
150
  } else {
163
151
  return super.getDevice(nativeId);
@@ -165,126 +153,14 @@ export class ReolinkNativeMultiFocalDevice extends CommonCameraMixin implements
165
153
  }
166
154
 
167
155
  async releaseDevice(id: string, nativeId: string) {
168
- this.cameraNativeMap.delete(nativeId);
156
+ this.lensDevicesMap.delete(nativeId);
169
157
  super.releaseDevice(id, nativeId);
170
158
  }
171
159
 
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
- */
182
- forwardNativeEvent(ev: ReolinkSimpleEvent): void {
183
- const logger = this.getBaichuanLogger();
184
- const eventChannel = ev?.channel;
185
-
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));
191
- }
192
-
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`);
202
- return;
203
- }
204
-
205
- logger.debug(`Forwarding event (channel=${eventChannel}) to MultiFocal itself and ${forwardedCount} lens device(s)`);
206
-
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
- }
216
- }
217
-
218
160
  async unsubscribeFromAllEvents(): Promise<void> {
219
161
  await super.unsubscribeFromEvents();
220
162
  }
221
163
 
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
-
288
164
  public async runDiagnostics(): Promise<void> {
289
165
  const logger = this.getBaichuanLogger();
290
166
  logger.log(`Starting Multifocal diagnostics...`);
@@ -342,5 +218,57 @@ export class ReolinkNativeMultiFocalDevice extends CommonCameraMixin implements
342
218
  // Otherwise, use base class createStreamClient which manages stream clients per streamKey
343
219
  return await super.createStreamClient(streamKey);
344
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
+ }
345
273
  }
346
274