@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/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/baichuan-base.ts +0 -4
- package/src/camera-battery.ts +4 -62
- package/src/camera.ts +0 -37
- package/src/common.ts +329 -74
- package/src/debug-options.ts +0 -8
- package/src/main.ts +17 -32
- package/src/multiFocal.ts +3 -42
- package/src/nvr.ts +26 -8
- package/src/stream-utils.ts +86 -11
- package/src/utils.ts +1 -0
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,
|
|
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
|
-
|
|
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
|
-
|
|
321
|
+
socketApiDebugLogs: {
|
|
262
322
|
subgroup: 'Advanced',
|
|
263
|
-
title: 'Debug Logs',
|
|
264
|
-
description: 'Enable specific debug logs.
|
|
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
|
-
|
|
547
|
-
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';
|
|
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.
|
|
651
|
+
const normalizedUid = this.isBattery ? normalizeUid(uid) : undefined;
|
|
559
652
|
|
|
560
|
-
if (this.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
684
|
-
|
|
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
|
|
697
|
-
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);
|
|
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
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
1575
|
-
|
|
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
|
-
|
|
1841
|
+
const { hasIntercom, hasPtz } = this.getAbilities();
|
|
1586
1842
|
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1843
|
+
if (hasIntercom) {
|
|
1844
|
+
this.intercom = new ReolinkBaichuanIntercom(this);
|
|
1845
|
+
}
|
|
1590
1846
|
|
|
1591
|
-
|
|
1592
|
-
|
|
1847
|
+
if (hasPtz) {
|
|
1848
|
+
const choices = (this.presets || []).map((preset: any) => preset.id + '=' + preset.name);
|
|
1593
1849
|
|
|
1594
|
-
|
|
1595
|
-
|
|
1850
|
+
this.storageSettings.settings.presets.choices = choices;
|
|
1851
|
+
this.storageSettings.settings.ptzSelectedPreset.choices = choices;
|
|
1596
1852
|
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
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
|
-
|
|
1606
|
-
}
|
|
1861
|
+
this.updatePtzCaps();
|
|
1607
1862
|
}
|
|
1608
1863
|
|
|
1609
1864
|
if (this.nvrDevice || this.multiFocalDevice) {
|
package/src/debug-options.ts
CHANGED
|
@@ -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
|
/**
|