@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/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.2.11",
3
+ "version": "0.2.14",
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",
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 listEnrichedVodFiles (library handles parsing correctly)
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
- // Prefer Hub-like event listing (alarm events) instead of full VOD.
794
- logger.debug(`[NVR EVENTS] Searching for alarm events: channel=${channel}, start=${start.toISOString()}, end=${end.toISOString()}`);
795
- const enrichedRecordings = await api.listNvrAlarmEventsEnrichedViaBaichuan({
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
- channels: [channel],
799
- streamType: "mainStream",
798
+ streamType: "main",
799
+ autoSearchByDay: true,
800
+ fetchStreamUrls: false,
800
801
  });
801
802
 
802
- logger.debug(`[NVR EVENTS] Found ${enrichedRecordings.length} enriched alarm events from NVR`);
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 EVENTS] Converted ${clips.length} video clips (limit: ${count || 'none'})`);
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 && videoId.includes('/');
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
- // Standalone camera (or fallback): reuse getVideoClipRtmpUrl (Baichuan RTMP)
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 && fileId.includes('/');
1117
+ const useNvr = clipsSource === "NVR" && this.nvrDevice;
1163
1118
 
1164
1119
  if (useNvr) {
1165
- logger.debug(`[getVideoClipRtmpUrl] Using NVR API for fileId="${fileId}", forThumbnail=${forThumbnail}`);
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
- const id = rec.id || rec.fileName;
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 ?? '');