@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/.vscode/settings.json +1 -1
- package/dist/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/baichuan-base.ts +3 -2
- package/src/common.ts +58 -91
- package/src/connect.ts +2 -0
- package/src/main.ts +1 -0
- package/src/nvr.ts +56 -32
- package/src/utils.ts +47 -0
package/dist/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/src/baichuan-base.ts
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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
|
-
|
|
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
|
|
780
|
+
return clips;
|
|
818
781
|
} else {
|
|
819
|
-
|
|
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:
|
|
789
|
+
fetchRtmpUrls: false
|
|
830
790
|
});
|
|
831
791
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
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
|
|
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
|
|
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: '
|
|
48
|
-
title: 'Run
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
*/
|