@apocaliss92/scrypted-reolink-native 0.1.19 → 0.1.21

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/multifocal.ts CHANGED
@@ -41,12 +41,12 @@ export class ReolinkNativeMultiFocalDevice extends BaseBaichuanClass implements
41
41
  },
42
42
  diagnosticsRun: {
43
43
  subgroup: 'Diagnostics',
44
- title: 'Run NVR Diagnostics',
45
- description: 'Collect NVR diagnostics and display results in logs.',
44
+ title: 'Run Diagnostics',
45
+ description: 'Collect diagnostics and display results in logs.',
46
46
  type: 'button',
47
47
  immediate: true,
48
48
  onPut: async () => {
49
- await this.runNvrDiagnostics();
49
+ await this.runDiagnostics();
50
50
  },
51
51
  },
52
52
  });
@@ -201,64 +201,70 @@ export class ReolinkNativeMultiFocalDevice extends BaseBaichuanClass implements
201
201
  const logger = this.getBaichuanLogger();
202
202
 
203
203
  try {
204
- const channelsInfo = await api.getNvrChannelsInfo();
205
- const deviceInfo = await api.getInfo();
204
+ // const channelsInfo = await api.getNvrChannelsInfo();
205
+ // const deviceInfo = await api.getInfo();
206
206
  const { support } = await api.getDeviceCapabilities();
207
207
  const channelNum = support?.channelNum ?? 1;
208
208
  logger.log(`Sync entities from remote for ${channelNum} channels`);
209
209
  const channels = Array.from({ length: channelNum }, (_, i) => i + 1);
210
210
 
211
- logger.log(JSON.stringify({ channelsInfo, deviceInfo, channels }));
212
-
213
- // for (const channel of channels) {
214
- // try {
215
- // const name = deviceInfo?.name || `Channel ${channel}`;
216
- // const uid = deviceInfo?.uid;
217
- // const isBattery = !!(abilities?.battery?.ver ?? 0);
218
-
219
- // const nativeId = this.buildNativeId(channel, uid, isBattery);
220
- // const interfaces = [ScryptedInterface.VideoCamera];
221
- // if (isBattery) {
222
- // interfaces.push(ScryptedInterface.Battery);
223
- // }
224
- // const type = abilities.supportDoorbellLight ? ScryptedDeviceType.Doorbell : ScryptedDeviceType.Camera;
225
-
226
- // const device: Device = {
227
- // nativeId,
228
- // name,
229
- // providerNativeId: this.nativeId,
230
- // interfaces,
231
- // type,
232
- // info: {
233
- // manufacturer: 'Reolink',
234
- // model: channelInfo?.typeInfo,
235
- // serialNumber: uid,
236
- // }
237
- // };
238
-
239
- // this.channelToNativeIdMap.set(channel, nativeId);
240
-
241
- // if (sdk.deviceManager.getNativeIds().includes(nativeId)) {
242
- // continue;
243
- // }
244
-
245
- // if (this.discoveredDevices.has(nativeId)) {
246
- // continue;
247
- // }
248
-
249
- // this.discoveredDevices.set(nativeId, {
250
- // device,
251
- // description: `${name} (Channel ${channel})`,
252
- // rtspChannel: channel,
253
- // deviceData: devicesData[channel],
254
- // });
255
-
256
- // logger.debug(`Discovered channel ${channel}: ${name}`);
257
- // } catch (e: any) {
258
- // logger.debug(`Error processing channel ${channel}: ${e?.message || String(e)}`);
259
- // }
211
+ const multifocalInfo = await api.getDualLensChannelInfo();
212
+
213
+ logger.log(`Multichannel info: ${JSON.stringify(multifocalInfo)}`);
214
+
215
+ // if (channelNum === 2) {
216
+
260
217
  // }
261
218
 
219
+ for (const channel of channels) {
220
+ // try {
221
+ // const name = deviceInfo?.name || `Channel ${channel}`;
222
+ // const uid = deviceInfo?.uid;
223
+ // const isBattery = !!(abilities?.battery?.ver ?? 0);
224
+
225
+ // const nativeId = this.buildNativeId(channel, uid, isBattery);
226
+ // const interfaces = [ScryptedInterface.VideoCamera];
227
+ // if (isBattery) {
228
+ // interfaces.push(ScryptedInterface.Battery);
229
+ // }
230
+ // const type = abilities.supportDoorbellLight ? ScryptedDeviceType.Doorbell : ScryptedDeviceType.Camera;
231
+
232
+ // const device: Device = {
233
+ // nativeId,
234
+ // name,
235
+ // providerNativeId: this.nativeId,
236
+ // interfaces,
237
+ // type,
238
+ // info: {
239
+ // manufacturer: 'Reolink',
240
+ // model: channelInfo?.typeInfo,
241
+ // serialNumber: uid,
242
+ // }
243
+ // };
244
+
245
+ // this.channelToNativeIdMap.set(channel, nativeId);
246
+
247
+ // if (sdk.deviceManager.getNativeIds().includes(nativeId)) {
248
+ // continue;
249
+ // }
250
+
251
+ // if (this.discoveredDevices.has(nativeId)) {
252
+ // continue;
253
+ // }
254
+
255
+ // this.discoveredDevices.set(nativeId, {
256
+ // device,
257
+ // description: `${name} (Channel ${channel})`,
258
+ // rtspChannel: channel,
259
+ // deviceData: devicesData[channel],
260
+ // });
261
+
262
+ // logger.debug(`Discovered channel ${channel}: ${name}`);
263
+ // } catch (e: any) {
264
+ // logger.debug(`Error processing channel ${channel}: ${e?.message || String(e)}`);
265
+ // }
266
+ }
267
+
262
268
  // logger.log(`Channel discovery completed. ${JSON.stringify({ devicesData, channels })}`);
263
269
  } catch (e) {
264
270
  logger.error('Failed to sync entities from remote', e);
@@ -391,7 +397,7 @@ export class ReolinkNativeMultiFocalDevice extends BaseBaichuanClass implements
391
397
 
392
398
  const nativeId = this.channelToNativeIdMap.get(channel);
393
399
  if (!nativeId) {
394
- logger.error(`No camera found for channel ${channel}, ignoring event`);
400
+ logger.debug(`No camera found for channel ${channel}, ignoring event`);
395
401
  return;
396
402
  }
397
403
 
@@ -413,9 +419,9 @@ export class ReolinkNativeMultiFocalDevice extends BaseBaichuanClass implements
413
419
  await this.subscribeToEvents();
414
420
  }
415
421
 
416
- private async runNvrDiagnostics(): Promise<void> {
422
+ private async runDiagnostics(): Promise<void> {
417
423
  const logger = this.getBaichuanLogger();
418
- logger.log(`Starting NVR diagnostics...`);
424
+ logger.log(`Starting Multifocal diagnostics...`);
419
425
 
420
426
  try {
421
427
  const { ipAddress, username, password } = this.storageSettings.values;
@@ -423,23 +429,12 @@ export class ReolinkNativeMultiFocalDevice extends BaseBaichuanClass implements
423
429
  throw new Error('Missing device credentials');
424
430
  }
425
431
 
426
- const { ReolinkCgiApi } = await import("@apocaliss92/reolink-baichuan-js");
427
- const cgiApi = new ReolinkCgiApi({
428
- host: ipAddress,
429
- username,
430
- password,
431
- });
432
+ const api = await this.ensureBaichuanClient();
432
433
 
433
- await cgiApi.login();
434
-
435
- const diagnostics = await cgiApi.collectNvrDiagnostics({
436
- logger: this.console,
437
- });
434
+ const multifocalDiagnostics = await api.collectMultifocalDiagnostics(logger);
438
435
 
439
436
  logger.log(`NVR diagnostics completed successfully.`);
440
-
441
- // Print diagnostics to console
442
- cgiApi.printNvrDiagnostics(diagnostics, this.console);
437
+ logger.log(JSON.stringify(multifocalDiagnostics));
443
438
  } catch (e) {
444
439
  logger.error('Failed to run NVR diagnostics', e);
445
440
  throw e;
package/src/nvr.ts CHANGED
@@ -208,7 +208,7 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
208
208
  const targetCamera = nativeId ? this.cameraNativeMap.get(nativeId) : undefined;
209
209
 
210
210
  if (!targetCamera) {
211
- logger.info(`No camera found for channel ${channel}, ignoring event`);
211
+ logger.debug(`No camera found for channel ${channel}, ignoring event`);
212
212
  return;
213
213
  }
214
214
 
@@ -262,18 +262,13 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
262
262
  }
263
263
 
264
264
  async subscribeToAllEvents(): Promise<void> {
265
- const logger = this.getBaichuanLogger();
266
265
  const eventSource = this.storageSettings.values.eventSource || 'Native';
267
266
 
268
- // Only subscribe if Native is selected
269
267
  if (eventSource !== 'Native') {
270
268
  await this.unsubscribeFromAllEvents();
271
- return;
269
+ } else {
270
+ await super.subscribeToEvents();
272
271
  }
273
-
274
- // Use base class implementation
275
- await super.subscribeToEvents();
276
- logger.log('Subscribed to all events for NVR cameras');
277
272
  }
278
273
 
279
274
  private async runNvrDiagnostics(): Promise<void> {
@@ -288,7 +283,7 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
288
283
  });
289
284
 
290
285
  logger.log(`NVR diagnostics completed successfully.`);
291
-
286
+
292
287
  cgiApi.printNvrDiagnostics(diagnostics, this.console);
293
288
  } catch (e) {
294
289
  logger.error('Failed to run NVR diagnostics', e);
@@ -340,8 +335,12 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
340
335
  }
341
336
 
342
337
  async init() {
343
- const api = await this.ensureClient();
344
338
  const logger = this.getBaichuanLogger();
339
+
340
+ // Ensure both APIs are ready before proceeding
341
+ const api = await this.ensureClient();
342
+ await this.ensureBaichuanClient();
343
+
345
344
  await this.updateDeviceInfo();
346
345
 
347
346
  // Initialize event subscriptions based on selected source
@@ -473,12 +472,40 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
473
472
  }
474
473
 
475
474
  async syncEntitiesFromRemote() {
476
- const api = await this.ensureClient();
477
475
  const logger = this.getBaichuanLogger();
476
+
477
+ // Ensure both APIs are ready before syncing
478
+ const api = await this.ensureClient();
479
+ const baichuanApi = await this.ensureBaichuanClient();
480
+
481
+ // Wait for Baichuan connection to be fully established
482
+ if (baichuanApi?.client) {
483
+ // Check if already connected
484
+ if (!baichuanApi.client.isSocketConnected()) {
485
+ logger.debug('Waiting for Baichuan connection to be established...');
486
+ // Wait up to 5 seconds for connection
487
+ let attempts = 0;
488
+ while (!baichuanApi.client.isSocketConnected() && attempts < 50) {
489
+ await new Promise(resolve => setTimeout(resolve, 100));
490
+ attempts++;
491
+ }
492
+ if (!baichuanApi.client.isSocketConnected()) {
493
+ logger.warn('Baichuan connection not established after waiting, proceeding anyway');
494
+ } else {
495
+ logger.debug('Baichuan connection established');
496
+ }
497
+ }
498
+ }
478
499
 
479
500
  const { devicesData, channels } = await api.getDevicesInfo();
501
+
502
+ if (!channels.length) {
503
+ logger.debug(`No channels found, ${JSON.stringify({ devicesData, channels })}`);
504
+ return;
505
+ }
506
+
480
507
  logger.log(`Sync entities from remote for ${channels.length} channels`);
481
- // Process each channel that was successfully discovered
508
+
482
509
  for (const channel of channels) {
483
510
  try {
484
511
  const { channelStatus, channelInfo, abilities } = devicesData[channel];
@@ -10,9 +10,7 @@ import sdk, {
10
10
  type RequestMediaStreamOptions,
11
11
  type ResponseMediaStreamOptions,
12
12
  } from "@scrypted/sdk";
13
-
14
13
  import type { UrlMediaStreamOptions } from "../../scrypted/plugins/rtsp/src/rtsp";
15
- import { ReolinkNativeNvrDevice } from "./nvr";
16
14
 
17
15
  export interface StreamManagerOptions {
18
16
  /**
@@ -60,145 +58,6 @@ export function parseStreamProfileFromId(id: string | undefined): StreamProfile
60
58
  return;
61
59
  }
62
60
 
63
- /**
64
- * Check if a stream ID represents a native Baichuan stream (prefixed with "native_")
65
- */
66
- export function isNativeStreamId(id: string | undefined): boolean {
67
- return id?.startsWith('native_') ?? false;
68
- }
69
-
70
- export async function fetchVideoStreamOptionsFromApi(
71
- client: ReolinkBaichuanApi,
72
- channel: number,
73
- logger: Console,
74
- ): Promise<UrlMediaStreamOptions[]> {
75
- const streamMetadata = await client.getStreamMetadata(channel);
76
-
77
- const streams: UrlMediaStreamOptions[] = [];
78
- const list = streamMetadata?.streams || [];
79
-
80
- for (const stream of list) {
81
- const profile = stream.profile as StreamProfile;
82
- const codec = String(stream.videoEncType || '').includes('264')
83
- ? 'h264'
84
- : String(stream.videoEncType || '').includes('265')
85
- ? 'h265'
86
- : String(stream.videoEncType || '').toLowerCase();
87
-
88
- streams.push({
89
- name: `Native ${profile}`,
90
- id: `native_${profile}`,
91
- container: 'rtp',
92
- video: { codec, width: stream.width, height: stream.height },
93
- url: ``,
94
- });
95
- }
96
-
97
- return streams;
98
- }
99
-
100
- export async function buildVideoStreamOptions(
101
- props: {
102
- client: ReolinkBaichuanApi,
103
- ipAddress: string,
104
- cachedNetPort: { rtsp?: { port?: number; enable?: number }; rtmp?: { port?: number; enable?: number } },
105
- nvrDevice?: ReolinkNativeNvrDevice,
106
- rtspChannel: number,
107
- logger: Console
108
- },
109
- ): Promise<UrlMediaStreamOptions[]> {
110
- const { client, ipAddress, cachedNetPort, rtspChannel, logger, nvrDevice } = props;
111
- const rtspStreams: UrlMediaStreamOptions[] = [];
112
- const rtmpStreams: UrlMediaStreamOptions[] = [];
113
-
114
- // Use cached net port if provided, otherwise fetch it
115
- const netPort = cachedNetPort || await client.getNetPort();
116
- const rtspEnabled = netPort.rtsp?.enable === 1;
117
- const rtmpEnabled = netPort.rtmp?.enable === 1;
118
- const rtspPort = netPort.rtsp?.port ?? 554;
119
- const rtmpPort = netPort.rtmp?.port ?? 1935;
120
-
121
- // Get stream metadata to build options
122
- const streamMetadata = await client.getStreamMetadata(rtspChannel);
123
- const list = streamMetadata?.streams || [];
124
-
125
- for (const stream of list) {
126
- const profile = stream.profile as StreamProfile;
127
- const codec = String(stream.videoEncType || '').includes('264')
128
- ? 'h264'
129
- : String(stream.videoEncType || '').includes('265')
130
- ? 'h265'
131
- : String(stream.videoEncType || '').toLowerCase();
132
-
133
- // Build RTSP URL if enabled (RTSP doesn't support ext stream, only main and sub)
134
- if (rtspEnabled && profile !== 'ext') {
135
- // RTSP format: rtsp://ip:port/h264Preview_XX_profile
136
- // XX is 1-based channel with 2-digit padding
137
- const channelStr = String(rtspChannel + 1).padStart(2, '0');
138
- const profileStr = profile === 'main' ? 'main' : 'sub';
139
- const rtspPath = `/h264Preview_${channelStr}_${profileStr}`;
140
- const rtspId = `h264Preview_${channelStr}_${profileStr}`;
141
-
142
- rtspStreams.push({
143
- name: `RTSP ${rtspId}`,
144
- id: rtspId,
145
- container: 'rtsp',
146
- video: { codec, width: stream.width, height: stream.height },
147
- url: `rtsp://${ipAddress}:${rtspPort}${rtspPath}`,
148
- });
149
- }
150
-
151
- // Build RTMP URL if enabled (RTMP supports main, sub, and ext streams)
152
- if (rtmpEnabled) {
153
- // RTMP format: /bcs/channelX_stream.bcs?channel=X&stream=stream_type&user=username&password=password
154
- // Based on reolink_aio api.py line 3295-3298:
155
- // - stream in path is "main", "sub", or "ext" (not "main.bcs")
156
- // - stream_type in query: 0 for main/ext, 1 for sub
157
- // - credentials: user and password as query parameters
158
- const streamName = profile === 'main' ? 'main' : profile === 'sub' ? 'sub' : 'ext';
159
- const streamType = profile === 'sub' ? 1 : 0; // 0 for main/ext, 1 for sub
160
- const rtmpId = `${streamName}.bcs`; // ID for Scrypted (main.bcs, sub.bcs, ext.bcs)
161
-
162
- // Use channel directly (0-based) in path, matching reolink_aio behavior
163
- const rtmpPath = `/bcs/channel${rtspChannel}_${streamName}.bcs`;
164
- const rtmpUrl = new URL(`rtmp://${ipAddress}:${rtmpPort}${rtmpPath}`);
165
- const params = rtmpUrl.searchParams;
166
- params.set('channel', rtspChannel.toString());
167
- params.set('stream', streamType.toString());
168
- // Credentials will be added by addRtspCredentials as user/password query params
169
-
170
- rtmpStreams.push({
171
- name: `RTMP ${rtmpId}`,
172
- id: rtmpId,
173
- container: 'rtmp',
174
- video: { codec, width: stream.width, height: stream.height },
175
- url: rtmpUrl.toString(),
176
- });
177
- }
178
- }
179
-
180
-
181
- const nativeStreams = await fetchVideoStreamOptionsFromApi(client, rtspChannel, logger);
182
-
183
- let streams: UrlMediaStreamOptions[] = [];
184
-
185
- if (nvrDevice && nvrDevice.info.model === 'HOMEHUB') {
186
- streams = [
187
- ...nativeStreams,
188
- ...rtspStreams,
189
- ...rtmpStreams,
190
- ];
191
- } else {
192
- streams = [
193
- ...rtspStreams,
194
- ...rtmpStreams,
195
- ...nativeStreams,
196
- ];
197
- }
198
-
199
- return streams;
200
- }
201
-
202
61
  export function selectStreamOption(
203
62
  vsos: UrlMediaStreamOptions[] | undefined,
204
63
  request: RequestMediaStreamOptions,
package/src/utils.ts CHANGED
@@ -1,6 +1,25 @@
1
1
  import type { DeviceCapabilities, ReolinkDeviceInfo } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
2
2
  import { DeviceBase, ScryptedDeviceType, ScryptedInterface } from "@scrypted/sdk";
3
3
 
4
+ /**
5
+ * Enumeration of operation types that may require specific channel assignments
6
+ */
7
+ export enum OperationChannelType {
8
+ PAN = 'pan',
9
+ TILT = 'tilt',
10
+ ZOOM = 'zoom',
11
+ INTERCOM = 'intercom',
12
+ GOTO = 'goto',
13
+ PRESET = 'preset',
14
+ PATROL = 'patrol',
15
+ TRACK = 'track',
16
+ }
17
+
18
+ /**
19
+ * Type for channel-specific operation mappings
20
+ */
21
+ export type OperationChannelMap = Partial<Record<OperationChannelType, number>>;
22
+
4
23
  export const nvrSuffix = `-nvr`;
5
24
  export const batteryCameraSuffix = `-battery-cam`;
6
25
  export const multifocalSuffix = `-multifocal`;