@apocaliss92/scrypted-reolink-native 0.1.30 → 0.1.32

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/common.ts CHANGED
@@ -1,17 +1,18 @@
1
1
  import type { DeviceCapabilities, PtzCommand, PtzPreset, ReolinkBaichuanApi, ReolinkSimpleEvent, ReolinkSupportedStream, StreamSamplingSelection } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
2
- import sdk, { BinarySensor, Brightness, Camera, Device, DeviceProvider, Intercom, MediaObject, MediaStreamUrl, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, OnOff, PanTiltZoom, PanTiltZoomCommand, RequestMediaStreamOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera, VideoTextOverlay, VideoTextOverlays } from "@scrypted/sdk";
2
+ import sdk, { BinarySensor, Brightness, Camera, Device, DeviceProvider, Intercom, MediaObject, MediaStreamUrl, ObjectDetectionTypes, ObjectDetector, ObjectsDetected, OnOff, PanTiltZoom, PanTiltZoomCommand, Reboot, RequestMediaStreamOptions, RequestPictureOptions, ResponsePictureOptions, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, ScryptedMimeTypes, Setting, Settings, SettingValue, VideoCamera, VideoClip, VideoClipOptions, VideoClips, VideoClipThumbnailOptions, VideoTextOverlay, VideoTextOverlays } from "@scrypted/sdk";
3
3
  import { StorageSettings } from "@scrypted/sdk/storage-settings";
4
4
  import path from 'path';
5
5
  import type { UrlMediaStreamOptions } from "../../scrypted/plugins/rtsp/src/rtsp";
6
6
  import { BaseBaichuanClass, type BaichuanConnectionCallbacks, type BaichuanConnectionConfig } from "./baichuan-base";
7
- import { normalizeUid, type BaichuanTransport } from "./connect";
8
- import { convertDebugLogsToApiOptions, DebugLogDisplayNames, DebugLogOption, getApiRelevantDebugLogs, getDebugLogChoices } from "./debug-options";
7
+ import { createBaichuanApi, normalizeUid, type BaichuanTransport } from "./connect";
8
+ import { convertDebugLogsToApiOptions, getApiRelevantDebugLogs, getDebugLogChoices } from "./debug-options";
9
9
  import { ReolinkBaichuanIntercom } from "./intercom";
10
10
  import ReolinkNativePlugin from "./main";
11
11
  import { ReolinkNativeMultiFocalDevice } from "./multiFocal";
12
12
  import { ReolinkNativeNvrDevice } from "./nvr";
13
13
  import { ReolinkPtzPresets } from "./presets";
14
14
  import {
15
+ createRfc4571CompositeMediaObjectFromStreamManager,
15
16
  createRfc4571MediaObjectFromStreamManager,
16
17
  expectedVideoTypeFromUrlMediaStreamOptions,
17
18
  parseStreamProfileFromId,
@@ -134,7 +135,8 @@ class ReolinkCameraPirSensor extends ScryptedDeviceBase implements OnOff, Settin
134
135
  }
135
136
 
136
137
  async getSettings(): Promise<Setting[]> {
137
- return this.storageSettings.getSettings();
138
+ const settings = await this.storageSettings.getSettings();
139
+ return settings;
138
140
  }
139
141
 
140
142
  async putSetting(key: string, value: SettingValue): Promise<void> {
@@ -187,7 +189,7 @@ class ReolinkCameraPirSensor extends ScryptedDeviceBase implements OnOff, Settin
187
189
  }
188
190
  }
189
191
 
190
- export abstract class CommonCameraMixin extends BaseBaichuanClass implements VideoCamera, Camera, Settings, DeviceProvider, ObjectDetector, PanTiltZoom, VideoTextOverlays, BinarySensor, Intercom {
192
+ export abstract class CommonCameraMixin extends BaseBaichuanClass implements VideoCamera, Camera, Settings, DeviceProvider, ObjectDetector, PanTiltZoom, VideoTextOverlays, BinarySensor, Intercom, Reboot, VideoClips {
191
193
  storageSettings = new StorageSettings(this, {
192
194
  // Basic connection settings
193
195
  ipAddress: {
@@ -197,12 +199,6 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
197
199
  await this.credentialsChanged();
198
200
  }
199
201
  },
200
- debugEvents: {
201
- title: 'Debug Events',
202
- type: 'boolean',
203
- immediate: true,
204
- hide: true,
205
- },
206
202
  username: {
207
203
  type: 'string',
208
204
  title: 'Username',
@@ -230,6 +226,65 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
230
226
  json: true,
231
227
  hide: true,
232
228
  },
229
+ // Multifocal composite stream PIP settings
230
+ pipPosition: {
231
+ title: 'PIP Position',
232
+ description: 'Position of the tele lens overlay on the wider lens view',
233
+ type: 'string',
234
+ defaultValue: 'bottom-right',
235
+ choices: [
236
+ 'top-left',
237
+ 'top-right',
238
+ 'bottom-left',
239
+ 'bottom-right',
240
+ 'center',
241
+ 'top-center',
242
+ 'bottom-center',
243
+ 'left-center',
244
+ 'right-center',
245
+ ],
246
+ hide: true, // Only show for multifocal devices via getAdditionalSettings
247
+ },
248
+ pipSize: {
249
+ title: 'PIP Size',
250
+ description: 'Relative size of the PIP overlay (0.1 = 10%, 0.3 = 30%, etc.)',
251
+ type: 'number',
252
+ defaultValue: 0.25,
253
+ hide: true,
254
+ onPut: async () => {
255
+ this.scheduleStreamManagerRestart('pipSize changed');
256
+ },
257
+ },
258
+ pipMargin: {
259
+ title: 'PIP Margin',
260
+ description: 'Margin from edge in pixels',
261
+ type: 'number',
262
+ defaultValue: 10,
263
+ hide: true,
264
+ onPut: async () => {
265
+ this.scheduleStreamManagerRestart('pipMargin changed');
266
+ },
267
+ },
268
+ widerChannel: {
269
+ title: 'Wider Channel',
270
+ description: 'Channel number for wider lens (typically 0)',
271
+ type: 'number',
272
+ defaultValue: 0,
273
+ hide: true,
274
+ onPut: async () => {
275
+ this.scheduleStreamManagerRestart('widerChannel changed');
276
+ },
277
+ },
278
+ teleChannel: {
279
+ title: 'Tele Channel',
280
+ description: 'Channel number for tele lens (typically 1)',
281
+ type: 'number',
282
+ defaultValue: 1,
283
+ hide: true,
284
+ onPut: async () => {
285
+ this.scheduleStreamManagerRestart('teleChannel changed');
286
+ },
287
+ },
233
288
  // Battery camera specific
234
289
  uid: {
235
290
  title: 'UID',
@@ -240,6 +295,11 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
240
295
  await this.credentialsChanged();
241
296
  }
242
297
  },
298
+ debugLogs: {
299
+ title: 'Debug logs',
300
+ type: 'boolean',
301
+ immediate: true,
302
+ },
243
303
  mixinsSetup: {
244
304
  type: 'boolean',
245
305
  hide: true,
@@ -258,10 +318,10 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
258
318
  await this.subscribeToEvents();
259
319
  },
260
320
  },
261
- debugLogs: {
321
+ socketApiDebugLogs: {
262
322
  subgroup: 'Advanced',
263
- title: 'Debug Logs',
264
- description: 'Enable specific debug logs. Baichuan client logs require reconnect; event logs are immediate.',
323
+ title: 'Socket API Debug Logs',
324
+ description: 'Enable specific debug logs.',
265
325
  multiple: true,
266
326
  combobox: true,
267
327
  immediate: true,
@@ -508,6 +568,11 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
508
568
  floodlight?: ReolinkCameraFloodlight;
509
569
  pirSensor?: ReolinkCameraPirSensor;
510
570
 
571
+
572
+ private lastPicture: { mo: MediaObject; atMs: number } | undefined;
573
+ private takePictureInFlight: Promise<MediaObject> | undefined;
574
+ forceNewSnapshot: boolean = false;
575
+
511
576
  // Video stream properties
512
577
  protected cachedVideoStreamOptions?: UrlMediaStreamOptions[];
513
578
  protected fetchingStreams = false;
@@ -529,7 +594,10 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
529
594
 
530
595
  protected nvrDevice?: ReolinkNativeNvrDevice;
531
596
  protected multiFocalDevice?: ReolinkNativeMultiFocalDevice;
532
- thisDevice: Settings
597
+ thisDevice: Settings;
598
+ isBattery: boolean;
599
+ isMultiFocal: boolean;
600
+ private streamManagerRestartTimeout: NodeJS.Timeout | undefined;
533
601
 
534
602
  constructor(
535
603
  nativeId: string,
@@ -543,21 +611,46 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
543
611
  this.multiFocalDevice = options.multiFocalDevice;
544
612
  this.thisDevice = sdk.systemManager.getDeviceById<Settings>(this.id);
545
613
 
546
- const isBattery = options.type === 'battery' || options.type === 'multi-focal-battery';
547
- this.protocol = isBattery ? 'udp' : 'tcp';
614
+ this.isBattery = options.type === 'battery' || options.type === 'multi-focal-battery';
615
+ this.isMultiFocal = options.type === 'multi-focal' || options.type === 'multi-focal-battery';
616
+ this.protocol = this.isBattery ? 'udp' : 'tcp';
548
617
 
549
618
  setTimeout(async () => {
550
619
  await this.parentInit();
551
620
  }, 2000);
552
621
  }
553
622
 
623
+ /**
624
+ * TODO: Implement video clip fetching using Baichuan/NVR recordings API.
625
+ */
626
+ async getVideoClips(options?: VideoClipOptions): Promise<VideoClip[]> {
627
+ throw new Error("getVideoClips is not implemented yet.");
628
+ }
629
+
630
+ getVideoClip(videoId: string): Promise<MediaObject> {
631
+ throw new Error("getVideoClip is not implemented yet.");
632
+ }
633
+
634
+ getVideoClipThumbnail(thumbnailId: string, options?: VideoClipThumbnailOptions): Promise<MediaObject> {
635
+ throw new Error("getVideoClipThumbnail is not implemented yet.");
636
+ }
637
+
638
+ removeVideoClips(...videoClipIds: string[]): Promise<void> {
639
+ throw new Error("removeVideoClips is not implemented yet.");
640
+ }
641
+
642
+ async reboot(): Promise<void> {
643
+ const api = await this.ensureBaichuanClient();
644
+ await api.reboot();
645
+ }
646
+
554
647
  // BaseBaichuanClass abstract methods implementation
555
648
  protected getConnectionConfig(): BaichuanConnectionConfig {
556
649
  const { ipAddress, username, password, uid } = this.storageSettings.values;
557
650
  const debugOptions = this.getBaichuanDebugOptions();
558
- const normalizedUid = this.protocol === 'udp' ? normalizeUid(uid) : undefined;
651
+ const normalizedUid = this.isBattery ? normalizeUid(uid) : undefined;
559
652
 
560
- if (this.protocol === 'udp' && !normalizedUid) {
653
+ if (this.isBattery && !normalizedUid) {
561
654
  throw new Error('UID is required for battery cameras (BCUDP)');
562
655
  }
563
656
 
@@ -581,8 +674,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
581
674
  // For battery cameras, don't auto-resubscribe after idle disconnects
582
675
  // (idle disconnects are normal for battery cameras to save power)
583
676
  // Events will be resubscribed when ensureClient() is called for actual operations
584
- const isBattery = this.options.type === 'battery';
585
- if (!isBattery) {
677
+ if (!this.isBattery) {
586
678
  // For non-battery cameras, resubscribe to events after reconnection
587
679
  setTimeout(async () => {
588
680
  try {
@@ -599,9 +691,8 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
599
691
  };
600
692
  }
601
693
 
602
-
603
694
  protected isDebugEnabled(): boolean {
604
- return this.isEventLogsEnabled();
695
+ return this.storageSettings.values.debugLogs;
605
696
  }
606
697
 
607
698
  protected getDeviceName(): string {
@@ -609,7 +700,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
609
700
  }
610
701
 
611
702
  async withBaichuanRetry<T>(fn: () => Promise<T>): Promise<T> {
612
- if (this.protocol === 'udp') {
703
+ if (this.isBattery) {
613
704
  return await fn();
614
705
  } else {
615
706
  try {
@@ -680,8 +771,38 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
680
771
  }
681
772
  }
682
773
 
683
- createStreamClient(): Promise<ReolinkBaichuanApi> {
684
- throw new Error("Method not implemented.");
774
+ /**
775
+ * Create a dedicated Baichuan API session for streaming (used by StreamManager).
776
+ *
777
+ * - For TCP devices (regular + multifocal), this creates a new TCP session with its own client.
778
+ * - For UDP/battery devices, this reuses the existing client via ensureClient().
779
+ */
780
+ async createStreamClient(): Promise<ReolinkBaichuanApi> {
781
+ // Battery / BCUDP path: reuse the main client to avoid extra wake-ups and sockets.
782
+ if (this.isBattery) {
783
+ return await this.ensureClient();
784
+ }
785
+
786
+ // TCP path: create a separate session for streaming (RFC4571/composite/NVR-friendly).
787
+ const { ipAddress, username, password } = this.storageSettings.values;
788
+ const logger = this.getBaichuanLogger();
789
+
790
+ const debugOptions = this.getBaichuanDebugOptions();
791
+ const api = await createBaichuanApi(
792
+ {
793
+ inputs: {
794
+ host: ipAddress,
795
+ username,
796
+ password,
797
+ logger,
798
+ debugOptions,
799
+ },
800
+ transport: 'tcp',
801
+ },
802
+ );
803
+
804
+ await api.login();
805
+ return api;
685
806
  }
686
807
 
687
808
  public getAbilities(): DeviceCapabilities {
@@ -693,8 +814,78 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
693
814
  }
694
815
 
695
816
  getBaichuanDebugOptions(): any | undefined {
696
- const debugLogs = this.storageSettings.values.debugLogs || [];
697
- return convertDebugLogsToApiOptions(debugLogs);
817
+ const socketDebugLogs = this.storageSettings.values.socketApiDebugLogs || [];
818
+ return convertDebugLogsToApiOptions(socketDebugLogs);
819
+ }
820
+
821
+ /**
822
+ * Initialize or recreate the StreamManager, taking into account multifocal composite options.
823
+ */
824
+ protected initStreamManager(logger: Console, forceRecreate: boolean = false): void {
825
+ const { username, password } = this.storageSettings.values;
826
+
827
+ const baseOptions: any = {
828
+ createStreamClient: () => this.createStreamClient(),
829
+ getLogger: () => logger,
830
+ credentials: {
831
+ username,
832
+ password,
833
+ },
834
+ sharedConnection: this.isBattery,
835
+ };
836
+
837
+ if (this.isMultiFocal) {
838
+ const values: any = this.storageSettings.values;
839
+ const pipPosition = values.pipPosition || 'bottom-right';
840
+ const pipSize = values.pipSize ?? 0.25;
841
+ const pipMargin = values.pipMargin ?? 10;
842
+ const widerChannel = values.widerChannel ?? 0;
843
+ const teleChannel = values.teleChannel ?? 1;
844
+
845
+ baseOptions.compositeOptions = {
846
+ widerChannel,
847
+ teleChannel,
848
+ pipPosition,
849
+ pipSize,
850
+ pipMargin,
851
+ };
852
+ }
853
+
854
+ if (!this.streamManager || forceRecreate) {
855
+ this.streamManager = new StreamManager(baseOptions);
856
+ }
857
+ }
858
+
859
+ /**
860
+ * Debounced restart of StreamManager when PIP/composite settings change.
861
+ * Also notifies listeners so that active streams (prebuffer, etc.) restart cleanly.
862
+ */
863
+ protected scheduleStreamManagerRestart(reason: string): void {
864
+ const logger = this.getBaichuanLogger();
865
+ logger.log(`Scheduling StreamManager restart (${reason})`);
866
+
867
+ if (this.streamManagerRestartTimeout) {
868
+ clearTimeout(this.streamManagerRestartTimeout);
869
+ this.streamManagerRestartTimeout = undefined;
870
+ }
871
+
872
+ this.streamManagerRestartTimeout = setTimeout(async () => {
873
+ this.streamManagerRestartTimeout = undefined;
874
+ const restartLogger = this.getBaichuanLogger();
875
+ try {
876
+ restartLogger.log('Restarting StreamManager due to PIP/composite settings change');
877
+ this.initStreamManager(restartLogger, true);
878
+
879
+ // Notify consumers (e.g. prebuffer) that stream configuration changed.
880
+ try {
881
+ this.onDeviceEvent(ScryptedInterface.VideoCamera, undefined);
882
+ } catch {
883
+ // best-effort
884
+ }
885
+ } catch (e) {
886
+ restartLogger.warn('Failed to restart StreamManager after settings change', e);
887
+ }
888
+ }, 500);
698
889
  }
699
890
 
700
891
  isRecoverableBaichuanError(e: any): boolean {
@@ -1029,11 +1220,6 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
1029
1220
  }
1030
1221
  }
1031
1222
 
1032
- isEventLogsEnabled(): boolean {
1033
- const debugLogs = this.storageSettings.values.debugLogs || [];
1034
- return debugLogs.includes(DebugLogDisplayNames[DebugLogOption.EventLogs]);
1035
- }
1036
-
1037
1223
  // BinarySensor interface implementation (for doorbell)
1038
1224
  handleDoorbellEvent(): void {
1039
1225
  if (!this.doorbellBinaryTimeout) {
@@ -1114,9 +1300,59 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
1114
1300
  await this.storageSettings.putSetting(key, value);
1115
1301
  }
1116
1302
 
1117
- // Camera interface methods (must be implemented by subclasses)
1118
- abstract takePicture(options?: any): Promise<MediaObject>;
1119
- abstract getPictureOptions(): Promise<any[]>;
1303
+ async takePicture(options?: RequestPictureOptions) {
1304
+ if (!this.isBattery) {
1305
+ try {
1306
+ return this.withBaichuanRetry(async () => {
1307
+ const client = await this.ensureClient();
1308
+ const snapshotBuffer = await client.getSnapshot(this.storageSettings.values.rtspChannel);
1309
+ const mo = await this.createMediaObject(snapshotBuffer, 'image/jpeg');
1310
+
1311
+ return mo;
1312
+ });
1313
+ } catch (e) {
1314
+ this.getBaichuanLogger().error('Error taking snapshot', e);
1315
+ throw e;
1316
+ }
1317
+ } else {
1318
+ const logger = this.getBaichuanLogger();
1319
+ const shouldTakeNewSnapshot = this.forceNewSnapshot;
1320
+
1321
+ if (!shouldTakeNewSnapshot && this.lastPicture) {
1322
+ logger.log(`Returning cached snapshot, taken at ${new Date(this.lastPicture.atMs).toLocaleString()}`);
1323
+ return this.lastPicture.mo;
1324
+ }
1325
+
1326
+ if (this.takePictureInFlight) {
1327
+ return await this.takePictureInFlight;
1328
+ }
1329
+
1330
+ logger.log(`Taking new snapshot from camera (forceNewSnapshot: ${this.forceNewSnapshot})`);
1331
+ this.forceNewSnapshot = false;
1332
+
1333
+ this.takePictureInFlight = (async () => {
1334
+ const channel = this.storageSettings.values.rtspChannel;
1335
+ const snapshotBuffer = await this.withBaichuanClient(async (api) => {
1336
+ return await api.getSnapshot(channel);
1337
+ });
1338
+ const mo = await sdk.mediaManager.createMediaObject(snapshotBuffer, 'image/jpeg');
1339
+ this.lastPicture = { mo, atMs: Date.now() };
1340
+ logger.log(`Snapshot taken at ${new Date(this.lastPicture.atMs).toLocaleString()}`);
1341
+ return mo;
1342
+ })();
1343
+
1344
+ try {
1345
+ return await this.takePictureInFlight;
1346
+ }
1347
+ finally {
1348
+ this.takePictureInFlight = undefined;
1349
+ }
1350
+ }
1351
+ }
1352
+
1353
+ async getPictureOptions(): Promise<ResponsePictureOptions[]> {
1354
+ return [];
1355
+ }
1120
1356
 
1121
1357
  // Intercom interface methods
1122
1358
  async startIntercom(media: MediaObject): Promise<void> {
@@ -1342,12 +1578,15 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
1342
1578
 
1343
1579
  const client = await this.ensureClient();
1344
1580
 
1345
- const { rtspChannel } = this.storageSettings.values;
1581
+ // For multifocal devices, use undefined channel to get composite streams
1582
+ const isMultiFocal = this.options.type === 'multi-focal' || this.options.type === 'multi-focal-battery';
1583
+ const channel = isMultiFocal ? undefined : this.storageSettings.values.rtspChannel;
1346
1584
 
1347
1585
  try {
1348
- const { nativeStreams, rtmpStreams, rtspStreams } = await client.buildVideoStreamOptions(rtspChannel);
1586
+ const { nativeStreams, rtmpStreams, rtspStreams } = await client.buildVideoStreamOptions(channel);
1349
1587
 
1350
1588
  let supportedStreams: ReolinkSupportedStream[] = [];
1589
+ // Homehub RTMP is not efficient, crashes, offers native streams to not overload the hub
1351
1590
  if (this.nvrDevice && this.nvrDevice.info.model === 'HOMEHUB') {
1352
1591
  supportedStreams = [...nativeStreams, ...rtspStreams, ...rtmpStreams];
1353
1592
  } else {
@@ -1408,6 +1647,28 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
1408
1647
  throw new Error('StreamManager not initialized');
1409
1648
  }
1410
1649
 
1650
+ // Check if this is a composite stream request (for multifocal devices)
1651
+ const isComposite = selected.id?.startsWith('composite_');
1652
+ if (isComposite && this.options && (this.options.type === 'multi-focal' || this.options.type === 'multi-focal-battery')) {
1653
+ const profile = parseStreamProfileFromId(selected.id.replace('composite_', '')) || 'main';
1654
+ const streamKey = `composite_${profile}`;
1655
+ const expectedVideoType = expectedVideoTypeFromUrlMediaStreamOptions(selected);
1656
+
1657
+ const createStreamFn = async () => {
1658
+ return await createRfc4571CompositeMediaObjectFromStreamManager({
1659
+ streamManager: this.streamManager!,
1660
+ profile,
1661
+ streamKey,
1662
+ expectedVideoType,
1663
+ selected,
1664
+ sourceId: this.id,
1665
+ });
1666
+ };
1667
+
1668
+ return await this.withBaichuanRetry(createStreamFn);
1669
+ }
1670
+
1671
+ // Regular stream for single channel
1411
1672
  const profile = parseStreamProfileFromId(selected.id) || 'main';
1412
1673
  const channel = this.storageSettings.values.rtspChannel;
1413
1674
  const streamKey = `${channel}_${profile}`;
@@ -1539,17 +1800,20 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
1539
1800
  }
1540
1801
 
1541
1802
  const { username, password } = this.storageSettings.values;
1542
- const isCamera = this.options.type === 'regular' || this.options.type === 'battery';
1543
- const isBatteryCamera = this.options.type === 'battery';
1544
- const isBatteryMultiFocal = this.options.type === 'multi-focal-battery';
1545
- const isBattery = isBatteryCamera || isBatteryMultiFocal;
1546
1803
 
1547
- this.storageSettings.settings.uid.hide = !isBattery;
1548
- this.storageSettings.settings.batteryUpdateIntervalMinutes.hide = !isBattery;
1549
- this.storageSettings.settings.lowThresholdBatteryRecording.hide = !isBattery;
1550
- this.storageSettings.settings.highThresholdBatteryRecording.hide = !isBattery;
1804
+ this.storageSettings.settings.uid.hide = !this.isBattery;
1805
+ this.storageSettings.settings.batteryUpdateIntervalMinutes.hide = !this.isBattery;
1806
+ this.storageSettings.settings.lowThresholdBatteryRecording.hide = !this.isBattery;
1807
+ this.storageSettings.settings.highThresholdBatteryRecording.hide = !this.isBattery;
1551
1808
 
1552
- if (isBatteryCamera && !this.storageSettings.values.mixinsSetup) {
1809
+ // Show PIP settings only for multifocal devices
1810
+ this.storageSettings.settings.pipPosition.hide = !this.isMultiFocal;
1811
+ this.storageSettings.settings.pipSize.hide = !this.isMultiFocal;
1812
+ this.storageSettings.settings.pipMargin.hide = !this.isMultiFocal;
1813
+ this.storageSettings.settings.widerChannel.hide = !this.isMultiFocal;
1814
+ this.storageSettings.settings.teleChannel.hide = !this.isMultiFocal;
1815
+
1816
+ if (this.isBattery && !this.storageSettings.values.mixinsSetup) {
1553
1817
  try {
1554
1818
  const device = sdk.systemManager.getDeviceById<Settings>(this.id);
1555
1819
  if (device) {
@@ -1571,39 +1835,30 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
1571
1835
  logger.warn('Failed to subscribe to Baichuan events', e);
1572
1836
  }
1573
1837
 
1574
- if (isCamera) {
1575
- this.streamManager = new StreamManager({
1576
- createStreamClient: () => this.createStreamClient(),
1577
- getLogger: () => logger as Console,
1578
- credentials: {
1579
- username,
1580
- password
1581
- },
1582
- sharedConnection: isBattery,
1583
- });
1838
+ // Initialize StreamManager (with composite options for multifocal devices)
1839
+ this.initStreamManager(logger);
1584
1840
 
1585
- const { hasIntercom, hasPtz } = this.getAbilities();
1841
+ const { hasIntercom, hasPtz } = this.getAbilities();
1586
1842
 
1587
- if (hasIntercom) {
1588
- this.intercom = new ReolinkBaichuanIntercom(this);
1589
- }
1843
+ if (hasIntercom) {
1844
+ this.intercom = new ReolinkBaichuanIntercom(this);
1845
+ }
1590
1846
 
1591
- if (hasPtz) {
1592
- const choices = (this.presets || []).map((preset: any) => preset.id + '=' + preset.name);
1847
+ if (hasPtz) {
1848
+ const choices = (this.presets || []).map((preset: any) => preset.id + '=' + preset.name);
1593
1849
 
1594
- this.storageSettings.settings.presets.choices = choices;
1595
- this.storageSettings.settings.ptzSelectedPreset.choices = choices;
1850
+ this.storageSettings.settings.presets.choices = choices;
1851
+ this.storageSettings.settings.ptzSelectedPreset.choices = choices;
1596
1852
 
1597
- this.storageSettings.settings.presets.hide = false;
1598
- this.storageSettings.settings.ptzMoveDurationMs.hide = false;
1599
- this.storageSettings.settings.ptzZoomStep.hide = false;
1600
- this.storageSettings.settings.ptzCreatePreset.hide = false;
1601
- this.storageSettings.settings.ptzSelectedPreset.hide = false;
1602
- this.storageSettings.settings.ptzUpdateSelectedPreset.hide = false;
1603
- this.storageSettings.settings.ptzDeleteSelectedPreset.hide = false;
1853
+ this.storageSettings.settings.presets.hide = false;
1854
+ this.storageSettings.settings.ptzMoveDurationMs.hide = false;
1855
+ this.storageSettings.settings.ptzZoomStep.hide = false;
1856
+ this.storageSettings.settings.ptzCreatePreset.hide = false;
1857
+ this.storageSettings.settings.ptzSelectedPreset.hide = false;
1858
+ this.storageSettings.settings.ptzUpdateSelectedPreset.hide = false;
1859
+ this.storageSettings.settings.ptzDeleteSelectedPreset.hide = false;
1604
1860
 
1605
- this.updatePtzCaps();
1606
- }
1861
+ this.updatePtzCaps();
1607
1862
  }
1608
1863
 
1609
1864
  if (this.nvrDevice || this.multiFocalDevice) {
@@ -18,10 +18,6 @@ export enum DebugLogOption {
18
18
  DebugH264 = 'debugH264',
19
19
  /** SPS/PPS parameter sets debug logs */
20
20
  DebugParamSets = 'debugParamSets',
21
- /** Event logs (plugin-specific, not passed to API) */
22
- EventLogs = 'eventLogs',
23
- /** Battery info logs (plugin-specific, not passed to API) */
24
- BatteryInfo = 'batteryInfo',
25
21
  }
26
22
 
27
23
  /**
@@ -36,8 +32,6 @@ export function mapDebugLogToApiOption(option: DebugLogOption): keyof DebugOptio
36
32
  [DebugLogOption.TraceEvents]: 'traceEvents',
37
33
  [DebugLogOption.DebugH264]: 'debugH264',
38
34
  [DebugLogOption.DebugParamSets]: 'debugParamSets',
39
- [DebugLogOption.EventLogs]: null, // Plugin-specific, not passed to API
40
- [DebugLogOption.BatteryInfo]: null, // Plugin-specific, not passed to API
41
35
  };
42
36
  return mapping[option];
43
37
  }
@@ -88,8 +82,6 @@ export const DebugLogDisplayNames: Record<DebugLogOption, string> = {
88
82
  [DebugLogOption.TraceEvents]: 'Trace events XML',
89
83
  [DebugLogOption.DebugH264]: 'H264',
90
84
  [DebugLogOption.DebugParamSets]: 'Video param sets',
91
- [DebugLogOption.EventLogs]: 'Object detection events',
92
- [DebugLogOption.BatteryInfo]: 'Battery info update',
93
85
  };
94
86
 
95
87
  /**