@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/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.35",
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,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, recordingFileToVideoClip, sirenSuffix, updateDeviceInfo, vodSearchResultsToVideoClips } from "./utils";
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
- 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) {
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
- // Use UTC to match API's dateToReolinkTime conversion
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.log(`[NVR VOD] Searching for video clips: channel=${channel}, start=${start.toISOString()}, end=${end.toISOString()}`);
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 this.nvrDevice.listEnrichedVodFiles({
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.log(`[NVR VOD] Found ${enrichedRecordings.length} enriched recordings from NVR`);
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: VideoClip[] = [];
775
-
776
- for (const rec of enrichedRecordings) {
777
- // Log detection flags before conversion
778
- const flags = {
779
- hasPerson: 'hasPerson' in rec ? rec.hasPerson : false,
780
- hasVehicle: 'hasVehicle' in rec ? rec.hasVehicle : false,
781
- hasAnimal: 'hasAnimal' in rec ? rec.hasAnimal : false,
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
- // Apply count limit if specified
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 finalClips;
780
+ return clips;
808
781
  } else {
809
- // Fetch directly from device using Baichuan API
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: true
789
+ fetchRtmpUrls: false
820
790
  });
821
791
 
822
- const clips: VideoClip[] = [];
823
-
824
- for (const rec of recordings) {
825
- const clip = await recordingFileToVideoClip(rec, {
826
- fallbackStart: start,
827
- api,
828
- logger,
829
- plugin: this,
830
- deviceId: this.id,
831
- useWebhook: true,
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
- logger.debug(`[VideoClip] Using cached file: fileId=${videoId}, size=${stats.size} bytes`);
891
- // Return cached file as MediaObject
892
- const mo = await sdk.mediaManager.createMediaObjectFromUrl(`file://${cachePath}`);
893
- return mo;
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 api = await this.ensureClient();
884
+ const { clipsSource } = this.storageSettings.values;
885
+ const useNvr = clipsSource === "NVR" && this.nvrDevice && videoId.includes('/');
908
886
 
909
- // videoId is the fileId (fileName or id from the recording)
910
- const { rtmpVodUrl } = await api.getRecordingPlaybackUrls({
911
- fileName: videoId,
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
- if (!rtmpVodUrl) {
915
- throw new Error(`No playback URL found for video ${videoId}`);
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
- // If caching is enabled, download and cache the video
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', rtmpVodUrl,
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
- logger.error(`ffmpeg failed to download video clip: ${errorOutput}`);
948
- reject(new Error(`ffmpeg failed with code ${code}: ${errorOutput}`));
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 RTMP URL directly
977
- const mo = await sdk.mediaManager.createMediaObjectFromUrl(rtmpVodUrl);
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
- logger.debug(`[Thumbnail] Using cached: fileId=${thumbnailId}, size=${stats.size} bytes`);
1016
- // Return cached thumbnail as MediaObject
1017
- const mo = await sdk.mediaManager.createMediaObjectFromUrl(`file://${cachePath}`);
1018
- return mo;
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
- logger: this.getBaichuanLogger(),
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
- logger: this.getBaichuanLogger(),
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.log(`[getVideoClipRtmpUrl] Using NVR API for fileId="${fileId}", forThumbnail=${forThumbnail}`);
1092
- const nvrApi = await this.nvrDevice.ensureClient();
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
- // For both thumbnails and video streaming, try Download first
1096
- // Download might return MP4 format which is better supported than FLV from Playback
1097
- const requestTypes = ["Download", "Playback"];
1098
-
1099
- for (const requestType of requestTypes) {
1100
- try {
1101
- logger.log(`[getVideoClipRtmpUrl] Trying getVodUrl with ${requestType} requestType...`);
1102
- const url = await nvrApi.getVodUrl(fileId, channel, {
1103
- requestType: requestType as "Playback" | "Download",
1104
- streamType: "main",
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.log(`[getVideoClipRtmpUrl] Getting RTMP URL from Baichuan API for fileId="${fileId}" (camera standalone)`);
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.log(`[getVideoClipRtmpUrl] Baichuan RTMP URL received: rtmpVodUrl="${result.rtmpVodUrl || 'none'}"`);
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
- logger.log('Auto-loading today\'s video clips and thumbnails...');
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 today's date range (start of today to now)
1216
+ // Get date range (start of N days ago to now)
1187
1217
  const now = new Date();
1188
- const startOfToday = new Date(now);
1189
- startOfToday.setUTCHours(0, 0, 0, 0);
1190
- startOfToday.setUTCMinutes(0, 0, 0);
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 today's video clips
1223
+ // Fetch video clips for the specified number of days
1193
1224
  const clips = await this.getVideoClips({
1194
- startTime: startOfToday.getTime(),
1225
+ startTime: startDate.getTime(),
1195
1226
  endTime: now.getTime(),
1196
1227
  });
1197
1228
 
1198
- logger.log(`Found ${clips.length} video clips for today`);
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
- supportedStreams = [...nativeStreams, ...rtspStreams, ...rtmpStreams];
2218
- } else {
2219
- supportedStreams = [...rtspStreams, ...rtmpStreams, ...nativeStreams];
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;