@apocaliss92/scrypted-reolink-native 0.2.8 → 0.2.10

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/camera.ts CHANGED
@@ -17,10 +17,7 @@ import { ReolinkNativeMultiFocalDevice } from "./multiFocal";
17
17
  import { ReolinkNativeNvrDevice } from "./nvr";
18
18
  import { ReolinkPtzPresets } from "./presets";
19
19
  import {
20
- createRfc4571CompositeMediaObjectFromStreamManager,
21
20
  createRfc4571MediaObjectFromStreamManager,
22
- extractVariantFromStreamId,
23
- parseStreamProfileFromId,
24
21
  selectStreamOption,
25
22
  StreamManager,
26
23
  StreamManagerOptions
@@ -48,7 +45,7 @@ class ReolinkCameraSiren extends ScryptedDeviceBase implements OnOff {
48
45
  this.camera.getBaichuanLogger().log(`Siren toggle: turnOff ok (device=${this.nativeId})`);
49
46
  }
50
47
  catch (e) {
51
- this.camera.getBaichuanLogger().warn(`Siren toggle: turnOff failed (device=${this.nativeId})`, e?.message || String(e));
48
+ this.camera.getBaichuanLogger().error(`Siren toggle: turnOff failed (device=${this.nativeId})`, e?.message || String(e));
52
49
  throw e;
53
50
  }
54
51
  }
@@ -61,7 +58,7 @@ class ReolinkCameraSiren extends ScryptedDeviceBase implements OnOff {
61
58
  this.camera.getBaichuanLogger().log(`Siren toggle: turnOn ok (device=${this.nativeId})`);
62
59
  }
63
60
  catch (e) {
64
- this.camera.getBaichuanLogger().warn(`Siren toggle: turnOn failed (device=${this.nativeId})`, e?.message || String(e));
61
+ this.camera.getBaichuanLogger().error(`Siren toggle: turnOn failed (device=${this.nativeId})`, e?.message || String(e));
65
62
  throw e;
66
63
  }
67
64
  }
@@ -513,6 +510,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
513
510
  subgroup: 'Diagnostics',
514
511
  description: "Directory where diagnostics files will be saved (default: plugin volume).",
515
512
  type: "string",
513
+ hide: true,
516
514
  defaultValue: path.join(process.env.SCRYPTED_PLUGIN_VOLUME, 'diagnostics', this.name),
517
515
  },
518
516
  enableVideoclips: {
@@ -521,6 +519,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
521
519
  type: "boolean",
522
520
  defaultValue: false,
523
521
  immediate: true,
522
+ hide: true,
524
523
  onPut: async () => {
525
524
  this.updateVideoClipsAutoLoad();
526
525
  },
@@ -532,6 +531,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
532
531
  type: "string",
533
532
  choices: ["NVR", "Device"],
534
533
  immediate: true,
534
+ hide: true,
535
535
  },
536
536
  loadVideoclips: {
537
537
  title: "Auto-load Video Clips",
@@ -540,6 +540,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
540
540
  type: "boolean",
541
541
  defaultValue: false,
542
542
  immediate: true,
543
+ hide: true,
543
544
  onPut: async () => {
544
545
  this.updateVideoClipsAutoLoad();
545
546
  },
@@ -550,6 +551,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
550
551
  description: "How often to check for new video clips and download thumbnails (default: 30 minutes).",
551
552
  type: "number",
552
553
  defaultValue: 30,
554
+ hide: true,
553
555
  onPut: async () => {
554
556
  this.updateVideoClipsAutoLoad();
555
557
  },
@@ -561,6 +563,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
561
563
  type: "boolean",
562
564
  defaultValue: false,
563
565
  immediate: true,
566
+ hide: true,
564
567
  onPut: async () => {
565
568
  this.updateVideoClipsAutoLoad();
566
569
  },
@@ -571,6 +574,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
571
574
  description: "Number of days to preload video clips and thumbnails (default: 1, only today).",
572
575
  type: "number",
573
576
  defaultValue: 3,
577
+ hide: true,
574
578
  onPut: async () => {
575
579
  this.updateVideoClipsAutoLoad();
576
580
  },
@@ -580,6 +584,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
580
584
  title: 'Run Diagnostics',
581
585
  description: 'Run all diagnostics and save results to the output path.',
582
586
  type: 'button',
587
+ hide: true,
583
588
  immediate: true,
584
589
  onPut: async () => {
585
590
  await this.runDiagnostics();
@@ -618,15 +623,38 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
618
623
  },
619
624
  pipMargin: {
620
625
  title: 'PIP Margin',
621
- description: 'Margin from edge in pixels',
626
+ description: 'Margin from edge as a fraction of the output size (e.g. 0.01 = 1%). Values > 1 are treated as pixels (legacy).',
622
627
  type: 'number',
623
- defaultValue: 10,
628
+ defaultValue: 0.01,
624
629
  group: 'Composite stream',
625
630
  hide: true,
626
631
  onPut: async () => {
627
632
  this.scheduleStreamManagerRestart('pipMargin changed');
628
633
  },
629
634
  },
635
+
636
+ compositeAssumeH264: {
637
+ title: 'Composite: Assume H.264 Inputs',
638
+ description: 'Assume both wider+tele inputs are H.264 (skips codec detection). Recommended when using sub+sub on TrackMix. If inputs are actually H.265, the composite may fail to start.',
639
+ type: 'boolean',
640
+ defaultValue: true,
641
+ group: 'Composite stream',
642
+ hide: true,
643
+ onPut: async () => {
644
+ this.scheduleStreamManagerRestart('compositeAssumeH264 changed');
645
+ },
646
+ },
647
+ compositeDisableTranscode: {
648
+ title: 'Composite: Disable Codec Transcode (Best-effort)',
649
+ description: 'Best-effort knob. Overlay requires re-encode in ffmpeg; this option only avoids HEVC->H264 codec assumptions when possible. Leave off unless you know what you are doing.',
650
+ type: 'boolean',
651
+ defaultValue: false,
652
+ group: 'Composite stream',
653
+ hide: true,
654
+ onPut: async () => {
655
+ this.scheduleStreamManagerRestart('compositeDisableTranscode changed');
656
+ },
657
+ },
630
658
  });
631
659
 
632
660
  ptzPresets = new ReolinkPtzPresets(this);
@@ -762,18 +790,16 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
762
790
  // Fetch from NVR using listEnrichedVodFiles (library handles parsing correctly)
763
791
  const channel = this.storageSettings.values.rtspChannel ?? 0;
764
792
 
765
- // Use listEnrichedVodFiles which properly parses filenames and extracts detection info
766
- logger.debug(`[NVR VOD] Searching for video clips: channel=${channel}, start=${start.toISOString()}, end=${end.toISOString()}`);
767
- // Filter to only include recordings within the requested time window
768
- const enrichedRecordings = await api.listNvrRecordings({
769
- channel,
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({
770
796
  start,
771
797
  end,
772
- streamType: "main",
773
- source: "baichuan"
798
+ channels: [channel],
799
+ streamType: "mainStream",
774
800
  });
775
801
 
776
- logger.debug(`[NVR VOD] Found ${enrichedRecordings.length} enriched recordings from NVR`);
802
+ logger.debug(`[NVR EVENTS] Found ${enrichedRecordings.length} enriched alarm events from NVR`);
777
803
 
778
804
  // Convert enriched recordings to VideoClip array using the shared parser
779
805
  const clips = await recordingsToVideoClips(enrichedRecordings, {
@@ -785,7 +811,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
785
811
  count,
786
812
  });
787
813
 
788
- logger.debug(`[NVR VOD] Converted ${clips.length} video clips (limit: ${count || 'none'})`);
814
+ logger.debug(`[NVR EVENTS] Converted ${clips.length} video clips (limit: ${count || 'none'})`);
789
815
 
790
816
  return clips;
791
817
  } else {
@@ -870,7 +896,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
870
896
  try {
871
897
  await fs.promises.unlink(cachePath);
872
898
  } catch (unlinkErr) {
873
- logger.warn(`[VideoClip] Failed to delete small cached file: fileId=${videoId}`, unlinkErr);
899
+ logger.warn(`[VideoClip] Failed to delete small cached file: fileId=${videoId}`, unlinkErr?.message || String(unlinkErr));
874
900
  }
875
901
  } else {
876
902
  logger.debug(`[VideoClip] Using cached file: fileId=${videoId}, size=${stats.size} bytes`);
@@ -988,7 +1014,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
988
1014
  });
989
1015
 
990
1016
  ffmpeg.on('error', (error) => {
991
- logger.error(`ffmpeg spawn error for video clip ${videoId}`, error);
1017
+ logger.error(`ffmpeg spawn error for video clip ${videoId}`, error?.message || String(error));
992
1018
  reject(error);
993
1019
  });
994
1020
 
@@ -1056,7 +1082,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
1056
1082
  try {
1057
1083
  await fs.promises.unlink(cachePath);
1058
1084
  } catch (unlinkErr) {
1059
- logger.warn(`[Thumbnail] Failed to delete small cached thumbnail: fileId=${thumbnailId}`, unlinkErr);
1085
+ logger.warn(`[Thumbnail] Failed to delete small cached thumbnail: fileId=${thumbnailId}`, unlinkErr?.message || String(unlinkErr));
1060
1086
  }
1061
1087
  } else {
1062
1088
  logger.debug(`[Thumbnail] Using cached: fileId=${thumbnailId}, size=${stats.size} bytes`);
@@ -1541,7 +1567,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
1541
1567
  };
1542
1568
 
1543
1569
  if (this.isMultiFocal) {
1544
- const { pipPosition, pipSize, pipMargin, rtspChannel } = this.storageSettings.values;
1570
+ const { pipPosition, pipSize, pipMargin, rtspChannel, compositeAssumeH264, compositeDisableTranscode } = this.storageSettings.values;
1545
1571
 
1546
1572
  // On NVR/Hub, TrackMix lenses are selected via stream variant, not via a separate channel.
1547
1573
  // Use rtspChannel for BOTH wide and tele so the library can request tele via streamType/variant.
@@ -1583,6 +1609,10 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
1583
1609
  pipSize,
1584
1610
  pipMargin,
1585
1611
  onNvr: this.isOnNvr,
1612
+ // Prefer H.264 for composite (sub+sub by default) to reduce GOP latency.
1613
+ forceH264: true,
1614
+ assumeH264Inputs: compositeAssumeH264 ?? true,
1615
+ disableTranscode: compositeDisableTranscode ?? false,
1586
1616
  };
1587
1617
  }
1588
1618
 
@@ -1618,14 +1648,9 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
1618
1648
  this.lastPicture = undefined;
1619
1649
  }
1620
1650
 
1621
- // Notify consumers (e.g. prebuffer) that stream configuration changed.
1622
- try {
1623
- this.onDeviceEvent(ScryptedInterface.VideoCamera, undefined);
1624
- } catch {
1625
- // best-effort
1626
- }
1651
+ this.onDeviceEvent(ScryptedInterface.VideoCamera, undefined);
1627
1652
  } catch (e) {
1628
- logger.warn('Failed to restart StreamManager after settings change', e);
1653
+ logger.error('Failed to restart StreamManager after settings change', e?.message || String(e));
1629
1654
  }
1630
1655
  }, 500);
1631
1656
  }
@@ -1686,14 +1711,14 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
1686
1711
  reason: ev?.type === 'sleeping' ? 'sleeping' : 'awake',
1687
1712
  state: ev.type === 'sleeping' ? 'sleeping' : 'awake',
1688
1713
  }).catch((e) => {
1689
- logger.warn('Error updating sleeping state', e);
1714
+ logger.warn('Error updating sleeping state', e?.message || String(e));
1690
1715
  });
1691
1716
  return;
1692
1717
 
1693
1718
  case 'offline':
1694
1719
  case 'online':
1695
1720
  this.updateOnlineState(ev.type === 'online').catch((e) => {
1696
- logger.warn('Error updating online state', e);
1721
+ logger.warn('Error updating online state', e?.message || String(e));
1697
1722
  });
1698
1723
  return;
1699
1724
 
@@ -1722,11 +1747,11 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
1722
1747
  }
1723
1748
 
1724
1749
  this.processEvents({ motion, objects }).catch((e) => {
1725
- logger.warn('Error processing events', e);
1750
+ logger.warn('Error processing events', e?.message || String(e));
1726
1751
  });
1727
1752
  }
1728
1753
  catch (e) {
1729
- logger.warn('Error in onSimpleEvent handler', e);
1754
+ logger.warn('Error in onSimpleEvent handler', e?.message || String(e));
1730
1755
  }
1731
1756
  }
1732
1757
 
@@ -1774,7 +1799,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
1774
1799
  logger.log(`Subscribed to events (${selection.join(', ')}) on ${this.protocol} connection`);
1775
1800
  }
1776
1801
  catch (e) {
1777
- logger.warn('Failed to subscribe to Baichuan events', e);
1802
+ logger.warn('Failed to subscribe to Baichuan events', e?.message || String(e));
1778
1803
  return;
1779
1804
  }
1780
1805
  }
@@ -2114,7 +2139,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
2114
2139
  return await this.takePictureInternal(client);
2115
2140
  });
2116
2141
  } catch (e) {
2117
- this.getBaichuanLogger().error('Error taking snapshot', e);
2142
+ this.getBaichuanLogger().error('Error taking snapshot', e?.message || String(e));
2118
2143
  throw e;
2119
2144
  }
2120
2145
  } else {
@@ -2190,7 +2215,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
2190
2215
  });
2191
2216
 
2192
2217
  } catch (e) {
2193
- logger.warn('Failed to fetch device info', e);
2218
+ logger.warn('Failed to fetch device info', e?.message || String(e));
2194
2219
  }
2195
2220
  }
2196
2221
 
@@ -2413,37 +2438,32 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
2413
2438
  // logger.log({ supportedStreams, variantType, lensParam, rtspChannel, onNvr: this.isOnNvr, nativeStreams: nativeStreams.map(s => ({ id: s.id, nativeVariant: s.nativeVariant, lens: s.lens })), rtspStreams: rtspStreams.map(s => ({ id: s.id, lens: s.lens })), rtmpStreams: rtmpStreams.map(s => ({ id: s.id, lens: s.lens })) });
2414
2439
 
2415
2440
  for (const supportedStream of supportedStreams) {
2416
- const { id, metadata, url, name, container, nativeVariant, lens } = supportedStream;
2441
+ const { id, metadata, url, name, container, lens } = supportedStream;
2417
2442
 
2418
2443
  // Composite streams are re-encoded to H.264 by the library (ffmpeg/libx264).
2419
2444
  // Do not infer codec from underlying camera metadata.
2420
2445
  const isComposite = id.startsWith('composite_') || lens === 'composite';
2421
- const codec = isComposite
2422
- ? 'h264'
2423
- : String(metadata.videoEncType || "").includes("264")
2424
- ? "h264"
2425
- : String(metadata.videoEncType || "").includes("265")
2426
- ? "h265"
2427
- : String(metadata.videoEncType || "").toLowerCase();
2428
-
2429
- // Preserve variant information for native RTP streams by ensuring the URL contains it.
2430
- let finalUrl = url;
2431
- const variantFromIdOrUrl = extractVariantFromStreamId(id, url);
2432
- const variantToInject = (nativeVariant && nativeVariant !== 'default')
2433
- ? nativeVariant
2434
- : variantFromIdOrUrl;
2435
-
2436
- if (variantToInject && container === 'rtp') {
2437
- try {
2438
- const urlObj = new URL(url);
2439
- if (!urlObj.searchParams.has('variant')) {
2440
- urlObj.searchParams.set('variant', variantToInject);
2441
- finalUrl = urlObj.toString();
2442
- }
2443
- } catch {
2444
- // Invalid URL, use original
2446
+ const codec = (() => {
2447
+ if (isComposite) return 'h264';
2448
+
2449
+ const enc = (metadata as any)?.videoEncType;
2450
+ // Many firmwares expose videoEncType as a numeric enum.
2451
+ // Observed: 0 => H.264, 1 => H.265.
2452
+ if (typeof enc === 'number') {
2453
+ if (enc === 0) return 'h264';
2454
+ if (enc === 1) return 'h265';
2445
2455
  }
2446
- }
2456
+
2457
+ const s = String(enc ?? '').toLowerCase();
2458
+ if (s === '0') return 'h264';
2459
+ if (s === '1') return 'h265';
2460
+ if (s.includes('264')) return 'h264';
2461
+ if (s.includes('265')) return 'h265';
2462
+ return s;
2463
+ })();
2464
+
2465
+ // For RTP (native RFC4571), stream identification happens via `id` (streamKey), not URL.
2466
+ const finalUrl = url;
2447
2467
 
2448
2468
  streams.push({
2449
2469
  id,
@@ -2486,43 +2506,6 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
2486
2506
 
2487
2507
  const selected = selectStreamOption(vsos, vso);
2488
2508
 
2489
- // If the request explicitly asks for a variant (e.g. native_telephoto_main),
2490
- // never override it with the device's variantType preference.
2491
- // const requestedVariant = vso?.id ? extractVariantFromStreamId(vso.id, undefined) : undefined;
2492
-
2493
- // If we have variantType set and the selected stream doesn't have the variant,
2494
- // try to find a stream with the correct variant that matches the profile
2495
- // const variantType = this.storageSettings.values.variantType;
2496
- // if (!requestedVariant && variantType && variantType !== 'default') {
2497
- // const profile = parseStreamProfileFromId(selected.id) || 'main';
2498
-
2499
- // // On NVR, firmwares vary: some expose the tele lens as 'autotrack', others as 'telephoto'.
2500
- // // When variantType is set, prefer that variant but fall back to the other tele variant if present.
2501
- // const preferred = variantType as 'autotrack' | 'telephoto';
2502
- // const fallbacks: Array<'autotrack' | 'telephoto'> = this.isOnNvr && preferred === 'telephoto'
2503
- // ? ['telephoto', 'autotrack']
2504
- // : this.isOnNvr && preferred === 'autotrack'
2505
- // ? ['autotrack', 'telephoto']
2506
- // : [preferred];
2507
-
2508
- // const extractedVariant = extractVariantFromStreamId(selected.id, selected.url);
2509
- // for (const v of fallbacks) {
2510
- // const variantId = `native_${v}_${profile}`;
2511
- // const variantStream = vsos?.find(s => s.id === variantId);
2512
- // if (!variantStream) {
2513
- // logger.debug(`Variant stream '${variantId}' not found in available streams`);
2514
- // continue;
2515
- // }
2516
- // // Only use variant stream if the selected one doesn't already have a variant,
2517
- // // or if the selected one has a different variant than what we want.
2518
- // if (!extractedVariant || extractedVariant !== v) {
2519
- // logger.log(`Preferring variant stream: '${variantId}' over '${selected.id}' (variantType='${variantType}')`);
2520
- // selected = variantStream;
2521
- // }
2522
- // break;
2523
- // }
2524
- // }
2525
-
2526
2509
  logger.log(`Selected stream: id='${selected.id}', url='${selected.url}'`);
2527
2510
 
2528
2511
  if (selected.url && (selected.container === 'rtsp' || selected.container === 'rtmp')) {
@@ -2539,76 +2522,21 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
2539
2522
  throw new Error('StreamManager not initialized');
2540
2523
  }
2541
2524
 
2542
- // Check if this is a composite stream request (for multifocal devices)
2543
- // const isComposite = selected.id?.startsWith('composite_');
2544
- // if (isComposite && this.options && (this.options.type === 'multi-focal' || this.options.type === 'multi-focal-battery')) {
2545
- if (selected.id?.startsWith('composite_')) {
2546
- const profile = parseStreamProfileFromId(selected.id.replace('composite_', '')) || 'main';
2547
- // Include variantType in streamKey to ensure each variantType has its own unique socket
2548
- // This is important for multifocal devices where different variantTypes may request composite streams
2549
- const variantType = this.storageSettings.values.variantType || 'default';
2550
- const streamKey = `composite_${variantType}_${profile}`;
2551
-
2552
- logger.log(`Creating composite stream: profile=${profile}, variantType=${variantType}, streamKey=${streamKey}`);
2553
-
2554
- const createStreamFn = async () => {
2555
- return await createRfc4571CompositeMediaObjectFromStreamManager({
2556
- streamManager: this.streamManager,
2557
- profile,
2558
- streamKey,
2559
- selected,
2560
- sourceId: this.id,
2561
- variantType,
2562
- });
2563
- };
2564
-
2565
- return await this.withBaichuanRetry(createStreamFn);
2566
- }
2567
-
2568
- // Regular stream for single channel
2569
- const profile = parseStreamProfileFromId(selected.id) || 'main';
2570
- const channel = this.storageSettings.values.rtspChannel;
2571
- // Extract variant from stream ID or URL if present (e.g., "autotrack" from "native_autotrack_main" or "?variant=autotrack")
2572
- let variant = extractVariantFromStreamId(selected.id, selected.url);
2573
-
2574
- // Fallback: if no variant found in stream ID/URL, use variantType from device settings.
2575
- // IMPORTANT:
2576
- // - On NVR/Hub multifocal setups, the tele lens is often selected via a variant (autotrack/telephoto) on the same channel.
2577
- // - On standalone TrackMix (no NVR), the tele lens is selected via channel=1 (no variant).
2578
- // Forcing a variant on standalone can result in a started stream with no frames.
2579
- if (!variant && this.storageSettings.values.variantType && this.storageSettings.values.variantType !== 'default') {
2580
- if (this.isOnNvr) {
2581
- variant = this.storageSettings.values.variantType as 'autotrack' | 'telephoto';
2582
- logger.log(`Using variant from device settings: '${variant}' (not found in stream ID/URL)`);
2583
- } else {
2584
- logger.log(
2585
- `Ignoring device variantType '${this.storageSettings.values.variantType}' for standalone stream (channel-based lens selection)`
2586
- );
2587
- }
2525
+ const streamKey = selected.id;
2526
+ if (!streamKey) {
2527
+ throw new Error('Missing streamKey (selected.id) for RTP stream');
2588
2528
  }
2589
2529
 
2590
- logger.log(`Stream selection: id='${selected.id}', profile='${profile}', channel=${channel}, variant='${variant || 'default'}'`);
2591
-
2592
- // Include variant in streamKey to distinguish streams with different variants
2593
- const streamKey = variant ? `${channel}_${variant}_${profile}` : `${channel}_${profile}`;
2594
-
2595
- const createStreamFn = async () => {
2596
- // Honor the requested variant. Some NVR firmwares label the tele lens as either
2597
- // 'autotrack' or 'telephoto', and the library exposes both when available.
2598
- logger.log(`Creating RFC4571 stream: channel=${channel}, profile=${profile}, variant=${variant || 'default'}, streamKey=${streamKey}`);
2530
+ logger.log(`Creating RFC4571 stream: streamKey='${streamKey}'`);
2599
2531
 
2532
+ return await this.withBaichuanRetry(async () => {
2600
2533
  return await createRfc4571MediaObjectFromStreamManager({
2601
2534
  streamManager: this.streamManager!,
2602
- channel,
2603
- profile,
2604
2535
  streamKey,
2605
- variant,
2606
2536
  selected,
2607
2537
  sourceId: this.id,
2608
2538
  });
2609
- };
2610
-
2611
- return await this.withBaichuanRetry(createStreamFn);
2539
+ });
2612
2540
  }
2613
2541
 
2614
2542
  async ensureClient(): Promise<ReolinkBaichuanApi> {
@@ -2619,7 +2547,6 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
2619
2547
  return await this.multiFocalDevice.ensureClient();
2620
2548
  }
2621
2549
 
2622
- // Use base class implementation
2623
2550
  return await this.ensureBaichuanClient();
2624
2551
  }
2625
2552
 
@@ -2627,7 +2554,6 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
2627
2554
  this.cachedVideoStreamOptions = undefined;
2628
2555
  }
2629
2556
 
2630
- // PTZ Presets methods
2631
2557
  getSelectedPresetId(): number | undefined {
2632
2558
  const s = this.storageSettings.values.ptzSelectedPreset;
2633
2559
  if (!s) return undefined;
@@ -2740,16 +2666,9 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
2740
2666
  this.storageSettings.settings.clipsSource.hide = !this.nvrDevice;
2741
2667
  this.storageSettings.settings.clipsSource.defaultValue = this.nvrDevice ? "NVR" : "Device";
2742
2668
 
2743
- // if (!!this.multiFocalDevice) {
2744
- // const allSettingKeys = Object.keys(this.storageSettings.settings);
2669
+ this.storageSettings.settings.diagnosticsRun.hide = !!this.multiFocalDevice;
2670
+ this.storageSettings.settings.diagnosticsOutputPath.hide = !!this.multiFocalDevice;
2745
2671
 
2746
- // for (const key of allSettingKeys) {
2747
- // const setting = this.storageSettings.settings[key];
2748
- // if (['Videoclips', 'PTZ'].includes(setting.subgroup)) {
2749
- // setting.hide = true;
2750
- // }
2751
- // }
2752
- // }
2753
2672
  this.storageSettings.settings.enableVideoclips.hide = !!this.multiFocalDevice;
2754
2673
  this.storageSettings.settings.videoclipsDaysToPreload.hide = !!this.multiFocalDevice;
2755
2674
  this.storageSettings.settings.videoclipsRegularChecks.hide = !!this.multiFocalDevice;
@@ -2790,14 +2709,14 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
2790
2709
  await this.subscribeToEvents();
2791
2710
  }
2792
2711
  catch (e) {
2793
- logger.warn('Failed to subscribe to Baichuan events', e);
2712
+ logger.error('Failed to subscribe to Baichuan events', e?.message || String(e));
2794
2713
  }
2795
2714
 
2796
2715
  try {
2797
2716
  this.initStreamManager();
2798
2717
  }
2799
2718
  catch (e) {
2800
- logger.warn('Failed to initialize StreamManager', e);
2719
+ logger.error('Failed to initialize StreamManager', e?.message || String(e));
2801
2720
  }
2802
2721
 
2803
2722
  const { hasIntercom, hasPtz } = this.getAbilities();
@@ -2806,7 +2725,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
2806
2725
  this.intercom = new ReolinkBaichuanIntercom(this);
2807
2726
  }
2808
2727
 
2809
- if (hasPtz && !this.multiFocalDevice) {
2728
+ if (hasPtz) {
2810
2729
  const choices = (this.presets || []).map((preset: any) => preset.id + '=' + preset.name);
2811
2730
 
2812
2731
  this.storageSettings.settings.presets.choices = choices;
@@ -2870,7 +2789,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
2870
2789
  }
2871
2790
  } catch (e) {
2872
2791
  // Silently ignore errors in sleep check to avoid spam
2873
- this.getBaichuanLogger().debug('Error in updateSleepingState:', e);
2792
+ this.getBaichuanLogger().debug('Error in updateSleepingState:', e?.message || String(e));
2874
2793
  }
2875
2794
  }
2876
2795
 
@@ -2885,7 +2804,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
2885
2804
  }
2886
2805
  } catch (e) {
2887
2806
  // Silently ignore errors in sleep check to avoid spam
2888
- this.getBaichuanLogger().debug('Error in updateOnlineState:', e);
2807
+ this.getBaichuanLogger().debug('Error in updateOnlineState:', e?.message || String(e));
2889
2808
  }
2890
2809
  }
2891
2810
 
@@ -2895,7 +2814,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
2895
2814
  await this.baichuanApi?.close();
2896
2815
  }
2897
2816
  catch (e) {
2898
- this.getBaichuanLogger().warn('Error closing Baichuan client during reset', e?.message || String(e));
2817
+ this.getBaichuanLogger().error('Error closing Baichuan client during reset', e?.message || String(e));
2899
2818
  }
2900
2819
  finally {
2901
2820
  this.baichuanApi = undefined;
@@ -2905,7 +2824,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
2905
2824
 
2906
2825
  if (reason) {
2907
2826
  const message = reason?.message || reason?.toString?.() || reason;
2908
- this.getBaichuanLogger().warn(`Baichuan client reset requested: ${message}`);
2827
+ this.getBaichuanLogger().error(`Baichuan client reset requested: ${message}`);
2909
2828
  }
2910
2829
  }
2911
2830
 
@@ -2995,7 +2914,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
2995
2914
  // Ensure we have a client connection
2996
2915
  const api = await this.ensureClient();
2997
2916
  if (!api) {
2998
- this.getBaichuanLogger().warn('Failed to ensure client connection for battery update');
2917
+ this.getBaichuanLogger().error('Failed to ensure client connection for battery update');
2999
2918
  return;
3000
2919
  }
3001
2920
 
@@ -3009,7 +2928,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
3009
2928
  await api.wakeUp(channel, { waitAfterWakeMs: 2000 });
3010
2929
  logger.log('Wake command sent, waiting for camera to wake up...');
3011
2930
  } catch (wakeError) {
3012
- logger.warn('Failed to wake up camera:', wakeError);
2931
+ logger.error('Failed to wake up camera:', wakeError?.message || String(wakeError));
3013
2932
  return;
3014
2933
  }
3015
2934
 
@@ -3030,7 +2949,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
3030
2949
  }
3031
2950
 
3032
2951
  if (!awake) {
3033
- logger.warn('Camera did not wake up within timeout, skipping update');
2952
+ logger.error('Camera did not wake up within timeout, skipping update');
3034
2953
  return;
3035
2954
  }
3036
2955
  } else if (sleepStatus.state === 'awake') {
@@ -3042,14 +2961,14 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
3042
2961
  try {
3043
2962
  await this.updateBatteryInfo();
3044
2963
  } catch (e) {
3045
- logger.warn('Failed to get battery info during periodic update:', e);
2964
+ logger.error('Failed to get battery info during periodic update:', e?.message || String(e));
3046
2965
  }
3047
2966
 
3048
2967
  // 2. Align auxiliary devices state
3049
2968
  try {
3050
2969
  await this.alignAuxDevicesState();
3051
2970
  } catch (e) {
3052
- logger.warn('Failed to align auxiliary devices state:', e);
2971
+ logger.error('Failed to align auxiliary devices state:', e?.message || String(e));
3053
2972
  }
3054
2973
 
3055
2974
  // 3. Update snapshot
@@ -3058,10 +2977,10 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
3058
2977
  await this.takePicture();
3059
2978
  logger.log('Snapshot updated during periodic update');
3060
2979
  } catch (snapshotError) {
3061
- logger.warn('Failed to update snapshot during periodic update:', snapshotError);
2980
+ logger.error('Failed to update snapshot during periodic update:', snapshotError?.message || String(snapshotError));
3062
2981
  }
3063
2982
  } catch (e) {
3064
- logger.warn('Failed to update battery and snapshot', e);
2983
+ logger.error('Failed to update battery and snapshot', e?.message || String(e));
3065
2984
  } finally {
3066
2985
  // Clear the promise when done (success or failure)
3067
2986
  this.batteryUpdatePromise = undefined;
@@ -3096,7 +3015,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
3096
3015
  const sleepStatus = api.getSleepStatus({ channel });
3097
3016
  await this.updateSleepingState(sleepStatus);
3098
3017
  } catch (e) {
3099
- logger.warn('Error checking sleeping state:', e?.message || String(e));
3018
+ logger.error('Error checking sleeping state:', e?.message || String(e));
3100
3019
  }
3101
3020
  }, 5_000);
3102
3021
  }
@@ -3108,7 +3027,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
3108
3027
  try {
3109
3028
  await this.updateBatteryAndSnapshot();
3110
3029
  } catch (e) {
3111
- logger.warn('Error updating battery and snapshot:', e?.message || String(e));
3030
+ logger.error('Error updating battery and snapshot:', e?.message || String(e));
3112
3031
  }
3113
3032
  }, updateIntervalMs);
3114
3033
 
@@ -3118,7 +3037,7 @@ export class ReolinkCamera extends BaseBaichuanClass implements VideoCamera, Cam
3118
3037
  try {
3119
3038
  await this.alignAuxDevicesState();
3120
3039
  } catch (e) {
3121
- logger.warn('Error aligning auxiliary devices state:', e?.message || String(e));
3040
+ logger.error('Error aligning auxiliary devices state:', e?.message || String(e));
3122
3041
  }
3123
3042
  }, 10_000);
3124
3043