@apocaliss92/scrypted-reolink-native 0.1.8 → 0.1.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/dist/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/baichuan-base.ts +478 -0
- package/src/camera-battery.ts +32 -32
- package/src/camera.ts +6 -9
- package/src/common.ts +107 -231
- package/src/connect.ts +1 -2
- package/src/debug-options.ts +1 -1
- package/src/intercom.ts +3 -3
- package/src/nvr.ts +179 -408
- package/src/stream-utils.ts +1 -1
- package/logs.txt +0 -7361
package/src/common.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { DeviceCapabilities, PtzCommand, PtzPreset, ReolinkBaichuanApi, Reo
|
|
|
2
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";
|
|
3
3
|
import { StorageSettings } from "@scrypted/sdk/storage-settings";
|
|
4
4
|
import type { UrlMediaStreamOptions } from "../../scrypted/plugins/rtsp/src/rtsp";
|
|
5
|
+
import { BaseBaichuanClass, type BaichuanConnectionConfig, type BaichuanConnectionCallbacks } from "./baichuan-base";
|
|
5
6
|
import { createBaichuanApi, normalizeUid, type BaichuanTransport } from "./connect";
|
|
6
7
|
import { convertDebugLogsToApiOptions, DebugLogDisplayNames, DebugLogOption, getApiRelevantDebugLogs, getDebugLogChoices } from "./debug-options";
|
|
7
8
|
import { ReolinkBaichuanIntercom } from "./intercom";
|
|
@@ -32,27 +33,27 @@ class ReolinkCameraSiren extends ScryptedDeviceBase implements OnOff {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
async turnOff(): Promise<void> {
|
|
35
|
-
this.camera.
|
|
36
|
+
this.camera.getBaichuanLogger().log(`Siren toggle: turnOff (device=${this.nativeId})`);
|
|
36
37
|
this.on = false;
|
|
37
38
|
try {
|
|
38
39
|
await this.camera.setSirenEnabled(false);
|
|
39
|
-
this.camera.
|
|
40
|
+
this.camera.getBaichuanLogger().log(`Siren toggle: turnOff ok (device=${this.nativeId})`);
|
|
40
41
|
}
|
|
41
42
|
catch (e) {
|
|
42
|
-
this.camera.
|
|
43
|
+
this.camera.getBaichuanLogger().warn(`Siren toggle: turnOff failed (device=${this.nativeId})`, e);
|
|
43
44
|
throw e;
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
async turnOn(): Promise<void> {
|
|
48
|
-
this.camera.
|
|
49
|
+
this.camera.getBaichuanLogger().log(`Siren toggle: turnOn (device=${this.nativeId})`);
|
|
49
50
|
this.on = true;
|
|
50
51
|
try {
|
|
51
52
|
await this.camera.setSirenEnabled(true);
|
|
52
|
-
this.camera.
|
|
53
|
+
this.camera.getBaichuanLogger().log(`Siren toggle: turnOn ok (device=${this.nativeId})`);
|
|
53
54
|
}
|
|
54
55
|
catch (e) {
|
|
55
|
-
this.camera.
|
|
56
|
+
this.camera.getBaichuanLogger().warn(`Siren toggle: turnOn failed (device=${this.nativeId})`, e);
|
|
56
57
|
throw e;
|
|
57
58
|
}
|
|
58
59
|
}
|
|
@@ -64,40 +65,40 @@ class ReolinkCameraFloodlight extends ScryptedDeviceBase implements OnOff, Brigh
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
async setBrightness(brightness: number): Promise<void> {
|
|
67
|
-
this.camera.
|
|
68
|
+
this.camera.getBaichuanLogger().log(`Floodlight toggle: setBrightness (device=${this.nativeId} brightness=${brightness})`);
|
|
68
69
|
this.brightness = brightness;
|
|
69
70
|
try {
|
|
70
71
|
await this.camera.setFloodlightState(undefined, brightness);
|
|
71
|
-
this.camera.
|
|
72
|
+
this.camera.getBaichuanLogger().log(`Floodlight toggle: setBrightness ok (device=${this.nativeId} brightness=${brightness})`);
|
|
72
73
|
}
|
|
73
74
|
catch (e) {
|
|
74
|
-
this.camera.
|
|
75
|
+
this.camera.getBaichuanLogger().warn(`Floodlight toggle: setBrightness failed (device=${this.nativeId} brightness=${brightness})`, e);
|
|
75
76
|
throw e;
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
async turnOff(): Promise<void> {
|
|
80
|
-
this.camera.
|
|
81
|
+
this.camera.getBaichuanLogger().log(`Floodlight toggle: turnOff (device=${this.nativeId})`);
|
|
81
82
|
this.on = false;
|
|
82
83
|
try {
|
|
83
84
|
await this.camera.setFloodlightState(false);
|
|
84
|
-
this.camera.
|
|
85
|
+
this.camera.getBaichuanLogger().log(`Floodlight toggle: turnOff ok (device=${this.nativeId})`);
|
|
85
86
|
}
|
|
86
87
|
catch (e) {
|
|
87
|
-
this.camera.
|
|
88
|
+
this.camera.getBaichuanLogger().warn(`Floodlight toggle: turnOff failed (device=${this.nativeId})`, e);
|
|
88
89
|
throw e;
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
async turnOn(): Promise<void> {
|
|
93
|
-
this.camera.
|
|
94
|
+
this.camera.getBaichuanLogger().log(`Floodlight toggle: turnOn (device=${this.nativeId})`);
|
|
94
95
|
this.on = true;
|
|
95
96
|
try {
|
|
96
97
|
await this.camera.setFloodlightState(true);
|
|
97
|
-
this.camera.
|
|
98
|
+
this.camera.getBaichuanLogger().log(`Floodlight toggle: turnOn ok (device=${this.nativeId})`);
|
|
98
99
|
}
|
|
99
100
|
catch (e) {
|
|
100
|
-
this.camera.
|
|
101
|
+
this.camera.getBaichuanLogger().warn(`Floodlight toggle: turnOn failed (device=${this.nativeId})`, e);
|
|
101
102
|
throw e;
|
|
102
103
|
}
|
|
103
104
|
}
|
|
@@ -185,7 +186,7 @@ class ReolinkCameraPirSensor extends ScryptedDeviceBase implements OnOff, Settin
|
|
|
185
186
|
}
|
|
186
187
|
}
|
|
187
188
|
|
|
188
|
-
export abstract class CommonCameraMixin extends
|
|
189
|
+
export abstract class CommonCameraMixin extends BaseBaichuanClass implements VideoCamera, Camera, Settings, DeviceProvider, ObjectDetector, PanTiltZoom, VideoTextOverlays, BinarySensor, Intercom {
|
|
189
190
|
storageSettings = new StorageSettings(this, {
|
|
190
191
|
// Basic connection settings
|
|
191
192
|
ipAddress: {
|
|
@@ -302,7 +303,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
302
303
|
// Trigger reconnection
|
|
303
304
|
await this.ensureClient();
|
|
304
305
|
} catch (e) {
|
|
305
|
-
this.
|
|
306
|
+
this.getBaichuanLogger().warn('Failed to reset client after debug logs change', e);
|
|
306
307
|
}
|
|
307
308
|
}, 2000);
|
|
308
309
|
}
|
|
@@ -386,7 +387,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
386
387
|
return;
|
|
387
388
|
}
|
|
388
389
|
|
|
389
|
-
const logger = this.
|
|
390
|
+
const logger = this.getBaichuanLogger();
|
|
390
391
|
logger.log(`PTZ presets: create preset requested (name=${name})`);
|
|
391
392
|
|
|
392
393
|
const preset = await this.withBaichuanRetry(async () => {
|
|
@@ -427,7 +428,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
427
428
|
throw new Error('No preset selected');
|
|
428
429
|
}
|
|
429
430
|
|
|
430
|
-
const logger = this.
|
|
431
|
+
const logger = this.getBaichuanLogger();
|
|
431
432
|
logger.log(`PTZ presets: update position requested (presetId=${presetId})`);
|
|
432
433
|
|
|
433
434
|
await this.withBaichuanRetry(async () => {
|
|
@@ -450,7 +451,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
450
451
|
throw new Error('No preset selected');
|
|
451
452
|
}
|
|
452
453
|
|
|
453
|
-
const logger = this.
|
|
454
|
+
const logger = this.getBaichuanLogger();
|
|
454
455
|
logger.log(`PTZ presets: delete requested (presetId=${presetId})`);
|
|
455
456
|
|
|
456
457
|
await this.withBaichuanRetry(async () => {
|
|
@@ -482,13 +483,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
482
483
|
protected lastNetPortCacheAttempt: number = 0;
|
|
483
484
|
protected netPortCacheBackoffMs: number = 5000; // 5 seconds backoff on failure
|
|
484
485
|
|
|
485
|
-
// Client management
|
|
486
|
-
protected baichuanApi: ReolinkBaichuanApi | undefined;
|
|
487
|
-
protected ensureClientPromise: Promise<ReolinkBaichuanApi> | undefined;
|
|
488
|
-
protected connectionTime: number | undefined;
|
|
489
|
-
private closeListener?: () => void;
|
|
490
|
-
private lastDisconnectTime: number = 0;
|
|
491
|
-
private readonly reconnectBackoffMs: number = 2000; // 2 seconds minimum between reconnects
|
|
486
|
+
// Client management (inherited from BaseBaichuanClass)
|
|
492
487
|
protected readonly protocol: BaichuanTransport;
|
|
493
488
|
private debugLogsResetTimeout: NodeJS.Timeout | undefined;
|
|
494
489
|
|
|
@@ -516,6 +511,60 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
516
511
|
await this.parentInit();
|
|
517
512
|
}, 2000);
|
|
518
513
|
}
|
|
514
|
+
|
|
515
|
+
// BaseBaichuanClass abstract methods implementation
|
|
516
|
+
protected getConnectionConfig(): BaichuanConnectionConfig {
|
|
517
|
+
const { ipAddress, username, password, uid } = this.storageSettings.values;
|
|
518
|
+
const debugOptions = this.getBaichuanDebugOptions();
|
|
519
|
+
const normalizedUid = this.protocol === 'udp' ? normalizeUid(uid) : undefined;
|
|
520
|
+
|
|
521
|
+
if (this.protocol === 'udp' && !normalizedUid) {
|
|
522
|
+
throw new Error('UID is required for battery cameras (BCUDP)');
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return {
|
|
526
|
+
host: ipAddress,
|
|
527
|
+
username,
|
|
528
|
+
password,
|
|
529
|
+
uid: normalizedUid,
|
|
530
|
+
transport: this.protocol,
|
|
531
|
+
logger: this.console,
|
|
532
|
+
debugOptions,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
protected getConnectionCallbacks(): BaichuanConnectionCallbacks {
|
|
537
|
+
return {
|
|
538
|
+
onError: undefined, // Use default error handling
|
|
539
|
+
onClose: async () => {
|
|
540
|
+
// Reset client state on close
|
|
541
|
+
// The base class already handles cleanup
|
|
542
|
+
},
|
|
543
|
+
onSimpleEvent: this.onSimpleEvent,
|
|
544
|
+
getEventSubscriptionEnabled: () => this.isEventDispatchEnabled?.() ?? false,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
protected isDebugEnabled(): boolean {
|
|
550
|
+
return this.isEventLogsEnabled();
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
protected getDeviceName(): string {
|
|
554
|
+
return this.name || 'Camera';
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
protected async onBeforeCleanup(): Promise<void> {
|
|
558
|
+
// Unsubscribe from events if needed
|
|
559
|
+
if (this.onSimpleEvent && this.baichuanApi) {
|
|
560
|
+
try {
|
|
561
|
+
this.baichuanApi.offSimpleEvent(this.onSimpleEvent);
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
// ignore
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
519
568
|
createStreamClient(): Promise<ReolinkBaichuanApi> {
|
|
520
569
|
throw new Error("Method not implemented.");
|
|
521
570
|
}
|
|
@@ -524,10 +573,6 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
524
573
|
return this.storageSettings.values.capabilities;
|
|
525
574
|
}
|
|
526
575
|
|
|
527
|
-
getLogger(): Console {
|
|
528
|
-
return this.console;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
576
|
getBaichuanDebugOptions(): any | undefined {
|
|
532
577
|
const debugLogs = this.storageSettings.values.debugLogs || [];
|
|
533
578
|
return convertDebugLogsToApiOptions(debugLogs);
|
|
@@ -557,14 +602,10 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
557
602
|
|
|
558
603
|
// Event subscription methods
|
|
559
604
|
unsubscribedToEvents(): void {
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
try {
|
|
563
|
-
api.offSimpleEvent(this.onSimpleEvent);
|
|
564
|
-
}
|
|
565
|
-
catch {
|
|
605
|
+
// Use base class unsubscribe
|
|
606
|
+
this.unsubscribeFromEvents().catch(() => {
|
|
566
607
|
// ignore
|
|
567
|
-
}
|
|
608
|
+
});
|
|
568
609
|
|
|
569
610
|
if (this.motionDetected) {
|
|
570
611
|
this.motionDetected = false;
|
|
@@ -573,24 +614,18 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
573
614
|
|
|
574
615
|
onSimpleEvent = (ev: ReolinkSimpleEvent) => {
|
|
575
616
|
try {
|
|
576
|
-
const logger = this.
|
|
617
|
+
const logger = this.getBaichuanLogger();
|
|
577
618
|
|
|
578
|
-
|
|
579
|
-
logger.log(`Baichuan event: ${JSON.stringify(ev)}`);
|
|
580
|
-
}
|
|
619
|
+
logger.debug(`Baichuan event: ${JSON.stringify(ev)}`);
|
|
581
620
|
|
|
582
621
|
if (!this.isEventDispatchEnabled()) {
|
|
583
|
-
|
|
584
|
-
logger.debug('Event dispatch is disabled, ignoring event');
|
|
585
|
-
}
|
|
622
|
+
logger.debug('Event dispatch is disabled, ignoring event');
|
|
586
623
|
return;
|
|
587
624
|
}
|
|
588
625
|
|
|
589
626
|
const channel = this.storageSettings.values.rtspChannel;
|
|
590
627
|
if (ev?.channel !== undefined && ev.channel !== channel) {
|
|
591
|
-
|
|
592
|
-
logger.debug(`Event channel ${ev.channel} does not match camera channel ${channel}, ignoring`);
|
|
593
|
-
}
|
|
628
|
+
logger.error(`Event channel ${ev.channel} does not match camera channel ${channel}, ignoring`);
|
|
594
629
|
return;
|
|
595
630
|
}
|
|
596
631
|
|
|
@@ -600,9 +635,6 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
600
635
|
switch (ev?.type) {
|
|
601
636
|
case 'motion':
|
|
602
637
|
motion = true;
|
|
603
|
-
if (this.isEventLogsEnabled()) {
|
|
604
|
-
logger.log(`Motion event received (may be PIR or MD)`);
|
|
605
|
-
}
|
|
606
638
|
break;
|
|
607
639
|
case 'doorbell':
|
|
608
640
|
this.handleDoorbellEvent();
|
|
@@ -618,9 +650,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
618
650
|
motion = true;
|
|
619
651
|
break;
|
|
620
652
|
default:
|
|
621
|
-
|
|
622
|
-
logger.debug(`Unknown event type: ${ev?.type}`);
|
|
623
|
-
}
|
|
653
|
+
logger.error(`Unknown event type: ${ev?.type}`);
|
|
624
654
|
return;
|
|
625
655
|
}
|
|
626
656
|
|
|
@@ -629,7 +659,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
629
659
|
});
|
|
630
660
|
}
|
|
631
661
|
catch (e) {
|
|
632
|
-
this.
|
|
662
|
+
this.getBaichuanLogger().warn('Error in onSimpleEvent handler', e);
|
|
633
663
|
}
|
|
634
664
|
}
|
|
635
665
|
|
|
@@ -638,7 +668,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
638
668
|
return;
|
|
639
669
|
}
|
|
640
670
|
|
|
641
|
-
const logger = this.
|
|
671
|
+
const logger = this.getBaichuanLogger();
|
|
642
672
|
const selection = Array.from(this.getDispatchEventsSelection?.() ?? new Set()).sort();
|
|
643
673
|
const enabled = selection.length > 0;
|
|
644
674
|
|
|
@@ -736,13 +766,13 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
736
766
|
if (preset !== undefined && preset !== null) {
|
|
737
767
|
const presetId = Number(preset);
|
|
738
768
|
if (!Number.isFinite(presetId)) {
|
|
739
|
-
this.
|
|
769
|
+
this.getBaichuanLogger().warn(`Invalid PTZ preset id: ${preset}`);
|
|
740
770
|
return;
|
|
741
771
|
}
|
|
742
772
|
if (this.ptzPresets) {
|
|
743
773
|
await this.ptzPresets.moveToPreset(presetId);
|
|
744
774
|
} else {
|
|
745
|
-
this.
|
|
775
|
+
this.getBaichuanLogger().warn('PTZ presets not available');
|
|
746
776
|
}
|
|
747
777
|
return;
|
|
748
778
|
}
|
|
@@ -777,14 +807,14 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
777
807
|
|
|
778
808
|
const step = Number(this.storageSettings.values.ptzZoomStep);
|
|
779
809
|
if (!Number.isFinite(step) || step <= 0) {
|
|
780
|
-
this.
|
|
810
|
+
this.getBaichuanLogger().warn('Invalid PTZ zoom step, using default 0.1');
|
|
781
811
|
return;
|
|
782
812
|
}
|
|
783
813
|
|
|
784
814
|
// Get current zoom factor and apply step
|
|
785
815
|
const info = await client.getZoomFocus(channel);
|
|
786
816
|
if (!info?.zoom) {
|
|
787
|
-
this.
|
|
817
|
+
this.getBaichuanLogger().warn('Zoom command requested but camera did not report zoom support.');
|
|
788
818
|
return;
|
|
789
819
|
}
|
|
790
820
|
|
|
@@ -1003,7 +1033,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
1003
1033
|
deviceData,
|
|
1004
1034
|
});
|
|
1005
1035
|
} catch (e) {
|
|
1006
|
-
this.
|
|
1036
|
+
this.getBaichuanLogger().warn('Failed to fetch device info', e);
|
|
1007
1037
|
}
|
|
1008
1038
|
}
|
|
1009
1039
|
|
|
@@ -1092,7 +1122,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
1092
1122
|
const sirenState = await api.getSiren(channel);
|
|
1093
1123
|
this.siren.on = sirenState.enabled;
|
|
1094
1124
|
} catch (e) {
|
|
1095
|
-
this.
|
|
1125
|
+
this.getBaichuanLogger().debug('Failed to align siren state', e);
|
|
1096
1126
|
}
|
|
1097
1127
|
}
|
|
1098
1128
|
|
|
@@ -1105,7 +1135,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
1105
1135
|
this.floodlight.brightness = wl.brightness;
|
|
1106
1136
|
}
|
|
1107
1137
|
} catch (e) {
|
|
1108
|
-
this.
|
|
1138
|
+
this.getBaichuanLogger().debug('Failed to align floodlight state', e);
|
|
1109
1139
|
}
|
|
1110
1140
|
}
|
|
1111
1141
|
|
|
@@ -1129,11 +1159,11 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
1129
1159
|
}
|
|
1130
1160
|
}
|
|
1131
1161
|
} catch (e) {
|
|
1132
|
-
this.
|
|
1162
|
+
this.getBaichuanLogger().debug('Failed to align PIR state', e);
|
|
1133
1163
|
}
|
|
1134
1164
|
}
|
|
1135
1165
|
} catch (e) {
|
|
1136
|
-
this.
|
|
1166
|
+
this.getBaichuanLogger().debug('Failed to align auxiliary devices state', e);
|
|
1137
1167
|
}
|
|
1138
1168
|
}
|
|
1139
1169
|
|
|
@@ -1163,7 +1193,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
1163
1193
|
return url.toString();
|
|
1164
1194
|
} catch (e) {
|
|
1165
1195
|
// If URL parsing fails, return original URL
|
|
1166
|
-
this.
|
|
1196
|
+
this.getBaichuanLogger().warn('Failed to parse URL for credentials', e);
|
|
1167
1197
|
return rtspUrl;
|
|
1168
1198
|
}
|
|
1169
1199
|
}
|
|
@@ -1204,7 +1234,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
1204
1234
|
} catch (e) {
|
|
1205
1235
|
// Only log if it's not a recoverable error to avoid spam
|
|
1206
1236
|
if (!this.isRecoverableBaichuanError?.(e)) {
|
|
1207
|
-
this.
|
|
1237
|
+
this.getBaichuanLogger().warn('Failed to get net port, using defaults', e);
|
|
1208
1238
|
}
|
|
1209
1239
|
// Use defaults if we can't get the ports
|
|
1210
1240
|
this.cachedNetPort = {
|
|
@@ -1215,7 +1245,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
1215
1245
|
}
|
|
1216
1246
|
|
|
1217
1247
|
async getVideoStreamOptions(): Promise<UrlMediaStreamOptions[]> {
|
|
1218
|
-
const logger = this.
|
|
1248
|
+
const logger = this.getBaichuanLogger();
|
|
1219
1249
|
|
|
1220
1250
|
if (this.cachedVideoStreamOptions?.length) {
|
|
1221
1251
|
return this.cachedVideoStreamOptions;
|
|
@@ -1260,7 +1290,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
1260
1290
|
}
|
|
1261
1291
|
|
|
1262
1292
|
if (streams.length) {
|
|
1263
|
-
|
|
1293
|
+
this.getBaichuanLogger().log('Fetched video stream options', { streams, netPort: this.cachedNetPort });
|
|
1264
1294
|
this.cachedVideoStreamOptions = streams;
|
|
1265
1295
|
return streams;
|
|
1266
1296
|
}
|
|
@@ -1333,165 +1363,11 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
1333
1363
|
async ensureClient(): Promise<ReolinkBaichuanApi> {
|
|
1334
1364
|
// If camera is connected to NVR, use NVR's shared Baichuan connection
|
|
1335
1365
|
if (this.nvrDevice) {
|
|
1336
|
-
const logger = this.getLogger();
|
|
1337
1366
|
return await this.nvrDevice.ensureBaichuanClient();
|
|
1338
1367
|
}
|
|
1339
1368
|
|
|
1340
|
-
//
|
|
1341
|
-
|
|
1342
|
-
return this.baichuanApi;
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
// Prevent concurrent login storms
|
|
1346
|
-
if (this.ensureClientPromise) return await this.ensureClientPromise;
|
|
1347
|
-
|
|
1348
|
-
// Apply backoff to avoid aggressive reconnection after disconnection
|
|
1349
|
-
if (this.lastDisconnectTime > 0) {
|
|
1350
|
-
const timeSinceDisconnect = Date.now() - this.lastDisconnectTime;
|
|
1351
|
-
if (timeSinceDisconnect < this.reconnectBackoffMs) {
|
|
1352
|
-
const waitTime = this.reconnectBackoffMs - timeSinceDisconnect;
|
|
1353
|
-
const logger = this.getLogger();
|
|
1354
|
-
logger.log(`[BaichuanClient] Waiting ${waitTime}ms before reconnection (backoff)`);
|
|
1355
|
-
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
this.ensureClientPromise = (async () => {
|
|
1360
|
-
const { ipAddress, username, password, uid } = this.storageSettings.values;
|
|
1361
|
-
|
|
1362
|
-
// Only tear down previous session if it exists and is not connected
|
|
1363
|
-
if (this.baichuanApi) {
|
|
1364
|
-
// Remove close listener from old client
|
|
1365
|
-
if (this.closeListener) {
|
|
1366
|
-
try {
|
|
1367
|
-
this.baichuanApi.client.off("close", this.closeListener);
|
|
1368
|
-
}
|
|
1369
|
-
catch {
|
|
1370
|
-
// ignore
|
|
1371
|
-
}
|
|
1372
|
-
this.closeListener = undefined;
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
const isConnected = this.baichuanApi.client.isSocketConnected();
|
|
1376
|
-
if (!isConnected) {
|
|
1377
|
-
try {
|
|
1378
|
-
this.baichuanApi.offSimpleEvent(this.onSimpleEvent);
|
|
1379
|
-
}
|
|
1380
|
-
catch {
|
|
1381
|
-
// ignore
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
|
-
try {
|
|
1385
|
-
await this.baichuanApi.close();
|
|
1386
|
-
}
|
|
1387
|
-
catch {
|
|
1388
|
-
// ignore
|
|
1389
|
-
}
|
|
1390
|
-
} else {
|
|
1391
|
-
// Socket is still connected, just re-attach event handler if needed
|
|
1392
|
-
if (this.isEventDispatchEnabled?.() && this.onSimpleEvent) {
|
|
1393
|
-
try {
|
|
1394
|
-
this.baichuanApi.offSimpleEvent(this.onSimpleEvent);
|
|
1395
|
-
this.baichuanApi.onSimpleEvent(this.onSimpleEvent);
|
|
1396
|
-
}
|
|
1397
|
-
catch {
|
|
1398
|
-
// ignore
|
|
1399
|
-
}
|
|
1400
|
-
}
|
|
1401
|
-
// Reuse existing client
|
|
1402
|
-
this.connectionTime = Date.now();
|
|
1403
|
-
return this.baichuanApi;
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
// Create new client
|
|
1408
|
-
const debugOptions = this.getBaichuanDebugOptions();
|
|
1409
|
-
const normalizedUid = this.protocol === 'udp' ? normalizeUid(uid) : undefined;
|
|
1410
|
-
|
|
1411
|
-
if (this.protocol === 'udp' && !normalizedUid) {
|
|
1412
|
-
throw new Error('UID is required for battery cameras (BCUDP)');
|
|
1413
|
-
}
|
|
1414
|
-
|
|
1415
|
-
const api = await createBaichuanApi(
|
|
1416
|
-
{
|
|
1417
|
-
inputs: {
|
|
1418
|
-
host: ipAddress,
|
|
1419
|
-
username: username,
|
|
1420
|
-
password: password,
|
|
1421
|
-
uid: normalizedUid,
|
|
1422
|
-
logger: this.console,
|
|
1423
|
-
debugOptions,
|
|
1424
|
-
},
|
|
1425
|
-
transport: this.protocol,
|
|
1426
|
-
logger: this.console,
|
|
1427
|
-
}
|
|
1428
|
-
);
|
|
1429
|
-
await api.login();
|
|
1430
|
-
|
|
1431
|
-
// Verify socket is connected before returning
|
|
1432
|
-
if (!api.client.isSocketConnected()) {
|
|
1433
|
-
throw new Error('Socket not connected after login');
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
this.baichuanApi = api;
|
|
1437
|
-
this.connectionTime = Date.now();
|
|
1438
|
-
|
|
1439
|
-
// Listen for socket disconnection to reset client state
|
|
1440
|
-
// This ensures ensureClient() will create a new connection on next call
|
|
1441
|
-
this.closeListener = () => {
|
|
1442
|
-
const logger = this.getLogger();
|
|
1443
|
-
if (this.baichuanApi === api) {
|
|
1444
|
-
const now = Date.now();
|
|
1445
|
-
const timeSinceLastDisconnect = now - this.lastDisconnectTime;
|
|
1446
|
-
this.lastDisconnectTime = now;
|
|
1447
|
-
|
|
1448
|
-
logger.log(`[BaichuanClient] Socket closed, resetting client state for reconnection (last disconnect ${timeSinceLastDisconnect}ms ago)`);
|
|
1449
|
-
|
|
1450
|
-
// Reset client state
|
|
1451
|
-
this.baichuanApi = undefined;
|
|
1452
|
-
this.ensureClientPromise = undefined;
|
|
1453
|
-
this.closeListener = undefined;
|
|
1454
|
-
|
|
1455
|
-
// Remove event handler to prevent operations during reconnection
|
|
1456
|
-
try {
|
|
1457
|
-
if (this.onSimpleEvent) {
|
|
1458
|
-
api.offSimpleEvent(this.onSimpleEvent);
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
catch {
|
|
1462
|
-
// ignore
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
};
|
|
1466
|
-
api.client.on("close", this.closeListener);
|
|
1467
|
-
|
|
1468
|
-
// Re-attach event handler if enabled
|
|
1469
|
-
// Note: We don't reattach here immediately to avoid operations being called
|
|
1470
|
-
// during reconnection. subscribeToEvents() will be called when needed.
|
|
1471
|
-
// However, if events were already subscribed, we need to reattach them.
|
|
1472
|
-
// We'll let subscribeToEvents() handle this, but we can also try here if needed.
|
|
1473
|
-
if (this.isEventDispatchEnabled?.() && this.onSimpleEvent) {
|
|
1474
|
-
try {
|
|
1475
|
-
// Verify connection is fully ready before subscribing
|
|
1476
|
-
if (api.client.isSocketConnected() && api.client.loggedIn) {
|
|
1477
|
-
api.onSimpleEvent(this.onSimpleEvent);
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
catch (e) {
|
|
1481
|
-
const logger = this.getLogger();
|
|
1482
|
-
logger.warn(`[BaichuanClient] Failed to reattach event handler after reconnection, will retry via subscribeToEvents()`, e);
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
return api;
|
|
1486
|
-
})();
|
|
1487
|
-
|
|
1488
|
-
try {
|
|
1489
|
-
return await this.ensureClientPromise;
|
|
1490
|
-
}
|
|
1491
|
-
finally {
|
|
1492
|
-
// Allow future reconnects and avoid pinning rejected promises
|
|
1493
|
-
this.ensureClientPromise = undefined;
|
|
1494
|
-
}
|
|
1369
|
+
// Use base class implementation
|
|
1370
|
+
return await this.ensureBaichuanClient();
|
|
1495
1371
|
}
|
|
1496
1372
|
|
|
1497
1373
|
async credentialsChanged(): Promise<void> {
|
|
@@ -1515,7 +1391,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
1515
1391
|
}
|
|
1516
1392
|
this.refreshingState = true;
|
|
1517
1393
|
|
|
1518
|
-
const logger = this.
|
|
1394
|
+
const logger = this.getBaichuanLogger();
|
|
1519
1395
|
const channel = this.storageSettings.values.rtspChannel;
|
|
1520
1396
|
|
|
1521
1397
|
try {
|
|
@@ -1543,14 +1419,14 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
1543
1419
|
info: this.info,
|
|
1544
1420
|
};
|
|
1545
1421
|
|
|
1546
|
-
|
|
1422
|
+
this.getBaichuanLogger().log(`Updating device interfaces: ${JSON.stringify(device)}`);
|
|
1547
1423
|
|
|
1548
1424
|
await sdk.deviceManager.onDeviceDiscovered(device);
|
|
1549
1425
|
} catch (e) {
|
|
1550
|
-
|
|
1426
|
+
this.getBaichuanLogger().error('Failed to update device interfaces', e);
|
|
1551
1427
|
}
|
|
1552
1428
|
|
|
1553
|
-
this.
|
|
1429
|
+
this.getBaichuanLogger().log(`Refreshed device capabilities: ${JSON.stringify({ capabilities, abilities, support, presets })}`);
|
|
1554
1430
|
}
|
|
1555
1431
|
catch (e) {
|
|
1556
1432
|
logger.error('Failed to refresh abilities', e);
|
|
@@ -1560,7 +1436,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
1560
1436
|
}
|
|
1561
1437
|
|
|
1562
1438
|
async parentInit(): Promise<void> {
|
|
1563
|
-
const logger = this.
|
|
1439
|
+
const logger = this.getBaichuanLogger();
|
|
1564
1440
|
|
|
1565
1441
|
try {
|
|
1566
1442
|
await this.ensureClient();
|
|
@@ -1612,7 +1488,7 @@ export abstract class CommonCameraMixin extends ScryptedDeviceBase implements Vi
|
|
|
1612
1488
|
|
|
1613
1489
|
this.streamManager = new StreamManager({
|
|
1614
1490
|
createStreamClient: () => this.createStreamClient(),
|
|
1615
|
-
getLogger: () => this.
|
|
1491
|
+
getLogger: () => this.getBaichuanLogger() as Console,
|
|
1616
1492
|
credentials: {
|
|
1617
1493
|
username,
|
|
1618
1494
|
password
|
package/src/connect.ts
CHANGED
|
@@ -51,7 +51,7 @@ export async function createBaichuanApi(props: {
|
|
|
51
51
|
host: inputs.host,
|
|
52
52
|
username: inputs.username,
|
|
53
53
|
password: inputs.password,
|
|
54
|
-
logger: inputs.logger
|
|
54
|
+
logger: logger, // Use the logger passed to createBaichuanApi, not inputs.logger
|
|
55
55
|
debugOptions: inputs.debugOptions ?? {}
|
|
56
56
|
};
|
|
57
57
|
|
|
@@ -60,7 +60,6 @@ export async function createBaichuanApi(props: {
|
|
|
60
60
|
// uncaught exception. Ensure we always have a listener.
|
|
61
61
|
try {
|
|
62
62
|
api.client.on("error", (err: unknown) => {
|
|
63
|
-
const logger = inputs.logger;
|
|
64
63
|
if (!logger) return;
|
|
65
64
|
const msg = (err as any)?.message || (err as any)?.toString?.() || String(err);
|
|
66
65
|
// Only log if it's not a recoverable error to avoid spam
|
package/src/debug-options.ts
CHANGED
|
@@ -60,7 +60,7 @@ export function convertDebugLogsToApiOptions(debugLogs: string[]): DebugOptions
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
// Removed debug log that was causing "[] {}" output
|
|
64
64
|
return Object.keys(apiOptions).length > 0 ? apiOptions : undefined;
|
|
65
65
|
}
|
|
66
66
|
|
package/src/intercom.ts
CHANGED
|
@@ -31,7 +31,7 @@ export class ReolinkBaichuanIntercom {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
async start(media: MediaObject): Promise<void> {
|
|
34
|
-
const logger = this.camera.
|
|
34
|
+
const logger = this.camera.getBaichuanLogger();
|
|
35
35
|
|
|
36
36
|
const ffmpegInput = await sdk.mediaManager.convertMediaObjectToJSON<FFmpegInput>(
|
|
37
37
|
media,
|
|
@@ -177,7 +177,7 @@ export class ReolinkBaichuanIntercom {
|
|
|
177
177
|
if (this.stopping) return this.stopping;
|
|
178
178
|
|
|
179
179
|
this.stopping = (async () => {
|
|
180
|
-
const logger = this.camera.
|
|
180
|
+
const logger = this.camera.getBaichuanLogger();
|
|
181
181
|
|
|
182
182
|
const ffmpeg = this.ffmpeg;
|
|
183
183
|
this.ffmpeg = undefined;
|
|
@@ -243,7 +243,7 @@ export class ReolinkBaichuanIntercom {
|
|
|
243
243
|
bytesNeeded: number,
|
|
244
244
|
blockSize: number,
|
|
245
245
|
): void {
|
|
246
|
-
const logger = this.camera.
|
|
246
|
+
const logger = this.camera.getBaichuanLogger();
|
|
247
247
|
|
|
248
248
|
this.sendChain = this.sendChain
|
|
249
249
|
.then(async () => {
|