@apocaliss92/scrypted-reolink-native 0.1.35 → 0.1.36
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/common.ts +139 -66
- package/src/main.ts +9 -5
- package/src/utils.ts +66 -30
package/dist/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/src/common.ts
CHANGED
|
@@ -5,6 +5,8 @@ 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, recordingFileToVideoClip, sanitizeFfmpegOutput, sirenSuffix, updateDeviceInfo, vodSearchResultsToVideoClips } from "./utils";
|
|
26
28
|
|
|
27
29
|
export type CameraType = 'battery' | 'regular' | 'multi-focal' | 'multi-focal-battery';
|
|
28
30
|
|
|
@@ -550,7 +552,6 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
550
552
|
},
|
|
551
553
|
enableVideoclips: {
|
|
552
554
|
title: "Enable Video Clips",
|
|
553
|
-
subgroup: 'Videoclips',
|
|
554
555
|
description: "Enable video clips functionality. If disabled, getVideoClips will return empty and all other videoclip settings are ignored.",
|
|
555
556
|
type: "boolean",
|
|
556
557
|
defaultValue: false,
|
|
@@ -599,6 +600,16 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
599
600
|
this.updateVideoClipsAutoLoad();
|
|
600
601
|
},
|
|
601
602
|
},
|
|
603
|
+
videoclipsDaysToPreload: {
|
|
604
|
+
title: "Days to Preload",
|
|
605
|
+
subgroup: 'Videoclips',
|
|
606
|
+
description: "Number of days to preload video clips and thumbnails (default: 1, only today).",
|
|
607
|
+
type: "number",
|
|
608
|
+
defaultValue: 3,
|
|
609
|
+
onPut: async () => {
|
|
610
|
+
this.updateVideoClipsAutoLoad();
|
|
611
|
+
},
|
|
612
|
+
},
|
|
602
613
|
diagnosticsRun: {
|
|
603
614
|
subgroup: 'Diagnostics',
|
|
604
615
|
title: 'Run Diagnostics',
|
|
@@ -727,10 +738,9 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
727
738
|
if (useNvr) {
|
|
728
739
|
// Fetch from NVR using listEnrichedVodFiles (library handles parsing correctly)
|
|
729
740
|
const channel = this.storageSettings.values.rtspChannel ?? 0;
|
|
730
|
-
logger.debug(`Fetching video clips from NVR for channel ${channel}`);
|
|
731
741
|
|
|
732
742
|
// Use listEnrichedVodFiles which properly parses filenames and extracts detection info
|
|
733
|
-
logger.
|
|
743
|
+
logger.debug(`[NVR VOD] Searching for video clips: channel=${channel}, start=${start.toISOString()}, end=${end.toISOString()}`);
|
|
734
744
|
// Filter to only include recordings within the requested time window
|
|
735
745
|
const enrichedRecordings = await this.nvrDevice.listEnrichedVodFiles({
|
|
736
746
|
channel,
|
|
@@ -741,14 +751,14 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
741
751
|
bypassCache: false,
|
|
742
752
|
});
|
|
743
753
|
|
|
744
|
-
logger.
|
|
754
|
+
logger.debug(`[NVR VOD] Found ${enrichedRecordings.length} enriched recordings from NVR`);
|
|
745
755
|
|
|
746
756
|
// Log sample of enriched recordings to see what the library returned
|
|
747
757
|
if (enrichedRecordings.length > 0) {
|
|
748
758
|
const sampleSize = Math.min(3, enrichedRecordings.length);
|
|
749
759
|
for (let i = 0; i < sampleSize; i++) {
|
|
750
760
|
const rec = enrichedRecordings[i];
|
|
751
|
-
logger.
|
|
761
|
+
logger.debug(`[NVR VOD] Sample enriched recording ${i + 1}/${enrichedRecordings.length}:`, {
|
|
752
762
|
fileName: rec.fileName,
|
|
753
763
|
startTimeMs: rec.startTimeMs,
|
|
754
764
|
endTimeMs: rec.endTimeMs,
|
|
@@ -794,7 +804,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
794
804
|
deviceId: this.id,
|
|
795
805
|
useWebhook: true,
|
|
796
806
|
});
|
|
797
|
-
|
|
807
|
+
|
|
798
808
|
// Log detection classes in the final clip
|
|
799
809
|
logger.debug(`[NVR VOD] Generated clip: id=${clip.id}, detectionClasses=${clip.detectionClasses?.join(',') || 'none'}`);
|
|
800
810
|
clips.push(clip);
|
|
@@ -802,7 +812,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
802
812
|
|
|
803
813
|
// Apply count limit if specified
|
|
804
814
|
const finalClips = count ? clips.slice(0, count) : clips;
|
|
805
|
-
logger.
|
|
815
|
+
logger.debug(`[NVR VOD] Converted ${finalClips.length} video clips (limit: ${count || 'none'})`);
|
|
806
816
|
|
|
807
817
|
return finalClips;
|
|
808
818
|
} else {
|
|
@@ -877,7 +887,8 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
877
887
|
async getVideoClip(videoId: string): Promise<MediaObject> {
|
|
878
888
|
const logger = this.getBaichuanLogger();
|
|
879
889
|
try {
|
|
880
|
-
const cacheEnabled = this.storageSettings.values.downloadVideoclipsLocally
|
|
890
|
+
const cacheEnabled = this.storageSettings.values.downloadVideoclipsLocally;
|
|
891
|
+
const MIN_VIDEO_CACHE_BYTES = 16 * 1024;
|
|
881
892
|
|
|
882
893
|
// Always check cache first, even if caching is disabled (in case user enabled it before)
|
|
883
894
|
const cachePath = this.getVideoClipCachePath(videoId);
|
|
@@ -887,10 +898,19 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
887
898
|
try {
|
|
888
899
|
await fs.promises.access(cachePath, fs.constants.F_OK);
|
|
889
900
|
const stats = await fs.promises.stat(cachePath);
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
901
|
+
if (stats.size < MIN_VIDEO_CACHE_BYTES) {
|
|
902
|
+
logger.warn(`[VideoClip] Cached file too small, deleting and re-downloading: fileId=${videoId}, size=${stats.size} bytes`);
|
|
903
|
+
try {
|
|
904
|
+
await fs.promises.unlink(cachePath);
|
|
905
|
+
} catch (unlinkErr) {
|
|
906
|
+
logger.warn(`[VideoClip] Failed to delete small cached file: fileId=${videoId}`, unlinkErr);
|
|
907
|
+
}
|
|
908
|
+
} else {
|
|
909
|
+
logger.debug(`[VideoClip] Using cached file: fileId=${videoId}, size=${stats.size} bytes`);
|
|
910
|
+
// Return cached file as MediaObject
|
|
911
|
+
const mo = await sdk.mediaManager.createMediaObjectFromUrl(`file://${cachePath}`);
|
|
912
|
+
return mo;
|
|
913
|
+
}
|
|
894
914
|
} catch (e) {
|
|
895
915
|
// File doesn't exist or error accessing it
|
|
896
916
|
logger.debug(`[VideoClip] Cache miss: fileId=${videoId}, error=${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -904,25 +924,71 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
904
924
|
await fs.promises.mkdir(cacheDir, { recursive: true });
|
|
905
925
|
}
|
|
906
926
|
|
|
907
|
-
const
|
|
927
|
+
const { clipsSource } = this.storageSettings.values;
|
|
928
|
+
const useNvr = clipsSource === "NVR" && this.nvrDevice && videoId.includes('/');
|
|
908
929
|
|
|
909
|
-
//
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
930
|
+
// NVR/HUB case: prefer Download endpoint (HTTP) instead of RTMP
|
|
931
|
+
if (useNvr && this.nvrDevice) {
|
|
932
|
+
// Reuse centralized logic for NVR VOD URL (Download)
|
|
933
|
+
const downloadUrl = await this.getVideoClipRtmpUrl(videoId);
|
|
934
|
+
|
|
935
|
+
// If caching is enabled, download via HTTP and cache as file
|
|
936
|
+
if (cacheEnabled) {
|
|
937
|
+
const cachePath = this.getVideoClipCachePath(videoId);
|
|
938
|
+
logger.log(`Downloading video clip from NVR to cache: fileId=${videoId}, path=${cachePath}`);
|
|
939
|
+
|
|
940
|
+
await new Promise<void>((resolve, reject) => {
|
|
941
|
+
const urlObj = new URL(downloadUrl);
|
|
942
|
+
const httpModule = urlObj.protocol === 'https:' ? https : http;
|
|
943
|
+
|
|
944
|
+
const fileStream = fs.createWriteStream(cachePath);
|
|
945
|
+
|
|
946
|
+
const req = httpModule.get(downloadUrl, (res) => {
|
|
947
|
+
if (!res.statusCode || res.statusCode >= 400) {
|
|
948
|
+
reject(new Error(`NVR download failed: ${res.statusCode} ${res.statusMessage}`));
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
res.pipe(fileStream);
|
|
953
|
+
|
|
954
|
+
res.on('error', (err) => {
|
|
955
|
+
reject(err);
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
fileStream.on('finish', () => {
|
|
959
|
+
resolve();
|
|
960
|
+
});
|
|
913
961
|
|
|
914
|
-
|
|
915
|
-
|
|
962
|
+
fileStream.on('error', (err) => {
|
|
963
|
+
reject(err);
|
|
964
|
+
});
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
req.on('error', (err) => {
|
|
968
|
+
reject(err);
|
|
969
|
+
});
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
const mo = await sdk.mediaManager.createMediaObjectFromUrl(`file://${cachePath}`);
|
|
973
|
+
return mo;
|
|
974
|
+
} else {
|
|
975
|
+
// Caching disabled: return HTTP Download URL directly
|
|
976
|
+
const mo = await sdk.mediaManager.createMediaObjectFromUrl(downloadUrl);
|
|
977
|
+
return mo;
|
|
978
|
+
}
|
|
916
979
|
}
|
|
917
980
|
|
|
918
|
-
//
|
|
981
|
+
// Standalone camera (or fallback): reuse getVideoClipRtmpUrl (Baichuan RTMP)
|
|
982
|
+
const playbackUrl = await this.getVideoClipRtmpUrl(videoId);
|
|
983
|
+
|
|
984
|
+
// If caching is enabled, download and cache the video via ffmpeg
|
|
919
985
|
if (cacheEnabled) {
|
|
920
986
|
const cachePath = this.getVideoClipCachePath(videoId);
|
|
921
987
|
|
|
922
988
|
// Download and convert RTMP to MP4 using ffmpeg
|
|
923
989
|
const ffmpegPath = await sdk.mediaManager.getFFmpegPath();
|
|
924
990
|
const ffmpegArgs = [
|
|
925
|
-
'-i',
|
|
991
|
+
'-i', playbackUrl,
|
|
926
992
|
'-c', 'copy', // Copy codecs without re-encoding
|
|
927
993
|
'-f', 'mp4',
|
|
928
994
|
'-movflags', 'frag_keyframe+empty_moov', // Enable streaming
|
|
@@ -944,8 +1010,9 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
944
1010
|
|
|
945
1011
|
ffmpeg.on('close', (code) => {
|
|
946
1012
|
if (code !== 0) {
|
|
947
|
-
|
|
948
|
-
|
|
1013
|
+
const sanitized = sanitizeFfmpegOutput(errorOutput);
|
|
1014
|
+
logger.error(`ffmpeg failed to download video clip: ${sanitized}`);
|
|
1015
|
+
reject(new Error(`ffmpeg failed with code ${code}: ${sanitized}`));
|
|
949
1016
|
return;
|
|
950
1017
|
}
|
|
951
1018
|
|
|
@@ -973,8 +1040,8 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
973
1040
|
const mo = await sdk.mediaManager.createMediaObjectFromUrl(`file://${cachePath}`);
|
|
974
1041
|
return mo;
|
|
975
1042
|
} else {
|
|
976
|
-
// Caching disabled, return
|
|
977
|
-
const mo = await sdk.mediaManager.createMediaObjectFromUrl(
|
|
1043
|
+
// Caching disabled, return playback URL directly (RTMP for standalone camera)
|
|
1044
|
+
const mo = await sdk.mediaManager.createMediaObjectFromUrl(playbackUrl);
|
|
978
1045
|
return mo;
|
|
979
1046
|
}
|
|
980
1047
|
} catch (e) {
|
|
@@ -1008,14 +1075,24 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1008
1075
|
// Check cache first
|
|
1009
1076
|
const cachePath = this.getThumbnailCachePath(thumbnailId);
|
|
1010
1077
|
const cacheDir = this.getThumbnailCacheDir();
|
|
1078
|
+
const MIN_THUMB_CACHE_BYTES = 512; // 0.5KB, evita file vuoti o quasi
|
|
1011
1079
|
|
|
1012
1080
|
try {
|
|
1013
1081
|
await fs.promises.access(cachePath, fs.constants.F_OK);
|
|
1014
1082
|
const stats = await fs.promises.stat(cachePath);
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1083
|
+
if (stats.size < MIN_THUMB_CACHE_BYTES) {
|
|
1084
|
+
logger.warn(`[Thumbnail] Cached thumbnail too small, deleting and regenerating: fileId=${thumbnailId}, size=${stats.size} bytes`);
|
|
1085
|
+
try {
|
|
1086
|
+
await fs.promises.unlink(cachePath);
|
|
1087
|
+
} catch (unlinkErr) {
|
|
1088
|
+
logger.warn(`[Thumbnail] Failed to delete small cached thumbnail: fileId=${thumbnailId}`, unlinkErr);
|
|
1089
|
+
}
|
|
1090
|
+
} else {
|
|
1091
|
+
logger.debug(`[Thumbnail] Using cached: fileId=${thumbnailId}, size=${stats.size} bytes`);
|
|
1092
|
+
// Return cached thumbnail as MediaObject
|
|
1093
|
+
const mo = await sdk.mediaManager.createMediaObjectFromUrl(`file://${cachePath}`);
|
|
1094
|
+
return mo;
|
|
1095
|
+
}
|
|
1019
1096
|
} catch {
|
|
1020
1097
|
// File doesn't exist, need to generate it
|
|
1021
1098
|
logger.debug(`[Thumbnail] Cache miss: fileId=${thumbnailId}`);
|
|
@@ -1043,7 +1120,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1043
1120
|
deviceId: this.id,
|
|
1044
1121
|
fileId: thumbnailId,
|
|
1045
1122
|
filePath: videoCachePath,
|
|
1046
|
-
|
|
1123
|
+
device: this,
|
|
1047
1124
|
});
|
|
1048
1125
|
} else {
|
|
1049
1126
|
// Get RTMP URL using the appropriate API (NVR or Baichuan)
|
|
@@ -1055,7 +1132,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1055
1132
|
deviceId: this.id,
|
|
1056
1133
|
fileId: thumbnailId,
|
|
1057
1134
|
rtmpUrl: rtmpVodUrl,
|
|
1058
|
-
|
|
1135
|
+
device: this,
|
|
1059
1136
|
});
|
|
1060
1137
|
}
|
|
1061
1138
|
|
|
@@ -1088,37 +1165,31 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1088
1165
|
const useNvr = clipsSource === "NVR" && this.nvrDevice && fileId.includes('/');
|
|
1089
1166
|
|
|
1090
1167
|
if (useNvr) {
|
|
1091
|
-
logger.
|
|
1168
|
+
logger.debug(`[getVideoClipRtmpUrl] Using NVR API for fileId="${fileId}", forThumbnail=${forThumbnail}`);
|
|
1092
1169
|
const nvrApi = await this.nvrDevice.ensureClient();
|
|
1093
1170
|
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
|
-
}
|
|
1171
|
+
|
|
1172
|
+
try {
|
|
1173
|
+
logger.debug(`[getVideoClipRtmpUrl] Trying getVodUrl with Download requestType...`);
|
|
1174
|
+
const url = await nvrApi.getVodUrl(fileId, channel, {
|
|
1175
|
+
requestType: "Download",
|
|
1176
|
+
streamType: "main",
|
|
1177
|
+
});
|
|
1178
|
+
logger.debug(`[getVideoClipRtmpUrl] NVR getVodUrl Download URL received: url="${url || 'none'}"`);
|
|
1179
|
+
if (url) return url;
|
|
1180
|
+
} catch (e: any) {
|
|
1181
|
+
logger.error(`[getVideoClipRtmpUrl] getVodUrl Download failed: ${e.message}`);
|
|
1111
1182
|
}
|
|
1112
|
-
|
|
1183
|
+
|
|
1113
1184
|
throw new Error(`No streaming URL found from NVR for file ${fileId} after trying Playback and Download methods`);
|
|
1114
1185
|
} else {
|
|
1115
1186
|
// Camera standalone: DEVE usare RTMP da Baichuan API
|
|
1116
|
-
logger.
|
|
1187
|
+
logger.debug(`[getVideoClipRtmpUrl] Getting RTMP URL from Baichuan API for fileId="${fileId}" (camera standalone)`);
|
|
1117
1188
|
const api = await this.ensureClient();
|
|
1118
1189
|
const result = await api.getRecordingPlaybackUrls({
|
|
1119
1190
|
fileName: fileId,
|
|
1120
1191
|
});
|
|
1121
|
-
logger.
|
|
1192
|
+
logger.debug(`[getVideoClipRtmpUrl] Baichuan RTMP URL received: rtmpVodUrl="${result.rtmpVodUrl || 'none'}"`);
|
|
1122
1193
|
if (!result.rtmpVodUrl) {
|
|
1123
1194
|
throw new Error(`No RTMP URL found from Baichuan API for file ${fileId}`);
|
|
1124
1195
|
}
|
|
@@ -1181,21 +1252,23 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1181
1252
|
this.videoClipsAutoLoadInProgress = true;
|
|
1182
1253
|
|
|
1183
1254
|
try {
|
|
1184
|
-
|
|
1255
|
+
const daysToPreload = this.storageSettings.values.videoclipsDaysToPreload ?? 1;
|
|
1256
|
+
logger.log(`Auto-loading video clips and thumbnails for the last ${daysToPreload} day(s)...`);
|
|
1185
1257
|
|
|
1186
|
-
// Get
|
|
1258
|
+
// Get date range (start of N days ago to now)
|
|
1187
1259
|
const now = new Date();
|
|
1188
|
-
const
|
|
1189
|
-
|
|
1190
|
-
|
|
1260
|
+
const startDate = new Date(now);
|
|
1261
|
+
startDate.setUTCDate(startDate.getUTCDate() - (daysToPreload - 1));
|
|
1262
|
+
startDate.setUTCHours(0, 0, 0, 0);
|
|
1263
|
+
startDate.setUTCMinutes(0, 0, 0);
|
|
1191
1264
|
|
|
1192
|
-
// Fetch
|
|
1265
|
+
// Fetch video clips for the specified number of days
|
|
1193
1266
|
const clips = await this.getVideoClips({
|
|
1194
|
-
startTime:
|
|
1267
|
+
startTime: startDate.getTime(),
|
|
1195
1268
|
endTime: now.getTime(),
|
|
1196
1269
|
});
|
|
1197
1270
|
|
|
1198
|
-
logger.log(`Found ${clips.length} video clips for
|
|
1271
|
+
logger.log(`Found ${clips.length} video clips for the last ${daysToPreload} day(s)`);
|
|
1199
1272
|
|
|
1200
1273
|
const downloadVideoclipsLocally = this.storageSettings.values.downloadVideoclipsLocally ?? false;
|
|
1201
1274
|
|
|
@@ -2213,11 +2286,11 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
2213
2286
|
|
|
2214
2287
|
let supportedStreams: ReolinkSupportedStream[] = [];
|
|
2215
2288
|
// 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
|
-
}
|
|
2289
|
+
// if (this.nvrDevice && this.nvrDevice.info.model === 'HOMEHUB') {
|
|
2290
|
+
supportedStreams = [...nativeStreams, ...rtspStreams, ...rtmpStreams];
|
|
2291
|
+
// } else {
|
|
2292
|
+
// supportedStreams = [...rtspStreams, ...rtmpStreams, ...nativeStreams];
|
|
2293
|
+
// }
|
|
2221
2294
|
|
|
2222
2295
|
for (const supportedStream of supportedStreams) {
|
|
2223
2296
|
const { id, metadata, url, name, container } = supportedStream;
|
package/src/main.ts
CHANGED
|
@@ -12,7 +12,8 @@ interface ThumbnailRequest {
|
|
|
12
12
|
fileId: string;
|
|
13
13
|
rtmpUrl?: string;
|
|
14
14
|
filePath?: string;
|
|
15
|
-
logger
|
|
15
|
+
logger?: Console;
|
|
16
|
+
device?: CommonCameraMixin;
|
|
16
17
|
resolve: (mo: MediaObject) => void;
|
|
17
18
|
reject: (error: Error) => void;
|
|
18
19
|
}
|
|
@@ -22,7 +23,8 @@ interface ThumbnailRequestInput {
|
|
|
22
23
|
fileId: string;
|
|
23
24
|
rtmpUrl?: string;
|
|
24
25
|
filePath?: string;
|
|
25
|
-
logger
|
|
26
|
+
logger?: Console;
|
|
27
|
+
device?: CommonCameraMixin;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider, DeviceCreator {
|
|
@@ -347,7 +349,9 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
347
349
|
*/
|
|
348
350
|
async generateThumbnail(request: ThumbnailRequestInput): Promise<MediaObject> {
|
|
349
351
|
const queueLength = this.thumbnailQueue.length;
|
|
350
|
-
|
|
352
|
+
// Use device logger if available, otherwise fallback to provided logger
|
|
353
|
+
const logger = request.device?.getBaichuanLogger?.() || request.logger || console;
|
|
354
|
+
logger.log(`[Thumbnail] Download start: fileId=${request.fileId}, queuePosition=${queueLength + 1}`);
|
|
351
355
|
|
|
352
356
|
return new Promise((resolve, reject) => {
|
|
353
357
|
this.thumbnailQueue.push({
|
|
@@ -371,11 +375,11 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
371
375
|
|
|
372
376
|
while (this.thumbnailQueue.length > 0) {
|
|
373
377
|
const request = this.thumbnailQueue.shift()!;
|
|
374
|
-
const logger = request.logger;
|
|
378
|
+
const logger = request.device?.getBaichuanLogger?.() || request.logger || console;
|
|
375
379
|
|
|
376
380
|
try {
|
|
377
381
|
const thumbnail = await extractThumbnailFromVideo(request);
|
|
378
|
-
logger.log(`[Thumbnail]
|
|
382
|
+
logger.log(`[Thumbnail] Download completed: fileId=${request.fileId}`);
|
|
379
383
|
request.resolve(thumbnail);
|
|
380
384
|
} catch (error) {
|
|
381
385
|
logger.error(`[Thumbnail] Error: fileId=${request.fileId}`, error);
|