@apocaliss92/scrypted-reolink-native 0.1.42 → 0.2.0
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/build-lib.sh +31 -0
- package/dist/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/package.json +2 -1
- package/src/baichuan-base.ts +149 -30
- package/src/camera-battery.ts +5 -58
- package/src/camera.ts +5 -2
- package/src/common.ts +471 -240
- package/src/intercom.ts +3 -3
- package/src/main.ts +38 -21
- package/src/multiFocal.ts +238 -144
- package/src/nvr.ts +194 -160
- package/src/stream-utils.ts +232 -101
- package/src/utils.ts +19 -19
package/src/common.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BaichuanClientOptions, DeviceCapabilities, PtzCommand, PtzPreset, ReolinkBaichuanApi, ReolinkSimpleEvent, ReolinkSupportedStream, StreamProfile, StreamSamplingSelection } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
|
|
1
|
+
import type { BaichuanClientOptions, DeviceCapabilities, NativeVideoStreamVariant, PtzCommand, PtzPreset, ReolinkBaichuanApi, ReolinkSimpleEvent, ReolinkSupportedStream, SleepStatus, StreamProfile, StreamSamplingSelection } from "@apocaliss92/reolink-baichuan-js" with { "resolution-mode": "import" };
|
|
2
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';
|
|
@@ -19,12 +19,13 @@ import { ReolinkPtzPresets } from "./presets";
|
|
|
19
19
|
import {
|
|
20
20
|
createRfc4571CompositeMediaObjectFromStreamManager,
|
|
21
21
|
createRfc4571MediaObjectFromStreamManager,
|
|
22
|
-
|
|
22
|
+
extractVariantFromStreamId,
|
|
23
23
|
parseStreamProfileFromId,
|
|
24
24
|
selectStreamOption,
|
|
25
|
-
StreamManager
|
|
25
|
+
StreamManager,
|
|
26
|
+
StreamManagerOptions
|
|
26
27
|
} from "./stream-utils";
|
|
27
|
-
import { floodlightSuffix, getDeviceInterfaces,
|
|
28
|
+
import { floodlightSuffix, getDeviceInterfaces, pirSuffix, recordingsToVideoClips, sanitizeFfmpegOutput, sirenSuffix, updateDeviceInfo } from "./utils";
|
|
28
29
|
|
|
29
30
|
export type CameraType = 'battery' | 'regular' | 'multi-focal' | 'multi-focal-battery';
|
|
30
31
|
|
|
@@ -47,7 +48,7 @@ class ReolinkCameraSiren extends ScryptedDeviceBase implements OnOff {
|
|
|
47
48
|
this.camera.getBaichuanLogger().log(`Siren toggle: turnOff ok (device=${this.nativeId})`);
|
|
48
49
|
}
|
|
49
50
|
catch (e) {
|
|
50
|
-
this.camera.getBaichuanLogger().warn(`Siren toggle: turnOff failed (device=${this.nativeId})`, e);
|
|
51
|
+
this.camera.getBaichuanLogger().warn(`Siren toggle: turnOff failed (device=${this.nativeId})`, e?.message || String(e));
|
|
51
52
|
throw e;
|
|
52
53
|
}
|
|
53
54
|
}
|
|
@@ -60,7 +61,7 @@ class ReolinkCameraSiren extends ScryptedDeviceBase implements OnOff {
|
|
|
60
61
|
this.camera.getBaichuanLogger().log(`Siren toggle: turnOn ok (device=${this.nativeId})`);
|
|
61
62
|
}
|
|
62
63
|
catch (e) {
|
|
63
|
-
this.camera.getBaichuanLogger().warn(`Siren toggle: turnOn failed (device=${this.nativeId})`, e);
|
|
64
|
+
this.camera.getBaichuanLogger().warn(`Siren toggle: turnOn failed (device=${this.nativeId})`, e?.message || String(e));
|
|
64
65
|
throw e;
|
|
65
66
|
}
|
|
66
67
|
}
|
|
@@ -79,7 +80,7 @@ class ReolinkCameraFloodlight extends ScryptedDeviceBase implements OnOff, Brigh
|
|
|
79
80
|
this.camera.getBaichuanLogger().log(`Floodlight toggle: setBrightness ok (device=${this.nativeId} brightness=${brightness})`);
|
|
80
81
|
}
|
|
81
82
|
catch (e) {
|
|
82
|
-
this.camera.getBaichuanLogger().warn(`Floodlight toggle: setBrightness failed (device=${this.nativeId} brightness=${brightness})`, e);
|
|
83
|
+
this.camera.getBaichuanLogger().warn(`Floodlight toggle: setBrightness failed (device=${this.nativeId} brightness=${brightness})`, e?.message || String(e));
|
|
83
84
|
throw e;
|
|
84
85
|
}
|
|
85
86
|
}
|
|
@@ -92,7 +93,7 @@ class ReolinkCameraFloodlight extends ScryptedDeviceBase implements OnOff, Brigh
|
|
|
92
93
|
this.camera.getBaichuanLogger().log(`Floodlight toggle: turnOff ok (device=${this.nativeId})`);
|
|
93
94
|
}
|
|
94
95
|
catch (e) {
|
|
95
|
-
this.camera.getBaichuanLogger().warn(`Floodlight toggle: turnOff failed (device=${this.nativeId})`, e);
|
|
96
|
+
this.camera.getBaichuanLogger().warn(`Floodlight toggle: turnOff failed (device=${this.nativeId})`, e?.message || String(e));
|
|
96
97
|
throw e;
|
|
97
98
|
}
|
|
98
99
|
}
|
|
@@ -105,7 +106,7 @@ class ReolinkCameraFloodlight extends ScryptedDeviceBase implements OnOff, Brigh
|
|
|
105
106
|
this.camera.getBaichuanLogger().log(`Floodlight toggle: turnOn ok (device=${this.nativeId})`);
|
|
106
107
|
}
|
|
107
108
|
catch (e) {
|
|
108
|
-
this.camera.getBaichuanLogger().warn(`Floodlight toggle: turnOn failed (device=${this.nativeId})`, e);
|
|
109
|
+
this.camera.getBaichuanLogger().warn(`Floodlight toggle: turnOn failed (device=${this.nativeId})`, e?.message || String(e));
|
|
109
110
|
throw e;
|
|
110
111
|
}
|
|
111
112
|
}
|
|
@@ -223,6 +224,12 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
223
224
|
hide: true,
|
|
224
225
|
defaultValue: 0,
|
|
225
226
|
},
|
|
227
|
+
variantType: {
|
|
228
|
+
type: 'string',
|
|
229
|
+
hide: true,
|
|
230
|
+
defaultValue: 'default',
|
|
231
|
+
choices: ['default', 'autotrack', 'telephoto'] as NativeVideoStreamVariant[],
|
|
232
|
+
},
|
|
226
233
|
capabilities: {
|
|
227
234
|
json: true,
|
|
228
235
|
hide: true,
|
|
@@ -231,65 +238,6 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
231
238
|
json: true,
|
|
232
239
|
hide: true,
|
|
233
240
|
},
|
|
234
|
-
// Multifocal composite stream PIP settings
|
|
235
|
-
pipPosition: {
|
|
236
|
-
title: 'PIP Position',
|
|
237
|
-
description: 'Position of the tele lens overlay on the wider lens view',
|
|
238
|
-
type: 'string',
|
|
239
|
-
defaultValue: 'bottom-right',
|
|
240
|
-
choices: [
|
|
241
|
-
'top-left',
|
|
242
|
-
'top-right',
|
|
243
|
-
'bottom-left',
|
|
244
|
-
'bottom-right',
|
|
245
|
-
'center',
|
|
246
|
-
'top-center',
|
|
247
|
-
'bottom-center',
|
|
248
|
-
'left-center',
|
|
249
|
-
'right-center',
|
|
250
|
-
],
|
|
251
|
-
hide: true, // Only show for multifocal devices via getAdditionalSettings
|
|
252
|
-
},
|
|
253
|
-
pipSize: {
|
|
254
|
-
title: 'PIP Size',
|
|
255
|
-
description: 'Relative size of the PIP overlay (0.1 = 10%, 0.3 = 30%, etc.)',
|
|
256
|
-
type: 'number',
|
|
257
|
-
defaultValue: 0.25,
|
|
258
|
-
hide: true,
|
|
259
|
-
onPut: async () => {
|
|
260
|
-
this.scheduleStreamManagerRestart('pipSize changed');
|
|
261
|
-
},
|
|
262
|
-
},
|
|
263
|
-
pipMargin: {
|
|
264
|
-
title: 'PIP Margin',
|
|
265
|
-
description: 'Margin from edge in pixels',
|
|
266
|
-
type: 'number',
|
|
267
|
-
defaultValue: 10,
|
|
268
|
-
hide: true,
|
|
269
|
-
onPut: async () => {
|
|
270
|
-
this.scheduleStreamManagerRestart('pipMargin changed');
|
|
271
|
-
},
|
|
272
|
-
},
|
|
273
|
-
widerChannel: {
|
|
274
|
-
title: 'Wider Channel',
|
|
275
|
-
description: 'Channel number for wider lens (typically 0)',
|
|
276
|
-
type: 'number',
|
|
277
|
-
defaultValue: 0,
|
|
278
|
-
hide: true,
|
|
279
|
-
onPut: async () => {
|
|
280
|
-
this.scheduleStreamManagerRestart('widerChannel changed');
|
|
281
|
-
},
|
|
282
|
-
},
|
|
283
|
-
teleChannel: {
|
|
284
|
-
title: 'Tele Channel',
|
|
285
|
-
description: 'Channel number for tele lens (typically 1)',
|
|
286
|
-
type: 'number',
|
|
287
|
-
defaultValue: 1,
|
|
288
|
-
hide: true,
|
|
289
|
-
onPut: async () => {
|
|
290
|
-
this.scheduleStreamManagerRestart('teleChannel changed');
|
|
291
|
-
},
|
|
292
|
-
},
|
|
293
241
|
// Battery camera specific
|
|
294
242
|
uid: {
|
|
295
243
|
title: 'UID',
|
|
@@ -307,6 +255,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
307
255
|
choices: ['local-direct', 'local-broadcast', 'remote', 'map', 'relay'],
|
|
308
256
|
defaultValue: 'local-direct',
|
|
309
257
|
hide: true,
|
|
258
|
+
subgroup: 'Advanced',
|
|
310
259
|
onPut: async () => {
|
|
311
260
|
await this.credentialsChanged();
|
|
312
261
|
}
|
|
@@ -370,7 +319,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
370
319
|
// Trigger reconnection
|
|
371
320
|
await this.ensureClient();
|
|
372
321
|
} catch (e) {
|
|
373
|
-
logger.warn('Failed to reset client after debug logs change', e);
|
|
322
|
+
logger.warn('Failed to reset client after debug logs change', e?.message || String(e));
|
|
374
323
|
}
|
|
375
324
|
}, 2000);
|
|
376
325
|
}
|
|
@@ -631,6 +580,48 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
631
580
|
await this.runDiagnostics();
|
|
632
581
|
},
|
|
633
582
|
},
|
|
583
|
+
// Multifocal composite stream PIP settings
|
|
584
|
+
pipPosition: {
|
|
585
|
+
title: 'PIP Position',
|
|
586
|
+
description: 'Position of the tele lens overlay on the wider lens view',
|
|
587
|
+
type: 'string',
|
|
588
|
+
defaultValue: 'bottom-right',
|
|
589
|
+
group: 'Composite stream',
|
|
590
|
+
choices: [
|
|
591
|
+
'top-left',
|
|
592
|
+
'top-right',
|
|
593
|
+
'bottom-left',
|
|
594
|
+
'bottom-right',
|
|
595
|
+
'center',
|
|
596
|
+
'top-center',
|
|
597
|
+
'bottom-center',
|
|
598
|
+
'left-center',
|
|
599
|
+
'right-center',
|
|
600
|
+
],
|
|
601
|
+
hide: true, // Only show for multifocal devices via getAdditionalSettings
|
|
602
|
+
},
|
|
603
|
+
pipSize: {
|
|
604
|
+
title: 'PIP Size',
|
|
605
|
+
description: 'Relative size of the PIP overlay (0.1 = 10%, 0.3 = 30%, etc.)',
|
|
606
|
+
type: 'number',
|
|
607
|
+
defaultValue: 0.25,
|
|
608
|
+
group: 'Composite stream',
|
|
609
|
+
hide: true,
|
|
610
|
+
onPut: async () => {
|
|
611
|
+
this.scheduleStreamManagerRestart('pipSize changed');
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
pipMargin: {
|
|
615
|
+
title: 'PIP Margin',
|
|
616
|
+
description: 'Margin from edge in pixels',
|
|
617
|
+
type: 'number',
|
|
618
|
+
defaultValue: 10,
|
|
619
|
+
group: 'Composite stream',
|
|
620
|
+
hide: true,
|
|
621
|
+
onPut: async () => {
|
|
622
|
+
this.scheduleStreamManagerRestart('pipMargin changed');
|
|
623
|
+
},
|
|
624
|
+
},
|
|
634
625
|
});
|
|
635
626
|
|
|
636
627
|
ptzPresets = new ReolinkPtzPresets(this);
|
|
@@ -651,20 +642,19 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
651
642
|
|
|
652
643
|
// Video stream properties
|
|
653
644
|
protected cachedVideoStreamOptions?: UrlMediaStreamOptions[];
|
|
654
|
-
protected
|
|
645
|
+
protected fetchingStreamsPromise: Promise<UrlMediaStreamOptions[]> | undefined;
|
|
655
646
|
protected lastNetPortCacheAttempt: number = 0;
|
|
656
647
|
protected netPortCacheBackoffMs: number = 5000; // 5 seconds backoff on failure
|
|
657
648
|
|
|
658
649
|
// Client management (inherited from BaseBaichuanClass)
|
|
659
|
-
protected readonly protocol: BaichuanTransport;
|
|
660
650
|
private debugLogsResetTimeout: NodeJS.Timeout | undefined;
|
|
661
651
|
|
|
662
|
-
// Abstract init method that subclasses must implement
|
|
663
652
|
abstract init(): Promise<void>;
|
|
664
653
|
|
|
654
|
+
abstract reportDevices(): Promise<void>;
|
|
655
|
+
|
|
665
656
|
motionTimeout?: NodeJS.Timeout;
|
|
666
657
|
doorbellBinaryTimeout?: NodeJS.Timeout;
|
|
667
|
-
initComplete?: boolean;
|
|
668
658
|
resetBaichuanClient?(reason?: any): Promise<void>;
|
|
669
659
|
|
|
670
660
|
protected nvrDevice?: ReolinkNativeNvrDevice;
|
|
@@ -672,6 +662,8 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
672
662
|
thisDevice: Settings;
|
|
673
663
|
isBattery: boolean;
|
|
674
664
|
isMultiFocal: boolean;
|
|
665
|
+
isOnNvr: boolean;
|
|
666
|
+
protocol: BaichuanTransport;
|
|
675
667
|
private streamManagerRestartTimeout: NodeJS.Timeout | undefined;
|
|
676
668
|
private videoClipsAutoLoadInterval: NodeJS.Timeout | undefined;
|
|
677
669
|
private videoClipsAutoLoadInProgress: boolean = false;
|
|
@@ -682,7 +674,9 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
682
674
|
public plugin: ReolinkNativePlugin,
|
|
683
675
|
public options: CommonCameraMixinOptions
|
|
684
676
|
) {
|
|
685
|
-
|
|
677
|
+
const isBattery = options.type === 'battery' || options.type === 'multi-focal-battery';
|
|
678
|
+
const transport = isBattery || !!options.nvrDevice ? 'udp' : 'tcp';
|
|
679
|
+
super(nativeId, transport);
|
|
686
680
|
this.plugin.mixinsMap.set(this.id, this);
|
|
687
681
|
|
|
688
682
|
// Store NVR device reference if provided
|
|
@@ -690,9 +684,10 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
690
684
|
this.multiFocalDevice = options.multiFocalDevice;
|
|
691
685
|
this.thisDevice = sdk.systemManager.getDeviceById<Settings>(this.id);
|
|
692
686
|
|
|
693
|
-
this.isBattery =
|
|
687
|
+
this.isBattery = isBattery;
|
|
694
688
|
this.isMultiFocal = options.type === 'multi-focal' || options.type === 'multi-focal-battery';
|
|
695
|
-
this.
|
|
689
|
+
this.isOnNvr = !!this.nvrDevice || !!this.multiFocalDevice?.nvrDevice;
|
|
690
|
+
this.protocol = transport;
|
|
696
691
|
|
|
697
692
|
setTimeout(async () => {
|
|
698
693
|
await this.parentInit();
|
|
@@ -1003,7 +998,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1003
998
|
return mo;
|
|
1004
999
|
}
|
|
1005
1000
|
} catch (e) {
|
|
1006
|
-
logger.error(`getVideoClip: failed to get video clip ${videoId}`, e);
|
|
1001
|
+
logger.error(`getVideoClip: failed to get video clip ${videoId}`, e?.message || String(e));
|
|
1007
1002
|
throw e;
|
|
1008
1003
|
}
|
|
1009
1004
|
}
|
|
@@ -1100,13 +1095,13 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1100
1095
|
await fs.promises.writeFile(cachePath, buffer);
|
|
1101
1096
|
logger.debug(`[Thumbnail] Cached: fileId=${thumbnailId}, size=${buffer.length} bytes`);
|
|
1102
1097
|
} catch (e) {
|
|
1103
|
-
logger.warn(`[Thumbnail] Failed to cache: fileId=${thumbnailId}`, e);
|
|
1098
|
+
logger.warn(`[Thumbnail] Failed to cache: fileId=${thumbnailId}`, e?.message || String(e));
|
|
1104
1099
|
// Continue even if caching fails
|
|
1105
1100
|
}
|
|
1106
1101
|
|
|
1107
1102
|
return thumbnail;
|
|
1108
1103
|
} catch (e) {
|
|
1109
|
-
logger.error(`[Thumbnail] Error: fileId=${thumbnailId}`, e);
|
|
1104
|
+
logger.error(`[Thumbnail] Error: fileId=${thumbnailId}`, e?.message || String(e));
|
|
1110
1105
|
throw e;
|
|
1111
1106
|
}
|
|
1112
1107
|
}
|
|
@@ -1136,7 +1131,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1136
1131
|
logger.debug(`[getVideoClipRtmpUrl] NVR getVodUrl Download URL received: url="${url || 'none'}"`);
|
|
1137
1132
|
if (url) return url;
|
|
1138
1133
|
} catch (e: any) {
|
|
1139
|
-
logger.error(`[getVideoClipRtmpUrl] getVodUrl Download failed: ${e
|
|
1134
|
+
logger.error(`[getVideoClipRtmpUrl] getVodUrl Download failed: ${e?.message || String(e)}`);
|
|
1140
1135
|
}
|
|
1141
1136
|
|
|
1142
1137
|
throw new Error(`No streaming URL found from NVR for file ${fileId} after trying Playback and Download methods`);
|
|
@@ -1271,7 +1266,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1271
1266
|
|
|
1272
1267
|
logger.log(`Completed auto-loading video clips and thumbnails`);
|
|
1273
1268
|
} catch (e) {
|
|
1274
|
-
logger.error('Error during auto-loading video clips:', e);
|
|
1269
|
+
logger.error('Error during auto-loading video clips:', e?.message || String(e));
|
|
1275
1270
|
} finally {
|
|
1276
1271
|
this.videoClipsAutoLoadInProgress = false;
|
|
1277
1272
|
this.videoClipsAutoLoadMode = false;
|
|
@@ -1279,7 +1274,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1279
1274
|
}
|
|
1280
1275
|
|
|
1281
1276
|
async reboot(): Promise<void> {
|
|
1282
|
-
const api = await this.
|
|
1277
|
+
const api = await this.ensureClient();
|
|
1283
1278
|
await api.reboot();
|
|
1284
1279
|
}
|
|
1285
1280
|
|
|
@@ -1293,6 +1288,12 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1293
1288
|
throw new Error('UID is required for battery cameras (BCUDP)');
|
|
1294
1289
|
}
|
|
1295
1290
|
|
|
1291
|
+
// Prevent accidental connections to localhost (Node will default host=127.0.0.1 when host is undefined).
|
|
1292
|
+
// This shows up as connect ECONNREFUSED 127.0.0.1:9000 and will never recover with socket resets.
|
|
1293
|
+
if (!this.isBattery && !ipAddress) {
|
|
1294
|
+
throw new Error('IP Address is required for TCP devices');
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1296
1297
|
return {
|
|
1297
1298
|
host: ipAddress,
|
|
1298
1299
|
username,
|
|
@@ -1304,23 +1305,33 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1304
1305
|
};
|
|
1305
1306
|
}
|
|
1306
1307
|
|
|
1308
|
+
protected getStreamClientInputs(): BaichuanConnectionConfig {
|
|
1309
|
+
const { ipAddress, username, password } = this.storageSettings.values;
|
|
1310
|
+
const debugOptions = this.getBaichuanDebugOptions();
|
|
1311
|
+
|
|
1312
|
+
return {
|
|
1313
|
+
host: ipAddress,
|
|
1314
|
+
username,
|
|
1315
|
+
password,
|
|
1316
|
+
transport: this.transport,
|
|
1317
|
+
debugOptions,
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1307
1321
|
protected getConnectionCallbacks(): BaichuanConnectionCallbacks {
|
|
1308
1322
|
return {
|
|
1309
|
-
onError: undefined, // Use default error handling
|
|
1310
1323
|
onClose: async () => {
|
|
1311
1324
|
// Reset client state on close
|
|
1312
1325
|
// The base class already handles cleanup
|
|
1313
1326
|
// For battery cameras, don't auto-resubscribe after idle disconnects
|
|
1314
1327
|
// (idle disconnects are normal for battery cameras to save power)
|
|
1315
|
-
// Events will be resubscribed when ensureClient() is called for actual operations
|
|
1316
1328
|
if (!this.isBattery) {
|
|
1317
|
-
// For non-battery cameras, resubscribe to events after reconnection
|
|
1318
1329
|
setTimeout(async () => {
|
|
1319
1330
|
try {
|
|
1320
1331
|
await this.subscribeToEvents();
|
|
1321
1332
|
} catch (e) {
|
|
1322
1333
|
const logger = this.getBaichuanLogger();
|
|
1323
|
-
logger.warn('Failed to resubscribe to events after reconnection', e);
|
|
1334
|
+
logger.warn('Failed to resubscribe to events after reconnection', e?.message || String(e));
|
|
1324
1335
|
}
|
|
1325
1336
|
}, 1000);
|
|
1326
1337
|
}
|
|
@@ -1339,8 +1350,6 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1339
1350
|
}
|
|
1340
1351
|
|
|
1341
1352
|
async withBaichuanRetry<T>(fn: () => Promise<T>): Promise<T> {
|
|
1342
|
-
return await fn();
|
|
1343
|
-
|
|
1344
1353
|
if (this.isBattery) {
|
|
1345
1354
|
return await fn();
|
|
1346
1355
|
} else {
|
|
@@ -1400,7 +1409,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1400
1409
|
logger.log(`Diagnostics file: ${result.diagnosticsPath}`);
|
|
1401
1410
|
logger.log(`Streams directory: ${result.streamsDir}`);
|
|
1402
1411
|
} catch (e) {
|
|
1403
|
-
logger.error('Failed to run diagnostics', e);
|
|
1412
|
+
logger.error('Failed to run diagnostics', e?.message || String(e));
|
|
1404
1413
|
throw e;
|
|
1405
1414
|
}
|
|
1406
1415
|
}
|
|
@@ -1423,46 +1432,64 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1423
1432
|
* - For TCP devices (regular + multifocal), this creates a new TCP session with its own client.
|
|
1424
1433
|
* - For UDP/battery devices, this reuses the existing client via ensureClient().
|
|
1425
1434
|
*/
|
|
1426
|
-
async createStreamClient(
|
|
1427
|
-
//
|
|
1428
|
-
|
|
1429
|
-
|
|
1435
|
+
async createStreamClient(streamKey: string): Promise<ReolinkBaichuanApi> {
|
|
1436
|
+
// Determine who should create the socket based on device hierarchy:
|
|
1437
|
+
// 1. Camera of multifocal with nvrDevice -> nvrDevice creates the socket
|
|
1438
|
+
// 2. Camera of multifocal (without nvrDevice) -> multiFocalDevice creates the socket
|
|
1439
|
+
// 3. Camera of nvr -> nvrDevice creates the socket
|
|
1440
|
+
// 4. Standalone camera -> camera creates its own socket (via base class)
|
|
1441
|
+
|
|
1442
|
+
// Case 1: Camera of multifocal with nvrDevice -> delegate to nvrDevice
|
|
1443
|
+
if (this.multiFocalDevice?.nvrDevice) {
|
|
1444
|
+
return await this.multiFocalDevice.nvrDevice.createStreamClient(streamKey);
|
|
1430
1445
|
}
|
|
1431
1446
|
|
|
1432
|
-
//
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
return await this.ensureClient();
|
|
1447
|
+
// Case 2: Camera of multifocal (without nvrDevice) -> delegate to multiFocalDevice
|
|
1448
|
+
if (this.multiFocalDevice) {
|
|
1449
|
+
return await this.multiFocalDevice.createStreamClient(streamKey);
|
|
1436
1450
|
}
|
|
1437
1451
|
|
|
1438
|
-
//
|
|
1439
|
-
|
|
1440
|
-
|
|
1452
|
+
// Case 3: Camera of nvr -> delegate to nvrDevice
|
|
1453
|
+
if (this.nvrDevice) {
|
|
1454
|
+
return await this.nvrDevice.createStreamClient(streamKey);
|
|
1455
|
+
}
|
|
1441
1456
|
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
username,
|
|
1448
|
-
password,
|
|
1449
|
-
logger,
|
|
1450
|
-
debugOptions,
|
|
1451
|
-
},
|
|
1452
|
-
transport: 'tcp',
|
|
1453
|
-
},
|
|
1454
|
-
);
|
|
1457
|
+
// Case 4: Standalone camera -> create its own socket using base class method
|
|
1458
|
+
// For battery cameras, reuse the main client
|
|
1459
|
+
// if (this.isBattery) {
|
|
1460
|
+
// return await this.ensureClient();
|
|
1461
|
+
// }
|
|
1455
1462
|
|
|
1456
|
-
|
|
1457
|
-
return
|
|
1463
|
+
// For TCP standalone cameras, use base class createStreamClient which manages stream clients per streamKey
|
|
1464
|
+
return await super.createStreamClient(streamKey);
|
|
1458
1465
|
}
|
|
1459
1466
|
|
|
1460
1467
|
public getAbilities(): DeviceCapabilities {
|
|
1461
1468
|
if (this.multiFocalDevice) {
|
|
1462
|
-
|
|
1469
|
+
const variantType = this.storageSettings.values.variantType;
|
|
1470
|
+
const ifaces = this.multiFocalDevice.getInterfaces(variantType);
|
|
1471
|
+
if (ifaces?.capabilities) return ifaces.capabilities;
|
|
1463
1472
|
} else {
|
|
1464
|
-
|
|
1473
|
+
const caps = this.storageSettings.values.capabilities;
|
|
1474
|
+
if (caps) return caps;
|
|
1465
1475
|
}
|
|
1476
|
+
|
|
1477
|
+
// Safe fallback to avoid crashes during init when connection hasn't succeeded yet.
|
|
1478
|
+
return {
|
|
1479
|
+
channel: this.storageSettings.values.rtspChannel ?? 0,
|
|
1480
|
+
ptzMode: 'none',
|
|
1481
|
+
hasPan: false,
|
|
1482
|
+
hasTilt: false,
|
|
1483
|
+
hasZoom: false,
|
|
1484
|
+
hasPresets: false,
|
|
1485
|
+
hasPtz: false,
|
|
1486
|
+
hasBattery: !!this.isBattery,
|
|
1487
|
+
hasIntercom: false,
|
|
1488
|
+
hasSiren: false,
|
|
1489
|
+
hasFloodlight: false,
|
|
1490
|
+
hasPir: false,
|
|
1491
|
+
isDoorbell: false,
|
|
1492
|
+
};
|
|
1466
1493
|
}
|
|
1467
1494
|
|
|
1468
1495
|
getBaichuanDebugOptions(): any | undefined {
|
|
@@ -1473,33 +1500,36 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1473
1500
|
/**
|
|
1474
1501
|
* Initialize or recreate the StreamManager, taking into account multifocal composite options.
|
|
1475
1502
|
*/
|
|
1476
|
-
protected initStreamManager(logger
|
|
1503
|
+
protected initStreamManager(logger?: Console, forceRecreate: boolean = false): void {
|
|
1477
1504
|
const { username, password } = this.storageSettings.values;
|
|
1505
|
+
// Ensure logger is always valid - use provided logger or get from device, fallback to console
|
|
1506
|
+
const validLogger = logger || this.getBaichuanLogger() || console;
|
|
1478
1507
|
|
|
1479
|
-
const baseOptions:
|
|
1480
|
-
createStreamClient:
|
|
1481
|
-
|
|
1508
|
+
const baseOptions: StreamManagerOptions = {
|
|
1509
|
+
createStreamClient: this.createStreamClient.bind(this),
|
|
1510
|
+
logger: validLogger,
|
|
1482
1511
|
credentials: {
|
|
1483
1512
|
username,
|
|
1484
1513
|
password,
|
|
1485
1514
|
},
|
|
1486
|
-
sharedConnection: this.isBattery,
|
|
1515
|
+
sharedConnection: this.isBattery || !!this.nvrDevice,
|
|
1487
1516
|
};
|
|
1488
1517
|
|
|
1489
1518
|
if (this.isMultiFocal) {
|
|
1490
|
-
const
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
const
|
|
1495
|
-
const
|
|
1519
|
+
const { pipPosition, pipSize, pipMargin, rtspChannel } = this.storageSettings.values;
|
|
1520
|
+
|
|
1521
|
+
// On NVR/Hub, TrackMix lenses are selected via stream variant, not via a separate channel.
|
|
1522
|
+
// Use rtspChannel for BOTH wide and tele so the library can request tele via streamType/variant.
|
|
1523
|
+
const wider = this.isOnNvr ? rtspChannel : undefined;
|
|
1524
|
+
const tele = this.isOnNvr ? rtspChannel : undefined;
|
|
1496
1525
|
|
|
1497
1526
|
baseOptions.compositeOptions = {
|
|
1498
|
-
widerChannel,
|
|
1499
|
-
teleChannel,
|
|
1527
|
+
widerChannel: wider,
|
|
1528
|
+
teleChannel: tele,
|
|
1500
1529
|
pipPosition,
|
|
1501
1530
|
pipSize,
|
|
1502
1531
|
pipMargin,
|
|
1532
|
+
onNvr: this.isOnNvr,
|
|
1503
1533
|
};
|
|
1504
1534
|
}
|
|
1505
1535
|
|
|
@@ -1523,10 +1553,10 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1523
1553
|
|
|
1524
1554
|
this.streamManagerRestartTimeout = setTimeout(async () => {
|
|
1525
1555
|
this.streamManagerRestartTimeout = undefined;
|
|
1526
|
-
const
|
|
1556
|
+
const logger = this.getBaichuanLogger();
|
|
1527
1557
|
try {
|
|
1528
|
-
|
|
1529
|
-
this.initStreamManager(
|
|
1558
|
+
logger.log('Restarting StreamManager due to PIP/composite settings change');
|
|
1559
|
+
this.initStreamManager(logger, true);
|
|
1530
1560
|
|
|
1531
1561
|
// Invalidate snapshot cache for battery/multifocal-battery so that
|
|
1532
1562
|
// the next snapshot reflects the new PIP/composite configuration.
|
|
@@ -1542,7 +1572,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1542
1572
|
// best-effort
|
|
1543
1573
|
}
|
|
1544
1574
|
} catch (e) {
|
|
1545
|
-
|
|
1575
|
+
logger.warn('Failed to restart StreamManager after settings change', e);
|
|
1546
1576
|
}
|
|
1547
1577
|
}, 500);
|
|
1548
1578
|
}
|
|
@@ -1598,6 +1628,29 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1598
1628
|
return;
|
|
1599
1629
|
}
|
|
1600
1630
|
|
|
1631
|
+
// Handle battery/online/sleeping events separately from motion events
|
|
1632
|
+
switch (ev?.type) {
|
|
1633
|
+
case 'awake':
|
|
1634
|
+
case 'sleeping':
|
|
1635
|
+
// Update sleeping state for battery cameras or devices that support it
|
|
1636
|
+
this.updateSleepingState({
|
|
1637
|
+
reason: ev?.type === 'sleeping' ? 'sleeping' : 'awake',
|
|
1638
|
+
state: ev.type === 'sleeping' ? 'sleeping' : 'awake',
|
|
1639
|
+
}).catch((e) => {
|
|
1640
|
+
logger.warn('Error updating sleeping state', e);
|
|
1641
|
+
});
|
|
1642
|
+
return; // These events are handled, no need to process as motion events
|
|
1643
|
+
|
|
1644
|
+
case 'offline':
|
|
1645
|
+
case 'online':
|
|
1646
|
+
// Update online state for battery cameras or devices that support it
|
|
1647
|
+
this.updateOnlineState(ev.type === 'online').catch((e) => {
|
|
1648
|
+
logger.warn('Error updating online state', e);
|
|
1649
|
+
});
|
|
1650
|
+
return; // These events are handled, no need to process as motion events
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
// Handle motion and object detection events
|
|
1601
1654
|
const objects: string[] = [];
|
|
1602
1655
|
let motion = false;
|
|
1603
1656
|
|
|
@@ -1632,11 +1685,21 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1632
1685
|
}
|
|
1633
1686
|
}
|
|
1634
1687
|
|
|
1688
|
+
/**
|
|
1689
|
+
* Subscribe to Baichuan events only if this is a standalone device (not a child of NVR or MultiFocal).
|
|
1690
|
+
* If this device has a parent (nvrDevice or multiFocalDevice), events will be forwarded from the parent.
|
|
1691
|
+
* This ensures that only the root device in the hierarchy subscribes to events, avoiding duplicate subscriptions.
|
|
1692
|
+
*/
|
|
1635
1693
|
async subscribeToEvents(): Promise<void> {
|
|
1694
|
+
// If this device has a parent (NVR or MultiFocal), don't subscribe - events will be forwarded from parent
|
|
1636
1695
|
if (this.nvrDevice || this.multiFocalDevice) {
|
|
1696
|
+
const logger = this.getBaichuanLogger();
|
|
1697
|
+
logger.debug(`Device has parent (nvrDevice=${!!this.nvrDevice}, multiFocalDevice=${!!this.multiFocalDevice}), skipping event subscription (events will be forwarded from parent)`);
|
|
1637
1698
|
return;
|
|
1638
1699
|
}
|
|
1639
1700
|
|
|
1701
|
+
const api = await this.ensureClient();
|
|
1702
|
+
|
|
1640
1703
|
const logger = this.getBaichuanLogger();
|
|
1641
1704
|
const selection = Array.from(this.getDispatchEventsSelection?.() ?? new Set()).sort();
|
|
1642
1705
|
const enabled = selection.length > 0;
|
|
@@ -1661,8 +1724,6 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1661
1724
|
return;
|
|
1662
1725
|
}
|
|
1663
1726
|
|
|
1664
|
-
const api = await this.ensureClient();
|
|
1665
|
-
|
|
1666
1727
|
try {
|
|
1667
1728
|
await api.onSimpleEvent(this.onSimpleEvent);
|
|
1668
1729
|
logger.log(`Subscribed to events (${selection.join(', ')}) on ${this.protocol} connection`);
|
|
@@ -1898,7 +1959,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1898
1959
|
this.binaryState = false;
|
|
1899
1960
|
}
|
|
1900
1961
|
|
|
1901
|
-
async
|
|
1962
|
+
async reportDevicesParent(): Promise<void> {
|
|
1902
1963
|
const abilities = this.getAbilities();
|
|
1903
1964
|
|
|
1904
1965
|
const { hasSiren, hasFloodlight, hasPir } = abilities;
|
|
@@ -1946,6 +2007,8 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1946
2007
|
type: ScryptedDeviceType.Switch,
|
|
1947
2008
|
};
|
|
1948
2009
|
sdk.deviceManager.onDeviceDiscovered(device);
|
|
2010
|
+
|
|
2011
|
+
this.reportDevices && await this.reportDevices();
|
|
1949
2012
|
}
|
|
1950
2013
|
}
|
|
1951
2014
|
|
|
@@ -1959,15 +2022,42 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1959
2022
|
await this.storageSettings.putSetting(key, value);
|
|
1960
2023
|
}
|
|
1961
2024
|
|
|
2025
|
+
async takePictureInternal(client: ReolinkBaichuanApi) {
|
|
2026
|
+
const { rtspChannel, variantType } = this.storageSettings.values;
|
|
2027
|
+
const logger = this.getBaichuanLogger();
|
|
2028
|
+
logger.log(`Taking new snapshot from camera: forceNewSnapshot=${this.forceNewSnapshot} channel=${rtspChannel} variant=${variantType}`);
|
|
2029
|
+
|
|
2030
|
+
const compositeOptions = this.isMultiFocal ? {
|
|
2031
|
+
widerChannel: this.isOnNvr ? rtspChannel : undefined,
|
|
2032
|
+
teleChannel: this.isOnNvr ? rtspChannel : undefined,
|
|
2033
|
+
pipPosition: this.storageSettings.values.pipPosition || 'bottom-right',
|
|
2034
|
+
pipSize: this.storageSettings.values.pipSize ?? 0.25,
|
|
2035
|
+
pipMargin: this.storageSettings.values.pipMargin ?? 10,
|
|
2036
|
+
onNvr: this.isOnNvr,
|
|
2037
|
+
} : undefined;
|
|
2038
|
+
|
|
2039
|
+
// For multifocal devices, request a composite snapshot by passing channel=undefined.
|
|
2040
|
+
const channelArg = this.isMultiFocal ? undefined : rtspChannel;
|
|
2041
|
+
|
|
2042
|
+
const snapshotBuffer = await client.getSnapshot(
|
|
2043
|
+
channelArg,
|
|
2044
|
+
{
|
|
2045
|
+
onNvr: this.isOnNvr,
|
|
2046
|
+
variant: variantType,
|
|
2047
|
+
...(compositeOptions ? { compositeOptions } : {}),
|
|
2048
|
+
}
|
|
2049
|
+
);
|
|
2050
|
+
const mo = await this.createMediaObject(snapshotBuffer, 'image/jpeg');
|
|
2051
|
+
|
|
2052
|
+
return mo;
|
|
2053
|
+
}
|
|
2054
|
+
|
|
1962
2055
|
async takePicture(options?: RequestPictureOptions) {
|
|
1963
2056
|
if (!this.isBattery) {
|
|
1964
2057
|
try {
|
|
1965
2058
|
return this.withBaichuanRetry(async () => {
|
|
1966
2059
|
const client = await this.ensureClient();
|
|
1967
|
-
|
|
1968
|
-
const mo = await this.createMediaObject(snapshotBuffer, 'image/jpeg');
|
|
1969
|
-
|
|
1970
|
-
return mo;
|
|
2060
|
+
return await this.takePictureInternal(client);
|
|
1971
2061
|
});
|
|
1972
2062
|
} catch (e) {
|
|
1973
2063
|
this.getBaichuanLogger().error('Error taking snapshot', e);
|
|
@@ -1985,16 +2075,11 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
1985
2075
|
if (this.takePictureInFlight) {
|
|
1986
2076
|
return await this.takePictureInFlight;
|
|
1987
2077
|
}
|
|
1988
|
-
|
|
1989
|
-
logger.log(`Taking new snapshot from camera (forceNewSnapshot: ${this.forceNewSnapshot})`);
|
|
1990
2078
|
this.forceNewSnapshot = false;
|
|
1991
2079
|
|
|
1992
2080
|
this.takePictureInFlight = (async () => {
|
|
1993
|
-
const
|
|
1994
|
-
const
|
|
1995
|
-
return await api.getSnapshot(channel);
|
|
1996
|
-
});
|
|
1997
|
-
const mo = await sdk.mediaManager.createMediaObject(snapshotBuffer, 'image/jpeg');
|
|
2081
|
+
const client = await this.ensureClient();
|
|
2082
|
+
const mo = await this.takePictureInternal(client);
|
|
1998
2083
|
this.lastPicture = { mo, atMs: Date.now() };
|
|
1999
2084
|
logger.log(`Snapshot taken at ${new Date(this.lastPicture.atMs).toLocaleString()}`);
|
|
2000
2085
|
return mo;
|
|
@@ -2231,71 +2316,153 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
2231
2316
|
return this.cachedVideoStreamOptions;
|
|
2232
2317
|
}
|
|
2233
2318
|
|
|
2234
|
-
|
|
2235
|
-
|
|
2319
|
+
// If there's already a fetch in progress, return the existing promise
|
|
2320
|
+
if (this.fetchingStreamsPromise) {
|
|
2321
|
+
return this.fetchingStreamsPromise;
|
|
2236
2322
|
}
|
|
2237
2323
|
|
|
2238
|
-
|
|
2324
|
+
// Create and save the promise
|
|
2325
|
+
this.fetchingStreamsPromise = (async (): Promise<UrlMediaStreamOptions[]> => {
|
|
2326
|
+
try {
|
|
2327
|
+
let streams: UrlMediaStreamOptions[] = [];
|
|
2239
2328
|
|
|
2240
|
-
|
|
2329
|
+
const client = await this.ensureClient();
|
|
2241
2330
|
|
|
2242
|
-
|
|
2331
|
+
const { rtspChannel, variantType } = this.storageSettings.values;
|
|
2243
2332
|
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2333
|
+
try {
|
|
2334
|
+
// Lens-scoped behavior: request streams only for the current lens/variant.
|
|
2335
|
+
// This keeps a single native_main and native_sub for the device.
|
|
2336
|
+
const lensParam: NativeVideoStreamVariant | undefined = variantType as any;
|
|
2337
|
+
|
|
2338
|
+
const { nativeStreams, rtmpStreams, rtspStreams } = await client.buildVideoStreamOptions({
|
|
2339
|
+
onNvr: this.isOnNvr,
|
|
2340
|
+
channel: rtspChannel,
|
|
2341
|
+
compositeOnly: this.isMultiFocal,
|
|
2342
|
+
...(lensParam !== undefined ? { lens: lensParam } : {})
|
|
2343
|
+
});
|
|
2247
2344
|
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2345
|
+
// const urls = client.getRtspUrl(rtspChannel);
|
|
2346
|
+
|
|
2347
|
+
// let supportedStreams: ReolinkSupportedStream[] = [];
|
|
2348
|
+
const supportedStreams = [...nativeStreams, ...rtspStreams, ...rtmpStreams];
|
|
2349
|
+
// logger.log({ supportedStreams, variantType, lensParam, rtspChannel, onNvr: this.isOnNvr, nativeStreams: nativeStreams.map(s => ({ id: s.id, nativeVariant: s.nativeVariant, lens: s.lens })), rtspStreams: rtspStreams.map(s => ({ id: s.id, lens: s.lens })), rtmpStreams: rtmpStreams.map(s => ({ id: s.id, lens: s.lens })) });
|
|
2350
|
+
|
|
2351
|
+
for (const supportedStream of supportedStreams) {
|
|
2352
|
+
const { id, metadata, url, name, container, nativeVariant, lens } = supportedStream;
|
|
2353
|
+
|
|
2354
|
+
// Composite streams are re-encoded to H.264 by the library (ffmpeg/libx264).
|
|
2355
|
+
// Do not infer codec from underlying camera metadata.
|
|
2356
|
+
const isComposite = id.startsWith('composite_') || lens === 'composite';
|
|
2357
|
+
const codec = isComposite
|
|
2358
|
+
? 'h264'
|
|
2359
|
+
: String(metadata.videoEncType || "").includes("264")
|
|
2360
|
+
? "h264"
|
|
2361
|
+
: String(metadata.videoEncType || "").includes("265")
|
|
2362
|
+
? "h265"
|
|
2363
|
+
: String(metadata.videoEncType || "").toLowerCase();
|
|
2364
|
+
|
|
2365
|
+
// Preserve variant information for native RTP streams by ensuring the URL contains it.
|
|
2366
|
+
let finalUrl = url;
|
|
2367
|
+
const variantFromIdOrUrl = extractVariantFromStreamId(id, url);
|
|
2368
|
+
const variantToInject = (nativeVariant && nativeVariant !== 'default')
|
|
2369
|
+
? nativeVariant
|
|
2370
|
+
: variantFromIdOrUrl;
|
|
2371
|
+
|
|
2372
|
+
if (variantToInject && container === 'rtp') {
|
|
2373
|
+
try {
|
|
2374
|
+
const urlObj = new URL(url);
|
|
2375
|
+
if (!urlObj.searchParams.has('variant')) {
|
|
2376
|
+
urlObj.searchParams.set('variant', variantToInject);
|
|
2377
|
+
finalUrl = urlObj.toString();
|
|
2378
|
+
}
|
|
2379
|
+
} catch {
|
|
2380
|
+
// Invalid URL, use original
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2282
2383
|
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2384
|
+
streams.push({
|
|
2385
|
+
id,
|
|
2386
|
+
name,
|
|
2387
|
+
url: finalUrl,
|
|
2388
|
+
container,
|
|
2389
|
+
video: { codec, width: metadata.width, height: metadata.height },
|
|
2390
|
+
// audio: { codec: metadata.audioCodec }
|
|
2391
|
+
})
|
|
2392
|
+
}
|
|
2393
|
+
} catch (e) {
|
|
2394
|
+
if (!this.isRecoverableBaichuanError?.(e)) {
|
|
2395
|
+
logger.warn('Failed to build RTSP/RTMP stream options, falling back to Native', e?.message || String(e));
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
if (streams.length) {
|
|
2400
|
+
logger.log('Fetched video stream options', streams.map((s) => s.name).join(', '));
|
|
2401
|
+
logger.debug(JSON.stringify(streams));
|
|
2402
|
+
this.cachedVideoStreamOptions = streams;
|
|
2403
|
+
return streams;
|
|
2404
|
+
}
|
|
2289
2405
|
|
|
2290
|
-
|
|
2406
|
+
return [];
|
|
2407
|
+
} finally {
|
|
2408
|
+
// Always clear the promise when done (success or failure)
|
|
2409
|
+
this.fetchingStreamsPromise = undefined;
|
|
2410
|
+
}
|
|
2411
|
+
})();
|
|
2412
|
+
|
|
2413
|
+
return this.fetchingStreamsPromise;
|
|
2291
2414
|
}
|
|
2292
2415
|
|
|
2293
2416
|
async getVideoStream(vso: RequestMediaStreamOptions): Promise<MediaObject> {
|
|
2294
2417
|
if (!vso) throw new Error("video streams not set up or no longer exists.");
|
|
2295
2418
|
|
|
2296
2419
|
const vsos = await this.getVideoStreamOptions();
|
|
2420
|
+
const logger = this.getBaichuanLogger();
|
|
2421
|
+
|
|
2422
|
+
logger.debug(`Available streams: ${vsos?.map(s => s.id).join(', ') || 'none'}`);
|
|
2423
|
+
logger.debug(`Requested stream ID: '${vso?.id}'`);
|
|
2424
|
+
|
|
2297
2425
|
const selected = selectStreamOption(vsos, vso);
|
|
2298
2426
|
|
|
2427
|
+
// If the request explicitly asks for a variant (e.g. native_telephoto_main),
|
|
2428
|
+
// never override it with the device's variantType preference.
|
|
2429
|
+
// const requestedVariant = vso?.id ? extractVariantFromStreamId(vso.id, undefined) : undefined;
|
|
2430
|
+
|
|
2431
|
+
// If we have variantType set and the selected stream doesn't have the variant,
|
|
2432
|
+
// try to find a stream with the correct variant that matches the profile
|
|
2433
|
+
// const variantType = this.storageSettings.values.variantType;
|
|
2434
|
+
// if (!requestedVariant && variantType && variantType !== 'default') {
|
|
2435
|
+
// const profile = parseStreamProfileFromId(selected.id) || 'main';
|
|
2436
|
+
|
|
2437
|
+
// // On NVR, firmwares vary: some expose the tele lens as 'autotrack', others as 'telephoto'.
|
|
2438
|
+
// // When variantType is set, prefer that variant but fall back to the other tele variant if present.
|
|
2439
|
+
// const preferred = variantType as 'autotrack' | 'telephoto';
|
|
2440
|
+
// const fallbacks: Array<'autotrack' | 'telephoto'> = this.isOnNvr && preferred === 'telephoto'
|
|
2441
|
+
// ? ['telephoto', 'autotrack']
|
|
2442
|
+
// : this.isOnNvr && preferred === 'autotrack'
|
|
2443
|
+
// ? ['autotrack', 'telephoto']
|
|
2444
|
+
// : [preferred];
|
|
2445
|
+
|
|
2446
|
+
// const extractedVariant = extractVariantFromStreamId(selected.id, selected.url);
|
|
2447
|
+
// for (const v of fallbacks) {
|
|
2448
|
+
// const variantId = `native_${v}_${profile}`;
|
|
2449
|
+
// const variantStream = vsos?.find(s => s.id === variantId);
|
|
2450
|
+
// if (!variantStream) {
|
|
2451
|
+
// logger.debug(`Variant stream '${variantId}' not found in available streams`);
|
|
2452
|
+
// continue;
|
|
2453
|
+
// }
|
|
2454
|
+
// // Only use variant stream if the selected one doesn't already have a variant,
|
|
2455
|
+
// // or if the selected one has a different variant than what we want.
|
|
2456
|
+
// if (!extractedVariant || extractedVariant !== v) {
|
|
2457
|
+
// logger.log(`Preferring variant stream: '${variantId}' over '${selected.id}' (variantType='${variantType}')`);
|
|
2458
|
+
// selected = variantStream;
|
|
2459
|
+
// }
|
|
2460
|
+
// break;
|
|
2461
|
+
// }
|
|
2462
|
+
// }
|
|
2463
|
+
|
|
2464
|
+
logger.log(`Selected stream: id='${selected.id}', url='${selected.url}'`);
|
|
2465
|
+
|
|
2299
2466
|
if (selected.url && (selected.container === 'rtsp' || selected.container === 'rtmp')) {
|
|
2300
2467
|
const urlWithCredentials = this.addRtspCredentials(selected.url);
|
|
2301
2468
|
const ret: MediaStreamUrl = {
|
|
@@ -2311,20 +2478,25 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
2311
2478
|
}
|
|
2312
2479
|
|
|
2313
2480
|
// Check if this is a composite stream request (for multifocal devices)
|
|
2314
|
-
const isComposite = selected.id?.startsWith('composite_');
|
|
2315
|
-
if (isComposite && this.options && (this.options.type === 'multi-focal' || this.options.type === 'multi-focal-battery')) {
|
|
2481
|
+
// const isComposite = selected.id?.startsWith('composite_');
|
|
2482
|
+
// if (isComposite && this.options && (this.options.type === 'multi-focal' || this.options.type === 'multi-focal-battery')) {
|
|
2483
|
+
if (selected.id?.startsWith('composite_')) {
|
|
2316
2484
|
const profile = parseStreamProfileFromId(selected.id.replace('composite_', '')) || 'main';
|
|
2317
|
-
|
|
2318
|
-
|
|
2485
|
+
// Include variantType in streamKey to ensure each variantType has its own unique socket
|
|
2486
|
+
// This is important for multifocal devices where different variantTypes may request composite streams
|
|
2487
|
+
const variantType = this.storageSettings.values.variantType || 'default';
|
|
2488
|
+
const streamKey = `composite_${variantType}_${profile}`;
|
|
2489
|
+
|
|
2490
|
+
logger.log(`Creating composite stream: profile=${profile}, variantType=${variantType}, streamKey=${streamKey}`);
|
|
2319
2491
|
|
|
2320
2492
|
const createStreamFn = async () => {
|
|
2321
2493
|
return await createRfc4571CompositeMediaObjectFromStreamManager({
|
|
2322
|
-
streamManager: this.streamManager
|
|
2494
|
+
streamManager: this.streamManager,
|
|
2323
2495
|
profile,
|
|
2324
2496
|
streamKey,
|
|
2325
|
-
expectedVideoType,
|
|
2326
2497
|
selected,
|
|
2327
2498
|
sourceId: this.id,
|
|
2499
|
+
variantType,
|
|
2328
2500
|
});
|
|
2329
2501
|
};
|
|
2330
2502
|
|
|
@@ -2334,28 +2506,34 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
2334
2506
|
// Regular stream for single channel
|
|
2335
2507
|
const profile = parseStreamProfileFromId(selected.id) || 'main';
|
|
2336
2508
|
const channel = this.storageSettings.values.rtspChannel;
|
|
2337
|
-
|
|
2338
|
-
|
|
2509
|
+
// Extract variant from stream ID or URL if present (e.g., "autotrack" from "native_autotrack_main" or "?variant=autotrack")
|
|
2510
|
+
let variant = extractVariantFromStreamId(selected.id, selected.url);
|
|
2511
|
+
|
|
2512
|
+
// Fallback: if no variant found in stream ID/URL, use variantType from device settings
|
|
2513
|
+
// This is important for multi-focal devices where the device has a variantType setting
|
|
2514
|
+
if (!variant && this.storageSettings.values.variantType && this.storageSettings.values.variantType !== 'default') {
|
|
2515
|
+
variant = this.storageSettings.values.variantType as 'autotrack' | 'telephoto';
|
|
2516
|
+
logger.log(`Using variant from device settings: '${variant}' (not found in stream ID/URL)`);
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
logger.log(`Stream selection: id='${selected.id}', profile='${profile}', channel=${channel}, variant='${variant || 'default'}'`);
|
|
2520
|
+
|
|
2521
|
+
// Include variant in streamKey to distinguish streams with different variants
|
|
2522
|
+
const streamKey = variant ? `${channel}_${variant}_${profile}` : `${channel}_${profile}`;
|
|
2339
2523
|
|
|
2340
2524
|
const createStreamFn = async () => {
|
|
2525
|
+
// Honor the requested variant. Some NVR firmwares label the tele lens as either
|
|
2526
|
+
// 'autotrack' or 'telephoto', and the library exposes both when available.
|
|
2527
|
+
logger.log(`Creating RFC4571 stream: channel=${channel}, profile=${profile}, variant=${variant || 'default'}, streamKey=${streamKey}`);
|
|
2528
|
+
|
|
2341
2529
|
return await createRfc4571MediaObjectFromStreamManager({
|
|
2342
2530
|
streamManager: this.streamManager!,
|
|
2343
2531
|
channel,
|
|
2344
2532
|
profile,
|
|
2345
2533
|
streamKey,
|
|
2346
|
-
|
|
2534
|
+
variant,
|
|
2347
2535
|
selected,
|
|
2348
2536
|
sourceId: this.id,
|
|
2349
|
-
// onDetectedCodec: (detectedCodec) => {
|
|
2350
|
-
// const prev = this.cachedVideoStreamOptions ?? [];
|
|
2351
|
-
// const next = prev.filter((s) => s.id !== nativeId);
|
|
2352
|
-
// next.push({
|
|
2353
|
-
// container: 'rtp',
|
|
2354
|
-
// video: { codec: detectedCodec },
|
|
2355
|
-
// url: ``
|
|
2356
|
-
// });
|
|
2357
|
-
// this.cachedVideoStreamOptions = next;
|
|
2358
|
-
// },
|
|
2359
2537
|
});
|
|
2360
2538
|
};
|
|
2361
2539
|
|
|
@@ -2364,10 +2542,10 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
2364
2542
|
|
|
2365
2543
|
async ensureClient(): Promise<ReolinkBaichuanApi> {
|
|
2366
2544
|
if (this.nvrDevice) {
|
|
2367
|
-
return await this.nvrDevice.
|
|
2545
|
+
return await this.nvrDevice.ensureClient();
|
|
2368
2546
|
}
|
|
2369
2547
|
if (this.multiFocalDevice) {
|
|
2370
|
-
return await this.multiFocalDevice.
|
|
2548
|
+
return await this.multiFocalDevice.ensureClient();
|
|
2371
2549
|
}
|
|
2372
2550
|
|
|
2373
2551
|
// Use base class implementation
|
|
@@ -2426,16 +2604,17 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
2426
2604
|
await sdk.deviceManager.onDeviceDiscovered(device);
|
|
2427
2605
|
|
|
2428
2606
|
logger.log(`Device interfaces updated`);
|
|
2607
|
+
logger.debug(JSON.stringify({ hasNvr: !!this.nvrDevice, hasMultiFocal: !!this.multiFocalDevice, hasPlugin: !!this.plugin }));
|
|
2429
2608
|
logger.debug(`${JSON.stringify(device)}`);
|
|
2430
2609
|
} catch (e) {
|
|
2431
|
-
logger.error('Failed to update device interfaces', e);
|
|
2610
|
+
logger.error('Failed to update device interfaces', e?.message || String(e));
|
|
2432
2611
|
}
|
|
2433
2612
|
|
|
2434
|
-
logger.log(`Refreshed device capabilities
|
|
2435
|
-
logger.debug(`Refreshed device capabilities: ${JSON.stringify({ abilities, support, presets, objects })}`);
|
|
2613
|
+
logger.log(`Refreshed device capabilities`);
|
|
2614
|
+
logger.debug(`Refreshed device capabilities: ${JSON.stringify({ capabilities, abilities, support, presets, objects })}`);
|
|
2436
2615
|
}
|
|
2437
2616
|
catch (e) {
|
|
2438
|
-
logger.error('Failed to refresh abilities', e);
|
|
2617
|
+
logger.error('Failed to refresh abilities', e?.message || String(e));
|
|
2439
2618
|
}
|
|
2440
2619
|
|
|
2441
2620
|
this.refreshingState = false;
|
|
@@ -2444,23 +2623,27 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
2444
2623
|
async parentInit(): Promise<void> {
|
|
2445
2624
|
const logger = this.getBaichuanLogger();
|
|
2446
2625
|
|
|
2626
|
+
this.init && await this.init();
|
|
2627
|
+
|
|
2447
2628
|
try {
|
|
2448
2629
|
await this.ensureClient();
|
|
2449
2630
|
await this.updateDeviceInfo();
|
|
2450
2631
|
}
|
|
2451
2632
|
catch (e) {
|
|
2452
|
-
logger.warn('Failed to update device info during init', e);
|
|
2633
|
+
logger.warn('Failed to update device info during init', e?.message || String(e));
|
|
2453
2634
|
}
|
|
2454
2635
|
|
|
2636
|
+
await this.refreshDeviceState();
|
|
2637
|
+
|
|
2455
2638
|
if (!this.multiFocalDevice) {
|
|
2456
2639
|
try {
|
|
2457
|
-
await this.refreshDeviceState();
|
|
2458
2640
|
await this.reportDevices();
|
|
2459
2641
|
}
|
|
2460
2642
|
catch (e) {
|
|
2461
|
-
logger.warn('Failed to connect/refresh during init', e);
|
|
2643
|
+
logger.warn('Failed to connect/refresh during init', e?.message || String(e));
|
|
2462
2644
|
}
|
|
2463
2645
|
}
|
|
2646
|
+
|
|
2464
2647
|
this.storageSettings.settings.socketApiDebugLogs.hide = !!this.nvrDevice;
|
|
2465
2648
|
this.storageSettings.settings.clipsSource.hide = !this.nvrDevice;
|
|
2466
2649
|
this.storageSettings.settings.clipsSource.defaultValue = this.nvrDevice ? "NVR" : "Device";
|
|
@@ -2475,10 +2658,8 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
2475
2658
|
this.storageSettings.settings.pipPosition.hide = !this.isMultiFocal;
|
|
2476
2659
|
this.storageSettings.settings.pipSize.hide = !this.isMultiFocal;
|
|
2477
2660
|
this.storageSettings.settings.pipMargin.hide = !this.isMultiFocal;
|
|
2478
|
-
this.storageSettings.settings.widerChannel.hide = !this.isMultiFocal;
|
|
2479
|
-
this.storageSettings.settings.teleChannel.hide = !this.isMultiFocal;
|
|
2480
2661
|
|
|
2481
|
-
this.storageSettings.settings.uid.hide = !this.isBattery
|
|
2662
|
+
this.storageSettings.settings.uid.hide = !this.isBattery
|
|
2482
2663
|
this.storageSettings.settings.discoveryMethod.hide = !this.isBattery && !this.nvrDevice;
|
|
2483
2664
|
|
|
2484
2665
|
if (this.isBattery && !this.storageSettings.values.mixinsSetup) {
|
|
@@ -2492,7 +2673,7 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
2492
2673
|
}
|
|
2493
2674
|
}
|
|
2494
2675
|
catch (e) {
|
|
2495
|
-
logger.warn('Failed to setup mixins during init', e);
|
|
2676
|
+
logger.warn('Failed to setup mixins during init', e?.message || String(e));
|
|
2496
2677
|
}
|
|
2497
2678
|
}
|
|
2498
2679
|
|
|
@@ -2503,8 +2684,12 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
2503
2684
|
logger.warn('Failed to subscribe to Baichuan events', e);
|
|
2504
2685
|
}
|
|
2505
2686
|
|
|
2506
|
-
|
|
2507
|
-
|
|
2687
|
+
try {
|
|
2688
|
+
this.initStreamManager();
|
|
2689
|
+
}
|
|
2690
|
+
catch (e) {
|
|
2691
|
+
logger.warn('Failed to initialize StreamManager', e);
|
|
2692
|
+
}
|
|
2508
2693
|
|
|
2509
2694
|
const { hasIntercom, hasPtz } = this.getAbilities();
|
|
2510
2695
|
|
|
@@ -2529,23 +2714,69 @@ export abstract class CommonCameraMixin extends BaseBaichuanClass implements Vid
|
|
|
2529
2714
|
this.updatePtzCaps();
|
|
2530
2715
|
}
|
|
2531
2716
|
|
|
2532
|
-
|
|
2717
|
+
const parentDevice = this.nvrDevice || this.multiFocalDevice;
|
|
2718
|
+
if (parentDevice) {
|
|
2533
2719
|
this.storageSettings.settings.username.hide = true;
|
|
2534
2720
|
this.storageSettings.settings.password.hide = true;
|
|
2535
2721
|
this.storageSettings.settings.ipAddress.hide = true;
|
|
2536
2722
|
|
|
2537
|
-
this.storageSettings.settings.username.defaultValue =
|
|
2538
|
-
this.storageSettings.settings.password.defaultValue =
|
|
2539
|
-
this.storageSettings.settings.ipAddress.defaultValue =
|
|
2723
|
+
this.storageSettings.settings.username.defaultValue = parentDevice.storageSettings.values.username;
|
|
2724
|
+
this.storageSettings.settings.password.defaultValue = parentDevice.storageSettings.values.password;
|
|
2725
|
+
this.storageSettings.settings.ipAddress.defaultValue = parentDevice.storageSettings.values.ipAddress;
|
|
2540
2726
|
}
|
|
2541
2727
|
|
|
2542
|
-
|
|
2728
|
+
this.updateVideoClipsAutoLoad();
|
|
2543
2729
|
|
|
2544
|
-
this.
|
|
2730
|
+
this.onDeviceEvent(ScryptedInterface.Settings, '');
|
|
2731
|
+
}
|
|
2545
2732
|
|
|
2546
|
-
|
|
2547
|
-
|
|
2733
|
+
async updateSleepingState(sleepStatus: SleepStatus): Promise<void> {
|
|
2734
|
+
try {
|
|
2735
|
+
if (this.isDebugEnabled()) {
|
|
2736
|
+
this.getBaichuanLogger().debug('getSleepStatus result:', JSON.stringify(sleepStatus));
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
if (sleepStatus.state === 'sleeping') {
|
|
2740
|
+
if (!this.sleeping) {
|
|
2741
|
+
this.getBaichuanLogger().log(`Camera is sleeping: ${sleepStatus.reason}`);
|
|
2742
|
+
this.sleeping = true;
|
|
2743
|
+
}
|
|
2744
|
+
} else if (sleepStatus.state === 'awake') {
|
|
2745
|
+
// Camera is awake
|
|
2746
|
+
const wasSleeping = this.sleeping;
|
|
2747
|
+
if (wasSleeping) {
|
|
2748
|
+
this.getBaichuanLogger().log(`Camera woke up: ${sleepStatus.reason}`);
|
|
2749
|
+
this.sleeping = false;
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
if (wasSleeping) {
|
|
2753
|
+
this.alignAuxDevicesState().catch(() => { });
|
|
2754
|
+
if (this.forceNewSnapshot) {
|
|
2755
|
+
this.takePicture().catch(() => { });
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
} else {
|
|
2759
|
+
// Unknown state
|
|
2760
|
+
this.getBaichuanLogger().debug(`Sleep status unknown: ${sleepStatus.reason}`);
|
|
2761
|
+
}
|
|
2762
|
+
} catch (e) {
|
|
2763
|
+
// Silently ignore errors in sleep check to avoid spam
|
|
2764
|
+
this.getBaichuanLogger().debug('Error in updateSleepingState:', e);
|
|
2765
|
+
}
|
|
2548
2766
|
}
|
|
2549
|
-
}
|
|
2550
2767
|
|
|
2768
|
+
async updateOnlineState(isOnline: boolean): Promise<void> {
|
|
2769
|
+
try {
|
|
2770
|
+
if (this.isDebugEnabled()) {
|
|
2771
|
+
this.getBaichuanLogger().debug('updateOnlineState result:', isOnline);
|
|
2772
|
+
}
|
|
2551
2773
|
|
|
2774
|
+
if (isOnline !== this.online) {
|
|
2775
|
+
this.online = isOnline;
|
|
2776
|
+
}
|
|
2777
|
+
} catch (e) {
|
|
2778
|
+
// Silently ignore errors in sleep check to avoid spam
|
|
2779
|
+
this.getBaichuanLogger().debug('Error in updateOnlineState:', e);
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
}
|