@apocaliss92/scrypted-reolink-native 0.2.12 → 0.2.15
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/camera.ts +24 -70
- package/src/main.ts +23 -1
- package/src/utils.ts +6 -1
package/dist/plugin.zip
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/src/camera.ts
CHANGED
|
@@ -787,22 +787,21 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
|
|
|
787
787
|
const api = await this.ensureClient();
|
|
788
788
|
|
|
789
789
|
if (useNvr) {
|
|
790
|
-
// Fetch from NVR using
|
|
790
|
+
// Fetch from NVR using VOD listing so clip.id/fileId is a real file path (e.g. /mnt/sda/...)
|
|
791
791
|
const channel = this.storageSettings.values.rtspChannel ?? 0;
|
|
792
792
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
793
|
+
logger.debug(`[NVR VOD] Listing recordings: channel=${channel}, start=${start.toISOString()}, end=${end.toISOString()}`);
|
|
794
|
+
const recordings = await api.listNvrRecordings({
|
|
795
|
+
channel,
|
|
796
796
|
start,
|
|
797
797
|
end,
|
|
798
|
-
|
|
799
|
-
|
|
798
|
+
streamType: "main",
|
|
799
|
+
autoSearchByDay: true,
|
|
800
|
+
fetchStreamUrls: false,
|
|
800
801
|
});
|
|
801
802
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
// Convert enriched recordings to VideoClip array using the shared parser
|
|
805
|
-
const clips = await recordingsToVideoClips(enrichedRecordings, {
|
|
803
|
+
// Convert VOD recordings to VideoClip array using the shared parser
|
|
804
|
+
const clips = await recordingsToVideoClips(recordings, {
|
|
806
805
|
fallbackStart: start,
|
|
807
806
|
logger,
|
|
808
807
|
plugin: this,
|
|
@@ -811,7 +810,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
|
|
|
811
810
|
count,
|
|
812
811
|
});
|
|
813
812
|
|
|
814
|
-
logger.debug(`[NVR
|
|
813
|
+
logger.debug(`[NVR VOD] Converted ${clips.length} video clips (limit: ${count || 'none'})`);
|
|
815
814
|
|
|
816
815
|
return clips;
|
|
817
816
|
} else {
|
|
@@ -918,60 +917,10 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
|
|
|
918
917
|
}
|
|
919
918
|
|
|
920
919
|
const { clipsSource } = this.storageSettings.values;
|
|
921
|
-
const useNvr = clipsSource === "NVR" && this.nvrDevice
|
|
922
|
-
|
|
923
|
-
// NVR/HUB case: prefer Download endpoint (HTTP) instead of RTMP
|
|
924
|
-
if (useNvr && this.nvrDevice) {
|
|
925
|
-
// Reuse centralized logic for NVR VOD URL (Download)
|
|
926
|
-
const downloadUrl = await this.getVideoClipRtmpUrl(videoId);
|
|
927
|
-
|
|
928
|
-
// If caching is enabled, download via HTTP and cache as file
|
|
929
|
-
if (cacheEnabled) {
|
|
930
|
-
const cachePath = this.getVideoClipCachePath(videoId);
|
|
931
|
-
logger.log(`Downloading video clip from NVR to cache: fileId=${videoId}, path=${cachePath}`);
|
|
932
|
-
|
|
933
|
-
await new Promise<void>((resolve, reject) => {
|
|
934
|
-
const urlObj = new URL(downloadUrl);
|
|
935
|
-
const httpModule = urlObj.protocol === 'https:' ? https : http;
|
|
936
|
-
|
|
937
|
-
const fileStream = fs.createWriteStream(cachePath);
|
|
938
|
-
|
|
939
|
-
const req = httpModule.get(downloadUrl, (res) => {
|
|
940
|
-
if (!res.statusCode || res.statusCode >= 400) {
|
|
941
|
-
reject(new Error(`NVR download failed: ${res.statusCode} ${res.statusMessage}`));
|
|
942
|
-
return;
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
res.pipe(fileStream);
|
|
946
|
-
|
|
947
|
-
res.on('error', (err) => {
|
|
948
|
-
reject(err);
|
|
949
|
-
});
|
|
950
|
-
|
|
951
|
-
fileStream.on('finish', () => {
|
|
952
|
-
resolve();
|
|
953
|
-
});
|
|
954
|
-
|
|
955
|
-
fileStream.on('error', (err) => {
|
|
956
|
-
reject(err);
|
|
957
|
-
});
|
|
958
|
-
});
|
|
959
|
-
|
|
960
|
-
req.on('error', (err) => {
|
|
961
|
-
reject(err);
|
|
962
|
-
});
|
|
963
|
-
});
|
|
964
|
-
|
|
965
|
-
const mo = await sdk.mediaManager.createMediaObjectFromUrl(`file://${cachePath}`);
|
|
966
|
-
return mo;
|
|
967
|
-
} else {
|
|
968
|
-
// Caching disabled: return HTTP Download URL directly
|
|
969
|
-
const mo = await sdk.mediaManager.createMediaObjectFromUrl(downloadUrl);
|
|
970
|
-
return mo;
|
|
971
|
-
}
|
|
972
|
-
}
|
|
920
|
+
const useNvr = clipsSource === "NVR" && this.nvrDevice;
|
|
973
921
|
|
|
974
|
-
//
|
|
922
|
+
// Both standalone and NVR now use a URL-based playback path.
|
|
923
|
+
// In NVR mode, `videoId` is expected to be a full recording path (e.g. /mnt/sda/...).
|
|
975
924
|
const playbackUrl = await this.getVideoClipRtmpUrl(videoId);
|
|
976
925
|
|
|
977
926
|
// If caching is enabled, download and cache the video via ffmpeg
|
|
@@ -1098,6 +1047,12 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
|
|
|
1098
1047
|
// Ensure cache directory exists
|
|
1099
1048
|
await fs.promises.mkdir(cacheDir, { recursive: true });
|
|
1100
1049
|
|
|
1050
|
+
const { clipsSource } = this.storageSettings.values;
|
|
1051
|
+
const useNvr = clipsSource === "NVR" && this.nvrDevice;
|
|
1052
|
+
|
|
1053
|
+
// NVR mode: `thumbnailId` is expected to be a full recording path (e.g. /mnt/sda/...).
|
|
1054
|
+
// Use the same ffmpeg-based thumbnail extraction flow as other sources.
|
|
1055
|
+
|
|
1101
1056
|
// Check if video clip is already cached locally - use it instead of calling camera
|
|
1102
1057
|
const videoCachePath = this.getVideoClipCachePath(thumbnailId);
|
|
1103
1058
|
let useLocalVideo = false;
|
|
@@ -1159,20 +1114,19 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
|
|
|
1159
1114
|
async getVideoClipRtmpUrl(fileId: string, forThumbnail: boolean = false): Promise<string> {
|
|
1160
1115
|
const logger = this.getBaichuanLogger();
|
|
1161
1116
|
const { clipsSource } = this.storageSettings.values;
|
|
1162
|
-
const useNvr = clipsSource === "NVR" && this.nvrDevice
|
|
1117
|
+
const useNvr = clipsSource === "NVR" && this.nvrDevice;
|
|
1163
1118
|
|
|
1164
1119
|
if (useNvr) {
|
|
1165
|
-
//
|
|
1120
|
+
// NVR mode: `fileId` is expected to be a full recording path (e.g. /mnt/sda/...).
|
|
1121
|
+
logger.debug(`[getVideoClipRtmpUrl] Using NVR VOD API for fileId="${fileId}"`);
|
|
1166
1122
|
const api = await this.ensureClient();
|
|
1167
|
-
const channel = this.storageSettings.values.rtspChannel ?? 0;
|
|
1168
1123
|
|
|
1124
|
+
const channel = this.storageSettings.values.rtspChannel ?? 0;
|
|
1169
1125
|
try {
|
|
1170
|
-
// logger.debug(`[getVideoClipRtmpUrl] Trying getVodUrl with Download requestType...`);
|
|
1171
1126
|
const url = await api.getVodUrl(fileId, channel, {
|
|
1172
1127
|
requestType: "Download",
|
|
1173
1128
|
streamType: "main",
|
|
1174
1129
|
});
|
|
1175
|
-
// logger.debug(`[getVideoClipRtmpUrl] NVR getVodUrl Download URL received: url="${url || 'none'}"`);
|
|
1176
1130
|
if (url) return url;
|
|
1177
1131
|
} catch (e: any) {
|
|
1178
1132
|
logger.error(`[getVideoClipRtmpUrl] getVodUrl Download failed: ${e?.message || String(e)}`);
|
|
@@ -1181,7 +1135,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
|
|
|
1181
1135
|
throw new Error(`No streaming URL found from NVR for file ${fileId} after trying Playback and Download methods`);
|
|
1182
1136
|
} else {
|
|
1183
1137
|
// Camera standalone: DEVE usare RTMP da Baichuan API
|
|
1184
|
-
|
|
1138
|
+
logger.debug(`[getVideoClipRtmpUrl] Getting RTMP URL from Baichuan API for fileId="${fileId}" (camera standalone)`);
|
|
1185
1139
|
const api = await this.ensureClient();
|
|
1186
1140
|
const result = await api.getRecordingPlaybackUrls({
|
|
1187
1141
|
fileName: fileId,
|
package/src/main.ts
CHANGED
|
@@ -32,6 +32,7 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
32
32
|
nvrDeviceId: string;
|
|
33
33
|
private thumbnailQueue: ThumbnailRequest[] = [];
|
|
34
34
|
private thumbnailProcessing = false;
|
|
35
|
+
private thumbnailPendingRequests = new Map<string, Promise<MediaObject>>();
|
|
35
36
|
|
|
36
37
|
constructor(nativeId: string) {
|
|
37
38
|
super(nativeId);
|
|
@@ -340,12 +341,23 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
340
341
|
* Add a thumbnail generation request to the queue
|
|
341
342
|
*/
|
|
342
343
|
async generateThumbnail(request: ThumbnailRequestInput): Promise<MediaObject> {
|
|
344
|
+
// Create a unique key for this request (deviceId:fileId)
|
|
345
|
+
const requestKey = `${request.deviceId}:${request.fileId}`;
|
|
346
|
+
|
|
347
|
+
// Check if this thumbnail is already in queue or being processed
|
|
348
|
+
const existingRequest = this.thumbnailPendingRequests.get(requestKey);
|
|
349
|
+
if (existingRequest) {
|
|
350
|
+
const logger = request.device?.getBaichuanLogger?.() || request.logger || console;
|
|
351
|
+
logger.debug(`[Thumbnail] Request already in queue: fileId=${request.fileId}, reusing existing promise`);
|
|
352
|
+
return existingRequest;
|
|
353
|
+
}
|
|
354
|
+
|
|
343
355
|
const queueLength = this.thumbnailQueue.length;
|
|
344
356
|
// Use device logger if available, otherwise fallback to provided logger
|
|
345
357
|
const logger = request.device?.getBaichuanLogger?.() || request.logger || console;
|
|
346
358
|
logger.log(`[Thumbnail] Download start: fileId=${request.fileId}, queuePosition=${queueLength + 1}`);
|
|
347
359
|
|
|
348
|
-
|
|
360
|
+
const promise = new Promise<MediaObject>((resolve, reject) => {
|
|
349
361
|
this.thumbnailQueue.push({
|
|
350
362
|
...request,
|
|
351
363
|
resolve,
|
|
@@ -353,6 +365,16 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
353
365
|
});
|
|
354
366
|
this.processThumbnailQueue();
|
|
355
367
|
});
|
|
368
|
+
|
|
369
|
+
// Track this request
|
|
370
|
+
this.thumbnailPendingRequests.set(requestKey, promise);
|
|
371
|
+
|
|
372
|
+
// Remove from tracking when the promise resolves or rejects
|
|
373
|
+
promise.finally(() => {
|
|
374
|
+
this.thumbnailPendingRequests.delete(requestKey);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
return promise;
|
|
356
378
|
}
|
|
357
379
|
|
|
358
380
|
/**
|
package/src/utils.ts
CHANGED
|
@@ -192,7 +192,12 @@ export async function recordingFileToVideoClip(
|
|
|
192
192
|
const recEndMs = Math.max(recEnd.getTime(), recStartMs);
|
|
193
193
|
const duration = recEndMs - recStartMs;
|
|
194
194
|
|
|
195
|
-
|
|
195
|
+
// IMPORTANT: For NVR/Hub, ensure the clip id (fileId) is the actual recording path (/mnt/...) when available.
|
|
196
|
+
// Some sources may provide an alternate id (e.g. eventId/Baichuan id); we prefer the filesystem path because
|
|
197
|
+
// downstream VOD download/playback endpoints expect it.
|
|
198
|
+
const id = typeof rec.fileName === 'string' && rec.fileName.startsWith('/mnt/')
|
|
199
|
+
? rec.fileName
|
|
200
|
+
: (rec.id || rec.fileName);
|
|
196
201
|
|
|
197
202
|
// Get video URL if not provided
|
|
198
203
|
let videoHref: string | undefined = providedVideoHref;
|