@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/src/utils.ts CHANGED
@@ -8,6 +8,25 @@ import fs from "fs";
8
8
  import path from "path";
9
9
  import crypto from "crypto";
10
10
  import { CommonCameraMixin } from "./common";
11
+ /**
12
+ * Sanitize FFmpeg output or URLs to avoid leaking credentials
13
+ */
14
+ export function sanitizeFfmpegOutput(text: string): string {
15
+ if (!text)
16
+ return text;
17
+
18
+ let sanitized = text;
19
+
20
+ // Remove user/password query parameters from URLs: ?user=xxx&password=yyy
21
+ sanitized = sanitized.replace(/(\buser=)[^&\s]*/gi, '$1***');
22
+ sanitized = sanitized.replace(/(\bpassword=)[^&\s]*/gi, '$1***');
23
+
24
+ // Remove credentials from URLs like rtmp://user:pass@host/...
25
+ sanitized = sanitized.replace(/(rtmp:\/\/)([^:@\/\s]+):([^@\/\s]+)@/gi, '$1$2:***@');
26
+
27
+ return sanitized;
28
+ }
29
+
11
30
 
12
31
  /**
13
32
  * Enumeration of operation types that may require specific channel assignments
@@ -186,6 +205,7 @@ export async function recordingFileToVideoClip(
186
205
  deviceId,
187
206
  fileId: id,
188
207
  plugin,
208
+ logger,
189
209
  });
190
210
  videoHref = videoUrl;
191
211
  thumbnailHref = thumbnailUrl;
@@ -316,10 +336,12 @@ export async function getVideoClipWebhookUrls(props: {
316
336
  deviceId: string;
317
337
  fileId: string;
318
338
  plugin: ScryptedDeviceBase;
339
+ logger?: Console;
319
340
  }): Promise<{ videoUrl: string; thumbnailUrl: string }> {
320
- const { deviceId, fileId, plugin } = props;
341
+ const { deviceId, fileId, plugin, logger } = props;
342
+ const log = logger || plugin.console;
321
343
 
322
- plugin.console.debug(`[getVideoClipWebhookUrls] Starting URL generation: deviceId=${deviceId}, fileId=${fileId}`);
344
+ // log.debug?.(`[getVideoClipWebhookUrls] Starting URL generation: deviceId=${deviceId}, fileId=${fileId}`);
323
345
 
324
346
  try {
325
347
  let endpoint: string;
@@ -327,13 +349,13 @@ export async function getVideoClipWebhookUrls(props: {
327
349
  try {
328
350
  endpoint = await sdk.endpointManager.getCloudEndpoint(undefined, { public: true });
329
351
  endpointSource = 'cloud';
330
- plugin.console.debug(`[getVideoClipWebhookUrls] Using cloud endpoint: ${endpoint}`);
352
+ // log.debug?.(`[getVideoClipWebhookUrls] Using cloud endpoint: ${endpoint}`);
331
353
  } catch (e) {
332
354
  // Fallback to local endpoint if cloud is not available (e.g., not logged in)
333
- plugin.console.debug(`[getVideoClipWebhookUrls] Cloud endpoint not available, using local endpoint: ${e instanceof Error ? e.message : String(e)}`);
355
+ log.debug?.(`[getVideoClipWebhookUrls] Cloud endpoint not available, using local endpoint: ${e instanceof Error ? e.message : String(e)}`);
334
356
  endpoint = await sdk.endpointManager.getLocalEndpoint(undefined, { public: true });
335
357
  endpointSource = 'local';
336
- plugin.console.debug(`[getVideoClipWebhookUrls] Using local endpoint: ${endpoint}`);
358
+ // log.debug?.(`[getVideoClipWebhookUrls] Using local endpoint: ${endpoint}`);
337
359
  }
338
360
 
339
361
  const encodedDeviceId = encodeURIComponent(deviceId);
@@ -341,8 +363,6 @@ export async function getVideoClipWebhookUrls(props: {
341
363
  const cleanFileId = fileId.startsWith('/') ? fileId.substring(1) : fileId;
342
364
  const encodedFileId = encodeURIComponent(cleanFileId);
343
365
 
344
- plugin.console.debug(`[getVideoClipWebhookUrls] Encoding: deviceId="${deviceId}" -> "${encodedDeviceId}", fileId="${fileId}" -> cleanFileId="${cleanFileId}" -> encodedFileId="${encodedFileId}"`);
345
-
346
366
  // Parse endpoint URL to extract query parameters (for authentication)
347
367
  const endpointUrl = new URL(endpoint);
348
368
  // Preserve query parameters (e.g., user_token for authentication)
@@ -350,22 +370,19 @@ export async function getVideoClipWebhookUrls(props: {
350
370
  // Remove query parameters from the base endpoint URL
351
371
  endpointUrl.search = '';
352
372
 
353
- plugin.console.debug(`[getVideoClipWebhookUrls] Parsed endpoint URL: base="${endpointUrl.toString()}", queryParams="${queryParams}"`);
354
-
355
373
  // Ensure endpoint has trailing slash
356
374
  const normalizedEndpoint = endpointUrl.toString().endsWith('/') ? endpointUrl.toString() : `${endpointUrl.toString()}/`;
357
375
 
358
- plugin.console.debug(`[getVideoClipWebhookUrls] Normalized endpoint: "${normalizedEndpoint}"`);
359
-
360
376
  // Build webhook URLs and append query parameters at the end
361
377
  const videoUrl = `${normalizedEndpoint}webhook/video/${encodedDeviceId}/${encodedFileId}${queryParams}`;
362
378
  const thumbnailUrl = `${normalizedEndpoint}webhook/thumbnail/${encodedDeviceId}/${encodedFileId}${queryParams}`;
363
379
 
364
- plugin.console.debug(`[getVideoClipWebhookUrls] Generated URLs: videoUrl="${videoUrl}", thumbnailUrl="${thumbnailUrl}"`);
365
-
366
380
  return { videoUrl, thumbnailUrl };
367
381
  } catch (e) {
368
- plugin.console.error(`[getVideoClipWebhookUrls] Failed to generate webhook URLs: deviceId=${deviceId}, fileId=${fileId}`, e);
382
+ log.error?.(
383
+ `[getVideoClipWebhookUrls] Failed to generate webhook URLs: deviceId=${deviceId}, fileId=${fileId}`,
384
+ e
385
+ );
369
386
  throw e;
370
387
  }
371
388
  }
@@ -378,9 +395,12 @@ export async function extractThumbnailFromVideo(props: {
378
395
  filePath?: string;
379
396
  fileId: string;
380
397
  deviceId: string;
381
- logger: Console;
398
+ logger?: Console;
399
+ device?: CommonCameraMixin;
382
400
  }): Promise<MediaObject> {
383
- const { rtmpUrl, filePath, fileId, deviceId, logger } = props;
401
+ const { rtmpUrl, filePath, fileId, deviceId, device } = props;
402
+ // Use device logger if available, otherwise fallback to provided logger
403
+ const logger = device?.getBaichuanLogger?.() || props.logger || console;
384
404
 
385
405
  // Use file path if available, otherwise use RTMP URL
386
406
  const inputSource = filePath || rtmpUrl;
@@ -427,12 +447,14 @@ export async function handleVideoClipRequest(props: {
427
447
  fileId: string;
428
448
  request: HttpRequest;
429
449
  response: HttpResponse;
430
- logger: Console;
450
+ logger?: Console;
431
451
  }): Promise<void> {
432
- const { device, deviceId, fileId, request, response, logger } = props;
452
+ const { device, deviceId, fileId, request, response } = props;
453
+ const logger = device.getBaichuanLogger?.() || props.logger || console;
433
454
 
434
455
  // Check if file is cached
435
456
  const cachePath = getVideoClipCachePath(deviceId, fileId);
457
+ const MIN_VIDEO_CACHE_BYTES = 16 * 1024; // 16KB, evita file quasi vuoti/corrotti
436
458
 
437
459
  try {
438
460
  // Check if cached file exists
@@ -440,6 +462,17 @@ export async function handleVideoClipRequest(props: {
440
462
  const fileSize = stat.size;
441
463
  const range = request.headers.range;
442
464
 
465
+ if (fileSize < MIN_VIDEO_CACHE_BYTES) {
466
+ logger.warn(`Cached video clip too small, deleting and reloading: fileId=${fileId}, size=${fileSize} bytes`);
467
+ try {
468
+ await fs.promises.unlink(cachePath);
469
+ } catch (unlinkErr) {
470
+ logger.warn(`Failed to delete small cached video clip: fileId=${fileId}`, unlinkErr);
471
+ }
472
+ // Force cache miss path below
473
+ throw new Error('Cached video too small, deleted');
474
+ }
475
+
443
476
  logger.log(`Serving cached video clip: fileId=${fileId}, size=${fileSize}, range=${range}`);
444
477
 
445
478
  if (range) {
@@ -503,12 +536,12 @@ export async function handleVideoClipRequest(props: {
503
536
 
504
537
  if (isHttpUrl) {
505
538
  // For HTTP URLs (Playback/Download from NVR), do direct HTTP proxy with ranged headers support
506
- logger.log(`Proxying HTTP URL directly: fileId=${fileId}, url=${rtmpVodUrl}`);
539
+ logger.debug(`Proxying HTTP URL directly: fileId=${fileId}, url=${rtmpVodUrl}`);
507
540
 
508
541
  const sendVideo = async () => {
509
542
  // Pre-fetch ffmpeg path in case we need it for FLV conversion
510
543
  const ffmpegPathPromise = sdk.mediaManager.getFFmpegPath();
511
-
544
+
512
545
  return new Promise<void>(async (resolve, reject) => {
513
546
  const urlObj = new URL(rtmpVodUrl);
514
547
  const httpModule = urlObj.protocol === 'https:' ? https : http;
@@ -531,7 +564,7 @@ export async function handleVideoClipRequest(props: {
531
564
  headers: requestHeaders,
532
565
  };
533
566
 
534
- logger.log(`Starting HTTP request: ${rtmpVodUrl}, headers: ${JSON.stringify(requestHeaders)}`);
567
+ logger.debug(`Starting HTTP request: ${rtmpVodUrl}, headers: ${JSON.stringify(requestHeaders)}`);
535
568
 
536
569
  httpModule.get(options, async (httpResponse) => {
537
570
  if (httpResponse.statusCode && httpResponse.statusCode >= 400) {
@@ -547,9 +580,9 @@ export async function handleVideoClipRequest(props: {
547
580
 
548
581
  // Check if we need to convert FLV to MP4
549
582
  const isFlv = typeof contentType === 'string' && (contentType === 'video/x-flv' || contentType === 'video/flv');
550
-
583
+
551
584
  if (isFlv) {
552
- logger.log(`Content-Type is FLV (${contentType}), will convert to MP4 using ffmpeg`);
585
+ logger.debug(`Content-Type is FLV (${contentType}), will convert to MP4 using ffmpeg`);
553
586
  }
554
587
 
555
588
  const responseHeaders: Record<string, string> = {
@@ -568,11 +601,11 @@ export async function handleVideoClipRequest(props: {
568
601
 
569
602
  const statusCode = httpResponse.statusCode || 200;
570
603
 
571
- logger.log(`HTTP response received: status=${statusCode}, contentType=${contentType}, contentLength=${contentLength || 'unknown'}`);
604
+ logger.debug(`HTTP response received: status=${statusCode}, contentType=${contentType}, contentLength=${contentLength || 'unknown'}`);
572
605
 
573
606
  try {
574
- if (isFlv) {
575
- // Convert FLV to MP4 using ffmpeg
607
+ if (isFlv) {
608
+ // Convert FLV to MP4 using ffmpeg
576
609
  const ffmpegPath = await ffmpegPathPromise;
577
610
  // Re-encode instead of copy because FLV codec might not be supported
578
611
  const ffmpegArgs: string[] = [
@@ -648,10 +681,11 @@ export async function handleVideoClipRequest(props: {
648
681
  // Handle ffmpeg errors
649
682
  ffmpeg.on('close', (code) => {
650
683
  if (code !== 0 && code !== null && !streamStarted) {
651
- logger.error(`FFmpeg conversion failed: fileId=${fileId}, code=${code}, error=${ffmpegError}`);
684
+ const sanitized = sanitizeFfmpegOutput(ffmpegError);
685
+ logger.error(`FFmpeg conversion failed: fileId=${fileId}, code=${code}, error=${sanitized}`);
652
686
  reject(new Error(`FFmpeg conversion failed: ${code}`));
653
687
  } else {
654
- logger.log(`FFmpeg conversion completed: fileId=${fileId}, code=${code}`);
688
+ logger.debug(`FFmpeg conversion completed: fileId=${fileId}, code=${code}`);
655
689
  resolve();
656
690
  }
657
691
  });
@@ -661,7 +695,7 @@ export async function handleVideoClipRequest(props: {
661
695
  reject(error);
662
696
  });
663
697
 
664
- logger.log(`FFmpeg conversion started: fileId=${fileId}`);
698
+ logger.debug(`FFmpeg conversion started: fileId=${fileId}`);
665
699
  } else {
666
700
  // Direct proxy for non-FLV content (should be MP4 already)
667
701
  // Stream directly without buffering - yield chunks as they arrive
@@ -757,7 +791,8 @@ export async function handleVideoClipRequest(props: {
757
791
  // Handle ffmpeg errors
758
792
  ffmpeg.on('close', (code) => {
759
793
  if (code !== 0 && code !== null && !streamStarted) {
760
- logger.error(`FFmpeg proxy failed for video: fileId=${fileId}, code=${code}, error=${ffmpegError}`);
794
+ const sanitized = sanitizeFfmpegOutput(ffmpegError);
795
+ logger.error(`FFmpeg proxy failed for video: fileId=${fileId}, code=${code}, error=${sanitized}`);
761
796
  }
762
797
  });
763
798
 
@@ -953,6 +988,7 @@ export async function vodSearchResultsToVideoClips(
953
988
  deviceId,
954
989
  fileId: fileName,
955
990
  plugin,
991
+ logger,
956
992
  });
957
993
  videoHref = videoUrl;
958
994
  thumbnailHref = thumbnailUrl;