@apocaliss92/scrypted-reolink-native 0.1.2 → 0.1.4

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/dist/plugin.zip CHANGED
Binary file
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@apocaliss92/scrypted-reolink-native",
3
- "version": "0.1.2",
4
- "description": "Reolink Native plugin for Scrypted",
3
+ "version": "0.1.4",
4
+ "description": "Use any reolink camera with Scrypted, even older/unsupported models without HTTP protocol support",
5
5
  "author": "@apocaliss92",
6
6
  "license": "Apache",
7
7
  "scripts": {
@@ -1,4 +1,4 @@
1
- import type { ReolinkBaichuanApi } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
1
+ import type { ReolinkBaichuanApi, SleepStatus } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
2
2
  import sdk, {
3
3
  type MediaObject,
4
4
  RequestPictureOptions,
@@ -28,16 +28,18 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
28
28
  return debugLogs.includes(DebugLogOption.BatteryInfo);
29
29
  }
30
30
 
31
- constructor(nativeId: string, public plugin: ReolinkNativePlugin) {
31
+ constructor(nativeId: string, public plugin: ReolinkNativePlugin, nvrDevice?: any) {
32
32
  super(nativeId, plugin, {
33
33
  type: 'battery',
34
+ nvrDevice,
34
35
  });
35
36
  }
36
37
 
37
38
  async takePicture(options?: RequestPictureOptions): Promise<MediaObject> {
38
- const { snapshotCacheMinutes = 5 } = this.storageSettings.values;
39
- const cacheMs = snapshotCacheMinutes * 60_000;
40
- if (!this.forceNewSnapshot && cacheMs > 0 && this.lastPicture && Date.now() - this.lastPicture.atMs < cacheMs) {
39
+ // const { snapshotCacheMinutes = 5 } = this.storageSettings.values;
40
+ // const cacheMs = snapshotCacheMinutes * 60_000;
41
+ // if (!this.forceNewSnapshot && cacheMs > 0 && this.lastPicture && Date.now() - this.lastPicture.atMs < cacheMs) {
42
+ if (!this.forceNewSnapshot && this.lastPicture) {
41
43
  this.console.log(`Returning cached snapshot, taken at ${new Date(this.lastPicture.atMs).toLocaleString()}`);
42
44
  return this.lastPicture.mo;
43
45
  }
@@ -50,7 +52,7 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
50
52
  this.forceNewSnapshot = false;
51
53
 
52
54
  this.takePictureInFlight = (async () => {
53
- const channel = this.getRtspChannel();
55
+ const channel = this.storageSettings.values.rtspChannel;
54
56
  const snapshotBuffer = await this.withBaichuanClient(async (api) => {
55
57
  return await api.getSnapshot(channel);
56
58
  });
@@ -75,6 +77,7 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
75
77
  async init(): Promise<void> {
76
78
  this.startPeriodicTasks();
77
79
  await this.alignAuxDevicesState();
80
+ await this.updateBatteryInfo();
78
81
  }
79
82
 
80
83
  async release(): Promise<void> {
@@ -101,9 +104,27 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
101
104
  this.console.log('Starting periodic tasks for battery camera');
102
105
 
103
106
  // Check sleeping state every 5 seconds (non-blocking)
104
- this.sleepCheckTimer = setInterval(() => {
105
- this.checkSleepingState().catch(() => { });
106
- }, 5_000);
107
+ if (!this.nvrDevice) {
108
+ this.sleepCheckTimer = setInterval(async () => {
109
+ try {
110
+ const api = this.baichuanApi;
111
+ const channel = this.storageSettings.values.rtspChannel;
112
+
113
+ if (!api) {
114
+ if (!this.sleeping) {
115
+ this.console.log('Camera is sleeping: no active Baichuan client');
116
+ this.sleeping = true;
117
+ }
118
+ return;
119
+ }
120
+
121
+ const sleepStatus = api.getSleepStatus({ channel });
122
+ await this.updateSleepingState(sleepStatus);
123
+ } catch (e) {
124
+ this.console.warn('Error checking sleeping state:', e);
125
+ }
126
+ }, 5_000);
127
+ }
107
128
 
108
129
  // Update battery and snapshot every N minutes
109
130
  const { batteryUpdateIntervalMinutes = 10 } = this.storageSettings.values;
@@ -115,30 +136,13 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
115
136
  this.console.log(`Periodic tasks started: sleep check every 5s, battery update every ${batteryUpdateIntervalMinutes} minutes`);
116
137
  }
117
138
 
118
- private async checkSleepingState(): Promise<void> {
139
+ async updateSleepingState(sleepStatus: SleepStatus): Promise<void> {
119
140
  try {
120
- // IMPORTANT: do not call ensureClient() here.
121
- // If the camera is asleep or disconnected, ensureClient() may reconnect/login and wake it.
122
- const api = this.baichuanApi;
123
- const channel = this.getRtspChannel();
124
-
125
- // If there is no existing client, assume sleeping/idle.
126
- if (!api) {
127
- if (!this.sleeping) {
128
- this.console.log('Camera is sleeping: no active Baichuan client');
129
- this.sleeping = true;
130
- }
131
- return;
132
- }
133
-
134
- // Passive sleep detection (no request sent to camera)
135
- const sleepStatus = api.getSleepStatus({ channel });
136
141
  if (this.isBatteryInfoLoggingEnabled()) {
137
142
  this.console.log('getSleepStatus result:', JSON.stringify(sleepStatus));
138
143
  }
139
144
 
140
145
  if (sleepStatus.state === 'sleeping') {
141
- // Camera is sleeping
142
146
  if (!this.sleeping) {
143
147
  this.console.log(`Camera is sleeping: ${sleepStatus.reason}`);
144
148
  this.sleeping = true;
@@ -151,15 +155,12 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
151
155
  this.sleeping = false;
152
156
  }
153
157
 
154
- // When camera wakes up (transition from sleeping to awake), align auxiliary devices state and force snapshot (once)
155
158
  if (wasSleeping) {
156
159
  this.alignAuxDevicesState().catch(() => { });
157
160
  if (this.forceNewSnapshot) {
158
161
  this.takePicture().catch(() => { });
159
162
  }
160
163
  }
161
- // NOTE: We don't call getBatteryInfo() here anymore to avoid timeouts.
162
- // Battery updates are handled by updateBatteryAndSnapshot() which properly wakes the camera.
163
164
  } else {
164
165
  // Unknown state
165
166
  this.console.debug(`Sleep status unknown: ${sleepStatus.reason}`);
@@ -170,6 +171,41 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
170
171
  }
171
172
  }
172
173
 
174
+ async updateBatteryInfo() {
175
+ const api = await this.ensureClient();
176
+ const channel = this.storageSettings.values.rtspChannel;
177
+
178
+ const batteryInfo = await api.getBatteryInfo(channel);
179
+ if (this.isBatteryInfoLoggingEnabled()) {
180
+ this.console.log('getBatteryInfo result:', JSON.stringify(batteryInfo));
181
+ }
182
+
183
+ if (batteryInfo.batteryPercent !== undefined) {
184
+ const oldLevel = this.lastBatteryLevel;
185
+ this.batteryLevel = batteryInfo.batteryPercent;
186
+ this.lastBatteryLevel = batteryInfo.batteryPercent;
187
+
188
+ // Log only if battery level changed
189
+ if (oldLevel !== undefined && oldLevel !== batteryInfo.batteryPercent) {
190
+ if (batteryInfo.chargeStatus !== undefined) {
191
+ // chargeStatus: "0"=charging, "1"=discharging, "2"=full
192
+ const charging = batteryInfo.chargeStatus === "0" || batteryInfo.chargeStatus === "2";
193
+ this.console.log(`Battery level changed: ${oldLevel}% → ${batteryInfo.batteryPercent}% (charging: ${charging})`);
194
+ } else {
195
+ this.console.log(`Battery level changed: ${oldLevel}% → ${batteryInfo.batteryPercent}%`);
196
+ }
197
+ } else if (oldLevel === undefined) {
198
+ // First time setting battery level
199
+ if (batteryInfo.chargeStatus !== undefined) {
200
+ const charging = batteryInfo.chargeStatus === "0" || batteryInfo.chargeStatus === "2";
201
+ this.console.log(`Battery level set: ${batteryInfo.batteryPercent}% (charging: ${charging})`);
202
+ } else {
203
+ this.console.log(`Battery level set: ${batteryInfo.batteryPercent}%`);
204
+ }
205
+ }
206
+ }
207
+ }
208
+
173
209
  private async updateBatteryAndSnapshot(): Promise<void> {
174
210
  // Prevent multiple simultaneous calls
175
211
  if (this.batteryUpdateInProgress) {
@@ -179,7 +215,7 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
179
215
 
180
216
  this.batteryUpdateInProgress = true;
181
217
  try {
182
- const channel = this.getRtspChannel();
218
+ const channel = this.storageSettings.values.rtspChannel;
183
219
  const updateIntervalMinutes = this.storageSettings.values.batteryUpdateIntervalMinutes ?? 10;
184
220
  this.console.log(`Force battery update interval started (every ${updateIntervalMinutes} minutes)`);
185
221
 
@@ -192,7 +228,7 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
192
228
 
193
229
  // Check current sleep status
194
230
  let sleepStatus = api.getSleepStatus({ channel });
195
-
231
+
196
232
  // If camera is sleeping, wake it up
197
233
  if (sleepStatus.state === 'sleeping') {
198
234
  this.console.log('Camera is sleeping, waking up for periodic update...');
@@ -208,7 +244,7 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
208
244
  const wakeTimeoutMs = 30000; // 30 seconds max
209
245
  const startWakePoll = Date.now();
210
246
  let awake = false;
211
-
247
+
212
248
  while (Date.now() - startWakePoll < wakeTimeoutMs) {
213
249
  await new Promise(resolve => setTimeout(resolve, 1000)); // Check every second
214
250
  sleepStatus = api.getSleepStatus({ channel });
@@ -231,35 +267,7 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
231
267
  // Now that camera is awake, update all states
232
268
  // 1. Update battery info
233
269
  try {
234
- const batteryInfo = await api.getBatteryInfo(channel);
235
- if (this.isBatteryInfoLoggingEnabled()) {
236
- this.console.log('getBatteryInfo result:', JSON.stringify(batteryInfo));
237
- }
238
-
239
- if (batteryInfo.batteryPercent !== undefined) {
240
- const oldLevel = this.lastBatteryLevel;
241
- this.batteryLevel = batteryInfo.batteryPercent;
242
- this.lastBatteryLevel = batteryInfo.batteryPercent;
243
-
244
- // Log only if battery level changed
245
- if (oldLevel !== undefined && oldLevel !== batteryInfo.batteryPercent) {
246
- if (batteryInfo.chargeStatus !== undefined) {
247
- // chargeStatus: "0"=charging, "1"=discharging, "2"=full
248
- const charging = batteryInfo.chargeStatus === "0" || batteryInfo.chargeStatus === "2";
249
- this.console.log(`Battery level changed: ${oldLevel}% → ${batteryInfo.batteryPercent}% (charging: ${charging})`);
250
- } else {
251
- this.console.log(`Battery level changed: ${oldLevel}% → ${batteryInfo.batteryPercent}%`);
252
- }
253
- } else if (oldLevel === undefined) {
254
- // First time setting battery level
255
- if (batteryInfo.chargeStatus !== undefined) {
256
- const charging = batteryInfo.chargeStatus === "0" || batteryInfo.chargeStatus === "2";
257
- this.console.log(`Battery level set: ${batteryInfo.batteryPercent}% (charging: ${charging})`);
258
- } else {
259
- this.console.log(`Battery level set: ${batteryInfo.batteryPercent}%`);
260
- }
261
- }
262
- }
270
+ await this.updateBatteryInfo();
263
271
  } catch (e) {
264
272
  this.console.warn('Failed to get battery info during periodic update:', e);
265
273
  }
@@ -289,14 +297,14 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
289
297
  async resetBaichuanClient(reason?: any): Promise<void> {
290
298
  try {
291
299
  this.unsubscribedToEvents?.();
292
-
300
+
293
301
  // Close all stream servers before closing the main connection
294
302
  // This ensures streams are properly cleaned up when using shared connection
295
303
  if (this.streamManager) {
296
304
  const reasonStr = reason?.message || reason?.toString?.() || 'connection reset';
297
305
  await this.streamManager.closeAllStreams(reasonStr);
298
306
  }
299
-
307
+
300
308
  await this.baichuanApi?.close();
301
309
  }
302
310
  catch (e) {
package/src/camera.ts CHANGED
@@ -32,9 +32,10 @@ export class ReolinkNativeCamera extends CommonCameraMixin {
32
32
  private statusPollTimer: NodeJS.Timeout | undefined;
33
33
 
34
34
 
35
- constructor(nativeId: string, public plugin: ReolinkNativePlugin) {
35
+ constructor(nativeId: string, public plugin: ReolinkNativePlugin, nvrDevice?: any) {
36
36
  super(nativeId, plugin, {
37
37
  type: 'regular',
38
+ nvrDevice,
38
39
  });
39
40
  }
40
41
 
@@ -101,17 +102,14 @@ export class ReolinkNativeCamera extends CommonCameraMixin {
101
102
 
102
103
  async createStreamClient(): Promise<ReolinkBaichuanApi> {
103
104
  const { ipAddress, username, password } = this.storageSettings.values;
104
- if (!ipAddress || !username || !password) {
105
- throw new Error('Missing camera credentials');
106
- }
107
105
 
108
106
  const debugOptions = this.getBaichuanDebugOptions();
109
107
  const api = await createBaichuanApi(
110
108
  {
111
109
  inputs: {
112
110
  host: ipAddress,
113
- username,
114
- password,
111
+ username: username,
112
+ password: password,
115
113
  logger: this.console,
116
114
  debugOptions
117
115
  },
@@ -208,18 +206,18 @@ export class ReolinkNativeCamera extends CommonCameraMixin {
208
206
  }
209
207
 
210
208
  async takePicture(options?: RequestPictureOptions) {
211
- return this.withBaichuanRetry(async () => {
212
- try {
209
+ try {
210
+ return this.withBaichuanRetry(async () => {
213
211
  const client = await this.ensureClient();
214
212
  const snapshotBuffer = await client.getSnapshot();
215
213
  const mo = await this.createMediaObject(snapshotBuffer, 'image/jpeg');
216
214
 
217
215
  return mo;
218
- } catch (e) {
219
- this.getLogger().error('Error taking snapshot', e);
220
- throw e;
221
- }
222
- });
216
+ });
217
+ } catch (e) {
218
+ this.getLogger().error('Error taking snapshot', e);
219
+ throw e;
220
+ }
223
221
  }
224
222
 
225
223
  async getPictureOptions(): Promise<ResponsePictureOptions[]> {
package/src/common.ts CHANGED
@@ -18,11 +18,13 @@ import {
18
18
  StreamManager,
19
19
  } from "./stream-utils";
20
20
  import { getDeviceInterfaces } from "./utils";
21
+ import { ReolinkNativeNvrDevice } from "./nvr";
21
22
 
22
23
  export type CameraType = 'battery' | 'regular';
23
24
 
24
25
  export interface CommonCameraMixinOptions {
25
26
  type: CameraType;
27
+ nvrDevice?: ReolinkNativeNvrDevice; // Optional reference to NVR device
26
28
  }
27
29
 
28
30
  class ReolinkCameraSiren extends ScryptedDeviceBase implements OnOff {
@@ -138,7 +140,7 @@ class ReolinkCameraPirSensor extends ScryptedDeviceBase implements OnOff, Settin
138
140
  await this.storageSettings.putSetting(key, value);
139
141
 
140
142
  // Apply the new settings to the camera
141
- const channel = this.camera.getRtspChannel();
143
+ const channel = this.camera.storageSettings.values.rtspChannel;
142
144
  const enabled = this.on ? 1 : 0;
143
145
  const sensitive = this.storageSettings.values.sensitive;
144
146
  const reduceAlarm = this.storageSettings.values.reduceAlarm ? 1 : 0;
@@ -166,7 +168,7 @@ class ReolinkCameraPirSensor extends ScryptedDeviceBase implements OnOff, Settin
166
168
  }
167
169
 
168
170
  private async updatePirSettings(): Promise<void> {
169
- const channel = this.camera.getRtspChannel();
171
+ const channel = this.camera.storageSettings.values.rtspChannel;
170
172
  const enabled = this.on ? 1 : 0;
171
173
  const sensitive = this.storageSettings.values.sensitive;
172
174
  const reduceAlarm = this.storageSettings.values.reduceAlarm ? 1 : 0;
@@ -227,24 +229,29 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
227
229
  await this.credentialsChanged();
228
230
  }
229
231
  },
230
- mixinsSetup: {
232
+ isFromNvr: {
231
233
  type: 'boolean',
232
234
  hide: true,
235
+ defaultValue: false,
233
236
  },
234
- snapshotCacheMinutes: {
235
- title: "Snapshot Cache Minutes",
236
- subgroup: 'Advanced',
237
- description: "Return a cached snapshot if taken within the last N minutes.",
238
- type: "number",
239
- defaultValue: 5,
237
+ mixinsSetup: {
238
+ type: 'boolean',
240
239
  hide: true,
241
240
  },
241
+ // snapshotCacheMinutes: {
242
+ // title: "Snapshot Cache Minutes",
243
+ // subgroup: 'Advanced',
244
+ // description: "Return a cached snapshot if taken within the last N minutes.",
245
+ // type: "number",
246
+ // defaultValue: 60,
247
+ // hide: true,
248
+ // },
242
249
  batteryUpdateIntervalMinutes: {
243
250
  title: "Battery Update Interval (minutes)",
244
251
  subgroup: 'Advanced',
245
- description: "How often to wake up the camera and update battery status and snapshot (default: 10 minutes).",
252
+ description: "How often to wake up the camera and update battery status and snapshot (default: 60 minutes).",
246
253
  type: "number",
247
- defaultValue: 10,
254
+ defaultValue: 60,
248
255
  hide: true,
249
256
  },
250
257
  // Regular camera specific
@@ -493,21 +500,15 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
493
500
  initComplete?: boolean;
494
501
  resetBaichuanClient?(reason?: any): Promise<void>;
495
502
 
503
+ protected nvrDevice?: any; // Optional reference to NVR device
504
+
496
505
  constructor(nativeId: string, public plugin: ReolinkNativePlugin, public options: CommonCameraMixinOptions) {
497
506
  super(nativeId);
498
507
  // Set protocol based on camera type
499
- this.protocol = options.type === 'battery' ? 'udp' : 'tcp';
508
+ this.protocol = !options.nvrDevice && options.type === 'battery' ? 'udp' : 'tcp';
500
509
 
501
- this.streamManager = new StreamManager({
502
- createStreamClient: () => this.createStreamClient(),
503
- getLogger: () => this.getLogger(),
504
- credentials: {
505
- username: this.storageSettings.values.username || '',
506
- password: this.storageSettings.values.password || '',
507
- },
508
- // For battery cameras, we use a shared connection
509
- sharedConnection: options.type === 'battery',
510
- });
510
+ // Store NVR device reference if provided
511
+ this.nvrDevice = options.nvrDevice;
511
512
 
512
513
  setTimeout(async () => {
513
514
  await this.parentInit();
@@ -517,12 +518,6 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
517
518
  throw new Error("Method not implemented.");
518
519
  }
519
520
 
520
- // Common method implementations
521
- public getRtspChannel(): number {
522
- const channel = this.storageSettings.values.rtspChannel;
523
- return channel !== undefined ? Number(channel) : 0;
524
- }
525
-
526
521
  public getAbilities(): DeviceCapabilities {
527
522
  return this.storageSettings.values.capabilities;
528
523
  }
@@ -589,7 +584,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
589
584
  return;
590
585
  }
591
586
 
592
- const channel = this.getRtspChannel();
587
+ const channel = this.storageSettings.values.rtspChannel;
593
588
  if (ev?.channel !== undefined && ev.channel !== channel) {
594
589
  if (this.isEventLogsEnabled()) {
595
590
  logger.debug(`Event channel ${ev.channel} does not match camera channel ${channel}, ignoring`);
@@ -637,6 +632,10 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
637
632
  }
638
633
 
639
634
  async subscribeToEvents(): Promise<void> {
635
+ if (this.nvrDevice) {
636
+ return;
637
+ }
638
+
640
639
  const logger = this.getLogger();
641
640
  const selection = Array.from(this.getDispatchEventsSelection?.() ?? new Set()).sort();
642
641
  const enabled = selection.length > 0;
@@ -676,7 +675,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
676
675
  // VideoTextOverlays interface implementation
677
676
  async getVideoTextOverlays(): Promise<Record<string, VideoTextOverlay>> {
678
677
  const client = await this.ensureClient();
679
- const channel = this.getRtspChannel();
678
+ const channel = this.storageSettings.values.rtspChannel;
680
679
 
681
680
  let osd = this.storageSettings.values.cachedOsd;
682
681
 
@@ -698,7 +697,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
698
697
 
699
698
  async setVideoTextOverlay(id: 'osdChannel' | 'osdTime', value: VideoTextOverlay): Promise<void> {
700
699
  const client = await this.ensureClient();
701
- const channel = this.getRtspChannel();
700
+ const channel = this.storageSettings.values.rtspChannel;
702
701
 
703
702
  const osd = await client.getOsd(channel);
704
703
 
@@ -728,7 +727,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
728
727
  return;
729
728
  }
730
729
 
731
- const channel = this.getRtspChannel();
730
+ const channel = this.storageSettings.values.rtspChannel;
732
731
 
733
732
  // Preset navigation.
734
733
  const preset = command.preset;
@@ -959,9 +958,10 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
959
958
  });
960
959
  }
961
960
 
962
- // Settings methods
963
961
  async getSettings(): Promise<Setting[]> {
964
- return await this.storageSettings.getSettings();
962
+ const settings = await this.storageSettings.getSettings();
963
+
964
+ return settings;
965
965
  }
966
966
 
967
967
  async putSetting(key: string, value: string): Promise<void> {
@@ -994,7 +994,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
994
994
  const ip = this.storageSettings.values.ipAddress;
995
995
  try {
996
996
  const api = await this.ensureClient();
997
- const deviceData = await api.getInfo();
997
+ const deviceData = await api.getInfo(this.storageSettings.values.rtspChannel);
998
998
  const info = this.info || {};
999
999
  info.ip = ip;
1000
1000
 
@@ -1002,7 +1002,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1002
1002
  info.firmware = deviceData?.firmwareVersion || deviceData?.firmVer;
1003
1003
  info.version = deviceData?.hardwareVersion || deviceData?.boardInfo;
1004
1004
  info.model = deviceData?.type || deviceData?.typeInfo;
1005
- info.manufacturer = 'Reolink native';
1005
+ info.manufacturer = 'Reolink';
1006
1006
  info.managementUrl = `http://${ip}`;
1007
1007
  this.info = info;
1008
1008
  } catch (e) {
@@ -1041,7 +1041,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1041
1041
  }
1042
1042
 
1043
1043
  async setSirenEnabled(enabled: boolean): Promise<void> {
1044
- const channel = this.getRtspChannel();
1044
+ const channel = this.storageSettings.values.rtspChannel;
1045
1045
 
1046
1046
  await this.withBaichuanRetry(async () => {
1047
1047
  const api = await this.ensureClient();
@@ -1050,7 +1050,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1050
1050
  }
1051
1051
 
1052
1052
  async setFloodlightState(on?: boolean, brightness?: number): Promise<void> {
1053
- const channel = this.getRtspChannel();
1053
+ const channel = this.storageSettings.values.rtspChannel;
1054
1054
 
1055
1055
  await this.withBaichuanRetry(async () => {
1056
1056
  const api = await this.ensureClient();
@@ -1059,7 +1059,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1059
1059
  }
1060
1060
 
1061
1061
  async setPirEnabled(enabled: boolean): Promise<void> {
1062
- const channel = this.getRtspChannel();
1062
+ const channel = this.storageSettings.values.rtspChannel;
1063
1063
 
1064
1064
  // Get current PIR settings from the sensor if available
1065
1065
  let sensitive: number | undefined;
@@ -1091,7 +1091,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1091
1091
  const api = this.baichuanApi;
1092
1092
  if (!api) return;
1093
1093
 
1094
- const channel = this.getRtspChannel();
1094
+ const channel = this.storageSettings.values.rtspChannel;
1095
1095
  const { hasSiren, hasFloodlight, hasPir } = this.getAbilities();
1096
1096
 
1097
1097
  try {
@@ -1234,16 +1234,13 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1234
1234
  return [];
1235
1235
  }
1236
1236
 
1237
- // while (this.fetchingStreams) {
1238
- // await new Promise((resolve) => setTimeout(resolve, 500));
1239
- // }
1240
1237
  this.fetchingStreams = true;
1241
1238
 
1242
1239
  let streams: UrlMediaStreamOptions[] = [];
1243
1240
 
1244
1241
  const client = await this.ensureClient();
1245
1242
 
1246
- const { ipAddress, username, password, rtspChannel } = this.storageSettings.values;
1243
+ const { ipAddress, rtspChannel, isFromNvr } = this.storageSettings.values;
1247
1244
 
1248
1245
  try {
1249
1246
  await this.ensureNetPortCache();
@@ -1255,12 +1252,14 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1255
1252
 
1256
1253
  try {
1257
1254
  streams = await buildVideoStreamOptionsFromRtspRtmp(
1258
- client,
1259
- rtspChannel,
1260
- ipAddress,
1261
- username,
1262
- password,
1263
- this.cachedNetPort,
1255
+ {
1256
+ client,
1257
+ ipAddress,
1258
+ cachedNetPort: this.cachedNetPort,
1259
+ isFromNvr,
1260
+ rtspChannel,
1261
+ logger,
1262
+ },
1264
1263
  );
1265
1264
  } catch (e) {
1266
1265
  if (!this.isRecoverableBaichuanError?.(e)) {
@@ -1269,12 +1268,8 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1269
1268
  this.cachedNetPort = undefined;
1270
1269
  }
1271
1270
 
1272
-
1273
- const nativeStreams = await fetchVideoStreamOptionsFromApi(client, rtspChannel, this.getLogger());
1274
- streams = [...streams, ...nativeStreams];
1275
-
1276
1271
  if (streams.length) {
1277
- logger.log('Fetched video stream options', streams);
1272
+ logger.log('Fetched video stream options', { streams, netPort: this.cachedNetPort });
1278
1273
  this.cachedVideoStreamOptions = streams;
1279
1274
  return streams;
1280
1275
  }
@@ -1308,7 +1303,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1308
1303
  }
1309
1304
 
1310
1305
  const profile = parseStreamProfileFromId(selected.id) || 'main';
1311
- const channel = this.getRtspChannel();
1306
+ const channel = this.storageSettings.values.rtspChannel;
1312
1307
  const streamKey = `${channel}_${profile}`;
1313
1308
  const expectedVideoType = expectedVideoTypeFromUrlMediaStreamOptions(selected);
1314
1309
 
@@ -1356,10 +1351,6 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1356
1351
  this.ensureClientPromise = (async () => {
1357
1352
  const { ipAddress, username, password, uid } = this.storageSettings.values;
1358
1353
 
1359
- if (!ipAddress || !username || !password) {
1360
- throw new Error('Missing camera credentials');
1361
- }
1362
-
1363
1354
  // Only tear down previous session if it exists and is not connected
1364
1355
  if (this.baichuanApi) {
1365
1356
  const isConnected = this.baichuanApi.client.isSocketConnected();
@@ -1409,8 +1400,8 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1409
1400
  {
1410
1401
  inputs: {
1411
1402
  host: ipAddress,
1412
- username,
1413
- password,
1403
+ username: username,
1404
+ password: password,
1414
1405
  uid: normalizedUid,
1415
1406
  logger: this.console,
1416
1407
  debugOptions,
@@ -1472,7 +1463,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1472
1463
  this.refreshingState = true;
1473
1464
 
1474
1465
  const logger = this.getLogger();
1475
- const channel = this.getRtspChannel();
1466
+ const channel = this.storageSettings.values.rtspChannel;
1476
1467
 
1477
1468
  try {
1478
1469
  const { capabilities, abilities, support, presets, objects } = await this.withBaichuanRetry(async () => {
@@ -1492,7 +1483,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1492
1483
 
1493
1484
  const device: Device = {
1494
1485
  nativeId: this.nativeId,
1495
- providerNativeId: this.plugin?.nativeId,
1486
+ providerNativeId: this.nvrDevice?.nativeId ?? this.plugin?.nativeId,
1496
1487
  name: this.name,
1497
1488
  interfaces,
1498
1489
  type,
@@ -1564,8 +1555,21 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1564
1555
  }
1565
1556
 
1566
1557
  const isBattery = this.options.type === 'battery';
1558
+ const { username, password } = this.storageSettings.values;
1567
1559
 
1568
- this.storageSettings.settings.snapshotCacheMinutes.hide = !isBattery;
1560
+ this.streamManager = new StreamManager({
1561
+ createStreamClient: () => this.createStreamClient(),
1562
+ getLogger: () => this.getLogger(),
1563
+ credentials: {
1564
+ username,
1565
+ password
1566
+ },
1567
+ // For battery cameras, we use a shared connection
1568
+ sharedConnection: isBattery,
1569
+ });
1570
+
1571
+
1572
+ // this.storageSettings.settings.snapshotCacheMinutes.hide = !isBattery;
1569
1573
  this.storageSettings.settings.uid.hide = !isBattery;
1570
1574
  this.storageSettings.settings.batteryUpdateIntervalMinutes.hide = !isBattery;
1571
1575
 
@@ -1591,6 +1595,19 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
1591
1595
  logger.warn('Failed to subscribe to Baichuan events', e);
1592
1596
  }
1593
1597
 
1598
+ const { isFromNvr } = this.storageSettings.values;
1599
+
1600
+ if (isFromNvr && this.nvrDevice) {
1601
+ this.storageSettings.settings.username.hide = true;
1602
+ this.storageSettings.settings.password.hide = true;
1603
+ this.storageSettings.settings.ipAddress.hide = true;
1604
+ this.storageSettings.settings.uid.hide = true;
1605
+
1606
+ this.storageSettings.settings.username.defaultValue = this.nvrDevice.storageSettings.values.username;
1607
+ this.storageSettings.settings.password.defaultValue = this.nvrDevice.storageSettings.values.password;
1608
+ this.storageSettings.settings.ipAddress.defaultValue = this.nvrDevice.storageSettings.values.ipAddress;
1609
+ }
1610
+
1594
1611
  await this.init();
1595
1612
  this.initComplete = true;
1596
1613
  }