@apocaliss92/scrypted-reolink-native 0.1.36 → 0.1.38

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@apocaliss92/scrypted-reolink-native",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
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",
@@ -1,4 +1,4 @@
1
- import type { ReolinkBaichuanApi, ReolinkSimpleEvent } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
1
+ import type { BaichuanClientOptions, ReolinkBaichuanApi, ReolinkSimpleEvent } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
2
2
  import { ScryptedDeviceBase } from "@scrypted/sdk";
3
3
  import { createBaichuanApi, type BaichuanTransport } from "./connect";
4
4
 
@@ -8,8 +8,8 @@ export interface BaichuanConnectionConfig {
8
8
  password: string;
9
9
  uid?: string;
10
10
  transport: BaichuanTransport;
11
- logger: Console;
12
11
  debugOptions?: any;
12
+ udpDiscoveryMethod?: BaichuanClientOptions["udpDiscoveryMethod"];
13
13
  }
14
14
 
15
15
  export interface BaichuanConnectionCallbacks {
@@ -259,6 +259,7 @@ export abstract class BaseBaichuanClass extends ScryptedDeviceBase {
259
259
  uid: config.uid,
260
260
  logger,
261
261
  debugOptions: config.debugOptions,
262
+ udpDiscoveryMethod: config.udpDiscoveryMethod,
262
263
  },
263
264
  transport: config.transport,
264
265
  });
@@ -128,7 +128,22 @@ export class ReolinkNativeBatteryCamera extends CommonCameraMixin {
128
128
  }
129
129
  } catch (e) {
130
130
  // Silently ignore errors in sleep check to avoid spam
131
- this.getBaichuanLogger().debug('Error in checkSleepingState:', e);
131
+ this.getBaichuanLogger().debug('Error in updateSleepingState:', e);
132
+ }
133
+ }
134
+
135
+ async updateOnlineState(isOnline: boolean): Promise<void> {
136
+ try {
137
+ if (this.isDebugEnabled()) {
138
+ this.getBaichuanLogger().debug('updateOnlineState result:', isOnline);
139
+ }
140
+
141
+ if (isOnline !== this.online) {
142
+ this.online = isOnline;
143
+ }
144
+ } catch (e) {
145
+ // Silently ignore errors in sleep check to avoid spam
146
+ this.getBaichuanLogger().debug('Error in updateOnlineState:', e);
132
147
  }
133
148
  }
134
149
 
package/src/common.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { DeviceCapabilities, PtzCommand, PtzPreset, ReolinkBaichuanApi, ReolinkSimpleEvent, ReolinkSupportedStream, StreamProfile, StreamSamplingSelection } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
1
+ import type { BaichuanClientOptions, DeviceCapabilities, PtzCommand, PtzPreset, ReolinkBaichuanApi, ReolinkSimpleEvent, ReolinkSupportedStream, StreamProfile, StreamSamplingSelection } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
2
2
  import sdk, { BinarySensor, Brightness, Camera, Device, DeviceProvider, Intercom, MediaObject, MediaStreamUrl, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, OnOff, PanTiltZoom, PanTiltZoomCommand, Reboot, RequestMediaStreamOptions, RequestPictureOptions, ResponsePictureOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera, VideoClip, VideoClipOptions, VideoClips, VideoClipThumbnailOptions, VideoTextOverlay, VideoTextOverlays } from "@scrypted/sdk";
3
3
  import { StorageSettings } from "@scrypted/sdk/storage-settings";
4
4
  import path from 'path';
@@ -24,7 +24,7 @@ import {
24
24
  selectStreamOption,
25
25
  StreamManager
26
26
  } from "./stream-utils";
27
- import { floodlightSuffix, getDeviceInterfaces, getVideoClipWebhookUrls, pirSuffix, recordingFileToVideoClip, sanitizeFfmpegOutput, sirenSuffix, updateDeviceInfo, vodSearchResultsToVideoClips } from "./utils";
27
+ import { floodlightSuffix, getDeviceInterfaces, getVideoClipWebhookUrls, pirSuffix, recordingsToVideoClips, sanitizeFfmpegOutput, sirenSuffix, updateDeviceInfo, vodSearchResultsToVideoClips } from "./utils";
28
28
 
29
29
  export type CameraType = 'battery' | 'regular' | 'multi-focal' | 'multi-focal-battery';
30
30
 
@@ -300,6 +300,17 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
300
300
  await this.credentialsChanged();
301
301
  }
302
302
  },
303
+ discoveryMethod: {
304
+ title: 'Discovery Method',
305
+ description: 'UDP discovery method for battery cameras (BCUDP).',
306
+ type: 'string',
307
+ choices: ['local', 'remote', 'map', 'relay'],
308
+ defaultValue: 'local',
309
+ hide: true,
310
+ onPut: async () => {
311
+ await this.credentialsChanged();
312
+ }
313
+ },
303
314
  debugLogs: {
304
315
  title: 'Debug logs',
305
316
  type: 'boolean',
@@ -664,6 +675,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
664
675
  private streamManagerRestartTimeout: NodeJS.Timeout | undefined;
665
676
  private videoClipsAutoLoadInterval: NodeJS.Timeout | undefined;
666
677
  private videoClipsAutoLoadInProgress: boolean = false;
678
+ private videoClipsAutoLoadMode: boolean = false;
667
679
 
668
680
  constructor(
669
681
  nativeId: string,
@@ -698,7 +710,8 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
698
710
  return [];
699
711
  }
700
712
 
701
- if (this.isBattery && this.sleeping) {
713
+ // Skip sleeping check during auto-load to allow auto-load to start for battery cameras
714
+ if (!this.videoClipsAutoLoadMode && this.isBattery && this.sleeping) {
702
715
  const logger = this.getBaichuanLogger();
703
716
  logger.debug('getVideoClips: disabled for battery devices');
704
717
  return [];
@@ -728,13 +741,14 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
728
741
 
729
742
  const start = new Date(startMs);
730
743
  const end = new Date(endMs);
731
- // Use UTC to match API's dateToReolinkTime conversion
732
- start.setUTCHours(0, 0, 0, 0);
744
+ start.setHours(0, 0, 0, 0);
733
745
 
734
746
  try {
735
747
  const { clipsSource } = this.storageSettings.values;
736
748
  const useNvr = clipsSource === "NVR" && this.nvrDevice;
737
749
 
750
+ const api = await this.ensureClient();
751
+
738
752
  if (useNvr) {
739
753
  // Fetch from NVR using listEnrichedVodFiles (library handles parsing correctly)
740
754
  const channel = this.storageSettings.values.rtspChannel ?? 0;
@@ -742,106 +756,49 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
742
756
  // Use listEnrichedVodFiles which properly parses filenames and extracts detection info
743
757
  logger.debug(`[NVR VOD] Searching for video clips: channel=${channel}, start=${start.toISOString()}, end=${end.toISOString()}`);
744
758
  // Filter to only include recordings within the requested time window
745
- const enrichedRecordings = await this.nvrDevice.listEnrichedVodFiles({
759
+ const enrichedRecordings = await api.listNvrRecordings({
746
760
  channel,
747
761
  start,
748
762
  end,
749
763
  streamType: "main",
750
- autoSearchByDay: false, // Disable autoSearchByDay to avoid searching past days
751
- bypassCache: false,
752
764
  });
753
765
 
754
766
  logger.debug(`[NVR VOD] Found ${enrichedRecordings.length} enriched recordings from NVR`);
755
767
 
756
- // Log sample of enriched recordings to see what the library returned
757
- if (enrichedRecordings.length > 0) {
758
- const sampleSize = Math.min(3, enrichedRecordings.length);
759
- for (let i = 0; i < sampleSize; i++) {
760
- const rec = enrichedRecordings[i];
761
- logger.debug(`[NVR VOD] Sample enriched recording ${i + 1}/${enrichedRecordings.length}:`, {
762
- fileName: rec.fileName,
763
- startTimeMs: rec.startTimeMs,
764
- endTimeMs: rec.endTimeMs,
765
- durationMs: rec.durationMs,
766
- hasPerson: rec.hasPerson,
767
- hasVehicle: rec.hasVehicle,
768
- hasAnimal: rec.hasAnimal,
769
- hasFace: rec.hasFace,
770
- hasMotion: rec.hasMotion,
771
- hasDoorbell: rec.hasDoorbell,
772
- hasPackage: rec.hasPackage,
773
- recordType: rec.recordType,
774
- parsedFileName: rec.parsedFileName ? {
775
- start: rec.parsedFileName.start?.toISOString(),
776
- end: rec.parsedFileName.end?.toISOString(),
777
- flags: rec.parsedFileName.flags,
778
- } : null,
779
- });
780
- }
781
- }
782
-
783
- // Convert enriched recordings to VideoClip array
784
- const clips: VideoClip[] = [];
785
-
786
- for (const rec of enrichedRecordings) {
787
- // Log detection flags before conversion
788
- const flags = {
789
- hasPerson: 'hasPerson' in rec ? rec.hasPerson : false,
790
- hasVehicle: 'hasVehicle' in rec ? rec.hasVehicle : false,
791
- hasAnimal: 'hasAnimal' in rec ? rec.hasAnimal : false,
792
- hasFace: 'hasFace' in rec ? rec.hasFace : false,
793
- hasMotion: 'hasMotion' in rec ? rec.hasMotion : false,
794
- hasDoorbell: 'hasDoorbell' in rec ? rec.hasDoorbell : false,
795
- hasPackage: 'hasPackage' in rec ? rec.hasPackage : false,
796
- recordType: rec.recordType || 'none',
797
- };
798
- logger.debug(`[NVR VOD] Processing recording: fileName=${rec.fileName}, flags=${JSON.stringify(flags)}`);
799
-
800
- const clip = await recordingFileToVideoClip(rec, {
801
- fallbackStart: start,
802
- logger,
803
- plugin: this,
804
- deviceId: this.id,
805
- useWebhook: true,
806
- });
807
-
808
- // Log detection classes in the final clip
809
- logger.debug(`[NVR VOD] Generated clip: id=${clip.id}, detectionClasses=${clip.detectionClasses?.join(',') || 'none'}`);
810
- clips.push(clip);
811
- }
768
+ // Convert enriched recordings to VideoClip array using the shared parser
769
+ const clips = await recordingsToVideoClips(enrichedRecordings, {
770
+ fallbackStart: start,
771
+ logger,
772
+ plugin: this,
773
+ deviceId: this.id,
774
+ useWebhook: true,
775
+ count,
776
+ });
812
777
 
813
- // Apply count limit if specified
814
- const finalClips = count ? clips.slice(0, count) : clips;
815
- logger.debug(`[NVR VOD] Converted ${finalClips.length} video clips (limit: ${count || 'none'})`);
778
+ logger.debug(`[NVR VOD] Converted ${clips.length} video clips (limit: ${count || 'none'})`);
816
779
 
817
- return finalClips;
780
+ return clips;
818
781
  } else {
819
- // Fetch directly from device using Baichuan API
820
- const api = await this.ensureClient();
821
-
822
- const recordings = await api.listEnrichedRecordingsByTime({
782
+ const recordings = await api.listDeviceRecordings({
823
783
  start,
824
784
  end,
825
785
  count,
826
786
  channel: this.storageSettings.values.rtspChannel,
827
787
  streamType: 'mainStream',
828
788
  httpFallback: false,
829
- fetchRtmpUrls: true
789
+ fetchRtmpUrls: false
830
790
  });
831
791
 
832
- const clips: VideoClip[] = [];
833
-
834
- for (const rec of recordings) {
835
- const clip = await recordingFileToVideoClip(rec, {
836
- fallbackStart: start,
837
- api,
838
- logger,
839
- plugin: this,
840
- deviceId: this.id,
841
- useWebhook: true,
842
- });
843
- clips.push(clip);
844
- }
792
+ // Convert recordings to VideoClip array using the shared parser
793
+ const clips = await recordingsToVideoClips(recordings, {
794
+ fallbackStart: start,
795
+ api,
796
+ logger,
797
+ plugin: this,
798
+ deviceId: this.id,
799
+ useWebhook: true,
800
+ count,
801
+ });
845
802
 
846
803
  logger.debug(`Videoclips found: ${clips.length}`);
847
804
 
@@ -1166,12 +1123,12 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
1166
1123
 
1167
1124
  if (useNvr) {
1168
1125
  logger.debug(`[getVideoClipRtmpUrl] Using NVR API for fileId="${fileId}", forThumbnail=${forThumbnail}`);
1169
- const nvrApi = await this.nvrDevice.ensureClient();
1126
+ const api = await this.ensureClient();
1170
1127
  const channel = this.storageSettings.values.rtspChannel ?? 0;
1171
1128
 
1172
1129
  try {
1173
1130
  logger.debug(`[getVideoClipRtmpUrl] Trying getVodUrl with Download requestType...`);
1174
- const url = await nvrApi.getVodUrl(fileId, channel, {
1131
+ const url = await api.getVodUrl(fileId, channel, {
1175
1132
  requestType: "Download",
1176
1133
  streamType: "main",
1177
1134
  });
@@ -1250,6 +1207,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
1250
1207
  const logger = this.getBaichuanLogger();
1251
1208
 
1252
1209
  this.videoClipsAutoLoadInProgress = true;
1210
+ this.videoClipsAutoLoadMode = true;
1253
1211
 
1254
1212
  try {
1255
1213
  const daysToPreload = this.storageSettings.values.videoclipsDaysToPreload ?? 1;
@@ -1315,6 +1273,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
1315
1273
  logger.error('Error during auto-loading video clips:', e);
1316
1274
  } finally {
1317
1275
  this.videoClipsAutoLoadInProgress = false;
1276
+ this.videoClipsAutoLoadMode = false;
1318
1277
  }
1319
1278
  }
1320
1279
 
@@ -1325,7 +1284,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
1325
1284
 
1326
1285
  // BaseBaichuanClass abstract methods implementation
1327
1286
  protected getConnectionConfig(): BaichuanConnectionConfig {
1328
- const { ipAddress, username, password, uid } = this.storageSettings.values;
1287
+ const { ipAddress, username, password, uid, discoveryMethod } = this.storageSettings.values;
1329
1288
  const debugOptions = this.getBaichuanDebugOptions();
1330
1289
  const normalizedUid = this.isBattery ? normalizeUid(uid) : undefined;
1331
1290
 
@@ -1333,15 +1292,14 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
1333
1292
  throw new Error('UID is required for battery cameras (BCUDP)');
1334
1293
  }
1335
1294
 
1336
- const logger = this.getBaichuanLogger();
1337
1295
  return {
1338
1296
  host: ipAddress,
1339
1297
  username,
1340
1298
  password,
1341
1299
  uid: normalizedUid,
1342
1300
  transport: this.protocol,
1343
- logger,
1344
1301
  debugOptions,
1302
+ udpDiscoveryMethod: discoveryMethod as BaichuanClientOptions["udpDiscoveryMethod"],
1345
1303
  };
1346
1304
  }
1347
1305
 
@@ -1424,12 +1382,17 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
1424
1382
 
1425
1383
  try {
1426
1384
  const api = await this.ensureClient();
1385
+ const { ipAddress, username, password } = this.storageSettings.values;
1427
1386
 
1428
1387
  const result = await api.runAllDiagnosticsConsecutively({
1388
+ host: ipAddress,
1389
+ username,
1390
+ password,
1429
1391
  outDir: outputPath,
1430
1392
  channel,
1431
1393
  durationSeconds,
1432
1394
  selection,
1395
+ api,
1433
1396
  });
1434
1397
 
1435
1398
  logger.log(`Diagnostics completed successfully. Output directory: ${result.runDir}`);
@@ -2497,6 +2460,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
2497
2460
  logger.warn('Failed to connect/refresh during init', e);
2498
2461
  }
2499
2462
  }
2463
+ this.storageSettings.settings.socketApiDebugLogs.hide = !!this.nvrDevice;
2500
2464
  this.storageSettings.settings.clipsSource.hide = !this.nvrDevice;
2501
2465
  this.storageSettings.settings.clipsSource.defaultValue = this.nvrDevice ? "NVR" : "Device";
2502
2466
 
@@ -2513,6 +2477,9 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
2513
2477
  this.storageSettings.settings.widerChannel.hide = !this.isMultiFocal;
2514
2478
  this.storageSettings.settings.teleChannel.hide = !this.isMultiFocal;
2515
2479
 
2480
+ this.storageSettings.settings.uid.hide = !this.isBattery;
2481
+ this.storageSettings.settings.discoveryMethod.hide = !this.isBattery;
2482
+
2516
2483
  if (this.isBattery && !this.storageSettings.values.mixinsSetup) {
2517
2484
  try {
2518
2485
  const device = sdk.systemManager.getDeviceById<Settings>(this.id);
package/src/connect.ts CHANGED
@@ -9,6 +9,7 @@ export type BaichuanConnectInputs = {
9
9
  uid?: string;
10
10
  logger?: Console;
11
11
  debugOptions?: BaichuanClientOptions['debugOptions'];
12
+ udpDiscoveryMethod?: "local" | "remote" | "map" | "relay";
12
13
  };
13
14
 
14
15
  export function normalizeUid(uid?: string): string | undefined {
@@ -81,6 +82,7 @@ export async function createBaichuanApi(props: {
81
82
  transport: "udp",
82
83
  uid,
83
84
  idleDisconnect: true,
85
+ udpDiscoveryMethod: inputs.udpDiscoveryMethod,
84
86
  });
85
87
  attachErrorHandler(api);
86
88
  return api;
package/src/main.ts CHANGED
@@ -200,6 +200,7 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
200
200
  device.storageSettings.values.ipAddress = ipAddress;
201
201
  device.storageSettings.values.capabilities = capabilities;
202
202
  device.storageSettings.values.uid = uid;
203
+ device.storageSettings.values.discoveryMethod = detection.udpDiscoveryMethod;
203
204
 
204
205
  return nativeId;
205
206
  }