@apocaliss92/scrypted-reolink-native 0.1.35 → 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/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 +192 -152
- package/src/connect.ts +2 -0
- package/src/main.ts +10 -5
- package/src/nvr.ts +56 -32
- package/src/utils.ts +113 -30
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,10 +1,12 @@
|
|
|
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';
|
|
5
5
|
import fs from 'fs';
|
|
6
6
|
import crypto from 'crypto';
|
|
7
7
|
import { spawn } from 'node:child_process';
|
|
8
|
+
import http from 'http';
|
|
9
|
+
import https from 'https';
|
|
8
10
|
import type { UrlMediaStreamOptions } from "../../scrypted/plugins/rtsp/src/rtsp";
|
|
9
11
|
import { BaseBaichuanClass, type BaichuanConnectionCallbacks, type BaichuanConnectionConfig } from "./baichuan-base";
|
|
10
12
|
import { createBaichuanApi, normalizeUid, type BaichuanTransport } from "./connect";
|
|
@@ -22,7 +24,7 @@ import {
|
|
|
22
24
|
selectStreamOption,
|
|
23
25
|
StreamManager
|
|
24
26
|
} from "./stream-utils";
|
|
25
|
-
import { floodlightSuffix, getDeviceInterfaces, getVideoClipWebhookUrls, pirSuffix,
|
|
27
|
+
import { floodlightSuffix, getDeviceInterfaces, getVideoClipWebhookUrls, pirSuffix, recordingsToVideoClips, sanitizeFfmpegOutput, sirenSuffix, updateDeviceInfo, vodSearchResultsToVideoClips } from "./utils";
|
|
26
28
|
|
|
27
29
|
export type CameraType = 'battery' | 'regular' | 'multi-focal' | 'multi-focal-battery';
|
|
28
30
|
|
|
@@ -298,6 +300,17 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
298
300
|
await this.credentialsChanged();
|
|
299
301
|
}
|
|
300
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
|
+
},
|
|
301
314
|
debugLogs: {
|
|
302
315
|
title: 'Debug logs',
|
|
303
316
|
type: 'boolean',
|
|
@@ -550,7 +563,6 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
550
563
|
},
|
|
551
564
|
enableVideoclips: {
|
|
552
565
|
title: "Enable Video Clips",
|
|
553
|
-
subgroup: 'Videoclips',
|
|
554
566
|
description: "Enable video clips functionality. If disabled, getVideoClips will return empty and all other videoclip settings are ignored.",
|
|
555
567
|
type: "boolean",
|
|
556
568
|
defaultValue: false,
|
|
@@ -599,6 +611,16 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
599
611
|
this.updateVideoClipsAutoLoad();
|
|
600
612
|
},
|
|
601
613
|
},
|
|
614
|
+
videoclipsDaysToPreload: {
|
|
615
|
+
title: "Days to Preload",
|
|
616
|
+
subgroup: 'Videoclips',
|
|
617
|
+
description: "Number of days to preload video clips and thumbnails (default: 1, only today).",
|
|
618
|
+
type: "number",
|
|
619
|
+
defaultValue: 3,
|
|
620
|
+
onPut: async () => {
|
|
621
|
+
this.updateVideoClipsAutoLoad();
|
|
622
|
+
},
|
|
623
|
+
},
|
|
602
624
|
diagnosticsRun: {
|
|
603
625
|
subgroup: 'Diagnostics',
|
|
604
626
|
title: 'Run Diagnostics',
|
|
@@ -653,6 +675,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
653
675
|
private streamManagerRestartTimeout: NodeJS.Timeout | undefined;
|
|
654
676
|
private videoClipsAutoLoadInterval: NodeJS.Timeout | undefined;
|
|
655
677
|
private videoClipsAutoLoadInProgress: boolean = false;
|
|
678
|
+
private videoClipsAutoLoadMode: boolean = false;
|
|
656
679
|
|
|
657
680
|
constructor(
|
|
658
681
|
nativeId: string,
|
|
@@ -687,7 +710,8 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
687
710
|
return [];
|
|
688
711
|
}
|
|
689
712
|
|
|
690
|
-
|
|
713
|
+
// Skip sleeping check during auto-load to allow auto-load to start for battery cameras
|
|
714
|
+
if (!this.videoClipsAutoLoadMode && this.isBattery && this.sleeping) {
|
|
691
715
|
const logger = this.getBaichuanLogger();
|
|
692
716
|
logger.debug('getVideoClips: disabled for battery devices');
|
|
693
717
|
return [];
|
|
@@ -717,121 +741,64 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
717
741
|
|
|
718
742
|
const start = new Date(startMs);
|
|
719
743
|
const end = new Date(endMs);
|
|
720
|
-
|
|
721
|
-
start.setUTCHours(0, 0, 0, 0);
|
|
744
|
+
start.setHours(0, 0, 0, 0);
|
|
722
745
|
|
|
723
746
|
try {
|
|
724
747
|
const { clipsSource } = this.storageSettings.values;
|
|
725
748
|
const useNvr = clipsSource === "NVR" && this.nvrDevice;
|
|
726
749
|
|
|
750
|
+
const api = await this.ensureClient();
|
|
751
|
+
|
|
727
752
|
if (useNvr) {
|
|
728
753
|
// Fetch from NVR using listEnrichedVodFiles (library handles parsing correctly)
|
|
729
754
|
const channel = this.storageSettings.values.rtspChannel ?? 0;
|
|
730
|
-
logger.debug(`Fetching video clips from NVR for channel ${channel}`);
|
|
731
755
|
|
|
732
756
|
// Use listEnrichedVodFiles which properly parses filenames and extracts detection info
|
|
733
|
-
logger.
|
|
757
|
+
logger.debug(`[NVR VOD] Searching for video clips: channel=${channel}, start=${start.toISOString()}, end=${end.toISOString()}`);
|
|
734
758
|
// Filter to only include recordings within the requested time window
|
|
735
|
-
const enrichedRecordings = await
|
|
759
|
+
const enrichedRecordings = await api.listNvrRecordings({
|
|
736
760
|
channel,
|
|
737
761
|
start,
|
|
738
762
|
end,
|
|
739
763
|
streamType: "main",
|
|
740
|
-
autoSearchByDay: false, // Disable autoSearchByDay to avoid searching past days
|
|
741
|
-
bypassCache: false,
|
|
742
764
|
});
|
|
743
765
|
|
|
744
|
-
logger.
|
|
745
|
-
|
|
746
|
-
// Log sample of enriched recordings to see what the library returned
|
|
747
|
-
if (enrichedRecordings.length > 0) {
|
|
748
|
-
const sampleSize = Math.min(3, enrichedRecordings.length);
|
|
749
|
-
for (let i = 0; i < sampleSize; i++) {
|
|
750
|
-
const rec = enrichedRecordings[i];
|
|
751
|
-
logger.log(`[NVR VOD] Sample enriched recording ${i + 1}/${enrichedRecordings.length}:`, {
|
|
752
|
-
fileName: rec.fileName,
|
|
753
|
-
startTimeMs: rec.startTimeMs,
|
|
754
|
-
endTimeMs: rec.endTimeMs,
|
|
755
|
-
durationMs: rec.durationMs,
|
|
756
|
-
hasPerson: rec.hasPerson,
|
|
757
|
-
hasVehicle: rec.hasVehicle,
|
|
758
|
-
hasAnimal: rec.hasAnimal,
|
|
759
|
-
hasFace: rec.hasFace,
|
|
760
|
-
hasMotion: rec.hasMotion,
|
|
761
|
-
hasDoorbell: rec.hasDoorbell,
|
|
762
|
-
hasPackage: rec.hasPackage,
|
|
763
|
-
recordType: rec.recordType,
|
|
764
|
-
parsedFileName: rec.parsedFileName ? {
|
|
765
|
-
start: rec.parsedFileName.start?.toISOString(),
|
|
766
|
-
end: rec.parsedFileName.end?.toISOString(),
|
|
767
|
-
flags: rec.parsedFileName.flags,
|
|
768
|
-
} : null,
|
|
769
|
-
});
|
|
770
|
-
}
|
|
771
|
-
}
|
|
766
|
+
logger.debug(`[NVR VOD] Found ${enrichedRecordings.length} enriched recordings from NVR`);
|
|
772
767
|
|
|
773
|
-
// Convert enriched recordings to VideoClip array
|
|
774
|
-
const clips
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
hasFace: 'hasFace' in rec ? rec.hasFace : false,
|
|
783
|
-
hasMotion: 'hasMotion' in rec ? rec.hasMotion : false,
|
|
784
|
-
hasDoorbell: 'hasDoorbell' in rec ? rec.hasDoorbell : false,
|
|
785
|
-
hasPackage: 'hasPackage' in rec ? rec.hasPackage : false,
|
|
786
|
-
recordType: rec.recordType || 'none',
|
|
787
|
-
};
|
|
788
|
-
logger.debug(`[NVR VOD] Processing recording: fileName=${rec.fileName}, flags=${JSON.stringify(flags)}`);
|
|
789
|
-
|
|
790
|
-
const clip = await recordingFileToVideoClip(rec, {
|
|
791
|
-
fallbackStart: start,
|
|
792
|
-
logger,
|
|
793
|
-
plugin: this,
|
|
794
|
-
deviceId: this.id,
|
|
795
|
-
useWebhook: true,
|
|
796
|
-
});
|
|
797
|
-
|
|
798
|
-
// Log detection classes in the final clip
|
|
799
|
-
logger.debug(`[NVR VOD] Generated clip: id=${clip.id}, detectionClasses=${clip.detectionClasses?.join(',') || 'none'}`);
|
|
800
|
-
clips.push(clip);
|
|
801
|
-
}
|
|
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
|
+
});
|
|
802
777
|
|
|
803
|
-
|
|
804
|
-
const finalClips = count ? clips.slice(0, count) : clips;
|
|
805
|
-
logger.log(`[NVR VOD] Converted ${finalClips.length} video clips (limit: ${count || 'none'})`);
|
|
778
|
+
logger.debug(`[NVR VOD] Converted ${clips.length} video clips (limit: ${count || 'none'})`);
|
|
806
779
|
|
|
807
|
-
return
|
|
780
|
+
return clips;
|
|
808
781
|
} else {
|
|
809
|
-
|
|
810
|
-
const api = await this.ensureClient();
|
|
811
|
-
|
|
812
|
-
const recordings = await api.listEnrichedRecordingsByTime({
|
|
782
|
+
const recordings = await api.listDeviceRecordings({
|
|
813
783
|
start,
|
|
814
784
|
end,
|
|
815
785
|
count,
|
|
816
786
|
channel: this.storageSettings.values.rtspChannel,
|
|
817
787
|
streamType: 'mainStream',
|
|
818
788
|
httpFallback: false,
|
|
819
|
-
fetchRtmpUrls:
|
|
789
|
+
fetchRtmpUrls: false
|
|
820
790
|
});
|
|
821
791
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
});
|
|
833
|
-
clips.push(clip);
|
|
834
|
-
}
|
|
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
|
+
});
|
|
835
802
|
|
|
836
803
|
logger.debug(`Videoclips found: ${clips.length}`);
|
|
837
804
|
|
|
@@ -877,7 +844,8 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
877
844
|
async getVideoClip(videoId: string): Promise<MediaObject> {
|
|
878
845
|
const logger = this.getBaichuanLogger();
|
|
879
846
|
try {
|
|
880
|
-
const cacheEnabled = this.storageSettings.values.downloadVideoclipsLocally
|
|
847
|
+
const cacheEnabled = this.storageSettings.values.downloadVideoclipsLocally;
|
|
848
|
+
const MIN_VIDEO_CACHE_BYTES = 16 * 1024;
|
|
881
849
|
|
|
882
850
|
// Always check cache first, even if caching is disabled (in case user enabled it before)
|
|
883
851
|
const cachePath = this.getVideoClipCachePath(videoId);
|
|
@@ -887,10 +855,19 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
887
855
|
try {
|
|
888
856
|
await fs.promises.access(cachePath, fs.constants.F_OK);
|
|
889
857
|
const stats = await fs.promises.stat(cachePath);
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
858
|
+
if (stats.size < MIN_VIDEO_CACHE_BYTES) {
|
|
859
|
+
logger.warn(`[VideoClip] Cached file too small, deleting and re-downloading: fileId=${videoId}, size=${stats.size} bytes`);
|
|
860
|
+
try {
|
|
861
|
+
await fs.promises.unlink(cachePath);
|
|
862
|
+
} catch (unlinkErr) {
|
|
863
|
+
logger.warn(`[VideoClip] Failed to delete small cached file: fileId=${videoId}`, unlinkErr);
|
|
864
|
+
}
|
|
865
|
+
} else {
|
|
866
|
+
logger.debug(`[VideoClip] Using cached file: fileId=${videoId}, size=${stats.size} bytes`);
|
|
867
|
+
// Return cached file as MediaObject
|
|
868
|
+
const mo = await sdk.mediaManager.createMediaObjectFromUrl(`file://${cachePath}`);
|
|
869
|
+
return mo;
|
|
870
|
+
}
|
|
894
871
|
} catch (e) {
|
|
895
872
|
// File doesn't exist or error accessing it
|
|
896
873
|
logger.debug(`[VideoClip] Cache miss: fileId=${videoId}, error=${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -904,25 +881,71 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
904
881
|
await fs.promises.mkdir(cacheDir, { recursive: true });
|
|
905
882
|
}
|
|
906
883
|
|
|
907
|
-
const
|
|
884
|
+
const { clipsSource } = this.storageSettings.values;
|
|
885
|
+
const useNvr = clipsSource === "NVR" && this.nvrDevice && videoId.includes('/');
|
|
908
886
|
|
|
909
|
-
//
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
887
|
+
// NVR/HUB case: prefer Download endpoint (HTTP) instead of RTMP
|
|
888
|
+
if (useNvr && this.nvrDevice) {
|
|
889
|
+
// Reuse centralized logic for NVR VOD URL (Download)
|
|
890
|
+
const downloadUrl = await this.getVideoClipRtmpUrl(videoId);
|
|
891
|
+
|
|
892
|
+
// If caching is enabled, download via HTTP and cache as file
|
|
893
|
+
if (cacheEnabled) {
|
|
894
|
+
const cachePath = this.getVideoClipCachePath(videoId);
|
|
895
|
+
logger.log(`Downloading video clip from NVR to cache: fileId=${videoId}, path=${cachePath}`);
|
|
896
|
+
|
|
897
|
+
await new Promise<void>((resolve, reject) => {
|
|
898
|
+
const urlObj = new URL(downloadUrl);
|
|
899
|
+
const httpModule = urlObj.protocol === 'https:' ? https : http;
|
|
900
|
+
|
|
901
|
+
const fileStream = fs.createWriteStream(cachePath);
|
|
913
902
|
|
|
914
|
-
|
|
915
|
-
|
|
903
|
+
const req = httpModule.get(downloadUrl, (res) => {
|
|
904
|
+
if (!res.statusCode || res.statusCode >= 400) {
|
|
905
|
+
reject(new Error(`NVR download failed: ${res.statusCode} ${res.statusMessage}`));
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
res.pipe(fileStream);
|
|
910
|
+
|
|
911
|
+
res.on('error', (err) => {
|
|
912
|
+
reject(err);
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
fileStream.on('finish', () => {
|
|
916
|
+
resolve();
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
fileStream.on('error', (err) => {
|
|
920
|
+
reject(err);
|
|
921
|
+
});
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
req.on('error', (err) => {
|
|
925
|
+
reject(err);
|
|
926
|
+
});
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
const mo = await sdk.mediaManager.createMediaObjectFromUrl(`file://${cachePath}`);
|
|
930
|
+
return mo;
|
|
931
|
+
} else {
|
|
932
|
+
// Caching disabled: return HTTP Download URL directly
|
|
933
|
+
const mo = await sdk.mediaManager.createMediaObjectFromUrl(downloadUrl);
|
|
934
|
+
return mo;
|
|
935
|
+
}
|
|
916
936
|
}
|
|
917
937
|
|
|
918
|
-
//
|
|
938
|
+
// Standalone camera (or fallback): reuse getVideoClipRtmpUrl (Baichuan RTMP)
|
|
939
|
+
const playbackUrl = await this.getVideoClipRtmpUrl(videoId);
|
|
940
|
+
|
|
941
|
+
// If caching is enabled, download and cache the video via ffmpeg
|
|
919
942
|
if (cacheEnabled) {
|
|
920
943
|
const cachePath = this.getVideoClipCachePath(videoId);
|
|
921
944
|
|
|
922
945
|
// Download and convert RTMP to MP4 using ffmpeg
|
|
923
946
|
const ffmpegPath = await sdk.mediaManager.getFFmpegPath();
|
|
924
947
|
const ffmpegArgs = [
|
|
925
|
-
'-i',
|
|
948
|
+
'-i', playbackUrl,
|
|
926
949
|
'-c', 'copy', // Copy codecs without re-encoding
|
|
927
950
|
'-f', 'mp4',
|
|
928
951
|
'-movflags', 'frag_keyframe+empty_moov', // Enable streaming
|
|
@@ -944,8 +967,9 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
944
967
|
|
|
945
968
|
ffmpeg.on('close', (code) => {
|
|
946
969
|
if (code !== 0) {
|
|
947
|
-
|
|
948
|
-
|
|
970
|
+
const sanitized = sanitizeFfmpegOutput(errorOutput);
|
|
971
|
+
logger.error(`ffmpeg failed to download video clip: ${sanitized}`);
|
|
972
|
+
reject(new Error(`ffmpeg failed with code ${code}: ${sanitized}`));
|
|
949
973
|
return;
|
|
950
974
|
}
|
|
951
975
|
|
|
@@ -973,8 +997,8 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
973
997
|
const mo = await sdk.mediaManager.createMediaObjectFromUrl(`file://${cachePath}`);
|
|
974
998
|
return mo;
|
|
975
999
|
} else {
|
|
976
|
-
// Caching disabled, return
|
|
977
|
-
const mo = await sdk.mediaManager.createMediaObjectFromUrl(
|
|
1000
|
+
// Caching disabled, return playback URL directly (RTMP for standalone camera)
|
|
1001
|
+
const mo = await sdk.mediaManager.createMediaObjectFromUrl(playbackUrl);
|
|
978
1002
|
return mo;
|
|
979
1003
|
}
|
|
980
1004
|
} catch (e) {
|
|
@@ -1008,14 +1032,24 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1008
1032
|
// Check cache first
|
|
1009
1033
|
const cachePath = this.getThumbnailCachePath(thumbnailId);
|
|
1010
1034
|
const cacheDir = this.getThumbnailCacheDir();
|
|
1035
|
+
const MIN_THUMB_CACHE_BYTES = 512; // 0.5KB, evita file vuoti o quasi
|
|
1011
1036
|
|
|
1012
1037
|
try {
|
|
1013
1038
|
await fs.promises.access(cachePath, fs.constants.F_OK);
|
|
1014
1039
|
const stats = await fs.promises.stat(cachePath);
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1040
|
+
if (stats.size < MIN_THUMB_CACHE_BYTES) {
|
|
1041
|
+
logger.warn(`[Thumbnail] Cached thumbnail too small, deleting and regenerating: fileId=${thumbnailId}, size=${stats.size} bytes`);
|
|
1042
|
+
try {
|
|
1043
|
+
await fs.promises.unlink(cachePath);
|
|
1044
|
+
} catch (unlinkErr) {
|
|
1045
|
+
logger.warn(`[Thumbnail] Failed to delete small cached thumbnail: fileId=${thumbnailId}`, unlinkErr);
|
|
1046
|
+
}
|
|
1047
|
+
} else {
|
|
1048
|
+
logger.debug(`[Thumbnail] Using cached: fileId=${thumbnailId}, size=${stats.size} bytes`);
|
|
1049
|
+
// Return cached thumbnail as MediaObject
|
|
1050
|
+
const mo = await sdk.mediaManager.createMediaObjectFromUrl(`file://${cachePath}`);
|
|
1051
|
+
return mo;
|
|
1052
|
+
}
|
|
1019
1053
|
} catch {
|
|
1020
1054
|
// File doesn't exist, need to generate it
|
|
1021
1055
|
logger.debug(`[Thumbnail] Cache miss: fileId=${thumbnailId}`);
|
|
@@ -1043,7 +1077,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1043
1077
|
deviceId: this.id,
|
|
1044
1078
|
fileId: thumbnailId,
|
|
1045
1079
|
filePath: videoCachePath,
|
|
1046
|
-
|
|
1080
|
+
device: this,
|
|
1047
1081
|
});
|
|
1048
1082
|
} else {
|
|
1049
1083
|
// Get RTMP URL using the appropriate API (NVR or Baichuan)
|
|
@@ -1055,7 +1089,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1055
1089
|
deviceId: this.id,
|
|
1056
1090
|
fileId: thumbnailId,
|
|
1057
1091
|
rtmpUrl: rtmpVodUrl,
|
|
1058
|
-
|
|
1092
|
+
device: this,
|
|
1059
1093
|
});
|
|
1060
1094
|
}
|
|
1061
1095
|
|
|
@@ -1088,37 +1122,31 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1088
1122
|
const useNvr = clipsSource === "NVR" && this.nvrDevice && fileId.includes('/');
|
|
1089
1123
|
|
|
1090
1124
|
if (useNvr) {
|
|
1091
|
-
logger.
|
|
1092
|
-
const
|
|
1125
|
+
logger.debug(`[getVideoClipRtmpUrl] Using NVR API for fileId="${fileId}", forThumbnail=${forThumbnail}`);
|
|
1126
|
+
const api = await this.ensureClient();
|
|
1093
1127
|
const channel = this.storageSettings.values.rtspChannel ?? 0;
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
});
|
|
1106
|
-
logger.log(`[getVideoClipRtmpUrl] NVR getVodUrl ${requestType} URL received: url="${url || 'none'}"`);
|
|
1107
|
-
if (url) return url;
|
|
1108
|
-
} catch (e: any) {
|
|
1109
|
-
logger.debug(`[getVideoClipRtmpUrl] getVodUrl ${requestType} failed: ${e.message}`);
|
|
1110
|
-
}
|
|
1128
|
+
|
|
1129
|
+
try {
|
|
1130
|
+
logger.debug(`[getVideoClipRtmpUrl] Trying getVodUrl with Download requestType...`);
|
|
1131
|
+
const url = await api.getVodUrl(fileId, channel, {
|
|
1132
|
+
requestType: "Download",
|
|
1133
|
+
streamType: "main",
|
|
1134
|
+
});
|
|
1135
|
+
logger.debug(`[getVideoClipRtmpUrl] NVR getVodUrl Download URL received: url="${url || 'none'}"`);
|
|
1136
|
+
if (url) return url;
|
|
1137
|
+
} catch (e: any) {
|
|
1138
|
+
logger.error(`[getVideoClipRtmpUrl] getVodUrl Download failed: ${e.message}`);
|
|
1111
1139
|
}
|
|
1112
|
-
|
|
1140
|
+
|
|
1113
1141
|
throw new Error(`No streaming URL found from NVR for file ${fileId} after trying Playback and Download methods`);
|
|
1114
1142
|
} else {
|
|
1115
1143
|
// Camera standalone: DEVE usare RTMP da Baichuan API
|
|
1116
|
-
logger.
|
|
1144
|
+
logger.debug(`[getVideoClipRtmpUrl] Getting RTMP URL from Baichuan API for fileId="${fileId}" (camera standalone)`);
|
|
1117
1145
|
const api = await this.ensureClient();
|
|
1118
1146
|
const result = await api.getRecordingPlaybackUrls({
|
|
1119
1147
|
fileName: fileId,
|
|
1120
1148
|
});
|
|
1121
|
-
logger.
|
|
1149
|
+
logger.debug(`[getVideoClipRtmpUrl] Baichuan RTMP URL received: rtmpVodUrl="${result.rtmpVodUrl || 'none'}"`);
|
|
1122
1150
|
if (!result.rtmpVodUrl) {
|
|
1123
1151
|
throw new Error(`No RTMP URL found from Baichuan API for file ${fileId}`);
|
|
1124
1152
|
}
|
|
@@ -1179,23 +1207,26 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1179
1207
|
const logger = this.getBaichuanLogger();
|
|
1180
1208
|
|
|
1181
1209
|
this.videoClipsAutoLoadInProgress = true;
|
|
1210
|
+
this.videoClipsAutoLoadMode = true;
|
|
1182
1211
|
|
|
1183
1212
|
try {
|
|
1184
|
-
|
|
1213
|
+
const daysToPreload = this.storageSettings.values.videoclipsDaysToPreload ?? 1;
|
|
1214
|
+
logger.log(`Auto-loading video clips and thumbnails for the last ${daysToPreload} day(s)...`);
|
|
1185
1215
|
|
|
1186
|
-
// Get
|
|
1216
|
+
// Get date range (start of N days ago to now)
|
|
1187
1217
|
const now = new Date();
|
|
1188
|
-
const
|
|
1189
|
-
|
|
1190
|
-
|
|
1218
|
+
const startDate = new Date(now);
|
|
1219
|
+
startDate.setUTCDate(startDate.getUTCDate() - (daysToPreload - 1));
|
|
1220
|
+
startDate.setUTCHours(0, 0, 0, 0);
|
|
1221
|
+
startDate.setUTCMinutes(0, 0, 0);
|
|
1191
1222
|
|
|
1192
|
-
// Fetch
|
|
1223
|
+
// Fetch video clips for the specified number of days
|
|
1193
1224
|
const clips = await this.getVideoClips({
|
|
1194
|
-
startTime:
|
|
1225
|
+
startTime: startDate.getTime(),
|
|
1195
1226
|
endTime: now.getTime(),
|
|
1196
1227
|
});
|
|
1197
1228
|
|
|
1198
|
-
logger.log(`Found ${clips.length} video clips for
|
|
1229
|
+
logger.log(`Found ${clips.length} video clips for the last ${daysToPreload} day(s)`);
|
|
1199
1230
|
|
|
1200
1231
|
const downloadVideoclipsLocally = this.storageSettings.values.downloadVideoclipsLocally ?? false;
|
|
1201
1232
|
|
|
@@ -1242,6 +1273,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1242
1273
|
logger.error('Error during auto-loading video clips:', e);
|
|
1243
1274
|
} finally {
|
|
1244
1275
|
this.videoClipsAutoLoadInProgress = false;
|
|
1276
|
+
this.videoClipsAutoLoadMode = false;
|
|
1245
1277
|
}
|
|
1246
1278
|
}
|
|
1247
1279
|
|
|
@@ -1252,7 +1284,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1252
1284
|
|
|
1253
1285
|
// BaseBaichuanClass abstract methods implementation
|
|
1254
1286
|
protected getConnectionConfig(): BaichuanConnectionConfig {
|
|
1255
|
-
const { ipAddress, username, password, uid } = this.storageSettings.values;
|
|
1287
|
+
const { ipAddress, username, password, uid, discoveryMethod } = this.storageSettings.values;
|
|
1256
1288
|
const debugOptions = this.getBaichuanDebugOptions();
|
|
1257
1289
|
const normalizedUid = this.isBattery ? normalizeUid(uid) : undefined;
|
|
1258
1290
|
|
|
@@ -1260,15 +1292,14 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1260
1292
|
throw new Error('UID is required for battery cameras (BCUDP)');
|
|
1261
1293
|
}
|
|
1262
1294
|
|
|
1263
|
-
const logger = this.getBaichuanLogger();
|
|
1264
1295
|
return {
|
|
1265
1296
|
host: ipAddress,
|
|
1266
1297
|
username,
|
|
1267
1298
|
password,
|
|
1268
1299
|
uid: normalizedUid,
|
|
1269
1300
|
transport: this.protocol,
|
|
1270
|
-
logger,
|
|
1271
1301
|
debugOptions,
|
|
1302
|
+
udpDiscoveryMethod: discoveryMethod as BaichuanClientOptions["udpDiscoveryMethod"],
|
|
1272
1303
|
};
|
|
1273
1304
|
}
|
|
1274
1305
|
|
|
@@ -1351,12 +1382,17 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1351
1382
|
|
|
1352
1383
|
try {
|
|
1353
1384
|
const api = await this.ensureClient();
|
|
1385
|
+
const { ipAddress, username, password } = this.storageSettings.values;
|
|
1354
1386
|
|
|
1355
1387
|
const result = await api.runAllDiagnosticsConsecutively({
|
|
1388
|
+
host: ipAddress,
|
|
1389
|
+
username,
|
|
1390
|
+
password,
|
|
1356
1391
|
outDir: outputPath,
|
|
1357
1392
|
channel,
|
|
1358
1393
|
durationSeconds,
|
|
1359
1394
|
selection,
|
|
1395
|
+
api,
|
|
1360
1396
|
});
|
|
1361
1397
|
|
|
1362
1398
|
logger.log(`Diagnostics completed successfully. Output directory: ${result.runDir}`);
|
|
@@ -2213,11 +2249,11 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
2213
2249
|
|
|
2214
2250
|
let supportedStreams: ReolinkSupportedStream[] = [];
|
|
2215
2251
|
// Homehub RTMP is not efficient, crashes, offers native streams to not overload the hub
|
|
2216
|
-
if (this.nvrDevice && this.nvrDevice.info.model === 'HOMEHUB') {
|
|
2217
|
-
|
|
2218
|
-
} else {
|
|
2219
|
-
|
|
2220
|
-
}
|
|
2252
|
+
// if (this.nvrDevice && this.nvrDevice.info.model === 'HOMEHUB') {
|
|
2253
|
+
supportedStreams = [...nativeStreams, ...rtspStreams, ...rtmpStreams];
|
|
2254
|
+
// } else {
|
|
2255
|
+
// supportedStreams = [...rtspStreams, ...rtmpStreams, ...nativeStreams];
|
|
2256
|
+
// }
|
|
2221
2257
|
|
|
2222
2258
|
for (const supportedStream of supportedStreams) {
|
|
2223
2259
|
const { id, metadata, url, name, container } = supportedStream;
|
|
@@ -2424,6 +2460,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
2424
2460
|
logger.warn('Failed to connect/refresh during init', e);
|
|
2425
2461
|
}
|
|
2426
2462
|
}
|
|
2463
|
+
this.storageSettings.settings.socketApiDebugLogs.hide = !!this.nvrDevice;
|
|
2427
2464
|
this.storageSettings.settings.clipsSource.hide = !this.nvrDevice;
|
|
2428
2465
|
this.storageSettings.settings.clipsSource.defaultValue = this.nvrDevice ? "NVR" : "Device";
|
|
2429
2466
|
|
|
@@ -2440,6 +2477,9 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
2440
2477
|
this.storageSettings.settings.widerChannel.hide = !this.isMultiFocal;
|
|
2441
2478
|
this.storageSettings.settings.teleChannel.hide = !this.isMultiFocal;
|
|
2442
2479
|
|
|
2480
|
+
this.storageSettings.settings.uid.hide = !this.isBattery;
|
|
2481
|
+
this.storageSettings.settings.discoveryMethod.hide = !this.isBattery;
|
|
2482
|
+
|
|
2443
2483
|
if (this.isBattery && !this.storageSettings.values.mixinsSetup) {
|
|
2444
2484
|
try {
|
|
2445
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;
|