@apocaliss92/scrypted-reolink-native 0.3.17 → 0.4.1
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/.vscode/settings.json +1 -1
- package/dist/main.nodejs.js +1 -1
- package/dist/plugin.zip +0 -0
- package/package.json +1 -1
- package/src/accessories/autotracking.ts +150 -0
- package/src/accessories/floodlight.ts +92 -0
- package/src/accessories/index.ts +7 -0
- package/src/accessories/motion-floodlight.ts +171 -0
- package/src/accessories/motion-siren.ts +165 -0
- package/src/accessories/pir-sensor.ts +138 -0
- package/src/accessories/siren.ts +63 -0
- package/src/baichuan-base.ts +863 -792
- package/src/camera.ts +3726 -2911
- package/src/intercom.ts +496 -476
- package/src/main.ts +378 -409
- package/src/multiFocal.ts +300 -270
- package/src/nvr.ts +588 -477
- package/src/stream-utils.ts +478 -427
- package/src/utils.ts +385 -1009
package/src/main.ts
CHANGED
|
@@ -1,436 +1,405 @@
|
|
|
1
|
-
import sdk, {
|
|
1
|
+
import sdk, {
|
|
2
|
+
DeviceCreator,
|
|
3
|
+
DeviceCreatorSettings,
|
|
4
|
+
DeviceProvider,
|
|
5
|
+
HttpRequest,
|
|
6
|
+
HttpResponse,
|
|
7
|
+
MediaObject,
|
|
8
|
+
ScryptedDeviceBase,
|
|
9
|
+
ScryptedDeviceType,
|
|
10
|
+
ScryptedInterface,
|
|
11
|
+
ScryptedMimeTypes,
|
|
12
|
+
ScryptedNativeId,
|
|
13
|
+
Setting,
|
|
14
|
+
VideoClips,
|
|
15
|
+
} from "@scrypted/sdk";
|
|
2
16
|
import { BaseBaichuanClass } from "./baichuan-base";
|
|
3
17
|
import { ReolinkNativeMultiFocalDevice } from "./multiFocal";
|
|
4
18
|
import { ReolinkNativeNvrDevice } from "./nvr";
|
|
5
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
batteryCameraSuffix,
|
|
21
|
+
batteryMultifocalSuffix,
|
|
22
|
+
cameraSuffix,
|
|
23
|
+
getDeviceInterfaces,
|
|
24
|
+
handleVideoClipRequest,
|
|
25
|
+
multifocalSuffix,
|
|
26
|
+
nvrSuffix,
|
|
27
|
+
} from "./utils";
|
|
6
28
|
import { randomBytes } from "crypto";
|
|
7
29
|
import { ReolinkCamera } from "./camera";
|
|
8
|
-
import type { AutoDetectMode } from "@apocaliss92/reolink-baichuan-js" with {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
private thumbnailQueue: ThumbnailRequest[] = [];
|
|
35
|
-
private thumbnailProcessing = false;
|
|
36
|
-
private thumbnailPendingRequests = new Map<string, Promise<MediaObject>>();
|
|
37
|
-
|
|
38
|
-
constructor(nativeId: string) {
|
|
39
|
-
super(nativeId);
|
|
40
|
-
|
|
41
|
-
const nvrDevice = sdk.systemManager.getDeviceByName('Scrypted NVR');
|
|
42
|
-
this.nvrDeviceId = nvrDevice?.id;
|
|
30
|
+
import type { AutoDetectMode } from "@apocaliss92/reolink-baichuan-js" with {
|
|
31
|
+
"resolution-mode": "import",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
class ReolinkNativePlugin
|
|
35
|
+
extends ScryptedDeviceBase
|
|
36
|
+
implements DeviceProvider, DeviceCreator
|
|
37
|
+
{
|
|
38
|
+
devices = new Map<string, BaseBaichuanClass>();
|
|
39
|
+
camerasMap = new Map<string, ReolinkCamera>();
|
|
40
|
+
nvrDeviceId: string;
|
|
41
|
+
|
|
42
|
+
constructor(nativeId: string) {
|
|
43
|
+
super(nativeId);
|
|
44
|
+
|
|
45
|
+
const nvrDevice = sdk.systemManager.getDeviceByName("Scrypted NVR");
|
|
46
|
+
this.nvrDeviceId = nvrDevice?.id;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getScryptedDeviceCreator(): string {
|
|
50
|
+
return "Reolink Native camera";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async getDevice(nativeId: ScryptedNativeId): Promise<BaseBaichuanClass> {
|
|
54
|
+
if (this.devices.has(nativeId)) {
|
|
55
|
+
return this.devices.get(nativeId)!;
|
|
43
56
|
}
|
|
44
57
|
|
|
45
|
-
|
|
46
|
-
|
|
58
|
+
const newCamera = this.createCamera(nativeId);
|
|
59
|
+
this.devices.set(nativeId, newCamera);
|
|
60
|
+
return newCamera;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async createDevice(
|
|
64
|
+
settings: DeviceCreatorSettings,
|
|
65
|
+
nativeId?: string,
|
|
66
|
+
): Promise<string> {
|
|
67
|
+
const ipAddress = settings.ip?.toString();
|
|
68
|
+
const username = settings.username?.toString();
|
|
69
|
+
const password = settings.password?.toString();
|
|
70
|
+
const uid = settings.uid?.toString();
|
|
71
|
+
|
|
72
|
+
if (!ipAddress || !username || !password) {
|
|
73
|
+
throw new Error("IP address, username, and password are required");
|
|
47
74
|
}
|
|
48
75
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
76
|
+
const deviceTypeSetting = settings.deviceType?.toString() || "Auto";
|
|
77
|
+
const forceType =
|
|
78
|
+
deviceTypeSetting === "Auto"
|
|
79
|
+
? undefined
|
|
80
|
+
: deviceTypeSetting.toLowerCase();
|
|
81
|
+
|
|
82
|
+
this.console.log(
|
|
83
|
+
`[AutoDetect] Starting device type detection for ${ipAddress}...${forceType ? ` (forcing type: ${forceType})` : ""}`,
|
|
84
|
+
);
|
|
85
|
+
const { autoDetectDeviceType } =
|
|
86
|
+
await import("@apocaliss92/reolink-baichuan-js");
|
|
87
|
+
// 'Auto', 'NVR', 'Battery Camera', 'Regular Camera
|
|
88
|
+
const mode: AutoDetectMode =
|
|
89
|
+
forceType === "Auto"
|
|
90
|
+
? "auto"
|
|
91
|
+
: forceType === "Battery Camera"
|
|
92
|
+
? "udp"
|
|
93
|
+
: forceType === "Regular Camera"
|
|
94
|
+
? "tcp"
|
|
95
|
+
: forceType === "NVR"
|
|
96
|
+
? "tcp"
|
|
97
|
+
: "auto";
|
|
98
|
+
|
|
99
|
+
const maxRetries = mode === "auto" ? 2 : 10;
|
|
100
|
+
|
|
101
|
+
const detection = await autoDetectDeviceType({
|
|
102
|
+
host: ipAddress,
|
|
103
|
+
username,
|
|
104
|
+
password,
|
|
105
|
+
uid,
|
|
106
|
+
logger: this.console,
|
|
107
|
+
mode,
|
|
108
|
+
maxRetries,
|
|
109
|
+
});
|
|
110
|
+
const { ip, mac } = detection.hostNetworkInfo ?? {};
|
|
111
|
+
|
|
112
|
+
this.console.log(
|
|
113
|
+
`[AutoDetect] Detected device type: ${detection.type} (transport: ${detection.transport}). Device info: ${JSON.stringify(detection.deviceInfo)}`,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// Use the API that was successfully used for detection
|
|
117
|
+
const detectedApi = detection.api;
|
|
118
|
+
const deviceInfo = detection.deviceInfo || {};
|
|
119
|
+
const name = deviceInfo?.name || `Reolink ${detection.type}`;
|
|
120
|
+
const identifier =
|
|
121
|
+
uid || mac || ip || name || randomBytes(4).toString("hex");
|
|
122
|
+
|
|
123
|
+
// Handle multi-focal device case
|
|
124
|
+
if (detection.type === "multifocal") {
|
|
125
|
+
const isBattery = detection.transport === "udp";
|
|
126
|
+
nativeId = `${identifier}${isBattery ? batteryMultifocalSuffix : multifocalSuffix}`;
|
|
127
|
+
|
|
128
|
+
settings.newCamera ||= name;
|
|
129
|
+
|
|
130
|
+
const { capabilities, objects, presets } =
|
|
131
|
+
await detectedApi.getDeviceCapabilities();
|
|
132
|
+
|
|
133
|
+
const { interfaces } = getDeviceInterfaces({
|
|
134
|
+
capabilities,
|
|
135
|
+
logger: this.console,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
await sdk.deviceManager.onDeviceDiscovered({
|
|
139
|
+
nativeId,
|
|
140
|
+
name,
|
|
141
|
+
interfaces,
|
|
142
|
+
type: ScryptedDeviceType.DeviceProvider,
|
|
143
|
+
providerNativeId: this.nativeId,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const device = await this.getDevice(nativeId);
|
|
147
|
+
if (!(device instanceof ReolinkNativeMultiFocalDevice)) {
|
|
148
|
+
throw new Error("Expected multi-focal device but got different type");
|
|
149
|
+
}
|
|
150
|
+
device.classes = objects;
|
|
151
|
+
device.presets = presets;
|
|
152
|
+
device.storageSettings.values.ipAddress = ipAddress;
|
|
153
|
+
device.storageSettings.values.username = username;
|
|
154
|
+
device.storageSettings.values.password = password;
|
|
155
|
+
device.storageSettings.values.uid = uid;
|
|
156
|
+
device.cachedCapabilities = capabilities;
|
|
157
|
+
|
|
158
|
+
return nativeId;
|
|
57
159
|
}
|
|
58
160
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
password,
|
|
88
|
-
uid,
|
|
89
|
-
logger: this.console,
|
|
90
|
-
mode,
|
|
91
|
-
maxRetries,
|
|
92
|
-
},
|
|
93
|
-
);
|
|
94
|
-
const { ip, mac } = detection.hostNetworkInfo ?? {}
|
|
95
|
-
|
|
96
|
-
this.console.log(`[AutoDetect] Detected device type: ${detection.type} (transport: ${detection.transport}). Device info: ${JSON.stringify(detection.deviceInfo)}`);
|
|
97
|
-
|
|
98
|
-
// Use the API that was successfully used for detection
|
|
99
|
-
const detectedApi = detection.api;
|
|
100
|
-
const deviceInfo = detection.deviceInfo || {};
|
|
101
|
-
const name = deviceInfo?.name || `Reolink ${detection.type}`;
|
|
102
|
-
const identifier = uid || mac || ip || name || randomBytes(4).toString('hex');
|
|
103
|
-
|
|
104
|
-
// Handle multi-focal device case
|
|
105
|
-
if (detection.type === 'multifocal') {
|
|
106
|
-
const isBattery = detection.transport === 'udp';
|
|
107
|
-
nativeId = `${identifier}${isBattery ? batteryMultifocalSuffix : multifocalSuffix}`;
|
|
108
|
-
|
|
109
|
-
settings.newCamera ||= name;
|
|
110
|
-
|
|
111
|
-
const { capabilities, objects, presets } = await detectedApi.getDeviceCapabilities();
|
|
112
|
-
|
|
113
|
-
const { interfaces } = getDeviceInterfaces({
|
|
114
|
-
capabilities,
|
|
115
|
-
logger: this.console,
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
await sdk.deviceManager.onDeviceDiscovered({
|
|
119
|
-
nativeId,
|
|
120
|
-
name,
|
|
121
|
-
interfaces,
|
|
122
|
-
type: ScryptedDeviceType.DeviceProvider,
|
|
123
|
-
providerNativeId: this.nativeId,
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
const device = await this.getDevice(nativeId);
|
|
127
|
-
if (!(device instanceof ReolinkNativeMultiFocalDevice)) {
|
|
128
|
-
throw new Error('Expected multi-focal device but got different type');
|
|
129
|
-
}
|
|
130
|
-
device.classes = objects;
|
|
131
|
-
device.presets = presets;
|
|
132
|
-
device.storageSettings.values.ipAddress = ipAddress;
|
|
133
|
-
device.storageSettings.values.username = username;
|
|
134
|
-
device.storageSettings.values.password = password;
|
|
135
|
-
device.storageSettings.values.uid = uid;
|
|
136
|
-
device.cachedCapabilities = capabilities;
|
|
137
|
-
|
|
138
|
-
return nativeId;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Handle NVR case
|
|
142
|
-
if (detection.type === 'nvr') {
|
|
143
|
-
nativeId = `${identifier}${nvrSuffix}`;
|
|
144
|
-
|
|
145
|
-
settings.newCamera ||= name;
|
|
146
|
-
|
|
147
|
-
await sdk.deviceManager.onDeviceDiscovered({
|
|
148
|
-
nativeId,
|
|
149
|
-
name,
|
|
150
|
-
interfaces: [
|
|
151
|
-
ScryptedInterface.Settings,
|
|
152
|
-
ScryptedInterface.DeviceDiscovery,
|
|
153
|
-
ScryptedInterface.DeviceProvider,
|
|
154
|
-
ScryptedInterface.Reboot,
|
|
155
|
-
],
|
|
156
|
-
type: ScryptedDeviceType.DeviceProvider,
|
|
157
|
-
providerNativeId: this.nativeId,
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
const device = await this.getDevice(nativeId);
|
|
161
|
-
if (!(device instanceof ReolinkNativeNvrDevice)) {
|
|
162
|
-
throw new Error('Expected NVR device but got different type');
|
|
163
|
-
}
|
|
164
|
-
device.storageSettings.values.ipAddress = ipAddress;
|
|
165
|
-
device.storageSettings.values.username = username;
|
|
166
|
-
device.storageSettings.values.password = password;
|
|
167
|
-
|
|
168
|
-
return nativeId;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Create nativeId based on device type
|
|
172
|
-
if (detection.type === 'battery-cam') {
|
|
173
|
-
nativeId = `${identifier}${batteryCameraSuffix}`;
|
|
174
|
-
} else {
|
|
175
|
-
nativeId = `${identifier}${cameraSuffix}`;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
settings.newCamera ||= name;
|
|
179
|
-
|
|
180
|
-
// Use the API that was successfully used for detection
|
|
181
|
-
try {
|
|
182
|
-
const rtspChannel = 0;
|
|
183
|
-
const { capabilities, objects, presets } = await detectedApi.getDeviceCapabilities(rtspChannel);
|
|
184
|
-
|
|
185
|
-
const { interfaces, type } = getDeviceInterfaces({
|
|
186
|
-
capabilities,
|
|
187
|
-
logger: this.console,
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
await sdk.deviceManager.onDeviceDiscovered({
|
|
191
|
-
nativeId,
|
|
192
|
-
name,
|
|
193
|
-
interfaces,
|
|
194
|
-
type,
|
|
195
|
-
providerNativeId: this.nativeId,
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
const device = await this.getDevice(nativeId) as ReolinkCamera;
|
|
199
|
-
|
|
200
|
-
device.info = deviceInfo;
|
|
201
|
-
device.classes = objects;
|
|
202
|
-
device.presets = presets;
|
|
203
|
-
device.storageSettings.values.username = username;
|
|
204
|
-
device.storageSettings.values.password = password;
|
|
205
|
-
device.storageSettings.values.rtspChannel = rtspChannel;
|
|
206
|
-
device.storageSettings.values.ipAddress = ipAddress;
|
|
207
|
-
device.storageSettings.values.uid = uid;
|
|
208
|
-
device.storageSettings.values.discoveryMethod = detection.udpDiscoveryMethod;
|
|
209
|
-
|
|
210
|
-
device.cachedCapabilities = capabilities;
|
|
211
|
-
|
|
212
|
-
return nativeId;
|
|
213
|
-
}
|
|
214
|
-
catch (e) {
|
|
215
|
-
this.console.error('Error adding Reolink device', e?.message || String(e));
|
|
216
|
-
throw e;
|
|
217
|
-
}
|
|
161
|
+
// Handle NVR case
|
|
162
|
+
if (detection.type === "nvr") {
|
|
163
|
+
nativeId = `${identifier}${nvrSuffix}`;
|
|
164
|
+
|
|
165
|
+
settings.newCamera ||= name;
|
|
166
|
+
|
|
167
|
+
await sdk.deviceManager.onDeviceDiscovered({
|
|
168
|
+
nativeId,
|
|
169
|
+
name,
|
|
170
|
+
interfaces: [
|
|
171
|
+
ScryptedInterface.Settings,
|
|
172
|
+
ScryptedInterface.DeviceDiscovery,
|
|
173
|
+
ScryptedInterface.DeviceProvider,
|
|
174
|
+
ScryptedInterface.Reboot,
|
|
175
|
+
],
|
|
176
|
+
type: ScryptedDeviceType.DeviceProvider,
|
|
177
|
+
providerNativeId: this.nativeId,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const device = await this.getDevice(nativeId);
|
|
181
|
+
if (!(device instanceof ReolinkNativeNvrDevice)) {
|
|
182
|
+
throw new Error("Expected NVR device but got different type");
|
|
183
|
+
}
|
|
184
|
+
device.storageSettings.values.ipAddress = ipAddress;
|
|
185
|
+
device.storageSettings.values.username = username;
|
|
186
|
+
device.storageSettings.values.password = password;
|
|
187
|
+
|
|
188
|
+
return nativeId;
|
|
218
189
|
}
|
|
219
190
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
this.devices.delete(nativeId);
|
|
227
|
-
}
|
|
191
|
+
// Create nativeId based on device type
|
|
192
|
+
if (detection.type === "battery-cam") {
|
|
193
|
+
nativeId = `${identifier}${batteryCameraSuffix}`;
|
|
194
|
+
} else {
|
|
195
|
+
nativeId = `${identifier}${cameraSuffix}`;
|
|
228
196
|
}
|
|
229
197
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
198
|
+
settings.newCamera ||= name;
|
|
199
|
+
|
|
200
|
+
// Use the API that was successfully used for detection
|
|
201
|
+
try {
|
|
202
|
+
const rtspChannel = 0;
|
|
203
|
+
const { capabilities, objects, presets } =
|
|
204
|
+
await detectedApi.getDeviceCapabilities(rtspChannel);
|
|
205
|
+
|
|
206
|
+
const { interfaces, type } = getDeviceInterfaces({
|
|
207
|
+
capabilities,
|
|
208
|
+
logger: this.console,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
await sdk.deviceManager.onDeviceDiscovered({
|
|
212
|
+
nativeId,
|
|
213
|
+
name,
|
|
214
|
+
interfaces,
|
|
215
|
+
type,
|
|
216
|
+
providerNativeId: this.nativeId,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const device = (await this.getDevice(nativeId)) as ReolinkCamera;
|
|
220
|
+
|
|
221
|
+
device.info = deviceInfo;
|
|
222
|
+
device.classes = objects;
|
|
223
|
+
device.presets = presets;
|
|
224
|
+
device.storageSettings.values.username = username;
|
|
225
|
+
device.storageSettings.values.password = password;
|
|
226
|
+
device.storageSettings.values.rtspChannel = rtspChannel;
|
|
227
|
+
device.storageSettings.values.ipAddress = ipAddress;
|
|
228
|
+
device.storageSettings.values.uid = uid;
|
|
229
|
+
device.storageSettings.values.discoveryMethod =
|
|
230
|
+
detection.udpDiscoveryMethod;
|
|
231
|
+
|
|
232
|
+
device.cachedCapabilities = capabilities;
|
|
233
|
+
|
|
234
|
+
return nativeId;
|
|
235
|
+
} catch (e) {
|
|
236
|
+
this.console.error(
|
|
237
|
+
"Error adding Reolink device",
|
|
238
|
+
e?.message || String(e),
|
|
239
|
+
);
|
|
240
|
+
throw e;
|
|
262
241
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async releaseDevice(id: string, nativeId: ScryptedNativeId): Promise<void> {
|
|
245
|
+
if (this.devices.has(nativeId)) {
|
|
246
|
+
const device = this.devices.get(nativeId);
|
|
247
|
+
if (
|
|
248
|
+
device &&
|
|
249
|
+
"release" in device &&
|
|
250
|
+
typeof device.release === "function"
|
|
251
|
+
) {
|
|
252
|
+
await device.release();
|
|
253
|
+
}
|
|
254
|
+
this.devices.delete(nativeId);
|
|
276
255
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
} else if (type === 'thumbnail') {
|
|
331
|
-
// Get thumbnail MediaObject
|
|
332
|
-
const mo = await device.getVideoClipThumbnail(fileId);
|
|
333
|
-
|
|
334
|
-
// Convert to buffer
|
|
335
|
-
const buffer = await sdk.mediaManager.convertMediaObjectToBuffer(mo, 'image/jpeg');
|
|
336
|
-
|
|
337
|
-
// Send image
|
|
338
|
-
response.send(buffer, {
|
|
339
|
-
code: 200,
|
|
340
|
-
headers: {
|
|
341
|
-
'Content-Type': 'image/jpeg',
|
|
342
|
-
'Cache-Control': 'max-age=31536000',
|
|
343
|
-
},
|
|
344
|
-
});
|
|
345
|
-
return;
|
|
346
|
-
} else {
|
|
347
|
-
response.send('Invalid webhook type', { code: 404 });
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
} catch (e: any) {
|
|
351
|
-
logger.error('Error in onRequest', e?.message || String(e));
|
|
352
|
-
response.send(`Error: ${e.message}`, {
|
|
353
|
-
code: 500,
|
|
354
|
-
});
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
onPush(request: HttpRequest): Promise<void> {
|
|
360
|
-
return this.onRequest(request, undefined);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async getCreateDeviceSettings(): Promise<Setting[]> {
|
|
259
|
+
return [
|
|
260
|
+
{
|
|
261
|
+
key: "ip",
|
|
262
|
+
title: "IP Address",
|
|
263
|
+
placeholder: "192.168.2.222",
|
|
264
|
+
value: "192.168.",
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
key: "username",
|
|
268
|
+
title: "Username",
|
|
269
|
+
value: "admin",
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
key: "password",
|
|
273
|
+
title: "Password",
|
|
274
|
+
type: "password",
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
key: "uid",
|
|
278
|
+
title: "UID",
|
|
279
|
+
description:
|
|
280
|
+
"Reolink UID (optional, required for battery cameras if TCP connection fails)",
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
key: "deviceType",
|
|
284
|
+
title: "Device Type",
|
|
285
|
+
description:
|
|
286
|
+
'Device type detection mode. Use "Auto" for automatic detection, or force a specific type.',
|
|
287
|
+
type: "string",
|
|
288
|
+
choices: ["Auto", "NVR", "Battery Camera", "Regular Camera"],
|
|
289
|
+
value: "Auto",
|
|
290
|
+
},
|
|
291
|
+
];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
createCamera(nativeId: string) {
|
|
295
|
+
if (nativeId.endsWith(batteryCameraSuffix)) {
|
|
296
|
+
return new ReolinkCamera(nativeId, this, { type: "battery" });
|
|
297
|
+
} else if (nativeId.endsWith(nvrSuffix)) {
|
|
298
|
+
return new ReolinkNativeNvrDevice(nativeId, this);
|
|
299
|
+
} else if (nativeId.endsWith(batteryMultifocalSuffix)) {
|
|
300
|
+
return new ReolinkNativeMultiFocalDevice(
|
|
301
|
+
nativeId,
|
|
302
|
+
this,
|
|
303
|
+
"multi-focal-battery",
|
|
304
|
+
);
|
|
305
|
+
} else if (nativeId.endsWith(multifocalSuffix)) {
|
|
306
|
+
return new ReolinkNativeMultiFocalDevice(nativeId, this, "multi-focal");
|
|
307
|
+
} else {
|
|
308
|
+
return new ReolinkCamera(nativeId, this, { type: "regular" });
|
|
361
309
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async onRequest(request: HttpRequest, response: HttpResponse): Promise<void> {
|
|
313
|
+
const logger = this.console;
|
|
314
|
+
const url = new URL(`http://localhost${request.url}`);
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
// Parse webhook path: /.../webhook/{type}/{deviceId}/{fileId}
|
|
318
|
+
// The path may include prefix like /endpoint/@apocaliss92/scrypted-reolink-native/public/webhook/...
|
|
319
|
+
const pathParts = url.pathname.split("/").filter((p) => p);
|
|
320
|
+
|
|
321
|
+
// Find the index of 'webhook' in the path
|
|
322
|
+
const webhookIndex = pathParts.indexOf("webhook");
|
|
323
|
+
if (webhookIndex === -1 || pathParts.length < webhookIndex + 4) {
|
|
324
|
+
response.send("Invalid webhook path", { code: 404 });
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Extract type, deviceId, and fileId after 'webhook'
|
|
329
|
+
const type = pathParts[webhookIndex + 1];
|
|
330
|
+
const encodedDeviceId = pathParts[webhookIndex + 2];
|
|
331
|
+
// fileId may contain slashes, so join all remaining parts
|
|
332
|
+
const encodedFileId = pathParts.slice(webhookIndex + 3).join("/");
|
|
333
|
+
const deviceId = decodeURIComponent(encodedDeviceId);
|
|
334
|
+
let fileId = decodeURIComponent(encodedFileId);
|
|
335
|
+
|
|
336
|
+
// Restore leading slash if the original fileId had it (we removed it during encoding)
|
|
337
|
+
// The API expects fileId with leading slash for absolute paths
|
|
338
|
+
if (!fileId.startsWith("/") && !fileId.startsWith("http")) {
|
|
339
|
+
// If it looks like an absolute path (starts with common path prefixes), add slash
|
|
340
|
+
if (
|
|
341
|
+
fileId.startsWith("mnt/") ||
|
|
342
|
+
fileId.startsWith("var/") ||
|
|
343
|
+
fileId.startsWith("tmp/")
|
|
344
|
+
) {
|
|
345
|
+
fileId = `/${fileId}`;
|
|
376
346
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// logger.log(`Webhook request: type=${type}, deviceId=${deviceId}, fileId=${fileId}`);
|
|
350
|
+
|
|
351
|
+
// Get the device
|
|
352
|
+
const device = this.camerasMap.get(deviceId);
|
|
353
|
+
if (!device) {
|
|
354
|
+
response.send("Device not found", { code: 404 });
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (type === "video") {
|
|
359
|
+
await handleVideoClipRequest({
|
|
360
|
+
device,
|
|
361
|
+
deviceId,
|
|
362
|
+
fileId,
|
|
363
|
+
request,
|
|
364
|
+
response,
|
|
365
|
+
logger,
|
|
390
366
|
});
|
|
367
|
+
return;
|
|
368
|
+
} else if (type === "thumbnail") {
|
|
369
|
+
// Get thumbnail MediaObject
|
|
370
|
+
const mo = await device.getVideoClipThumbnail(fileId);
|
|
371
|
+
|
|
372
|
+
// Convert to buffer
|
|
373
|
+
const buffer = await sdk.mediaManager.convertMediaObjectToBuffer(
|
|
374
|
+
mo,
|
|
375
|
+
"image/jpeg",
|
|
376
|
+
);
|
|
391
377
|
|
|
392
|
-
//
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
378
|
+
// Send image
|
|
379
|
+
response.send(buffer, {
|
|
380
|
+
code: 200,
|
|
381
|
+
headers: {
|
|
382
|
+
"Content-Type": "image/jpeg",
|
|
383
|
+
"Cache-Control": "max-age=31536000",
|
|
384
|
+
},
|
|
398
385
|
});
|
|
399
|
-
|
|
400
|
-
|
|
386
|
+
return;
|
|
387
|
+
} else {
|
|
388
|
+
response.send("Invalid webhook type", { code: 404 });
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
} catch (e: any) {
|
|
392
|
+
logger.error("Error in onRequest", e?.message || String(e));
|
|
393
|
+
response.send(`Error: ${e.message}`, {
|
|
394
|
+
code: 500,
|
|
395
|
+
});
|
|
396
|
+
return;
|
|
401
397
|
}
|
|
398
|
+
}
|
|
402
399
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
private async processThumbnailQueue(): Promise<void> {
|
|
407
|
-
if (this.thumbnailProcessing || this.thumbnailQueue.length === 0) {
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
this.thumbnailProcessing = true;
|
|
412
|
-
|
|
413
|
-
while (this.thumbnailQueue.length > 0) {
|
|
414
|
-
const request = this.thumbnailQueue.shift()!;
|
|
415
|
-
const logger = request.device?.getBaichuanLogger?.() || request.logger || console;
|
|
416
|
-
|
|
417
|
-
try {
|
|
418
|
-
const thumbnail = await extractThumbnailFromVideo(request);
|
|
419
|
-
logger.log(`[Thumbnail] Download completed: fileId=${request.fileId}`);
|
|
420
|
-
request.resolve(thumbnail);
|
|
421
|
-
} catch (error) {
|
|
422
|
-
logger.error(`[Thumbnail] Error: fileId=${request.fileId}`, error?.message || String(error));
|
|
423
|
-
request.reject(error instanceof Error ? error : new Error(String(error)));
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// Add 2 second delay between thumbnails (except after the last one)
|
|
427
|
-
if (this.thumbnailQueue.length > 0) {
|
|
428
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
this.thumbnailProcessing = false;
|
|
433
|
-
}
|
|
400
|
+
onPush(request: HttpRequest): Promise<void> {
|
|
401
|
+
return this.onRequest(request, undefined);
|
|
402
|
+
}
|
|
434
403
|
}
|
|
435
404
|
|
|
436
405
|
export default ReolinkNativePlugin;
|