@apocaliss92/scrypted-reolink-native 0.1.36 → 0.1.37

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.37",
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
  });
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
  }
package/src/nvr.ts CHANGED
@@ -1,9 +1,10 @@
1
- import type { DeviceInfoResponse, EnrichedRecordingFile, EventsResponse, ReolinkBaichuanApi, ReolinkCgiApi, ReolinkSimpleEvent } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
1
+ import type { DeviceInfoResponse, EnrichedRecordingFile, EventsResponse, ListNvrRecordingsParams, ReolinkBaichuanApi, ReolinkCgiApi, ReolinkSimpleEvent } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
2
2
  import sdk, { AdoptDevice, Device, DeviceDiscovery, DeviceProvider, DiscoveredDevice, Reboot, ScryptedDeviceType, ScryptedInterface, Setting, Settings, SettingValue } from "@scrypted/sdk";
3
3
  import { StorageSettings } from "@scrypted/sdk/storage-settings";
4
4
  import { BaseBaichuanClass, type BaichuanConnectionCallbacks, type BaichuanConnectionConfig } from "./baichuan-base";
5
5
  import { ReolinkNativeCamera } from "./camera";
6
6
  import { ReolinkNativeBatteryCamera } from "./camera-battery";
7
+ import { convertDebugLogsToApiOptions, getApiRelevantDebugLogs, getDebugLogChoices } from "./debug-options";
7
8
  import { normalizeUid } from "./connect";
8
9
  import ReolinkNativePlugin from "./main";
9
10
  import { getDeviceInterfaces, updateDeviceInfo } from "./utils";
@@ -44,8 +45,8 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
44
45
  onPut: async () => await this.reinit()
45
46
  },
46
47
  diagnosticsRun: {
47
- subgroup: 'Diagnostics',
48
- title: 'Run NVR Diagnostics',
48
+ subgroup: 'Advanced',
49
+ title: 'Run Diagnostics',
49
50
  description: 'Collect NVR diagnostics and display results in logs.',
50
51
  type: 'button',
51
52
  immediate: true,
@@ -53,6 +54,47 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
53
54
  await this.runNvrDiagnostics();
54
55
  },
55
56
  },
57
+ socketApiDebugLogs: {
58
+ subgroup: 'Advanced',
59
+ title: 'Socket API Debug Logs',
60
+ description: 'Enable specific debug logs.',
61
+ multiple: true,
62
+ combobox: true,
63
+ immediate: true,
64
+ defaultValue: [],
65
+ choices: getDebugLogChoices(),
66
+ onPut: async (ov, value) => {
67
+ const logger = this.getBaichuanLogger();
68
+ const oldApiOptions = getApiRelevantDebugLogs(ov || []);
69
+ const newApiOptions = getApiRelevantDebugLogs(value || []);
70
+
71
+ const oldSel = new Set(oldApiOptions);
72
+ const newSel = new Set(newApiOptions);
73
+
74
+ const changed = oldSel.size !== newSel.size || Array.from(oldSel).some((k) => !newSel.has(k));
75
+ if (changed) {
76
+ // Clear any existing timeout
77
+ if (this.debugLogsResetTimeout) {
78
+ clearTimeout(this.debugLogsResetTimeout);
79
+ this.debugLogsResetTimeout = undefined;
80
+ }
81
+
82
+ // Defer reset by 2 seconds to allow settings to settle
83
+ this.debugLogsResetTimeout = setTimeout(async () => {
84
+ this.debugLogsResetTimeout = undefined;
85
+ try {
86
+ // Force reconnection with new debug options
87
+ this.baichuanApi = undefined;
88
+ this.ensureClientPromise = undefined;
89
+ // Trigger reconnection
90
+ await this.ensureBaichuanClient();
91
+ } catch (e) {
92
+ logger.warn('Failed to reset client after debug logs change', e);
93
+ }
94
+ }, 2000);
95
+ }
96
+ },
97
+ },
56
98
  });
57
99
  plugin: ReolinkNativePlugin;
58
100
  nvrApi: ReolinkCgiApi | undefined;
@@ -71,6 +113,7 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
71
113
  private discoverDevicesPromise: Promise<DiscoveredDevice[]> | undefined;
72
114
  processing = false;
73
115
  private initReinitTimeout: NodeJS.Timeout | undefined;
116
+ private debugLogsResetTimeout: NodeJS.Timeout | undefined;
74
117
 
75
118
  constructor(nativeId: string, plugin: ReolinkNativePlugin) {
76
119
  super(nativeId);
@@ -84,22 +127,28 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
84
127
  await api.reboot();
85
128
  }
86
129
 
87
- // BaseBaichuanClass abstract methods implementation
88
130
  protected getConnectionConfig(): BaichuanConnectionConfig {
89
131
  const { ipAddress, username, password } = this.storageSettings.values;
90
132
  if (!ipAddress || !username || !password) {
91
133
  throw new Error('Missing NVR credentials');
92
134
  }
93
135
 
136
+ const debugOptions = this.getBaichuanDebugOptions();
137
+
94
138
  return {
95
139
  host: ipAddress,
96
140
  username,
97
141
  password,
98
142
  transport: 'tcp',
99
- logger: this.console,
143
+ debugOptions,
100
144
  };
101
145
  }
102
146
 
147
+ getBaichuanDebugOptions(): any | undefined {
148
+ const socketDebugLogs = this.storageSettings.values.socketApiDebugLogs || [];
149
+ return convertDebugLogsToApiOptions(socketDebugLogs);
150
+ }
151
+
103
152
  protected getConnectionCallbacks(): BaichuanConnectionCallbacks {
104
153
  return {
105
154
  onError: undefined, // Use default error handling
@@ -187,29 +236,6 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
187
236
  return this.nvrApi;
188
237
  }
189
238
 
190
- /**
191
- * List enriched VOD files (with proper parsing and detection info)
192
- * This uses the library's enrichVodFile which handles all parsing correctly
193
- */
194
- async listEnrichedVodFiles(params: {
195
- channel: number;
196
- start: Date;
197
- end: Date;
198
- streamType?: "main" | "sub";
199
- autoSearchByDay?: boolean;
200
- bypassCache?: boolean;
201
- }): Promise<Array<EnrichedRecordingFile>> {
202
- const api = await this.ensureClient();
203
- return await api.listEnrichedVodFiles({
204
- channel: params.channel,
205
- start: params.start,
206
- end: params.end,
207
- streamType: params.streamType,
208
- autoSearchByDay: params.autoSearchByDay,
209
- bypassCache: params.bypassCache,
210
- });
211
- }
212
-
213
239
  private forwardNativeEvent(ev: ReolinkSimpleEvent): void {
214
240
  const logger = this.getBaichuanLogger();
215
241
 
@@ -298,15 +324,13 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
298
324
  logger.log(`Starting NVR diagnostics...`);
299
325
 
300
326
  try {
301
- const cgiApi = await this.ensureClient();
327
+ const cgiApi = await this.ensureBaichuanClient();
302
328
 
303
329
  const diagnostics = await cgiApi.collectNvrDiagnostics({
304
330
  logger: this.console,
305
331
  });
306
332
 
307
333
  logger.log(`NVR diagnostics completed successfully.`);
308
-
309
- cgiApi.printNvrDiagnostics(diagnostics, this.console);
310
334
  } catch (e) {
311
335
  logger.error('Failed to run NVR diagnostics', e);
312
336
  throw e;
@@ -427,7 +451,7 @@ export class ReolinkNativeNvrDevice extends BaseBaichuanClass implements Setting
427
451
 
428
452
  const { ipAddress } = this.storageSettings.values;
429
453
  try {
430
- const api = await this.ensureClient();
454
+ const api = await this.ensureBaichuanClient();
431
455
  const deviceData = await api.getInfo();
432
456
 
433
457
  await updateDeviceInfo({
package/src/utils.ts CHANGED
@@ -329,6 +329,53 @@ export async function recordingFileToVideoClip(
329
329
  };
330
330
  }
331
331
 
332
+ /**
333
+ * Convert an array of RecordingFile or EnrichedRecordingFile to VideoClip array
334
+ * Uses recordingFileToVideoClip for each recording
335
+ * Handles both NVR (EnrichedRecordingFile) and device standalone (RecordingFile) cases
336
+ */
337
+ export async function recordingsToVideoClips(
338
+ recordings: (RecordingFile | EnrichedRecordingFile)[],
339
+ options: {
340
+ /** Fallback start date if recording doesn't have one */
341
+ fallbackStart: Date;
342
+ /** API instance to get playback URLs (optional, for device standalone recordings) */
343
+ api?: ReolinkBaichuanApi;
344
+ /** Logger for debug messages */
345
+ logger?: Console;
346
+ /** Plugin instance for generating webhook URLs */
347
+ plugin?: ScryptedDeviceBase;
348
+ /** Device ID for webhook URLs */
349
+ deviceId?: string;
350
+ /** Use webhook URLs instead of direct RTMP URLs */
351
+ useWebhook?: boolean;
352
+ /** Maximum number of clips to return (optional) */
353
+ count?: number;
354
+ }
355
+ ): Promise<VideoClip[]> {
356
+ const { fallbackStart, api, logger, plugin, deviceId, useWebhook, count } = options;
357
+ const clips: VideoClip[] = [];
358
+
359
+ for (const rec of recordings) {
360
+ try {
361
+ const clip = await recordingFileToVideoClip(rec, {
362
+ fallbackStart,
363
+ api,
364
+ logger,
365
+ plugin,
366
+ deviceId,
367
+ useWebhook,
368
+ });
369
+ clips.push(clip);
370
+ } catch (e) {
371
+ logger?.warn(`Failed to convert recording to video clip: fileName=${rec.fileName}`, e);
372
+ }
373
+ }
374
+
375
+ // Apply count limit if specified
376
+ return count ? clips.slice(0, count) : clips;
377
+ }
378
+
332
379
  /**
333
380
  * Generate webhook URLs for video clips
334
381
  */