@apocaliss92/scrypted-reolink-native 0.2.11 → 0.2.14
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 +23 -69
- package/src/utils.ts +12 -7
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)}`);
|
package/src/utils.ts
CHANGED
|
@@ -192,17 +192,22 @@ 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;
|
|
199
204
|
let thumbnailHref: string | undefined;
|
|
200
205
|
|
|
201
|
-
logger?.debug(`[recordingFileToVideoClip] URL generation: useWebhook=${useWebhook}, hasPlugin=${!!plugin}, deviceId=${deviceId}, providedVideoHref=${providedVideoHref || 'none'}, hasApi=${!!api}`);
|
|
206
|
+
// logger?.debug(`[recordingFileToVideoClip] URL generation: useWebhook=${useWebhook}, hasPlugin=${!!plugin}, deviceId=${deviceId}, providedVideoHref=${providedVideoHref || 'none'}, hasApi=${!!api}`);
|
|
202
207
|
|
|
203
208
|
// If webhook is enabled, generate webhook URLs
|
|
204
209
|
if (useWebhook && plugin && deviceId) {
|
|
205
|
-
logger?.debug(`[recordingFileToVideoClip] Generating webhook URLs for fileId=${id}`);
|
|
210
|
+
// logger?.debug(`[recordingFileToVideoClip] Generating webhook URLs for fileId=${id}`);
|
|
206
211
|
try {
|
|
207
212
|
const { videoUrl, thumbnailUrl } = await getVideoClipWebhookUrls({
|
|
208
213
|
deviceId,
|
|
@@ -212,24 +217,24 @@ export async function recordingFileToVideoClip(
|
|
|
212
217
|
});
|
|
213
218
|
videoHref = videoUrl;
|
|
214
219
|
thumbnailHref = thumbnailUrl;
|
|
215
|
-
logger?.debug(`[recordingFileToVideoClip] Webhook URLs generated successfully: videoHref="${videoHref}", thumbnailHref="${thumbnailHref}"`);
|
|
220
|
+
// logger?.debug(`[recordingFileToVideoClip] Webhook URLs generated successfully: videoHref="${videoHref}", thumbnailHref="${thumbnailHref}"`);
|
|
216
221
|
} catch (e) {
|
|
217
222
|
logger?.error(`[recordingFileToVideoClip] Failed to generate webhook URLs for fileId=${id}:`, e?.message || String(e));
|
|
218
223
|
}
|
|
219
224
|
} else if (!videoHref && api) {
|
|
220
225
|
// Fallback to direct RTMP URL if webhook is not used
|
|
221
|
-
logger?.debug(`[recordingFileToVideoClip] Fetching RTMP playback URL for fileName=${rec.fileName}`);
|
|
226
|
+
// logger?.debug(`[recordingFileToVideoClip] Fetching RTMP playback URL for fileName=${rec.fileName}`);
|
|
222
227
|
try {
|
|
223
228
|
const { rtmpVodUrl } = await api.getRecordingPlaybackUrls({
|
|
224
229
|
fileName: rec.fileName,
|
|
225
230
|
});
|
|
226
231
|
videoHref = rtmpVodUrl;
|
|
227
|
-
logger?.debug(`[recordingFileToVideoClip] RTMP URL fetched successfully: videoHref="${videoHref}"`);
|
|
232
|
+
// logger?.debug(`[recordingFileToVideoClip] RTMP URL fetched successfully: videoHref="${videoHref}"`);
|
|
228
233
|
} catch (e) {
|
|
229
234
|
logger?.debug(`[recordingFileToVideoClip] Failed to build playback URL for recording fileName=${rec.fileName}:`, e?.message || String(e));
|
|
230
235
|
}
|
|
231
236
|
} else {
|
|
232
|
-
logger?.debug(`[recordingFileToVideoClip] No URL generation: useWebhook=${useWebhook}, hasPlugin=${!!plugin}, deviceId=${deviceId}, providedVideoHref=${providedVideoHref || 'none'}, hasApi=${!!api}`);
|
|
237
|
+
// logger?.debug(`[recordingFileToVideoClip] No URL generation: useWebhook=${useWebhook}, hasPlugin=${!!plugin}, deviceId=${deviceId}, providedVideoHref=${providedVideoHref || 'none'}, hasApi=${!!api}`);
|
|
233
238
|
}
|
|
234
239
|
|
|
235
240
|
const description = ('name' in rec && typeof rec.name === 'string' && rec.name) ? rec.name : (rec.fileName ?? rec.id ?? '');
|