@apocaliss92/scrypted-reolink-native 0.1.31 → 0.1.33
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 +3 -2
- package/src/baichuan-base.ts +35 -7
- package/src/camera-battery.ts +0 -9
- package/src/camera.ts +1 -58
- package/src/common.ts +669 -47
- package/src/debug-options.ts +4 -0
- package/src/main.ts +160 -6
- package/src/multiFocal.ts +3 -103
- package/src/nvr.ts +1 -3
- package/src/stream-utils.ts +49 -96
- package/src/utils.ts +472 -2
package/src/common.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
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";
|
|
1
|
+
import type { DeviceCapabilities, PtzCommand, PtzPreset, ReolinkBaichuanApi, ReolinkSimpleEvent, ReolinkSupportedStream, StreamProfile, 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, 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
|
+
import fs from 'fs';
|
|
6
|
+
import crypto from 'crypto';
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
5
8
|
import type { UrlMediaStreamOptions } from "../../scrypted/plugins/rtsp/src/rtsp";
|
|
6
9
|
import { BaseBaichuanClass, type BaichuanConnectionCallbacks, type BaichuanConnectionConfig } from "./baichuan-base";
|
|
7
|
-
import { normalizeUid, type BaichuanTransport } from "./connect";
|
|
10
|
+
import { createBaichuanApi, normalizeUid, type BaichuanTransport } from "./connect";
|
|
8
11
|
import { convertDebugLogsToApiOptions, getApiRelevantDebugLogs, getDebugLogChoices } from "./debug-options";
|
|
9
12
|
import { ReolinkBaichuanIntercom } from "./intercom";
|
|
10
13
|
import ReolinkNativePlugin from "./main";
|
|
@@ -19,7 +22,7 @@ import {
|
|
|
19
22
|
selectStreamOption,
|
|
20
23
|
StreamManager
|
|
21
24
|
} from "./stream-utils";
|
|
22
|
-
import { floodlightSuffix, getDeviceInterfaces, pirSuffix, sirenSuffix, updateDeviceInfo } from "./utils";
|
|
25
|
+
import { floodlightSuffix, getDeviceInterfaces, pirSuffix, recordingFileToVideoClip, sirenSuffix, updateDeviceInfo } from "./utils";
|
|
23
26
|
|
|
24
27
|
export type CameraType = 'battery' | 'regular' | 'multi-focal' | 'multi-focal-battery';
|
|
25
28
|
|
|
@@ -189,7 +192,7 @@ class ReolinkCameraPirSensor extends ScryptedDeviceBase implements OnOff, Settin
|
|
|
189
192
|
}
|
|
190
193
|
}
|
|
191
194
|
|
|
192
|
-
export abstract class CommonCameraMixin extends BaseBaichuanClass implements VideoCamera, Camera, Settings, DeviceProvider, ObjectDetector, PanTiltZoom, VideoTextOverlays, BinarySensor, Intercom, Reboot {
|
|
195
|
+
export abstract class CommonCameraMixin extends BaseBaichuanClass implements VideoCamera, Camera, Settings, DeviceProvider, ObjectDetector, PanTiltZoom, VideoTextOverlays, BinarySensor, Intercom, Reboot, VideoClips {
|
|
193
196
|
storageSettings = new StorageSettings(this, {
|
|
194
197
|
// Basic connection settings
|
|
195
198
|
ipAddress: {
|
|
@@ -250,28 +253,40 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
250
253
|
description: 'Relative size of the PIP overlay (0.1 = 10%, 0.3 = 30%, etc.)',
|
|
251
254
|
type: 'number',
|
|
252
255
|
defaultValue: 0.25,
|
|
253
|
-
hide: true,
|
|
256
|
+
hide: true,
|
|
257
|
+
onPut: async () => {
|
|
258
|
+
this.scheduleStreamManagerRestart('pipSize changed');
|
|
259
|
+
},
|
|
254
260
|
},
|
|
255
261
|
pipMargin: {
|
|
256
262
|
title: 'PIP Margin',
|
|
257
263
|
description: 'Margin from edge in pixels',
|
|
258
264
|
type: 'number',
|
|
259
265
|
defaultValue: 10,
|
|
260
|
-
hide: true,
|
|
266
|
+
hide: true,
|
|
267
|
+
onPut: async () => {
|
|
268
|
+
this.scheduleStreamManagerRestart('pipMargin changed');
|
|
269
|
+
},
|
|
261
270
|
},
|
|
262
271
|
widerChannel: {
|
|
263
272
|
title: 'Wider Channel',
|
|
264
273
|
description: 'Channel number for wider lens (typically 0)',
|
|
265
274
|
type: 'number',
|
|
266
275
|
defaultValue: 0,
|
|
267
|
-
hide: true,
|
|
276
|
+
hide: true,
|
|
277
|
+
onPut: async () => {
|
|
278
|
+
this.scheduleStreamManagerRestart('widerChannel changed');
|
|
279
|
+
},
|
|
268
280
|
},
|
|
269
281
|
teleChannel: {
|
|
270
282
|
title: 'Tele Channel',
|
|
271
283
|
description: 'Channel number for tele lens (typically 1)',
|
|
272
284
|
type: 'number',
|
|
273
285
|
defaultValue: 1,
|
|
274
|
-
hide: true,
|
|
286
|
+
hide: true,
|
|
287
|
+
onPut: async () => {
|
|
288
|
+
this.scheduleStreamManagerRestart('teleChannel changed');
|
|
289
|
+
},
|
|
275
290
|
},
|
|
276
291
|
// Battery camera specific
|
|
277
292
|
uid: {
|
|
@@ -533,6 +548,49 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
533
548
|
type: "string",
|
|
534
549
|
defaultValue: path.join(process.env.SCRYPTED_PLUGIN_VOLUME, 'diagnostics', this.name),
|
|
535
550
|
},
|
|
551
|
+
enableVideoclips: {
|
|
552
|
+
title: "Enable Video Clips",
|
|
553
|
+
subgroup: 'Videoclips',
|
|
554
|
+
description: "Enable video clips functionality. If disabled, getVideoClips will return empty and all other videoclip settings are ignored.",
|
|
555
|
+
type: "boolean",
|
|
556
|
+
defaultValue: false,
|
|
557
|
+
immediate: true,
|
|
558
|
+
onPut: async () => {
|
|
559
|
+
this.updateVideoClipsAutoLoad();
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
loadVideoclips: {
|
|
563
|
+
title: "Auto-load Video Clips",
|
|
564
|
+
subgroup: 'Videoclips',
|
|
565
|
+
description: "Automatically fetch today's video clips and download missing thumbnails at regular intervals.",
|
|
566
|
+
type: "boolean",
|
|
567
|
+
defaultValue: false,
|
|
568
|
+
immediate: true,
|
|
569
|
+
onPut: async () => {
|
|
570
|
+
this.updateVideoClipsAutoLoad();
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
videoclipsRegularChecks: {
|
|
574
|
+
title: "Video Clips Check Interval (minutes)",
|
|
575
|
+
subgroup: 'Videoclips',
|
|
576
|
+
description: "How often to check for new video clips and download thumbnails (default: 30 minutes).",
|
|
577
|
+
type: "number",
|
|
578
|
+
defaultValue: 30,
|
|
579
|
+
onPut: async () => {
|
|
580
|
+
this.updateVideoClipsAutoLoad();
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
downloadVideoclipsLocally: {
|
|
584
|
+
title: "Download Video Clips Locally",
|
|
585
|
+
subgroup: 'Videoclips',
|
|
586
|
+
description: "Automatically download and cache video clips to local filesystem during auto-load.",
|
|
587
|
+
type: "boolean",
|
|
588
|
+
defaultValue: false,
|
|
589
|
+
immediate: true,
|
|
590
|
+
onPut: async () => {
|
|
591
|
+
this.updateVideoClipsAutoLoad();
|
|
592
|
+
},
|
|
593
|
+
},
|
|
536
594
|
diagnosticsRun: {
|
|
537
595
|
subgroup: 'Diagnostics',
|
|
538
596
|
title: 'Run Diagnostics',
|
|
@@ -574,7 +632,6 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
574
632
|
// Abstract init method that subclasses must implement
|
|
575
633
|
abstract init(): Promise<void>;
|
|
576
634
|
|
|
577
|
-
protected withBaichuanClient?<T>(fn: (api: ReolinkBaichuanApi) => Promise<T>): Promise<T>;
|
|
578
635
|
motionTimeout?: NodeJS.Timeout;
|
|
579
636
|
doorbellBinaryTimeout?: NodeJS.Timeout;
|
|
580
637
|
initComplete?: boolean;
|
|
@@ -582,7 +639,12 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
582
639
|
|
|
583
640
|
protected nvrDevice?: ReolinkNativeNvrDevice;
|
|
584
641
|
protected multiFocalDevice?: ReolinkNativeMultiFocalDevice;
|
|
585
|
-
thisDevice: Settings
|
|
642
|
+
thisDevice: Settings;
|
|
643
|
+
isBattery: boolean;
|
|
644
|
+
isMultiFocal: boolean;
|
|
645
|
+
private streamManagerRestartTimeout: NodeJS.Timeout | undefined;
|
|
646
|
+
private videoClipsAutoLoadInterval: NodeJS.Timeout | undefined;
|
|
647
|
+
private videoClipsAutoLoadInProgress: boolean = false;
|
|
586
648
|
|
|
587
649
|
constructor(
|
|
588
650
|
nativeId: string,
|
|
@@ -590,20 +652,468 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
590
652
|
public options: CommonCameraMixinOptions
|
|
591
653
|
) {
|
|
592
654
|
super(nativeId);
|
|
655
|
+
this.plugin.mixinsMap.set(this.id, this);
|
|
593
656
|
|
|
594
657
|
// Store NVR device reference if provided
|
|
595
658
|
this.nvrDevice = options.nvrDevice;
|
|
596
659
|
this.multiFocalDevice = options.multiFocalDevice;
|
|
597
660
|
this.thisDevice = sdk.systemManager.getDeviceById<Settings>(this.id);
|
|
598
661
|
|
|
599
|
-
|
|
600
|
-
this.
|
|
662
|
+
this.isBattery = options.type === 'battery' || options.type === 'multi-focal-battery';
|
|
663
|
+
this.isMultiFocal = options.type === 'multi-focal' || options.type === 'multi-focal-battery';
|
|
664
|
+
this.protocol = this.isBattery ? 'udp' : 'tcp';
|
|
601
665
|
|
|
602
666
|
setTimeout(async () => {
|
|
603
667
|
await this.parentInit();
|
|
604
668
|
}, 2000);
|
|
605
669
|
}
|
|
606
670
|
|
|
671
|
+
protected async withBaichuanClient<T>(fn: (api: ReolinkBaichuanApi) => Promise<T>): Promise<T> {
|
|
672
|
+
const client = await this.ensureClient();
|
|
673
|
+
return fn(client);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
async getVideoClips(options?: VideoClipOptions): Promise<VideoClip[]> {
|
|
677
|
+
// Check if videoclips are enabled
|
|
678
|
+
if (!this.storageSettings.values.enableVideoclips) {
|
|
679
|
+
return [];
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (this.isBattery && this.sleeping) {
|
|
683
|
+
const logger = this.getBaichuanLogger();
|
|
684
|
+
logger.debug('getVideoClips: disabled for battery devices');
|
|
685
|
+
return [];
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const logger = this.getBaichuanLogger();
|
|
689
|
+
|
|
690
|
+
// Determine time window
|
|
691
|
+
const nowMs = Date.now();
|
|
692
|
+
const defaultWindowMs = 60 * 60 * 1000; // last 60 minutes
|
|
693
|
+
|
|
694
|
+
const startMs = options?.startTime ?? (nowMs - defaultWindowMs);
|
|
695
|
+
let endMs = options?.endTime ?? nowMs;
|
|
696
|
+
const count = options?.count;
|
|
697
|
+
|
|
698
|
+
if (endMs > nowMs) {
|
|
699
|
+
endMs = nowMs;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (endMs <= startMs) {
|
|
703
|
+
logger.warn('getVideoClips: invalid time window, endTime <= startTime', {
|
|
704
|
+
startTime: startMs,
|
|
705
|
+
endTime: endMs,
|
|
706
|
+
});
|
|
707
|
+
return [];
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const start = new Date(startMs);
|
|
711
|
+
const end = new Date(endMs);
|
|
712
|
+
start.setHours(0, 0, 0, 0);
|
|
713
|
+
|
|
714
|
+
try {
|
|
715
|
+
const api = await this.ensureClient();
|
|
716
|
+
const recordings = await api.listEnrichedRecordingsByTime({
|
|
717
|
+
start,
|
|
718
|
+
end,
|
|
719
|
+
count,
|
|
720
|
+
streamType: 'mainStream',
|
|
721
|
+
httpFallback: false,
|
|
722
|
+
fetchRtmpUrls: true
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
const clips: VideoClip[] = [];
|
|
726
|
+
|
|
727
|
+
for (const rec of recordings) {
|
|
728
|
+
const clip = await recordingFileToVideoClip(rec, {
|
|
729
|
+
fallbackStart: start,
|
|
730
|
+
api,
|
|
731
|
+
logger,
|
|
732
|
+
plugin: this,
|
|
733
|
+
deviceId: this.id,
|
|
734
|
+
useWebhook: true,
|
|
735
|
+
});
|
|
736
|
+
clips.push(clip);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
logger.debug(`Videoclips found: ${JSON.stringify(clips)}`);
|
|
740
|
+
|
|
741
|
+
return clips;
|
|
742
|
+
} catch (e: any) {
|
|
743
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
744
|
+
|
|
745
|
+
if (message?.includes('UID is required to access recordings')) {
|
|
746
|
+
logger.log('getVideoClips: recordings not available or UID not resolvable for this device', {
|
|
747
|
+
error: message,
|
|
748
|
+
});
|
|
749
|
+
} else {
|
|
750
|
+
logger.warn('getVideoClips: failed to list recordings', {
|
|
751
|
+
error: message,
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
return [];
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Get the cache directory for video clips and thumbnails
|
|
760
|
+
*/
|
|
761
|
+
private getVideoClipCacheDir(): string {
|
|
762
|
+
const pluginVolume = process.env.SCRYPTED_PLUGIN_VOLUME || '';
|
|
763
|
+
const cameraId = this.id;
|
|
764
|
+
return path.join(pluginVolume, 'videoclips', cameraId);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Get cache file path for a video clip
|
|
769
|
+
*/
|
|
770
|
+
getVideoClipCachePath(videoId: string): string {
|
|
771
|
+
// Create a safe filename from videoId using hash
|
|
772
|
+
const hash = crypto.createHash('md5').update(videoId).digest('hex');
|
|
773
|
+
// Keep original extension if present, otherwise use .mp4
|
|
774
|
+
const ext = videoId.includes('.') ? path.extname(videoId) : '.mp4';
|
|
775
|
+
const cacheDir = this.getVideoClipCacheDir();
|
|
776
|
+
return path.join(cacheDir, `${hash}${ext}`);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
async getVideoClip(videoId: string): Promise<MediaObject> {
|
|
780
|
+
const logger = this.getBaichuanLogger();
|
|
781
|
+
try {
|
|
782
|
+
const cacheEnabled = this.storageSettings.values.downloadVideoclipsLocally
|
|
783
|
+
|
|
784
|
+
// Always check cache first, even if caching is disabled (in case user enabled it before)
|
|
785
|
+
const cachePath = this.getVideoClipCachePath(videoId);
|
|
786
|
+
const cacheDir = this.getVideoClipCacheDir();
|
|
787
|
+
|
|
788
|
+
// Check if cached file exists
|
|
789
|
+
try {
|
|
790
|
+
await fs.promises.access(cachePath, fs.constants.F_OK);
|
|
791
|
+
const stats = await fs.promises.stat(cachePath);
|
|
792
|
+
logger.debug(`[VideoClip] Using cached file: fileId=${videoId}, size=${stats.size} bytes`);
|
|
793
|
+
// Return cached file as MediaObject
|
|
794
|
+
const mo = await sdk.mediaManager.createMediaObjectFromUrl(`file://${cachePath}`);
|
|
795
|
+
return mo;
|
|
796
|
+
} catch (e) {
|
|
797
|
+
// File doesn't exist or error accessing it
|
|
798
|
+
logger.debug(`[VideoClip] Cache miss: fileId=${videoId}, error=${e instanceof Error ? e.message : String(e)}`);
|
|
799
|
+
if (cacheEnabled) {
|
|
800
|
+
logger.debug(`[VideoClip] Will download and cache: fileId=${videoId}`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// If caching is enabled, ensure cache directory exists
|
|
805
|
+
if (cacheEnabled) {
|
|
806
|
+
await fs.promises.mkdir(cacheDir, { recursive: true });
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const api = await this.ensureClient();
|
|
810
|
+
|
|
811
|
+
// videoId is the fileId (fileName or id from the recording)
|
|
812
|
+
const { rtmpVodUrl } = await api.getRecordingPlaybackUrls({
|
|
813
|
+
fileName: videoId,
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
if (!rtmpVodUrl) {
|
|
817
|
+
throw new Error(`No playback URL found for video ${videoId}`);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// If caching is enabled, download and cache the video
|
|
821
|
+
if (cacheEnabled) {
|
|
822
|
+
const cachePath = this.getVideoClipCachePath(videoId);
|
|
823
|
+
|
|
824
|
+
// Download and convert RTMP to MP4 using ffmpeg
|
|
825
|
+
const ffmpegPath = await sdk.mediaManager.getFFmpegPath();
|
|
826
|
+
const ffmpegArgs = [
|
|
827
|
+
'-i', rtmpVodUrl,
|
|
828
|
+
'-c', 'copy', // Copy codecs without re-encoding
|
|
829
|
+
'-f', 'mp4',
|
|
830
|
+
'-movflags', 'frag_keyframe+empty_moov', // Enable streaming
|
|
831
|
+
cachePath,
|
|
832
|
+
];
|
|
833
|
+
|
|
834
|
+
logger.log(`Downloading video clip to cache: ${cachePath}`);
|
|
835
|
+
|
|
836
|
+
await new Promise<void>((resolve, reject) => {
|
|
837
|
+
const ffmpeg = spawn(ffmpegPath, ffmpegArgs, {
|
|
838
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
let errorOutput = '';
|
|
842
|
+
|
|
843
|
+
ffmpeg.stderr.on('data', (chunk: Buffer) => {
|
|
844
|
+
errorOutput += chunk.toString();
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
ffmpeg.on('close', (code) => {
|
|
848
|
+
if (code !== 0) {
|
|
849
|
+
logger.error(`ffmpeg failed to download video clip: ${errorOutput}`);
|
|
850
|
+
reject(new Error(`ffmpeg failed with code ${code}: ${errorOutput}`));
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
logger.log(`Video clip cached successfully: ${cachePath}`);
|
|
855
|
+
resolve();
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
ffmpeg.on('error', (error) => {
|
|
859
|
+
logger.error(`ffmpeg spawn error for video clip ${videoId}`, error);
|
|
860
|
+
reject(error);
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
// Timeout after 5 minutes
|
|
864
|
+
const timeout = setTimeout(() => {
|
|
865
|
+
ffmpeg.kill('SIGKILL');
|
|
866
|
+
reject(new Error('Video clip download timeout'));
|
|
867
|
+
}, 5 * 60 * 1000);
|
|
868
|
+
|
|
869
|
+
ffmpeg.on('close', () => {
|
|
870
|
+
clearTimeout(timeout);
|
|
871
|
+
});
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
// Return cached file as MediaObject
|
|
875
|
+
const mo = await sdk.mediaManager.createMediaObjectFromUrl(`file://${cachePath}`);
|
|
876
|
+
return mo;
|
|
877
|
+
} else {
|
|
878
|
+
// Caching disabled, return RTMP URL directly
|
|
879
|
+
const mo = await sdk.mediaManager.createMediaObjectFromUrl(rtmpVodUrl);
|
|
880
|
+
return mo;
|
|
881
|
+
}
|
|
882
|
+
} catch (e) {
|
|
883
|
+
logger.error(`getVideoClip: failed to get video clip ${videoId}`, e);
|
|
884
|
+
throw e;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Get the cache directory for thumbnails (same as video clips)
|
|
890
|
+
*/
|
|
891
|
+
private getThumbnailCacheDir(): string {
|
|
892
|
+
// Use the same directory as video clips
|
|
893
|
+
return this.getVideoClipCacheDir();
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Get cache file path for a thumbnail
|
|
898
|
+
*/
|
|
899
|
+
private getThumbnailCachePath(fileId: string): string {
|
|
900
|
+
// Use the same hash and base name as video clips, but with .jpg extension
|
|
901
|
+
const hash = crypto.createHash('md5').update(fileId).digest('hex');
|
|
902
|
+
const cacheDir = this.getThumbnailCacheDir();
|
|
903
|
+
return path.join(cacheDir, `${hash}.jpg`);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
async getVideoClipThumbnail(thumbnailId: string, options?: VideoClipThumbnailOptions): Promise<MediaObject> {
|
|
907
|
+
const logger = this.getBaichuanLogger();
|
|
908
|
+
|
|
909
|
+
try {
|
|
910
|
+
// Check cache first
|
|
911
|
+
const cachePath = this.getThumbnailCachePath(thumbnailId);
|
|
912
|
+
const cacheDir = this.getThumbnailCacheDir();
|
|
913
|
+
|
|
914
|
+
try {
|
|
915
|
+
await fs.promises.access(cachePath, fs.constants.F_OK);
|
|
916
|
+
const stats = await fs.promises.stat(cachePath);
|
|
917
|
+
logger.debug(`[Thumbnail] Using cached: fileId=${thumbnailId}, size=${stats.size} bytes`);
|
|
918
|
+
// Return cached thumbnail as MediaObject
|
|
919
|
+
const mo = await sdk.mediaManager.createMediaObjectFromUrl(`file://${cachePath}`);
|
|
920
|
+
return mo;
|
|
921
|
+
} catch {
|
|
922
|
+
// File doesn't exist, need to generate it
|
|
923
|
+
logger.debug(`[Thumbnail] Cache miss: fileId=${thumbnailId}`);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Ensure cache directory exists
|
|
927
|
+
await fs.promises.mkdir(cacheDir, { recursive: true });
|
|
928
|
+
|
|
929
|
+
// Check if video clip is already cached locally - use it instead of calling camera
|
|
930
|
+
const videoCachePath = this.getVideoClipCachePath(thumbnailId);
|
|
931
|
+
let useLocalVideo = false;
|
|
932
|
+
try {
|
|
933
|
+
await fs.promises.access(videoCachePath, fs.constants.F_OK);
|
|
934
|
+
useLocalVideo = true;
|
|
935
|
+
logger.debug(`[Thumbnail] Using local video file for thumbnail extraction: fileId=${thumbnailId}`);
|
|
936
|
+
} catch {
|
|
937
|
+
// Video not cached locally, will use RTMP URL
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
let thumbnail: MediaObject;
|
|
941
|
+
|
|
942
|
+
if (useLocalVideo) {
|
|
943
|
+
// Extract thumbnail from local video file
|
|
944
|
+
thumbnail = await this.plugin.generateThumbnail({
|
|
945
|
+
deviceId: this.id,
|
|
946
|
+
fileId: thumbnailId,
|
|
947
|
+
filePath: videoCachePath,
|
|
948
|
+
logger: this.getBaichuanLogger(),
|
|
949
|
+
});
|
|
950
|
+
} else {
|
|
951
|
+
// Ensure client is connected and logged in (reuses existing connection if available)
|
|
952
|
+
// This ensures no new sessions are created during thumbnail operations
|
|
953
|
+
const api = await this.ensureClient();
|
|
954
|
+
|
|
955
|
+
if (!api.client.isSocketConnected() || !api.client.loggedIn) {
|
|
956
|
+
logger.warn(`[Thumbnail] Client not ready, waiting for connection: fileId=${thumbnailId}`);
|
|
957
|
+
// ensureClient should have already handled connection, but wait a bit if needed
|
|
958
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Get RTMP URL from fileId
|
|
962
|
+
// Note: getRecordingPlaybackUrls internally calls login(), but it should be idempotent
|
|
963
|
+
// if ensureClient() already established the connection
|
|
964
|
+
const { rtmpVodUrl } = await api.getRecordingPlaybackUrls({
|
|
965
|
+
fileName: thumbnailId,
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
if (!rtmpVodUrl) {
|
|
969
|
+
throw new Error(`No playback URL found for video ${thumbnailId}`);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Use the plugin's thumbnail generation queue with RTMP URL
|
|
973
|
+
thumbnail = await this.plugin.generateThumbnail({
|
|
974
|
+
deviceId: this.id,
|
|
975
|
+
fileId: thumbnailId,
|
|
976
|
+
rtmpUrl: rtmpVodUrl,
|
|
977
|
+
logger: this.getBaichuanLogger(),
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Cache the thumbnail
|
|
982
|
+
try {
|
|
983
|
+
const buffer = await sdk.mediaManager.convertMediaObjectToBuffer(thumbnail, 'image/jpeg');
|
|
984
|
+
await fs.promises.writeFile(cachePath, buffer);
|
|
985
|
+
logger.debug(`[Thumbnail] Cached: fileId=${thumbnailId}, size=${buffer.length} bytes`);
|
|
986
|
+
} catch (e) {
|
|
987
|
+
logger.warn(`[Thumbnail] Failed to cache: fileId=${thumbnailId}`, e);
|
|
988
|
+
// Continue even if caching fails
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
return thumbnail;
|
|
992
|
+
} catch (e) {
|
|
993
|
+
logger.error(`[Thumbnail] Error: fileId=${thumbnailId}`, e);
|
|
994
|
+
throw e;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
removeVideoClips(...videoClipIds: string[]): Promise<void> {
|
|
999
|
+
throw new Error("removeVideoClips is not implemented yet.");
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* Update video clips auto-load timer based on settings
|
|
1004
|
+
*/
|
|
1005
|
+
private updateVideoClipsAutoLoad(): void {
|
|
1006
|
+
// Clear existing interval if any
|
|
1007
|
+
if (this.videoClipsAutoLoadInterval) {
|
|
1008
|
+
clearInterval(this.videoClipsAutoLoadInterval);
|
|
1009
|
+
this.videoClipsAutoLoadInterval = undefined;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// Check if videoclips are enabled at all
|
|
1013
|
+
const { enableVideoclips, loadVideoclips, videoclipsRegularChecks } = this.storageSettings.values;
|
|
1014
|
+
if (!enableVideoclips) {
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
if (!loadVideoclips) {
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const logger = this.getBaichuanLogger();
|
|
1024
|
+
const intervalMs = videoclipsRegularChecks * 60 * 1000;
|
|
1025
|
+
|
|
1026
|
+
logger.log(`Starting video clips auto-load: checking every ${videoclipsRegularChecks} minutes`);
|
|
1027
|
+
|
|
1028
|
+
// Run immediately on start
|
|
1029
|
+
this.loadTodayVideoClipsAndThumbnails();
|
|
1030
|
+
|
|
1031
|
+
// Then run at regular intervals
|
|
1032
|
+
this.videoClipsAutoLoadInterval = setInterval(() => {
|
|
1033
|
+
this.loadTodayVideoClipsAndThumbnails();
|
|
1034
|
+
}, intervalMs);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/**
|
|
1038
|
+
* Load today's video clips and download missing thumbnails
|
|
1039
|
+
*/
|
|
1040
|
+
private async loadTodayVideoClipsAndThumbnails(): Promise<void> {
|
|
1041
|
+
// Prevent concurrent executions
|
|
1042
|
+
if (this.videoClipsAutoLoadInProgress) {
|
|
1043
|
+
const logger = this.getBaichuanLogger();
|
|
1044
|
+
logger.debug('Video clips auto-load already in progress, skipping...');
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
const logger = this.getBaichuanLogger();
|
|
1049
|
+
|
|
1050
|
+
this.videoClipsAutoLoadInProgress = true;
|
|
1051
|
+
|
|
1052
|
+
try {
|
|
1053
|
+
logger.log('Auto-loading today\'s video clips and thumbnails...');
|
|
1054
|
+
|
|
1055
|
+
// Get today's date range (start of today to now)
|
|
1056
|
+
const now = new Date();
|
|
1057
|
+
const startOfToday = new Date(now);
|
|
1058
|
+
startOfToday.setHours(0, 0, 0, 0);
|
|
1059
|
+
startOfToday.setMinutes(0, 0, 0);
|
|
1060
|
+
|
|
1061
|
+
// Fetch today's video clips
|
|
1062
|
+
const clips = await this.getVideoClips({
|
|
1063
|
+
startTime: startOfToday.getTime(),
|
|
1064
|
+
endTime: now.getTime(),
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
logger.log(`Found ${clips.length} video clips for today`);
|
|
1068
|
+
|
|
1069
|
+
const downloadVideoclipsLocally = this.storageSettings.values.downloadVideoclipsLocally ?? false;
|
|
1070
|
+
|
|
1071
|
+
// Track processed clips to avoid duplicate calls to the camera
|
|
1072
|
+
const processedClips = new Set<string>();
|
|
1073
|
+
|
|
1074
|
+
// Download videos first (if enabled), then thumbnails for each clip
|
|
1075
|
+
for (const clip of clips) {
|
|
1076
|
+
// Skip if already processed (avoid duplicate calls)
|
|
1077
|
+
if (processedClips.has(clip.id)) {
|
|
1078
|
+
logger.debug(`Skipping already processed clip: ${clip.id}`);
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1081
|
+
processedClips.add(clip.id);
|
|
1082
|
+
|
|
1083
|
+
try {
|
|
1084
|
+
// If downloadVideoclipsLocally is enabled, download the video clip first
|
|
1085
|
+
// This allows the thumbnail to use the local file instead of calling the camera
|
|
1086
|
+
if (downloadVideoclipsLocally) {
|
|
1087
|
+
try {
|
|
1088
|
+
// Call getVideoClip to trigger download and caching
|
|
1089
|
+
await this.getVideoClip(clip.id);
|
|
1090
|
+
logger.debug(`Downloaded video clip: ${clip.id}`);
|
|
1091
|
+
} catch (e) {
|
|
1092
|
+
logger.warn(`Failed to download video clip ${clip.id}:`, e instanceof Error ? e.message : String(e));
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// Then get the thumbnail - this will use the local video file if available
|
|
1097
|
+
// or call the camera if the video wasn't downloaded
|
|
1098
|
+
try {
|
|
1099
|
+
await this.getVideoClipThumbnail(clip.id);
|
|
1100
|
+
logger.debug(`Downloaded thumbnail for clip: ${clip.id}`);
|
|
1101
|
+
} catch (e) {
|
|
1102
|
+
logger.warn(`Failed to load thumbnail for clip ${clip.id}:`, e instanceof Error ? e.message : String(e));
|
|
1103
|
+
}
|
|
1104
|
+
} catch (e) {
|
|
1105
|
+
logger.warn(`Error processing clip ${clip.id}:`, e instanceof Error ? e.message : String(e));
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
logger.log(`Completed auto-loading video clips and thumbnails`);
|
|
1110
|
+
} catch (e) {
|
|
1111
|
+
logger.error('Error during auto-loading video clips:', e);
|
|
1112
|
+
} finally {
|
|
1113
|
+
this.videoClipsAutoLoadInProgress = false;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
607
1117
|
async reboot(): Promise<void> {
|
|
608
1118
|
const api = await this.ensureBaichuanClient();
|
|
609
1119
|
await api.reboot();
|
|
@@ -613,19 +1123,20 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
613
1123
|
protected getConnectionConfig(): BaichuanConnectionConfig {
|
|
614
1124
|
const { ipAddress, username, password, uid } = this.storageSettings.values;
|
|
615
1125
|
const debugOptions = this.getBaichuanDebugOptions();
|
|
616
|
-
const normalizedUid = this.
|
|
1126
|
+
const normalizedUid = this.isBattery ? normalizeUid(uid) : undefined;
|
|
617
1127
|
|
|
618
|
-
if (this.
|
|
1128
|
+
if (this.isBattery && !normalizedUid) {
|
|
619
1129
|
throw new Error('UID is required for battery cameras (BCUDP)');
|
|
620
1130
|
}
|
|
621
1131
|
|
|
1132
|
+
const logger = this.getBaichuanLogger();
|
|
622
1133
|
return {
|
|
623
1134
|
host: ipAddress,
|
|
624
1135
|
username,
|
|
625
1136
|
password,
|
|
626
1137
|
uid: normalizedUid,
|
|
627
1138
|
transport: this.protocol,
|
|
628
|
-
logger
|
|
1139
|
+
logger,
|
|
629
1140
|
debugOptions,
|
|
630
1141
|
};
|
|
631
1142
|
}
|
|
@@ -639,8 +1150,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
639
1150
|
// For battery cameras, don't auto-resubscribe after idle disconnects
|
|
640
1151
|
// (idle disconnects are normal for battery cameras to save power)
|
|
641
1152
|
// Events will be resubscribed when ensureClient() is called for actual operations
|
|
642
|
-
|
|
643
|
-
if (!isBattery) {
|
|
1153
|
+
if (!this.isBattery) {
|
|
644
1154
|
// For non-battery cameras, resubscribe to events after reconnection
|
|
645
1155
|
setTimeout(async () => {
|
|
646
1156
|
try {
|
|
@@ -666,7 +1176,9 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
666
1176
|
}
|
|
667
1177
|
|
|
668
1178
|
async withBaichuanRetry<T>(fn: () => Promise<T>): Promise<T> {
|
|
669
|
-
|
|
1179
|
+
return await fn();
|
|
1180
|
+
|
|
1181
|
+
if (this.isBattery) {
|
|
670
1182
|
return await fn();
|
|
671
1183
|
} else {
|
|
672
1184
|
try {
|
|
@@ -737,8 +1249,44 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
737
1249
|
}
|
|
738
1250
|
}
|
|
739
1251
|
|
|
740
|
-
|
|
741
|
-
|
|
1252
|
+
/**
|
|
1253
|
+
* Create a dedicated Baichuan API session for streaming (used by StreamManager).
|
|
1254
|
+
*
|
|
1255
|
+
* - For TCP devices (regular + multifocal), this creates a new TCP session with its own client.
|
|
1256
|
+
* - For UDP/battery devices, this reuses the existing client via ensureClient().
|
|
1257
|
+
*/
|
|
1258
|
+
async createStreamClient(profile?: StreamProfile): Promise<ReolinkBaichuanApi> {
|
|
1259
|
+
// Battery / BCUDP path: reuse the main client to avoid extra wake-ups and sockets.
|
|
1260
|
+
if (this.isBattery) {
|
|
1261
|
+
return await this.ensureClient();
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// For TCP path: create a new client ONLY for "ext" profile
|
|
1265
|
+
// For other profiles (main, sub), reuse the main client
|
|
1266
|
+
if (profile !== 'ext') {
|
|
1267
|
+
return await this.ensureClient();
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// TCP path with ext profile: create a separate session for streaming (RFC4571/composite/NVR-friendly).
|
|
1271
|
+
const { ipAddress, username, password } = this.storageSettings.values;
|
|
1272
|
+
const logger = this.getBaichuanLogger();
|
|
1273
|
+
|
|
1274
|
+
const debugOptions = this.getBaichuanDebugOptions();
|
|
1275
|
+
const api = await createBaichuanApi(
|
|
1276
|
+
{
|
|
1277
|
+
inputs: {
|
|
1278
|
+
host: ipAddress,
|
|
1279
|
+
username,
|
|
1280
|
+
password,
|
|
1281
|
+
logger,
|
|
1282
|
+
debugOptions,
|
|
1283
|
+
},
|
|
1284
|
+
transport: 'tcp',
|
|
1285
|
+
},
|
|
1286
|
+
);
|
|
1287
|
+
|
|
1288
|
+
await api.login();
|
|
1289
|
+
return api;
|
|
742
1290
|
}
|
|
743
1291
|
|
|
744
1292
|
public getAbilities(): DeviceCapabilities {
|
|
@@ -750,8 +1298,85 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
750
1298
|
}
|
|
751
1299
|
|
|
752
1300
|
getBaichuanDebugOptions(): any | undefined {
|
|
753
|
-
const
|
|
754
|
-
return convertDebugLogsToApiOptions(
|
|
1301
|
+
const socketDebugLogs = this.storageSettings.values.socketApiDebugLogs || [];
|
|
1302
|
+
return convertDebugLogsToApiOptions(socketDebugLogs);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/**
|
|
1306
|
+
* Initialize or recreate the StreamManager, taking into account multifocal composite options.
|
|
1307
|
+
*/
|
|
1308
|
+
protected initStreamManager(logger: Console, forceRecreate: boolean = false): void {
|
|
1309
|
+
const { username, password } = this.storageSettings.values;
|
|
1310
|
+
|
|
1311
|
+
const baseOptions: any = {
|
|
1312
|
+
createStreamClient: (profile?: StreamProfile) => this.createStreamClient(profile),
|
|
1313
|
+
getLogger: () => logger,
|
|
1314
|
+
credentials: {
|
|
1315
|
+
username,
|
|
1316
|
+
password,
|
|
1317
|
+
},
|
|
1318
|
+
sharedConnection: this.isBattery,
|
|
1319
|
+
};
|
|
1320
|
+
|
|
1321
|
+
if (this.isMultiFocal) {
|
|
1322
|
+
const values: any = this.storageSettings.values;
|
|
1323
|
+
const pipPosition = values.pipPosition || 'bottom-right';
|
|
1324
|
+
const pipSize = values.pipSize ?? 0.25;
|
|
1325
|
+
const pipMargin = values.pipMargin ?? 10;
|
|
1326
|
+
const widerChannel = values.widerChannel ?? 0;
|
|
1327
|
+
const teleChannel = values.teleChannel ?? 1;
|
|
1328
|
+
|
|
1329
|
+
baseOptions.compositeOptions = {
|
|
1330
|
+
widerChannel,
|
|
1331
|
+
teleChannel,
|
|
1332
|
+
pipPosition,
|
|
1333
|
+
pipSize,
|
|
1334
|
+
pipMargin,
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
if (!this.streamManager || forceRecreate) {
|
|
1339
|
+
this.streamManager = new StreamManager(baseOptions);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/**
|
|
1344
|
+
* Debounced restart of StreamManager when PIP/composite settings change.
|
|
1345
|
+
* Also notifies listeners so that active streams (prebuffer, etc.) restart cleanly.
|
|
1346
|
+
*/
|
|
1347
|
+
protected scheduleStreamManagerRestart(reason: string): void {
|
|
1348
|
+
const logger = this.getBaichuanLogger();
|
|
1349
|
+
logger.log(`Scheduling StreamManager restart (${reason})`);
|
|
1350
|
+
|
|
1351
|
+
if (this.streamManagerRestartTimeout) {
|
|
1352
|
+
clearTimeout(this.streamManagerRestartTimeout);
|
|
1353
|
+
this.streamManagerRestartTimeout = undefined;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
this.streamManagerRestartTimeout = setTimeout(async () => {
|
|
1357
|
+
this.streamManagerRestartTimeout = undefined;
|
|
1358
|
+
const restartLogger = this.getBaichuanLogger();
|
|
1359
|
+
try {
|
|
1360
|
+
restartLogger.log('Restarting StreamManager due to PIP/composite settings change');
|
|
1361
|
+
this.initStreamManager(restartLogger, true);
|
|
1362
|
+
|
|
1363
|
+
// Invalidate snapshot cache for battery/multifocal-battery so that
|
|
1364
|
+
// the next snapshot reflects the new PIP/composite configuration.
|
|
1365
|
+
if (this.isBattery) {
|
|
1366
|
+
this.forceNewSnapshot = true;
|
|
1367
|
+
this.lastPicture = undefined;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// Notify consumers (e.g. prebuffer) that stream configuration changed.
|
|
1371
|
+
try {
|
|
1372
|
+
this.onDeviceEvent(ScryptedInterface.VideoCamera, undefined);
|
|
1373
|
+
} catch {
|
|
1374
|
+
// best-effort
|
|
1375
|
+
}
|
|
1376
|
+
} catch (e) {
|
|
1377
|
+
restartLogger.warn('Failed to restart StreamManager after settings change', e);
|
|
1378
|
+
}
|
|
1379
|
+
}, 500);
|
|
755
1380
|
}
|
|
756
1381
|
|
|
757
1382
|
isRecoverableBaichuanError(e: any): boolean {
|
|
@@ -1167,7 +1792,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1167
1792
|
}
|
|
1168
1793
|
|
|
1169
1794
|
async takePicture(options?: RequestPictureOptions) {
|
|
1170
|
-
if (this.
|
|
1795
|
+
if (!this.isBattery) {
|
|
1171
1796
|
try {
|
|
1172
1797
|
return this.withBaichuanRetry(async () => {
|
|
1173
1798
|
const client = await this.ensureClient();
|
|
@@ -1276,6 +1901,10 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1276
1901
|
}
|
|
1277
1902
|
}
|
|
1278
1903
|
|
|
1904
|
+
async release() {
|
|
1905
|
+
this.plugin.mixinsMap.delete(this.id);
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1279
1908
|
async releaseDevice(id: string, nativeId: string): Promise<void> {
|
|
1280
1909
|
if (nativeId.endsWith(sirenSuffix)) {
|
|
1281
1910
|
this.siren = undefined;
|
|
@@ -1665,23 +2294,21 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1665
2294
|
}
|
|
1666
2295
|
}
|
|
1667
2296
|
|
|
1668
|
-
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
2297
|
|
|
1672
|
-
this.storageSettings.settings.
|
|
1673
|
-
|
|
1674
|
-
this.storageSettings.settings.
|
|
1675
|
-
this.storageSettings.settings.
|
|
2298
|
+
this.storageSettings.settings.videoclipsRegularChecks.defaultValue = this.isBattery ? 120 : 30;
|
|
2299
|
+
|
|
2300
|
+
this.storageSettings.settings.batteryUpdateIntervalMinutes.hide = !this.isBattery;
|
|
2301
|
+
this.storageSettings.settings.lowThresholdBatteryRecording.hide = !this.isBattery;
|
|
2302
|
+
this.storageSettings.settings.highThresholdBatteryRecording.hide = !this.isBattery;
|
|
1676
2303
|
|
|
1677
2304
|
// 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;
|
|
2305
|
+
this.storageSettings.settings.pipPosition.hide = !this.isMultiFocal;
|
|
2306
|
+
this.storageSettings.settings.pipSize.hide = !this.isMultiFocal;
|
|
2307
|
+
this.storageSettings.settings.pipMargin.hide = !this.isMultiFocal;
|
|
2308
|
+
this.storageSettings.settings.widerChannel.hide = !this.isMultiFocal;
|
|
2309
|
+
this.storageSettings.settings.teleChannel.hide = !this.isMultiFocal;
|
|
1683
2310
|
|
|
1684
|
-
if (isBattery && !this.storageSettings.values.mixinsSetup) {
|
|
2311
|
+
if (this.isBattery && !this.storageSettings.values.mixinsSetup) {
|
|
1685
2312
|
try {
|
|
1686
2313
|
const device = sdk.systemManager.getDeviceById<Settings>(this.id);
|
|
1687
2314
|
if (device) {
|
|
@@ -1703,15 +2330,8 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1703
2330
|
logger.warn('Failed to subscribe to Baichuan events', e);
|
|
1704
2331
|
}
|
|
1705
2332
|
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
getLogger: () => logger,
|
|
1709
|
-
credentials: {
|
|
1710
|
-
username,
|
|
1711
|
-
password
|
|
1712
|
-
},
|
|
1713
|
-
sharedConnection: isBattery,
|
|
1714
|
-
});
|
|
2333
|
+
// Initialize StreamManager (with composite options for multifocal devices)
|
|
2334
|
+
this.initStreamManager(logger);
|
|
1715
2335
|
|
|
1716
2336
|
const { hasIntercom, hasPtz } = this.getAbilities();
|
|
1717
2337
|
|
|
@@ -1740,7 +2360,6 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1740
2360
|
this.storageSettings.settings.username.hide = true;
|
|
1741
2361
|
this.storageSettings.settings.password.hide = true;
|
|
1742
2362
|
this.storageSettings.settings.ipAddress.hide = true;
|
|
1743
|
-
this.storageSettings.settings.uid.hide = true;
|
|
1744
2363
|
|
|
1745
2364
|
this.storageSettings.settings.username.defaultValue = this.nvrDevice.storageSettings.values.username;
|
|
1746
2365
|
this.storageSettings.settings.password.defaultValue = this.nvrDevice.storageSettings.values.password;
|
|
@@ -1750,6 +2369,9 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1750
2369
|
await this.init();
|
|
1751
2370
|
|
|
1752
2371
|
this.initComplete = true;
|
|
2372
|
+
|
|
2373
|
+
// Initialize video clips auto-load if enabled
|
|
2374
|
+
this.updateVideoClipsAutoLoad();
|
|
1753
2375
|
}
|
|
1754
2376
|
}
|
|
1755
2377
|
|