@camstack/addon-provider-onvif 0.1.2 → 0.1.4
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/addon.d.mts +14 -0
- package/dist/addon.d.ts +14 -0
- package/dist/addon.js +727 -0
- package/dist/addon.js.map +1 -0
- package/dist/addon.mjs +7 -0
- package/dist/addon.mjs.map +1 -0
- package/dist/chunk-3EI55RSX.mjs +705 -0
- package/dist/chunk-3EI55RSX.mjs.map +1 -0
- package/dist/index.d.mts +3 -13
- package/dist/index.d.ts +3 -13
- package/dist/index.mjs +7 -696
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
package/dist/index.mjs
CHANGED
|
@@ -1,699 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
this.password = password;
|
|
9
|
-
this.logger = logger;
|
|
10
|
-
}
|
|
11
|
-
cam = null;
|
|
12
|
-
/** Connect to the camera */
|
|
13
|
-
async connect() {
|
|
14
|
-
return new Promise((resolve, reject) => {
|
|
15
|
-
this.cam = new Cam(
|
|
16
|
-
{
|
|
17
|
-
hostname: this.host,
|
|
18
|
-
port: this.port,
|
|
19
|
-
username: this.username,
|
|
20
|
-
password: this.password
|
|
21
|
-
},
|
|
22
|
-
(err) => {
|
|
23
|
-
if (err) {
|
|
24
|
-
reject(err);
|
|
25
|
-
} else {
|
|
26
|
-
this.logger.info(`Connected to ONVIF camera at ${this.host}:${this.port}`);
|
|
27
|
-
resolve();
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
);
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
/** Get device info */
|
|
34
|
-
async getDeviceInfo() {
|
|
35
|
-
return new Promise((resolve, reject) => {
|
|
36
|
-
if (!this.cam) return reject(new Error("Not connected"));
|
|
37
|
-
this.cam.getDeviceInformation((err, info) => {
|
|
38
|
-
if (err) reject(err);
|
|
39
|
-
else resolve(info);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
/** Get RTSP stream URI */
|
|
44
|
-
async getStreamUri(profileToken) {
|
|
45
|
-
return new Promise((resolve, reject) => {
|
|
46
|
-
if (!this.cam) return reject(new Error("Not connected"));
|
|
47
|
-
const options = { protocol: "RTSP" };
|
|
48
|
-
if (profileToken) options.profileToken = profileToken;
|
|
49
|
-
this.cam.getStreamUri(options, (err, stream) => {
|
|
50
|
-
if (err) reject(err);
|
|
51
|
-
else resolve(stream.uri);
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
/** Get available media profiles */
|
|
56
|
-
async getProfiles() {
|
|
57
|
-
return new Promise((resolve, reject) => {
|
|
58
|
-
if (!this.cam) return reject(new Error("Not connected"));
|
|
59
|
-
this.cam.getProfiles((err, profiles) => {
|
|
60
|
-
if (err) reject(err);
|
|
61
|
-
else
|
|
62
|
-
resolve(
|
|
63
|
-
profiles.map((p) => ({
|
|
64
|
-
token: p.$.token ?? p.token,
|
|
65
|
-
name: p.name,
|
|
66
|
-
videoWidth: p.videoEncoderConfiguration?.resolution?.width,
|
|
67
|
-
videoHeight: p.videoEncoderConfiguration?.resolution?.height
|
|
68
|
-
}))
|
|
69
|
-
);
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
/** Get snapshot URI */
|
|
74
|
-
async getSnapshotUri(profileToken) {
|
|
75
|
-
return new Promise((resolve, reject) => {
|
|
76
|
-
if (!this.cam) return reject(new Error("Not connected"));
|
|
77
|
-
const options = {};
|
|
78
|
-
if (profileToken) options.profileToken = profileToken;
|
|
79
|
-
this.cam.getSnapshotUri(options, (err, res) => {
|
|
80
|
-
if (err) reject(err);
|
|
81
|
-
else resolve(res.uri);
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
/** Check if PTZ is supported */
|
|
86
|
-
hasPtz() {
|
|
87
|
-
return this.cam?.ptzUri != null;
|
|
88
|
-
}
|
|
89
|
-
/** PTZ move (continuous) */
|
|
90
|
-
async ptzMove(options) {
|
|
91
|
-
return new Promise((resolve, reject) => {
|
|
92
|
-
if (!this.cam) return reject(new Error("Not connected"));
|
|
93
|
-
this.cam.continuousMove(
|
|
94
|
-
{
|
|
95
|
-
x: options.x ?? 0,
|
|
96
|
-
y: options.y ?? 0,
|
|
97
|
-
zoom: options.zoom ?? 0,
|
|
98
|
-
timeout: 1e3
|
|
99
|
-
},
|
|
100
|
-
(err) => {
|
|
101
|
-
if (err) reject(err);
|
|
102
|
-
else resolve();
|
|
103
|
-
}
|
|
104
|
-
);
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
/** PTZ stop */
|
|
108
|
-
async ptzStop() {
|
|
109
|
-
return new Promise((resolve, reject) => {
|
|
110
|
-
if (!this.cam) return reject(new Error("Not connected"));
|
|
111
|
-
this.cam.stop({}, (err) => {
|
|
112
|
-
if (err) reject(err);
|
|
113
|
-
else resolve();
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
/** PTZ absolute move */
|
|
118
|
-
async ptzAbsoluteMove(options) {
|
|
119
|
-
return new Promise((resolve, reject) => {
|
|
120
|
-
if (!this.cam) return reject(new Error("Not connected"));
|
|
121
|
-
this.cam.absoluteMove(
|
|
122
|
-
{
|
|
123
|
-
x: options.x,
|
|
124
|
-
y: options.y,
|
|
125
|
-
zoom: options.zoom
|
|
126
|
-
},
|
|
127
|
-
(err) => {
|
|
128
|
-
if (err) reject(err);
|
|
129
|
-
else resolve();
|
|
130
|
-
}
|
|
131
|
-
);
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
/** Get PTZ presets */
|
|
135
|
-
async getPtzPresets() {
|
|
136
|
-
return new Promise((resolve, reject) => {
|
|
137
|
-
if (!this.cam) return reject(new Error("Not connected"));
|
|
138
|
-
this.cam.getPresets({}, (err, presets) => {
|
|
139
|
-
if (err) reject(err);
|
|
140
|
-
else {
|
|
141
|
-
const list = Array.isArray(presets) ? presets : [presets];
|
|
142
|
-
resolve(
|
|
143
|
-
list.filter(Boolean).map((p) => ({
|
|
144
|
-
token: p.$.token ?? String(p.token),
|
|
145
|
-
name: p.name ?? p.$.token ?? "Preset"
|
|
146
|
-
}))
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
/** Go to PTZ preset */
|
|
153
|
-
async gotoPreset(presetToken) {
|
|
154
|
-
return new Promise((resolve, reject) => {
|
|
155
|
-
if (!this.cam) return reject(new Error("Not connected"));
|
|
156
|
-
this.cam.gotoPreset({ preset: presetToken }, (err) => {
|
|
157
|
-
if (err) reject(err);
|
|
158
|
-
else resolve();
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
disconnect() {
|
|
163
|
-
this.cam = null;
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
// src/onvif-device.ts
|
|
168
|
-
import { DeviceType } from "@camstack/types";
|
|
169
|
-
var OnvifDevice = class {
|
|
170
|
-
constructor(client, config, ctx) {
|
|
171
|
-
this.client = client;
|
|
172
|
-
this.config = config;
|
|
173
|
-
this.id = `${config.providerId}/${config.cameraId}`;
|
|
174
|
-
this.name = config.cameraName;
|
|
175
|
-
this.providerId = config.providerId;
|
|
176
|
-
this.ctx = ctx;
|
|
177
|
-
const caps = ["camera"];
|
|
178
|
-
if (config.hasPtz) caps.push("panTiltZoom");
|
|
179
|
-
this.capabilities = caps;
|
|
180
|
-
this.capabilityMap.set("camera", this.createCamera());
|
|
181
|
-
if (config.hasPtz) this.capabilityMap.set("panTiltZoom", this.createPtz());
|
|
182
|
-
}
|
|
183
|
-
id;
|
|
184
|
-
name;
|
|
185
|
-
providerId;
|
|
186
|
-
type = DeviceType.Camera;
|
|
187
|
-
capabilities;
|
|
188
|
-
ctx;
|
|
189
|
-
capabilityMap = /* @__PURE__ */ new Map();
|
|
190
|
-
getCapability(cap) {
|
|
191
|
-
return this.capabilityMap.get(cap) ?? null;
|
|
192
|
-
}
|
|
193
|
-
hasCapability(cap) {
|
|
194
|
-
return this.capabilityMap.has(cap);
|
|
195
|
-
}
|
|
196
|
-
getState() {
|
|
197
|
-
return { online: true };
|
|
198
|
-
}
|
|
199
|
-
getMetadata() {
|
|
200
|
-
return {
|
|
201
|
-
manufacturer: this.config.manufacturer ?? "ONVIF",
|
|
202
|
-
model: this.config.model,
|
|
203
|
-
firmware: this.config.firmware
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
createCamera() {
|
|
207
|
-
const { config } = this;
|
|
208
|
-
return {
|
|
209
|
-
kind: "camera",
|
|
210
|
-
async getSnapshot() {
|
|
211
|
-
if (config.snapshotUrl) {
|
|
212
|
-
const res = await fetch(config.snapshotUrl);
|
|
213
|
-
return Buffer.from(await res.arrayBuffer());
|
|
214
|
-
}
|
|
215
|
-
return Buffer.alloc(0);
|
|
216
|
-
},
|
|
217
|
-
async getStreamOptions() {
|
|
218
|
-
const options = [
|
|
219
|
-
{
|
|
220
|
-
id: `${config.cameraId}_main`,
|
|
221
|
-
label: "Main",
|
|
222
|
-
protocol: "rtsp",
|
|
223
|
-
quality: "main",
|
|
224
|
-
url: config.rtspUrl
|
|
225
|
-
}
|
|
226
|
-
];
|
|
227
|
-
if (config.subStreamUrl) {
|
|
228
|
-
options.push({
|
|
229
|
-
id: `${config.cameraId}_sub`,
|
|
230
|
-
label: "Sub",
|
|
231
|
-
protocol: "rtsp",
|
|
232
|
-
quality: "sub",
|
|
233
|
-
url: config.subStreamUrl
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
return options;
|
|
237
|
-
},
|
|
238
|
-
async getStreamUrl(option) {
|
|
239
|
-
return option.url ?? config.rtspUrl;
|
|
240
|
-
},
|
|
241
|
-
getConnectionMode() {
|
|
242
|
-
return "on-demand";
|
|
243
|
-
},
|
|
244
|
-
async setConnectionMode() {
|
|
245
|
-
}
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
createPtz() {
|
|
249
|
-
const { client } = this;
|
|
250
|
-
return {
|
|
251
|
-
kind: "panTiltZoom",
|
|
252
|
-
async move(cmd) {
|
|
253
|
-
await client.ptzMove({ x: cmd.pan, y: cmd.tilt, zoom: cmd.zoom, speed: cmd.speed });
|
|
254
|
-
},
|
|
255
|
-
async continuousMove(cmd) {
|
|
256
|
-
await client.ptzMove({ x: cmd.pan, y: cmd.tilt, zoom: cmd.zoom });
|
|
257
|
-
},
|
|
258
|
-
async stop() {
|
|
259
|
-
await client.ptzStop();
|
|
260
|
-
},
|
|
261
|
-
async getPresets() {
|
|
262
|
-
const presets = await client.getPtzPresets();
|
|
263
|
-
return presets.map((p) => ({ id: p.token, name: p.name }));
|
|
264
|
-
},
|
|
265
|
-
async goToPreset(presetId) {
|
|
266
|
-
await client.gotoPreset(presetId);
|
|
267
|
-
},
|
|
268
|
-
async goHome() {
|
|
269
|
-
await client.ptzAbsoluteMove({ x: 0, y: 0, zoom: 0 });
|
|
270
|
-
},
|
|
271
|
-
async getPosition() {
|
|
272
|
-
return { pan: 0, tilt: 0, zoom: 0 };
|
|
273
|
-
}
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
// src/onvif-discovery.ts
|
|
279
|
-
import { Discovery } from "onvif";
|
|
280
|
-
async function discoverOnvifCameras(timeout = 5e3) {
|
|
281
|
-
return new Promise((resolve) => {
|
|
282
|
-
const cameras = [];
|
|
283
|
-
Discovery.on("device", (cam) => {
|
|
284
|
-
cameras.push({
|
|
285
|
-
host: cam.hostname,
|
|
286
|
-
port: cam.port ?? 80,
|
|
287
|
-
name: cam.name,
|
|
288
|
-
manufacturer: cam.manufacturer,
|
|
289
|
-
model: cam.model,
|
|
290
|
-
scopes: cam.scopes
|
|
291
|
-
});
|
|
292
|
-
});
|
|
293
|
-
Discovery.probe({ timeout });
|
|
294
|
-
setTimeout(() => {
|
|
295
|
-
Discovery.removeAllListeners("device");
|
|
296
|
-
resolve(cameras);
|
|
297
|
-
}, timeout + 500);
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// src/element-config-store.ts
|
|
302
|
-
var ElementConfigStore = class {
|
|
303
|
-
constructor(elementId, storage) {
|
|
304
|
-
this.elementId = elementId;
|
|
305
|
-
this.storage = storage;
|
|
306
|
-
}
|
|
307
|
-
cache = {};
|
|
308
|
-
listeners = /* @__PURE__ */ new Set();
|
|
309
|
-
loaded = false;
|
|
310
|
-
/** Load config from storage into cache. Called once on first access. */
|
|
311
|
-
async ensureLoaded() {
|
|
312
|
-
if (this.loaded) return;
|
|
313
|
-
if (!this.storage.structured) {
|
|
314
|
-
this.loaded = true;
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
try {
|
|
318
|
-
const records = await this.storage.structured.query("config", {
|
|
319
|
-
where: { id: this.elementId },
|
|
320
|
-
limit: 1
|
|
321
|
-
});
|
|
322
|
-
if (records.length > 0) {
|
|
323
|
-
this.cache = records[0].data ?? {};
|
|
324
|
-
}
|
|
325
|
-
} catch {
|
|
326
|
-
}
|
|
327
|
-
this.loaded = true;
|
|
328
|
-
}
|
|
329
|
-
getAll() {
|
|
330
|
-
return { ...this.cache };
|
|
331
|
-
}
|
|
332
|
-
get(key) {
|
|
333
|
-
const parts = key.split(".");
|
|
334
|
-
let current = this.cache;
|
|
335
|
-
for (const part of parts) {
|
|
336
|
-
if (current == null || typeof current !== "object") return void 0;
|
|
337
|
-
current = current[part];
|
|
338
|
-
}
|
|
339
|
-
return current;
|
|
340
|
-
}
|
|
341
|
-
async set(key, value) {
|
|
342
|
-
await this.ensureLoaded();
|
|
343
|
-
setNestedValue(this.cache, key, value);
|
|
344
|
-
await this.persist();
|
|
345
|
-
this.notifyListeners();
|
|
346
|
-
}
|
|
347
|
-
async setAll(config) {
|
|
348
|
-
await this.ensureLoaded();
|
|
349
|
-
this.cache = { ...config };
|
|
350
|
-
await this.persist();
|
|
351
|
-
this.notifyListeners();
|
|
352
|
-
}
|
|
353
|
-
onChange(callback) {
|
|
354
|
-
this.listeners.add(callback);
|
|
355
|
-
return () => {
|
|
356
|
-
this.listeners.delete(callback);
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
/** Initialize from storage — called by ContextFactory after creation */
|
|
360
|
-
async load() {
|
|
361
|
-
await this.ensureLoaded();
|
|
362
|
-
}
|
|
363
|
-
/** Initialize with default values (doesn't overwrite existing) */
|
|
364
|
-
async loadDefaults(defaults) {
|
|
365
|
-
await this.ensureLoaded();
|
|
366
|
-
if (Object.keys(this.cache).length === 0) {
|
|
367
|
-
this.cache = { ...defaults };
|
|
368
|
-
await this.persist();
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
async persist() {
|
|
372
|
-
if (!this.storage.structured) return;
|
|
373
|
-
try {
|
|
374
|
-
const existing = await this.storage.structured.query("config", {
|
|
375
|
-
where: { id: this.elementId },
|
|
376
|
-
limit: 1
|
|
377
|
-
});
|
|
378
|
-
if (existing.length > 0) {
|
|
379
|
-
await this.storage.structured.update("config", this.elementId, this.cache);
|
|
380
|
-
} else {
|
|
381
|
-
await this.storage.structured.insert({
|
|
382
|
-
collection: "config",
|
|
383
|
-
id: this.elementId,
|
|
384
|
-
data: this.cache
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
} catch {
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
notifyListeners() {
|
|
391
|
-
const snapshot = this.getAll();
|
|
392
|
-
for (const listener of this.listeners) {
|
|
393
|
-
try {
|
|
394
|
-
listener(snapshot);
|
|
395
|
-
} catch {
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
};
|
|
400
|
-
function setNestedValue(obj, path, value) {
|
|
401
|
-
const parts = path.split(".");
|
|
402
|
-
let current = obj;
|
|
403
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
404
|
-
const part = parts[i];
|
|
405
|
-
if (!(part in current) || typeof current[part] !== "object" || current[part] === null) {
|
|
406
|
-
current[part] = {};
|
|
407
|
-
}
|
|
408
|
-
current = current[part];
|
|
409
|
-
}
|
|
410
|
-
current[parts[parts.length - 1]] = value;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// src/onvif-provider.ts
|
|
414
|
-
var OnvifProvider = class {
|
|
415
|
-
constructor(config, ctx) {
|
|
416
|
-
this.config = config;
|
|
417
|
-
this.id = config.id;
|
|
418
|
-
this.name = config.name;
|
|
419
|
-
this.ctx = ctx;
|
|
420
|
-
}
|
|
421
|
-
id;
|
|
422
|
-
type = "onvif";
|
|
423
|
-
name;
|
|
424
|
-
discoveryMode = "both";
|
|
425
|
-
ctx;
|
|
426
|
-
devices = [];
|
|
427
|
-
clients = /* @__PURE__ */ new Map();
|
|
428
|
-
lastDiscoveredCameras = [];
|
|
429
|
-
async start() {
|
|
430
|
-
if (this.config.discovery?.enabled !== false) {
|
|
431
|
-
const timeout = this.config.discovery?.timeout ?? 5e3;
|
|
432
|
-
this.ctx.logger.info(`Starting ONVIF discovery (timeout: ${timeout}ms)`);
|
|
433
|
-
const discovered = await discoverOnvifCameras(timeout);
|
|
434
|
-
this.ctx.logger.info(`Discovered ${discovered.length} ONVIF camera(s)`);
|
|
435
|
-
for (const cam of discovered) {
|
|
436
|
-
await this.addCamera({
|
|
437
|
-
id: cam.host.replace(/\./g, "-"),
|
|
438
|
-
name: cam.name ?? cam.host,
|
|
439
|
-
host: cam.host,
|
|
440
|
-
port: cam.port,
|
|
441
|
-
username: this.config.defaultUsername,
|
|
442
|
-
password: this.config.defaultPassword
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
for (const cam of this.config.cameras ?? []) {
|
|
447
|
-
await this.addCamera(cam);
|
|
448
|
-
}
|
|
449
|
-
await this.loadAdoptedCameras();
|
|
450
|
-
this.ctx.logger.info(
|
|
451
|
-
`ONVIF provider started with ${this.devices.length} camera(s)`
|
|
452
|
-
);
|
|
453
|
-
}
|
|
454
|
-
async addCamera(cam) {
|
|
455
|
-
try {
|
|
456
|
-
const client = new OnvifClient(
|
|
457
|
-
cam.host,
|
|
458
|
-
cam.port ?? 80,
|
|
459
|
-
cam.username ?? "",
|
|
460
|
-
cam.password ?? "",
|
|
461
|
-
this.ctx.logger.child(cam.name)
|
|
462
|
-
);
|
|
463
|
-
await client.connect();
|
|
464
|
-
const info = await client.getDeviceInfo();
|
|
465
|
-
const profiles = await client.getProfiles();
|
|
466
|
-
const mainProfile = profiles[0];
|
|
467
|
-
const subProfile = profiles.length > 1 ? profiles[1] : void 0;
|
|
468
|
-
const rtspUrl = cam.rtspUrl ?? await client.getStreamUri(mainProfile?.token);
|
|
469
|
-
const subStreamUrl = subProfile ? await client.getStreamUri(subProfile.token) : void 0;
|
|
470
|
-
let snapshotUrl;
|
|
471
|
-
try {
|
|
472
|
-
snapshotUrl = await client.getSnapshotUri(mainProfile?.token);
|
|
473
|
-
} catch {
|
|
474
|
-
}
|
|
475
|
-
const deviceCtx = {
|
|
476
|
-
id: `device:${this.id}/${cam.id}`,
|
|
477
|
-
logger: this.ctx.logger.child(cam.name),
|
|
478
|
-
eventBus: this.ctx.eventBus,
|
|
479
|
-
storage: this.ctx.storage,
|
|
480
|
-
config: new ElementConfigStore(`device:${this.id}/${cam.id}`, this.ctx.storage)
|
|
481
|
-
};
|
|
482
|
-
const device = new OnvifDevice(
|
|
483
|
-
client,
|
|
484
|
-
{
|
|
485
|
-
cameraId: cam.id,
|
|
486
|
-
cameraName: cam.name,
|
|
487
|
-
providerId: this.id,
|
|
488
|
-
rtspUrl,
|
|
489
|
-
subStreamUrl,
|
|
490
|
-
snapshotUrl,
|
|
491
|
-
hasPtz: client.hasPtz(),
|
|
492
|
-
profiles,
|
|
493
|
-
manufacturer: info.manufacturer,
|
|
494
|
-
model: info.model,
|
|
495
|
-
firmware: info.firmwareVersion
|
|
496
|
-
},
|
|
497
|
-
deviceCtx
|
|
498
|
-
);
|
|
499
|
-
this.devices = [...this.devices, device];
|
|
500
|
-
this.clients.set(cam.id, client);
|
|
501
|
-
this.ctx.logger.info(
|
|
502
|
-
`Camera ${cam.name} (${cam.host}) connected \u2014 PTZ: ${client.hasPtz()}, profiles: ${profiles.length}`
|
|
503
|
-
);
|
|
504
|
-
} catch (err) {
|
|
505
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
506
|
-
this.ctx.logger.warn(`Failed to connect to ${cam.host}: ${message}`);
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
async loadAdoptedCameras() {
|
|
510
|
-
const storage = this.ctx.storage;
|
|
511
|
-
if (!storage.structured) return;
|
|
512
|
-
try {
|
|
513
|
-
const records = await storage.structured.query("adopted-cameras", {});
|
|
514
|
-
for (const record of records) {
|
|
515
|
-
const cam = record.data;
|
|
516
|
-
const alreadyLoaded = this.devices.some(
|
|
517
|
-
(d) => d.id === `${this.id}/${cam.id}`
|
|
518
|
-
);
|
|
519
|
-
if (!alreadyLoaded) {
|
|
520
|
-
await this.addCamera(cam);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
if (records.length > 0) {
|
|
524
|
-
this.ctx.logger.info(`Restored ${records.length} adopted camera(s) from storage`);
|
|
525
|
-
}
|
|
526
|
-
} catch {
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
async stop() {
|
|
530
|
-
for (const client of this.clients.values()) {
|
|
531
|
-
client.disconnect();
|
|
532
|
-
}
|
|
533
|
-
this.devices = [];
|
|
534
|
-
this.clients.clear();
|
|
535
|
-
this.ctx.logger.info("ONVIF provider stopped");
|
|
536
|
-
}
|
|
537
|
-
getStatus() {
|
|
538
|
-
return { connected: true, deviceCount: this.devices.length };
|
|
539
|
-
}
|
|
540
|
-
async discoverDevices() {
|
|
541
|
-
const cameras = await discoverOnvifCameras();
|
|
542
|
-
this.lastDiscoveredCameras = cameras;
|
|
543
|
-
return cameras.map((c) => ({
|
|
544
|
-
externalId: c.host,
|
|
545
|
-
name: c.name ?? c.host,
|
|
546
|
-
type: "camera",
|
|
547
|
-
capabilities: ["camera"],
|
|
548
|
-
metadata: { manufacturer: c.manufacturer, model: c.model }
|
|
549
|
-
}));
|
|
550
|
-
}
|
|
551
|
-
async adoptDevice(externalId, config) {
|
|
552
|
-
const existingDevice = this.devices.find(
|
|
553
|
-
(d) => d.id === `${this.id}/${externalId.replace(/\./g, "-")}`
|
|
554
|
-
);
|
|
555
|
-
if (existingDevice) {
|
|
556
|
-
return existingDevice;
|
|
557
|
-
}
|
|
558
|
-
let discovered = this.lastDiscoveredCameras.find((c) => c.host === externalId);
|
|
559
|
-
if (!discovered) {
|
|
560
|
-
this.ctx.logger.info(`Camera ${externalId} not in cache, re-discovering...`);
|
|
561
|
-
const cameras = await discoverOnvifCameras();
|
|
562
|
-
this.lastDiscoveredCameras = cameras;
|
|
563
|
-
discovered = cameras.find((c) => c.host === externalId);
|
|
564
|
-
}
|
|
565
|
-
if (!discovered) {
|
|
566
|
-
throw new Error(`Camera with externalId "${externalId}" not found on network`);
|
|
567
|
-
}
|
|
568
|
-
const cameraId = externalId.replace(/\./g, "-");
|
|
569
|
-
const cameraConfig = {
|
|
570
|
-
id: cameraId,
|
|
571
|
-
name: config?.["name"] ?? discovered.name ?? externalId,
|
|
572
|
-
host: discovered.host,
|
|
573
|
-
port: discovered.port,
|
|
574
|
-
username: config?.["username"] ?? this.config.defaultUsername,
|
|
575
|
-
password: config?.["password"] ?? this.config.defaultPassword
|
|
576
|
-
};
|
|
577
|
-
await this.addCamera(cameraConfig);
|
|
578
|
-
const device = this.devices.find((d) => d.id === `${this.id}/${cameraId}`);
|
|
579
|
-
if (!device) {
|
|
580
|
-
throw new Error(`Failed to create device for camera ${externalId}`);
|
|
581
|
-
}
|
|
582
|
-
await this.persistAdoptedCamera(cameraConfig);
|
|
583
|
-
this.ctx.logger.info(`Adopted camera ${cameraConfig.name} (${externalId})`);
|
|
584
|
-
return device;
|
|
585
|
-
}
|
|
586
|
-
async persistAdoptedCamera(cam) {
|
|
587
|
-
const storage = this.ctx.storage;
|
|
588
|
-
if (!storage.structured) return;
|
|
589
|
-
try {
|
|
590
|
-
const existing = await storage.structured.query("adopted-cameras", {
|
|
591
|
-
where: { id: cam.id },
|
|
592
|
-
limit: 1
|
|
593
|
-
});
|
|
594
|
-
if (existing.length === 0) {
|
|
595
|
-
await storage.structured.insert({
|
|
596
|
-
collection: "adopted-cameras",
|
|
597
|
-
id: cam.id,
|
|
598
|
-
data: { ...cam }
|
|
599
|
-
});
|
|
600
|
-
}
|
|
601
|
-
} catch (err) {
|
|
602
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
603
|
-
this.ctx.logger.warn(`Failed to persist adopted camera ${cam.id}: ${message}`);
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
getDevices() {
|
|
607
|
-
return [...this.devices];
|
|
608
|
-
}
|
|
609
|
-
getDeviceConfigSchema() {
|
|
610
|
-
return {
|
|
611
|
-
id: "string",
|
|
612
|
-
name: "string",
|
|
613
|
-
host: "string (IP or hostname)",
|
|
614
|
-
port: "number (default: 80)",
|
|
615
|
-
username: "string (optional)",
|
|
616
|
-
password: "string (optional)",
|
|
617
|
-
rtspUrl: "string (optional, override ONVIF stream URI)"
|
|
618
|
-
};
|
|
619
|
-
}
|
|
620
|
-
subscribeLiveEvents(_callback) {
|
|
621
|
-
return () => {
|
|
622
|
-
};
|
|
623
|
-
}
|
|
624
|
-
};
|
|
625
|
-
|
|
626
|
-
// src/addon.ts
|
|
627
|
-
var OnvifProviderAddon = class {
|
|
628
|
-
manifest = {
|
|
629
|
-
id: "provider-onvif",
|
|
630
|
-
name: "ONVIF Camera Provider",
|
|
631
|
-
version: "0.1.0",
|
|
632
|
-
description: "Discovery automatica di camere ONVIF sulla rete locale",
|
|
633
|
-
capabilities: ["device-provider"]
|
|
634
|
-
};
|
|
635
|
-
provider = null;
|
|
636
|
-
async initialize(context) {
|
|
637
|
-
const config = context.addonConfig;
|
|
638
|
-
const providerConfig = {
|
|
639
|
-
id: config.id ?? "onvif-default",
|
|
640
|
-
name: config.name ?? "ONVIF Cameras",
|
|
641
|
-
discovery: config.discovery,
|
|
642
|
-
cameras: config.cameras,
|
|
643
|
-
defaultUsername: config.defaultUsername,
|
|
644
|
-
defaultPassword: config.defaultPassword
|
|
645
|
-
};
|
|
646
|
-
this.provider = new OnvifProvider(providerConfig, {
|
|
647
|
-
id: context.id,
|
|
648
|
-
logger: context.logger,
|
|
649
|
-
eventBus: context.eventBus,
|
|
650
|
-
storage: context.storage,
|
|
651
|
-
config: context.config
|
|
652
|
-
});
|
|
653
|
-
context.logger.info("ONVIF provider addon initialized");
|
|
654
|
-
}
|
|
655
|
-
async shutdown() {
|
|
656
|
-
await this.provider?.stop();
|
|
657
|
-
this.provider = null;
|
|
658
|
-
}
|
|
659
|
-
getCapabilityProvider(name) {
|
|
660
|
-
if (name === "device-provider" && this.provider) {
|
|
661
|
-
return this.provider;
|
|
662
|
-
}
|
|
663
|
-
return null;
|
|
664
|
-
}
|
|
665
|
-
getConfigSchema() {
|
|
666
|
-
return {
|
|
667
|
-
sections: [
|
|
668
|
-
{
|
|
669
|
-
id: "discovery",
|
|
670
|
-
title: "ONVIF Discovery",
|
|
671
|
-
description: "Auto-discover ONVIF cameras on the local network",
|
|
672
|
-
columns: 2,
|
|
673
|
-
fields: [
|
|
674
|
-
{ type: "boolean", key: "discovery.enabled", label: "Enable Auto-Discovery" },
|
|
675
|
-
{ type: "number", key: "discovery.timeout", label: "Discovery Timeout", unit: "ms", min: 1e3, max: 3e4, step: 1e3 }
|
|
676
|
-
]
|
|
677
|
-
},
|
|
678
|
-
{
|
|
679
|
-
id: "credentials",
|
|
680
|
-
title: "Default Credentials",
|
|
681
|
-
description: "Default credentials for discovered cameras",
|
|
682
|
-
columns: 2,
|
|
683
|
-
fields: [
|
|
684
|
-
{ type: "text", key: "defaultUsername", label: "Default Username" },
|
|
685
|
-
{ type: "password", key: "defaultPassword", label: "Default Password", showToggle: true }
|
|
686
|
-
]
|
|
687
|
-
}
|
|
688
|
-
]
|
|
689
|
-
};
|
|
690
|
-
}
|
|
691
|
-
getConfig() {
|
|
692
|
-
return {};
|
|
693
|
-
}
|
|
694
|
-
async onConfigChange(_config) {
|
|
695
|
-
}
|
|
696
|
-
};
|
|
1
|
+
import {
|
|
2
|
+
OnvifClient,
|
|
3
|
+
OnvifDevice,
|
|
4
|
+
OnvifProvider,
|
|
5
|
+
OnvifProviderAddon,
|
|
6
|
+
discoverOnvifCameras
|
|
7
|
+
} from "./chunk-3EI55RSX.mjs";
|
|
697
8
|
export {
|
|
698
9
|
OnvifClient,
|
|
699
10
|
OnvifDevice,
|