@apocaliss92/scrypted-reolink-native 0.1.31 → 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/dist/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/logs/composite-stream.txt +16390 -0
- package/logs/lense.txt +44 -0
- package/logs/multifocal.txt +136 -0
- package/logs/multifocal2.txt +3585 -0
- package/package.json +1 -1
- package/src/camera-battery.ts +0 -3
- package/src/camera.ts +0 -22
- package/src/common.ts +166 -41
- package/src/main.ts +2 -2
- package/src/multiFocal.ts +2 -74
- package/src/stream-utils.ts +25 -89
- package/src/utils.ts +1 -0
package/package.json
CHANGED
package/src/camera-battery.ts
CHANGED
package/src/camera.ts
CHANGED
|
@@ -74,28 +74,6 @@ export class ReolinkNativeCamera extends CommonCameraMixin {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
|
|
77
|
-
async createStreamClient(): Promise<ReolinkBaichuanApi> {
|
|
78
|
-
const { ipAddress, username, password } = this.storageSettings.values;
|
|
79
|
-
const logger = this.getBaichuanLogger();
|
|
80
|
-
|
|
81
|
-
const debugOptions = this.getBaichuanDebugOptions();
|
|
82
|
-
const api = await createBaichuanApi(
|
|
83
|
-
{
|
|
84
|
-
inputs: {
|
|
85
|
-
host: ipAddress,
|
|
86
|
-
username: username,
|
|
87
|
-
password: password,
|
|
88
|
-
logger,
|
|
89
|
-
debugOptions
|
|
90
|
-
},
|
|
91
|
-
transport: 'tcp',
|
|
92
|
-
},
|
|
93
|
-
);
|
|
94
|
-
await api.login();
|
|
95
|
-
|
|
96
|
-
return api;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
77
|
private passiveRefreshTimer: ReturnType<typeof setTimeout> | undefined;
|
|
100
78
|
|
|
101
79
|
async release() {
|
package/src/common.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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, Reboot, RequestMediaStreamOptions, RequestPictureOptions, ResponsePictureOptions, 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";
|
|
7
|
+
import { createBaichuanApi, normalizeUid, type BaichuanTransport } from "./connect";
|
|
8
8
|
import { convertDebugLogsToApiOptions, getApiRelevantDebugLogs, getDebugLogChoices } from "./debug-options";
|
|
9
9
|
import { ReolinkBaichuanIntercom } from "./intercom";
|
|
10
10
|
import ReolinkNativePlugin from "./main";
|
|
@@ -189,7 +189,7 @@ class ReolinkCameraPirSensor extends ScryptedDeviceBase implements OnOff, Settin
|
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
export abstract class CommonCameraMixin extends BaseBaichuanClass implements VideoCamera, Camera, Settings, DeviceProvider, ObjectDetector, PanTiltZoom, VideoTextOverlays, BinarySensor, Intercom, Reboot {
|
|
192
|
+
export abstract class CommonCameraMixin extends BaseBaichuanClass implements VideoCamera, Camera, Settings, DeviceProvider, ObjectDetector, PanTiltZoom, VideoTextOverlays, BinarySensor, Intercom, Reboot, VideoClips {
|
|
193
193
|
storageSettings = new StorageSettings(this, {
|
|
194
194
|
// Basic connection settings
|
|
195
195
|
ipAddress: {
|
|
@@ -250,28 +250,40 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
250
250
|
description: 'Relative size of the PIP overlay (0.1 = 10%, 0.3 = 30%, etc.)',
|
|
251
251
|
type: 'number',
|
|
252
252
|
defaultValue: 0.25,
|
|
253
|
-
hide: true,
|
|
253
|
+
hide: true,
|
|
254
|
+
onPut: async () => {
|
|
255
|
+
this.scheduleStreamManagerRestart('pipSize changed');
|
|
256
|
+
},
|
|
254
257
|
},
|
|
255
258
|
pipMargin: {
|
|
256
259
|
title: 'PIP Margin',
|
|
257
260
|
description: 'Margin from edge in pixels',
|
|
258
261
|
type: 'number',
|
|
259
262
|
defaultValue: 10,
|
|
260
|
-
hide: true,
|
|
263
|
+
hide: true,
|
|
264
|
+
onPut: async () => {
|
|
265
|
+
this.scheduleStreamManagerRestart('pipMargin changed');
|
|
266
|
+
},
|
|
261
267
|
},
|
|
262
268
|
widerChannel: {
|
|
263
269
|
title: 'Wider Channel',
|
|
264
270
|
description: 'Channel number for wider lens (typically 0)',
|
|
265
271
|
type: 'number',
|
|
266
272
|
defaultValue: 0,
|
|
267
|
-
hide: true,
|
|
273
|
+
hide: true,
|
|
274
|
+
onPut: async () => {
|
|
275
|
+
this.scheduleStreamManagerRestart('widerChannel changed');
|
|
276
|
+
},
|
|
268
277
|
},
|
|
269
278
|
teleChannel: {
|
|
270
279
|
title: 'Tele Channel',
|
|
271
280
|
description: 'Channel number for tele lens (typically 1)',
|
|
272
281
|
type: 'number',
|
|
273
282
|
defaultValue: 1,
|
|
274
|
-
hide: true,
|
|
283
|
+
hide: true,
|
|
284
|
+
onPut: async () => {
|
|
285
|
+
this.scheduleStreamManagerRestart('teleChannel changed');
|
|
286
|
+
},
|
|
275
287
|
},
|
|
276
288
|
// Battery camera specific
|
|
277
289
|
uid: {
|
|
@@ -582,7 +594,10 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
582
594
|
|
|
583
595
|
protected nvrDevice?: ReolinkNativeNvrDevice;
|
|
584
596
|
protected multiFocalDevice?: ReolinkNativeMultiFocalDevice;
|
|
585
|
-
thisDevice: Settings
|
|
597
|
+
thisDevice: Settings;
|
|
598
|
+
isBattery: boolean;
|
|
599
|
+
isMultiFocal: boolean;
|
|
600
|
+
private streamManagerRestartTimeout: NodeJS.Timeout | undefined;
|
|
586
601
|
|
|
587
602
|
constructor(
|
|
588
603
|
nativeId: string,
|
|
@@ -596,14 +611,34 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
596
611
|
this.multiFocalDevice = options.multiFocalDevice;
|
|
597
612
|
this.thisDevice = sdk.systemManager.getDeviceById<Settings>(this.id);
|
|
598
613
|
|
|
599
|
-
|
|
600
|
-
this.
|
|
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';
|
|
601
617
|
|
|
602
618
|
setTimeout(async () => {
|
|
603
619
|
await this.parentInit();
|
|
604
620
|
}, 2000);
|
|
605
621
|
}
|
|
606
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
|
+
|
|
607
642
|
async reboot(): Promise<void> {
|
|
608
643
|
const api = await this.ensureBaichuanClient();
|
|
609
644
|
await api.reboot();
|
|
@@ -613,9 +648,9 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
613
648
|
protected getConnectionConfig(): BaichuanConnectionConfig {
|
|
614
649
|
const { ipAddress, username, password, uid } = this.storageSettings.values;
|
|
615
650
|
const debugOptions = this.getBaichuanDebugOptions();
|
|
616
|
-
const normalizedUid = this.
|
|
651
|
+
const normalizedUid = this.isBattery ? normalizeUid(uid) : undefined;
|
|
617
652
|
|
|
618
|
-
if (this.
|
|
653
|
+
if (this.isBattery && !normalizedUid) {
|
|
619
654
|
throw new Error('UID is required for battery cameras (BCUDP)');
|
|
620
655
|
}
|
|
621
656
|
|
|
@@ -639,8 +674,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
639
674
|
// For battery cameras, don't auto-resubscribe after idle disconnects
|
|
640
675
|
// (idle disconnects are normal for battery cameras to save power)
|
|
641
676
|
// Events will be resubscribed when ensureClient() is called for actual operations
|
|
642
|
-
|
|
643
|
-
if (!isBattery) {
|
|
677
|
+
if (!this.isBattery) {
|
|
644
678
|
// For non-battery cameras, resubscribe to events after reconnection
|
|
645
679
|
setTimeout(async () => {
|
|
646
680
|
try {
|
|
@@ -666,7 +700,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
666
700
|
}
|
|
667
701
|
|
|
668
702
|
async withBaichuanRetry<T>(fn: () => Promise<T>): Promise<T> {
|
|
669
|
-
if (this.
|
|
703
|
+
if (this.isBattery) {
|
|
670
704
|
return await fn();
|
|
671
705
|
} else {
|
|
672
706
|
try {
|
|
@@ -737,8 +771,38 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
737
771
|
}
|
|
738
772
|
}
|
|
739
773
|
|
|
740
|
-
|
|
741
|
-
|
|
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;
|
|
742
806
|
}
|
|
743
807
|
|
|
744
808
|
public getAbilities(): DeviceCapabilities {
|
|
@@ -750,8 +814,78 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
750
814
|
}
|
|
751
815
|
|
|
752
816
|
getBaichuanDebugOptions(): any | undefined {
|
|
753
|
-
const
|
|
754
|
-
return convertDebugLogsToApiOptions(
|
|
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);
|
|
755
889
|
}
|
|
756
890
|
|
|
757
891
|
isRecoverableBaichuanError(e: any): boolean {
|
|
@@ -1167,7 +1301,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1167
1301
|
}
|
|
1168
1302
|
|
|
1169
1303
|
async takePicture(options?: RequestPictureOptions) {
|
|
1170
|
-
if (this.
|
|
1304
|
+
if (!this.isBattery) {
|
|
1171
1305
|
try {
|
|
1172
1306
|
return this.withBaichuanRetry(async () => {
|
|
1173
1307
|
const client = await this.ensureClient();
|
|
@@ -1666,22 +1800,20 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1666
1800
|
}
|
|
1667
1801
|
|
|
1668
1802
|
const { username, password } = this.storageSettings.values;
|
|
1669
|
-
const isBattery = ['multi-focal-battery', 'battery'].includes(this.options.type);
|
|
1670
|
-
const isMultiFocal = ['multi-focal', 'multi-focal'].includes(this.options.type);
|
|
1671
1803
|
|
|
1672
|
-
this.storageSettings.settings.uid.hide = !isBattery;
|
|
1673
|
-
this.storageSettings.settings.batteryUpdateIntervalMinutes.hide = !isBattery;
|
|
1674
|
-
this.storageSettings.settings.lowThresholdBatteryRecording.hide = !isBattery;
|
|
1675
|
-
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;
|
|
1676
1808
|
|
|
1677
1809
|
// Show PIP settings only for multifocal devices
|
|
1678
|
-
this.storageSettings.settings.pipPosition.hide = !isMultiFocal;
|
|
1679
|
-
this.storageSettings.settings.pipSize.hide = !isMultiFocal;
|
|
1680
|
-
this.storageSettings.settings.pipMargin.hide = !isMultiFocal;
|
|
1681
|
-
this.storageSettings.settings.widerChannel.hide = !isMultiFocal;
|
|
1682
|
-
this.storageSettings.settings.teleChannel.hide = !isMultiFocal;
|
|
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;
|
|
1683
1815
|
|
|
1684
|
-
if (isBattery && !this.storageSettings.values.mixinsSetup) {
|
|
1816
|
+
if (this.isBattery && !this.storageSettings.values.mixinsSetup) {
|
|
1685
1817
|
try {
|
|
1686
1818
|
const device = sdk.systemManager.getDeviceById<Settings>(this.id);
|
|
1687
1819
|
if (device) {
|
|
@@ -1703,15 +1835,8 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1703
1835
|
logger.warn('Failed to subscribe to Baichuan events', e);
|
|
1704
1836
|
}
|
|
1705
1837
|
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
getLogger: () => logger,
|
|
1709
|
-
credentials: {
|
|
1710
|
-
username,
|
|
1711
|
-
password
|
|
1712
|
-
},
|
|
1713
|
-
sharedConnection: isBattery,
|
|
1714
|
-
});
|
|
1838
|
+
// Initialize StreamManager (with composite options for multifocal devices)
|
|
1839
|
+
this.initStreamManager(logger);
|
|
1715
1840
|
|
|
1716
1841
|
const { hasIntercom, hasPtz } = this.getAbilities();
|
|
1717
1842
|
|
package/src/main.ts
CHANGED
|
@@ -96,7 +96,7 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
96
96
|
device.storageSettings.values.ipAddress = ipAddress;
|
|
97
97
|
device.storageSettings.values.username = username;
|
|
98
98
|
device.storageSettings.values.password = password;
|
|
99
|
-
device.storageSettings.values.uid =
|
|
99
|
+
device.storageSettings.values.uid = uid;
|
|
100
100
|
device.storageSettings.values.capabilities = capabilities;
|
|
101
101
|
|
|
102
102
|
return nativeId;
|
|
@@ -177,7 +177,7 @@ class ReolinkNativePlugin extends ScryptedDeviceBase implements DeviceProvider,
|
|
|
177
177
|
device.storageSettings.values.rtspChannel = rtspChannel;
|
|
178
178
|
device.storageSettings.values.ipAddress = ipAddress;
|
|
179
179
|
device.storageSettings.values.capabilities = capabilities;
|
|
180
|
-
device.storageSettings.values.uid =
|
|
180
|
+
device.storageSettings.values.uid = uid;
|
|
181
181
|
|
|
182
182
|
return nativeId;
|
|
183
183
|
}
|
package/src/multiFocal.ts
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import type { DeviceCapabilities, DualLensChannelAnalysis, ReolinkSimpleEvent
|
|
2
|
-
import sdk, { Device, DeviceProvider,
|
|
3
|
-
import { type BaichuanConnectionCallbacks } from "./baichuan-base";
|
|
1
|
+
import type { DeviceCapabilities, DualLensChannelAnalysis, ReolinkSimpleEvent } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
|
|
2
|
+
import sdk, { Device, DeviceProvider, Reboot, ScryptedDeviceType, Setting, Settings, SettingValue } from "@scrypted/sdk";
|
|
4
3
|
import { ReolinkNativeCamera } from "./camera";
|
|
5
4
|
import { ReolinkNativeBatteryCamera } from "./camera-battery";
|
|
6
5
|
import { CameraType, CommonCameraMixin } from "./common";
|
|
7
6
|
import ReolinkNativePlugin from "./main";
|
|
8
|
-
import { StreamManager } from "./stream-utils";
|
|
9
|
-
import type { UrlMediaStreamOptions } from "../../scrypted/plugins/rtsp/src/rtsp";
|
|
10
7
|
import { batteryCameraSuffix, cameraSuffix, getDeviceInterfaces, updateDeviceInfo } from "./utils";
|
|
11
8
|
|
|
12
9
|
export class ReolinkNativeMultiFocalDevice extends CommonCameraMixin implements Settings, DeviceProvider, Reboot {
|
|
@@ -36,28 +33,6 @@ export class ReolinkNativeMultiFocalDevice extends CommonCameraMixin implements
|
|
|
36
33
|
}
|
|
37
34
|
}
|
|
38
35
|
|
|
39
|
-
protected getConnectionCallbacks(): BaichuanConnectionCallbacks {
|
|
40
|
-
return {
|
|
41
|
-
onError: undefined, // Use default error handling
|
|
42
|
-
onClose: async () => {
|
|
43
|
-
// Reinit after cleanup
|
|
44
|
-
await this.reinit();
|
|
45
|
-
if (!this.isBattery) {
|
|
46
|
-
setTimeout(async () => {
|
|
47
|
-
try {
|
|
48
|
-
await this.subscribeToEvents();
|
|
49
|
-
} catch (e) {
|
|
50
|
-
const logger = this.getBaichuanLogger();
|
|
51
|
-
logger.warn('Failed to resubscribe to events after reconnection', e);
|
|
52
|
-
}
|
|
53
|
-
}, 1000);
|
|
54
|
-
}
|
|
55
|
-
},
|
|
56
|
-
onSimpleEvent: (ev) => this.forwardNativeEvent(ev),
|
|
57
|
-
getEventSubscriptionEnabled: () => true,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
36
|
protected async onBeforeCleanup(): Promise<void> {
|
|
62
37
|
await this.unsubscribeFromAllEvents();
|
|
63
38
|
}
|
|
@@ -220,53 +195,6 @@ export class ReolinkNativeMultiFocalDevice extends CommonCameraMixin implements
|
|
|
220
195
|
}
|
|
221
196
|
|
|
222
197
|
await super.reportDevices();
|
|
223
|
-
|
|
224
|
-
// Initialize StreamManager with composite options for multifocal device
|
|
225
|
-
// Use saved settings or defaults
|
|
226
|
-
const values = this.storageSettings.values as any;
|
|
227
|
-
const pipPosition = (values.pipPosition || 'bottom-right') as any;
|
|
228
|
-
const pipSize = values.pipSize ?? 0.25;
|
|
229
|
-
const pipMargin = values.pipMargin ?? 10;
|
|
230
|
-
const widerChannel = values.widerChannel ?? 0;
|
|
231
|
-
const teleChannel = values.teleChannel ?? 1;
|
|
232
|
-
|
|
233
|
-
if (!this.streamManager) {
|
|
234
|
-
this.streamManager = new StreamManager({
|
|
235
|
-
createStreamClient: () => this.createStreamClient(),
|
|
236
|
-
getLogger: () => logger,
|
|
237
|
-
credentials: {
|
|
238
|
-
username,
|
|
239
|
-
password
|
|
240
|
-
},
|
|
241
|
-
sharedConnection: this.isBattery,
|
|
242
|
-
compositeOptions: {
|
|
243
|
-
widerChannel,
|
|
244
|
-
teleChannel,
|
|
245
|
-
pipPosition: pipPosition as any,
|
|
246
|
-
pipSize,
|
|
247
|
-
pipMargin,
|
|
248
|
-
},
|
|
249
|
-
});
|
|
250
|
-
} else {
|
|
251
|
-
// Recreate StreamManager with new settings if they changed
|
|
252
|
-
// StreamManager doesn't expose opts, so we need to recreate it
|
|
253
|
-
this.streamManager = new StreamManager({
|
|
254
|
-
createStreamClient: () => this.createStreamClient(),
|
|
255
|
-
getLogger: () => logger,
|
|
256
|
-
credentials: {
|
|
257
|
-
username,
|
|
258
|
-
password
|
|
259
|
-
},
|
|
260
|
-
sharedConnection: this.isBattery,
|
|
261
|
-
compositeOptions: {
|
|
262
|
-
widerChannel,
|
|
263
|
-
teleChannel,
|
|
264
|
-
pipPosition: pipPosition as any,
|
|
265
|
-
pipSize,
|
|
266
|
-
pipMargin,
|
|
267
|
-
},
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
198
|
}
|
|
271
199
|
|
|
272
200
|
async getDevice(nativeId: string) {
|
package/src/stream-utils.ts
CHANGED
|
@@ -113,11 +113,15 @@ export async function createRfc4571MediaObjectFromStreamManager(params: {
|
|
|
113
113
|
mso.audio.sampleRate = audio.sampleRate;
|
|
114
114
|
mso.audio.channels = audio.channels;
|
|
115
115
|
}
|
|
116
|
-
|
|
116
|
+
|
|
117
|
+
const url = new URL(`tcp://${host}`);
|
|
117
118
|
url.port = port.toString();
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
if (username) {
|
|
120
|
+
url.username = username;
|
|
121
|
+
}
|
|
122
|
+
if (password) {
|
|
123
|
+
url.password = password;
|
|
124
|
+
}
|
|
121
125
|
|
|
122
126
|
const rfc = {
|
|
123
127
|
url,
|
|
@@ -200,11 +204,14 @@ export class StreamManager {
|
|
|
200
204
|
return this.opts.getLogger() ;
|
|
201
205
|
}
|
|
202
206
|
|
|
203
|
-
private async
|
|
207
|
+
private async ensureRfcServer(
|
|
204
208
|
streamKey: string,
|
|
205
|
-
channel: number,
|
|
206
209
|
profile: StreamProfile,
|
|
207
|
-
expectedVideoType
|
|
210
|
+
expectedVideoType: 'H264' | 'H265' | undefined,
|
|
211
|
+
options: {
|
|
212
|
+
channel?: number;
|
|
213
|
+
compositeOptions?: CompositeStreamPipOptions;
|
|
214
|
+
},
|
|
208
215
|
): Promise<RfcServerInfo> {
|
|
209
216
|
const existingCreate = this.nativeRfcServerCreatePromises.get(streamKey);
|
|
210
217
|
if (existingCreate) {
|
|
@@ -215,8 +222,9 @@ export class StreamManager {
|
|
|
215
222
|
const cached = this.nativeRfcServers.get(streamKey);
|
|
216
223
|
if (cached?.server?.listening) {
|
|
217
224
|
if (expectedVideoType && cached.videoType !== expectedVideoType) {
|
|
225
|
+
const kind = options.channel === undefined ? 'composite' : 'native';
|
|
218
226
|
this.getLogger().warn(
|
|
219
|
-
`Native RFC cache codec mismatch for ${streamKey}: cached=${cached.videoType} expected=${expectedVideoType}; recreating server.`,
|
|
227
|
+
`Native RFC ${kind} cache codec mismatch for ${streamKey}: cached=${cached.videoType} expected=${expectedVideoType}; recreating server.`,
|
|
220
228
|
);
|
|
221
229
|
}
|
|
222
230
|
else {
|
|
@@ -252,13 +260,14 @@ export class StreamManager {
|
|
|
252
260
|
|
|
253
261
|
const created = await createRfc4571TcpServer({
|
|
254
262
|
api,
|
|
255
|
-
channel,
|
|
263
|
+
channel: options.channel,
|
|
256
264
|
profile,
|
|
257
265
|
logger: this.getLogger(),
|
|
258
266
|
expectedVideoType: expectedVideoType as VideoType | undefined,
|
|
259
267
|
closeApiOnTeardown,
|
|
260
268
|
username,
|
|
261
269
|
password,
|
|
270
|
+
...(options.compositeOptions ? { compositeOptions: options.compositeOptions } : {}),
|
|
262
271
|
});
|
|
263
272
|
|
|
264
273
|
this.nativeRfcServers.set(streamKey, created);
|
|
@@ -292,7 +301,9 @@ export class StreamManager {
|
|
|
292
301
|
streamKey: string,
|
|
293
302
|
expectedVideoType?: 'H264' | 'H265',
|
|
294
303
|
): Promise<RfcServerInfo> {
|
|
295
|
-
return await this.
|
|
304
|
+
return await this.ensureRfcServer(streamKey, profile, expectedVideoType, {
|
|
305
|
+
channel,
|
|
306
|
+
});
|
|
296
307
|
}
|
|
297
308
|
|
|
298
309
|
async getRfcCompositeStream(
|
|
@@ -300,85 +311,10 @@ export class StreamManager {
|
|
|
300
311
|
streamKey: string,
|
|
301
312
|
expectedVideoType?: 'H264' | 'H265',
|
|
302
313
|
): Promise<RfcServerInfo> {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const createPromise = (async () => {
|
|
309
|
-
const cached = this.nativeRfcServers.get(streamKey);
|
|
310
|
-
if (cached?.server?.listening) {
|
|
311
|
-
if (expectedVideoType && cached.videoType !== expectedVideoType) {
|
|
312
|
-
this.getLogger().warn(
|
|
313
|
-
`Native RFC composite cache codec mismatch for ${streamKey}: cached=${cached.videoType} expected=${expectedVideoType}; recreating server.`,
|
|
314
|
-
);
|
|
315
|
-
}
|
|
316
|
-
else {
|
|
317
|
-
return {
|
|
318
|
-
host: cached.host,
|
|
319
|
-
port: cached.port,
|
|
320
|
-
sdp: cached.sdp,
|
|
321
|
-
audio: cached.audio,
|
|
322
|
-
username: (cached as any).username || this.opts.credentials.username,
|
|
323
|
-
password: (cached as any).password || this.opts.credentials.password,
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
if (cached) {
|
|
329
|
-
try {
|
|
330
|
-
await cached.close('recreate');
|
|
331
|
-
}
|
|
332
|
-
catch {
|
|
333
|
-
// ignore
|
|
334
|
-
}
|
|
335
|
-
this.nativeRfcServers.delete(streamKey);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const api = await this.opts.createStreamClient();
|
|
339
|
-
const { createRfc4571TcpServer } = await import('@apocaliss92/reolink-baichuan-js');
|
|
340
|
-
|
|
341
|
-
// Use the same credentials as the main connection
|
|
342
|
-
const { username, password } = this.opts.credentials;
|
|
343
|
-
|
|
344
|
-
// If connection is shared, don't close it when stream teardown happens
|
|
345
|
-
const closeApiOnTeardown = !(this.opts.sharedConnection ?? false);
|
|
346
|
-
|
|
347
|
-
const created = await createRfc4571TcpServer({
|
|
348
|
-
api,
|
|
349
|
-
channel: undefined, // Undefined channel indicates composite stream
|
|
350
|
-
profile,
|
|
351
|
-
logger: this.getLogger(),
|
|
352
|
-
expectedVideoType: expectedVideoType as VideoType | undefined,
|
|
353
|
-
closeApiOnTeardown,
|
|
354
|
-
username,
|
|
355
|
-
password,
|
|
356
|
-
compositeOptions: this.opts.compositeOptions,
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
this.nativeRfcServers.set(streamKey, created);
|
|
360
|
-
created.server.once('close', () => {
|
|
361
|
-
const current = this.nativeRfcServers.get(streamKey);
|
|
362
|
-
if (current?.server === created.server) this.nativeRfcServers.delete(streamKey);
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
return {
|
|
366
|
-
host: created.host,
|
|
367
|
-
port: created.port,
|
|
368
|
-
sdp: created.sdp,
|
|
369
|
-
audio: created.audio,
|
|
370
|
-
username: (created as any).username || this.opts.credentials.username,
|
|
371
|
-
password: (created as any).password || this.opts.credentials.password,
|
|
372
|
-
};
|
|
373
|
-
})();
|
|
374
|
-
|
|
375
|
-
this.nativeRfcServerCreatePromises.set(streamKey, createPromise);
|
|
376
|
-
try {
|
|
377
|
-
return await createPromise;
|
|
378
|
-
}
|
|
379
|
-
finally {
|
|
380
|
-
this.nativeRfcServerCreatePromises.delete(streamKey);
|
|
381
|
-
}
|
|
314
|
+
return await this.ensureRfcServer(streamKey, profile, expectedVideoType, {
|
|
315
|
+
channel: undefined, // Undefined channel indicates composite stream
|
|
316
|
+
compositeOptions: this.opts.compositeOptions,
|
|
317
|
+
});
|
|
382
318
|
}
|
|
383
319
|
|
|
384
320
|
/**
|