@camstack/addon-provider-rtsp 0.1.13 → 0.1.15

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.mjs CHANGED
@@ -1,7 +1,553 @@
1
- import {
2
- RtspProviderAddon
3
- } from "./chunk-2B5J5HPN.mjs";
1
+ import { BaseDevice, DeviceType, hydrateSchema, maskUrlCredentials, classifyStreams, snapshotCapability, BaseDeviceProvider, EventCategory } from "@camstack/types";
2
+ import { z } from "zod";
3
+ const streamEntrySchema = z.object({
4
+ id: z.string().regex(/^stream-\d+$/).describe("Stable slot id (stream-1/2/3)"),
5
+ url: z.string().describe("RTSP stream URL"),
6
+ profileHint: z.enum(["high", "mid", "low"]).optional().describe("Provider-suggested profile — advisory only"),
7
+ resolution: z.object({ width: z.number().int().positive(), height: z.number().int().positive() }).optional().describe("Probed resolution, if known")
8
+ });
9
+ const streamsField = z.preprocess(
10
+ (raw) => {
11
+ if (!Array.isArray(raw)) return raw;
12
+ return raw.map((entry, i) => {
13
+ if (!entry || typeof entry !== "object") return entry;
14
+ const o = entry;
15
+ if (typeof o["id"] === "string") return entry;
16
+ if (typeof o["label"] === "string" && typeof o["url"] === "string") {
17
+ return {
18
+ id: `stream-${i + 1}`,
19
+ url: o["url"],
20
+ profileHint: o["label"]
21
+ };
22
+ }
23
+ return entry;
24
+ });
25
+ },
26
+ z.array(streamEntrySchema).min(1)
27
+ );
28
+ const rtspCameraSchema = z.object({
29
+ // `name` is now base meta (DB column on `device-meta`, not in this
30
+ // hardware-config schema). Read via `this.name` (resolved by
31
+ // `BaseDevice` from `ctx.deviceMeta.name`); mutated via
32
+ // `kernel.devices.setName(id, name)`.
33
+ streams: streamsField.describe("Published RTSP streams"),
34
+ snapshotUrl: z.string().default("").describe("Snapshot URL (optional)"),
35
+ username: z.string().default("").describe("Username"),
36
+ password: z.string().default("").describe("Password")
37
+ });
38
+ const legacyStreamSchema = z.object({
39
+ label: z.enum(["high", "mid", "low"]),
40
+ url: z.string()
41
+ });
42
+ class RtspCamera extends BaseDevice {
43
+ type = DeviceType.Camera;
44
+ features = [];
45
+ constructor(ctx) {
46
+ super(
47
+ ctx,
48
+ rtspCameraSchema,
49
+ { type: DeviceType.Camera }
50
+ );
51
+ this.migrateLegacyStreamsIfNeeded(ctx.persistedConfig ?? {});
52
+ this.registerSnapshotProvider();
53
+ }
54
+ /**
55
+ * Detect legacy `{label, url}` stream shapes and reshape to
56
+ * `{id, url, profileHint}`. Persists via `config.setAll` once, so
57
+ * subsequent boots find the new shape and this branch is inert.
58
+ *
59
+ * Runs fire-and-forget — the constructor can't await. The reshape is
60
+ * additive (ids are newly minted) so a failed persist is recoverable
61
+ * on next boot.
62
+ */
63
+ migrateLegacyStreamsIfNeeded(initialData) {
64
+ const raw = initialData["streams"];
65
+ if (!Array.isArray(raw)) return;
66
+ if (raw.every((s) => s && typeof s === "object" && typeof s["id"] === "string")) {
67
+ return;
68
+ }
69
+ const migrated = [];
70
+ let nextIdx = 1;
71
+ for (const entry of raw) {
72
+ const parsed = legacyStreamSchema.safeParse(entry);
73
+ if (!parsed.success) continue;
74
+ migrated.push({
75
+ id: `stream-${nextIdx++}`,
76
+ url: parsed.data.url,
77
+ profileHint: parsed.data.label
78
+ });
79
+ }
80
+ if (migrated.length === 0) return;
81
+ const snapshotUrl = this.config.get("snapshotUrl") ?? "";
82
+ const username = this.config.get("username") ?? "";
83
+ const password = this.config.get("password") ?? "";
84
+ this.ctx.logger.info("Migrating legacy {label,url} streams to {id,url,profileHint}", {
85
+ tags: { stableId: this.stableId },
86
+ meta: { streamCount: migrated.length }
87
+ });
88
+ void this.config.setAll({ streams: migrated, snapshotUrl, username, password }).catch((err) => {
89
+ this.ctx.logger.warn("Legacy stream migration failed", {
90
+ tags: { stableId: this.stableId },
91
+ meta: { error: err instanceof Error ? err.message : String(err) }
92
+ });
93
+ });
94
+ }
95
+ /**
96
+ * Publish every configured stream to the system stream-broker as a
97
+ * `pull-rtsp` cam stream. Idempotent — the broker's publishCameraStream
98
+ * entry is keyed by `(deviceId, camStreamId)` so callers may invoke
99
+ * this at will (e.g. on stream-broker restart). Fire-and-forget safe.
100
+ */
101
+ /**
102
+ * Lifecycle hook fired by the kernel after registration. Publishes
103
+ * the camera's streams to the broker — the kernel doesn't know
104
+ * about the broker, but the camera does. Best-effort: if the
105
+ * broker isn't ready, the provider's `system.ready-state`
106
+ * subscription re-publishes on the next ready fire.
107
+ */
108
+ async onActivate() {
109
+ await this.publishToBroker().catch((err) => {
110
+ this.ctx.logger.warn("publishToBroker on activate failed — will retry on broker ready", {
111
+ meta: { error: err instanceof Error ? err.message : String(err) }
112
+ });
113
+ });
114
+ }
115
+ async publishToBroker() {
116
+ const streams = this.config.get("streams");
117
+ for (const [i, s] of streams.entries()) {
118
+ try {
119
+ await this.ctx.api.streamBroker.publishCameraStream.mutate({
120
+ deviceId: this.id,
121
+ camStreamId: s.id,
122
+ kind: "pull-rtsp",
123
+ url: s.url,
124
+ ...s.resolution ? { resolution: s.resolution } : {},
125
+ label: `Stream ${i + 1}`
126
+ });
127
+ } catch (err) {
128
+ this.ctx.logger.warn("publishCameraStream failed", {
129
+ tags: { deviceId: this.id, camStreamId: s.id },
130
+ meta: { error: err instanceof Error ? err.message : String(err) }
131
+ });
132
+ }
133
+ }
134
+ }
135
+ /**
136
+ * Thin compat shim: reflects the stored `{id, url, profileHint?}`
137
+ * streams into the legacy `StreamSourceEntry` shape so
138
+ * `ICameraDevice`-aware consumers (device-manager legacy paths,
139
+ * device-cap-proxy fallback) still find data. The broker itself does
140
+ * NOT read this anymore — it consumes `publishCameraStream`. This
141
+ * shim goes away when every consumer migrates (C5/C7).
142
+ */
143
+ async getStreamSources() {
144
+ const streams = this.config.get("streams");
145
+ return streams.map((s) => ({
146
+ id: s.id,
147
+ label: s.profileHint ?? s.id,
148
+ protocol: "rtsp",
149
+ url: s.url,
150
+ ...s.profileHint ? { profileHint: s.profileHint } : {},
151
+ ...s.resolution ? { resolution: s.resolution } : {}
152
+ }));
153
+ }
154
+ async removeDevice() {
155
+ this.ctx.logger.info("Removing RTSP camera", { meta: { stableId: this.stableId } });
156
+ const streams = this.config.get("streams");
157
+ await Promise.all(
158
+ streams.map(
159
+ (s) => this.ctx.api.streamBroker.retractCameraStream.mutate({ deviceId: this.id, camStreamId: s.id }).catch((err) => {
160
+ this.ctx.logger.warn("retractCameraStream failed", {
161
+ tags: { deviceId: this.id, camStreamId: s.id },
162
+ meta: { error: err instanceof Error ? err.message : String(err) }
163
+ });
164
+ })
165
+ )
166
+ );
167
+ }
168
+ // ── Driver-authored settings UI ────────────────────────────────────────
169
+ //
170
+ // Streams are stored as `Array<{id, url, profileHint?, resolution?}>`
171
+ // but the settings UI exposes them as a `multiple` probe field of raw
172
+ // URLs — the form only cares about the ordered list of URLs, the ids
173
+ // and classification live in storage. Values are projected from
174
+ // storage into the flat form keys referenced by the schema.
175
+ getSettingsUISchema() {
176
+ const schema = {
177
+ sections: [
178
+ // The `name` field is now base meta — rendered by the
179
+ // device-manager's own settings contribution (with the
180
+ // location field), NOT by the driver's hardware-config
181
+ // schema. Driver UIs only carry technical knobs.
182
+ {
183
+ id: "streams",
184
+ title: "RTSP Streams",
185
+ description: "Provide 1–3 RTSP URLs for this camera. After save, each URL is probed and the stream-broker assigns the (high / mid / low) profiles by resolution (highest pixel count → high). Input slot order in this form does not affect the broker profile mapping.",
186
+ columns: 1,
187
+ fields: [
188
+ {
189
+ type: "probe",
190
+ key: "streams",
191
+ label: "RTSP stream URL",
192
+ required: true,
193
+ placeholder: "rtsp://user:pass@host:554/Streaming/Channels/101",
194
+ inputType: "url",
195
+ multiple: {
196
+ min: 1,
197
+ max: 3,
198
+ addLabel: "Add stream",
199
+ itemLabel: "Stream ${n}",
200
+ itemDefault: ""
201
+ }
202
+ }
203
+ ]
204
+ },
205
+ {
206
+ id: "snapshot",
207
+ title: "Snapshot",
208
+ columns: 1,
209
+ fields: [
210
+ { type: "probe", key: "snapshotUrl", label: "Snapshot URL", description: "Optional HTTP(S) endpoint that returns a JPEG still.", placeholder: "http://host/ISAPI/Streaming/channels/101/picture", inputType: "url" }
211
+ ]
212
+ },
213
+ {
214
+ id: "credentials",
215
+ title: "Credentials",
216
+ description: "Optional. If the RTSP URL already contains user:pass@ you can leave these empty.",
217
+ columns: 2,
218
+ fields: [
219
+ { type: "text", key: "username", label: "Username" },
220
+ { type: "password", key: "password", label: "Password", showToggle: true }
221
+ ]
222
+ }
223
+ ]
224
+ };
225
+ return hydrateSchema(schema, this.collectFormValues());
226
+ }
227
+ /**
228
+ * Project stored config into the flat UI keys referenced by the schema.
229
+ * Form only sees URLs — the internal id/profileHint/resolution stay
230
+ * invisible to the operator (the broker decides profiles from probed
231
+ * metadata at save time).
232
+ */
233
+ collectFormValues() {
234
+ const stored = this.config.get("streams");
235
+ const sorted = [...stored].sort((a, b) => a.id.localeCompare(b.id, "en", { numeric: true }));
236
+ const urls = sorted.map((s) => s.url).filter((u) => u.length > 0);
237
+ return {
238
+ streams: urls,
239
+ snapshotUrl: this.config.get("snapshotUrl"),
240
+ username: this.config.get("username"),
241
+ password: this.config.get("password")
242
+ };
243
+ }
244
+ async applySettingsPatch(patch) {
245
+ const current = this.collectFormValues();
246
+ const merged = { ...current, ...patch };
247
+ const pickStr = (k) => typeof merged[k] === "string" ? merged[k].trim() : "";
248
+ const next = {};
249
+ const rawStreams = merged["streams"];
250
+ const incomingUrls = Array.isArray(rawStreams) ? rawStreams.filter((s) => typeof s === "string").map((s) => s.trim()).filter((s) => s.length > 0) : [];
251
+ if (incomingUrls.length === 0) {
252
+ next.streams = this.config.get("streams");
253
+ } else {
254
+ const probed = await this.probeAndClassify(incomingUrls.slice(0, 3));
255
+ next.streams = probed;
256
+ }
257
+ next.snapshotUrl = pickStr("snapshotUrl");
258
+ next.username = pickStr("username");
259
+ next.password = typeof merged["password"] === "string" ? merged["password"] : this.config.get("password");
260
+ await this.config.setAll(next);
261
+ await this.publishToBroker();
262
+ }
263
+ /**
264
+ * Probe a list of RTSP URLs through the kernel stream-probe and return
265
+ * the new `{id, url, profileHint?, resolution?}` entries. Ids are
266
+ * assigned `stream-1/2/3` in input order so the UI's slot ordering is
267
+ * preserved across edits. The broker is in charge of the actual
268
+ * (high/mid/low) profile assignment via `computeInitialAssignment`.
269
+ */
270
+ async probeAndClassify(urls) {
271
+ const streamProbe = this.ctx.streamProbe;
272
+ if (!streamProbe) {
273
+ this.ctx.logger.warn(
274
+ "[rtsp-edit] ctx.streamProbe unavailable — falling back to input-order profile assignment",
275
+ { meta: { streamCount: urls.length } }
276
+ );
277
+ const sequence = urls.length === 1 ? ["high"] : urls.length === 2 ? ["high", "low"] : ["high", "mid", "low"];
278
+ return urls.map((url, i) => ({
279
+ id: `stream-${i + 1}`,
280
+ url,
281
+ profileHint: sequence[i]
282
+ }));
283
+ }
284
+ const probed = await Promise.all(
285
+ urls.map(async (url) => {
286
+ try {
287
+ const metadata = await streamProbe.probe(url, { force: false });
288
+ return { url, metadata };
289
+ } catch (err) {
290
+ this.ctx.logger.warn(
291
+ "[rtsp-edit] probe failed — landing in the lowest tier via classifyStreams",
292
+ { meta: { url: maskUrlCredentials(url), error: err instanceof Error ? err.message : String(err) } }
293
+ );
294
+ const emptyMetadata = {};
295
+ return { url, metadata: emptyMetadata };
296
+ }
297
+ })
298
+ );
299
+ const classified = classifyStreams(probed);
300
+ const metadataByUrl = new Map(probed.map((p) => [p.url, p.metadata]));
301
+ const classifiedByUrl = new Map(classified.map((c) => [c.url, c]));
302
+ return urls.map((url, i) => {
303
+ const tag = classifiedByUrl.get(url);
304
+ const meta = metadataByUrl.get(url);
305
+ const entry = { id: `stream-${i + 1}`, url };
306
+ if (tag) entry.profileHint = tag.quality;
307
+ if (meta?.width && meta?.height) entry.resolution = { width: meta.width, height: meta.height };
308
+ return entry;
309
+ });
310
+ }
311
+ // ── Native capabilities ────────────────────────────────────────────────
312
+ registerSnapshotProvider() {
313
+ const snapshotUrl = (this.config.get("snapshotUrl") ?? "").trim();
314
+ if (!snapshotUrl) return;
315
+ const provider = {
316
+ getSnapshot: async ({ deviceId }) => {
317
+ if (deviceId !== this.id) {
318
+ throw new Error(`RtspCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
319
+ }
320
+ const buf = await this.fetchHttpSnapshot(snapshotUrl);
321
+ if (buf.length === 0) return null;
322
+ return { base64: buf.toString("base64"), contentType: "image/jpeg" };
323
+ },
324
+ invalidateCache: async () => {
325
+ }
326
+ };
327
+ this.ctx.registerNativeCap(snapshotCapability, provider);
328
+ }
329
+ async fetchHttpSnapshot(url) {
330
+ const username = (this.config.get("username") ?? "").trim();
331
+ const password = (this.config.get("password") ?? "").trim();
332
+ const headers = {};
333
+ if (username || password) {
334
+ const creds = Buffer.from(`${username}:${password}`).toString("base64");
335
+ headers["authorization"] = `Basic ${creds}`;
336
+ }
337
+ const res = await fetch(url, { headers });
338
+ if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
339
+ const ab = await res.arrayBuffer();
340
+ return Buffer.from(ab);
341
+ }
342
+ }
343
+ function getString(obj, key) {
344
+ const v = obj[key];
345
+ return typeof v === "string" ? v : "";
346
+ }
347
+ function getStringArray(obj, key) {
348
+ const v = obj[key];
349
+ if (!Array.isArray(v)) return [];
350
+ return v.filter((s) => typeof s === "string").map((s) => s.trim()).filter((s) => s.length > 0);
351
+ }
352
+ function buildCreationFormSchema() {
353
+ return {
354
+ sections: [
355
+ {
356
+ id: "identity",
357
+ title: "Camera",
358
+ columns: 1,
359
+ fields: [
360
+ { type: "text", key: "name", label: "Name", required: true, placeholder: "Living room" }
361
+ ]
362
+ },
363
+ {
364
+ id: "streams",
365
+ title: "RTSP Streams",
366
+ description: "Provide 1–3 RTSP URLs for this camera. Each URL is probed and published to the stream-broker; the broker assigns the (high / mid / low) profiles by resolution — you do not need to label the streams manually.",
367
+ columns: 1,
368
+ fields: [
369
+ {
370
+ type: "probe",
371
+ key: "streams",
372
+ label: "RTSP stream URL",
373
+ required: true,
374
+ placeholder: "rtsp://user:pass@host:554/Streaming/Channels/101",
375
+ inputType: "url",
376
+ multiple: {
377
+ min: 1,
378
+ max: 3,
379
+ addLabel: "Add stream",
380
+ itemLabel: "Stream ${n}",
381
+ itemDefault: ""
382
+ }
383
+ }
384
+ ]
385
+ },
386
+ {
387
+ id: "snapshot",
388
+ title: "Snapshot",
389
+ columns: 1,
390
+ fields: [
391
+ { type: "probe", key: "snapshotUrl", label: "Snapshot URL", description: "Optional HTTP(S) endpoint that returns a JPEG still.", placeholder: "http://host/ISAPI/Streaming/channels/101/picture", inputType: "url" }
392
+ ]
393
+ },
394
+ {
395
+ id: "credentials",
396
+ title: "Credentials",
397
+ description: "Optional. If the RTSP URL already contains user:pass@ you can leave these empty.",
398
+ columns: 2,
399
+ fields: [
400
+ { type: "text", key: "username", label: "Username" },
401
+ { type: "password", key: "password", label: "Password", showToggle: true }
402
+ ]
403
+ }
404
+ ]
405
+ };
406
+ }
407
+ function isStreamBrokerReady(event) {
408
+ if (event.category !== "system.ready-state") return false;
409
+ const data = event.data;
410
+ return data["capName"] === "stream-broker" && data["state"] === "ready";
411
+ }
412
+ class RtspProviderAddon extends BaseDeviceProvider {
413
+ addonId = "provider-rtsp";
414
+ providerName = "RTSP";
415
+ deviceClasses = {
416
+ [DeviceType.Camera]: RtspCamera
417
+ };
418
+ constructor() {
419
+ super({});
420
+ }
421
+ async onInitialize() {
422
+ const regs = await super.onInitialize();
423
+ this.subscribe(
424
+ { category: EventCategory.DeviceStateChanged },
425
+ (event) => {
426
+ const data = event.data;
427
+ if (data.capName !== "camera-streams") return;
428
+ const deviceId = data.deviceId;
429
+ if (typeof deviceId !== "number") return;
430
+ const registry = this.ctx.kernel.deviceRegistry;
431
+ if (!registry) return;
432
+ if (registry.getAddonId(deviceId) !== this.addonId) return;
433
+ const device = registry.getById(deviceId);
434
+ if (!device) return;
435
+ const online = data.slice?.online === true;
436
+ if (device.online !== online) device.online = online;
437
+ }
438
+ );
439
+ this.subscribe(
440
+ { category: EventCategory.SystemReadyState },
441
+ (event) => {
442
+ if (!isStreamBrokerReady(event)) return;
443
+ void this.republishAll().catch((err) => {
444
+ this.ctx.logger.warn("Failed to re-publish RTSP streams after broker ready", {
445
+ meta: { error: err instanceof Error ? err.message : String(err) }
446
+ });
447
+ });
448
+ }
449
+ );
450
+ return regs;
451
+ }
452
+ // ── Creation ─────────────────────────────────────────────────────────
453
+ async onGetCreationSchema(type) {
454
+ if (type !== DeviceType.Camera) return null;
455
+ return buildCreationFormSchema();
456
+ }
457
+ async onCreateDevice(type, config) {
458
+ if (type !== DeviceType.Camera) {
459
+ throw new Error(`RTSP provider does not support device type: ${type}`);
460
+ }
461
+ const name = getString(config, "name").trim();
462
+ if (!name) throw new Error("Camera name is required");
463
+ const rawStreams = getStringArray(config, "streams");
464
+ if (rawStreams.length === 0) throw new Error("At least one RTSP stream URL is required");
465
+ if (rawStreams.length > 3) throw new Error("At most 3 RTSP stream URLs are supported");
466
+ const streamProbe = this.ctx.kernel.streamProbe;
467
+ if (!streamProbe) throw new Error("ctx.kernel.streamProbe unavailable — cannot probe streams");
468
+ const probed = await Promise.all(
469
+ rawStreams.map(async (url) => {
470
+ try {
471
+ const metadata = await streamProbe.probe(url, { force: false });
472
+ return { url, metadata, ok: true };
473
+ } catch (err) {
474
+ this.ctx.logger.warn(
475
+ "[rtsp-create] probe failed — publishing without probed metadata",
476
+ { meta: { url: maskUrlCredentials(url), error: err instanceof Error ? err.message : String(err) } }
477
+ );
478
+ return { url, metadata: void 0, ok: false };
479
+ }
480
+ })
481
+ );
482
+ const classified = classifyStreams(
483
+ probed.map((p) => ({ url: p.url, metadata: p.metadata ?? {} }))
484
+ );
485
+ const hintByUrl = new Map(classified.map((c) => [c.url, c.quality]));
486
+ const streams = probed.map((p, i) => {
487
+ const entry = { id: `stream-${i + 1}`, url: p.url };
488
+ const hint = hintByUrl.get(p.url);
489
+ if (hint) entry.profileHint = hint;
490
+ if (p.metadata?.width && p.metadata?.height) {
491
+ entry.resolution = { width: p.metadata.width, height: p.metadata.height };
492
+ }
493
+ return entry;
494
+ });
495
+ const parsed = rtspCameraSchema.parse({
496
+ streams,
497
+ snapshotUrl: getString(config, "snapshotUrl"),
498
+ username: getString(config, "username"),
499
+ password: getString(config, "password")
500
+ });
501
+ return {
502
+ meta: { type: DeviceType.Camera, name },
503
+ config: parsed
504
+ };
505
+ }
506
+ // ── Field probing ────────────────────────────────────────────────────
507
+ async testCreationField(input) {
508
+ if (input.type !== DeviceType.Camera) {
509
+ return { status: "error", error: `Unsupported device type: ${input.type}` };
510
+ }
511
+ const streamProbe = this.ctx.kernel.streamProbe;
512
+ if (!streamProbe) {
513
+ return { status: "error", error: "ctx.kernel.streamProbe unavailable — cannot reach kernel probe" };
514
+ }
515
+ return streamProbe.probeField(input.key, input.value);
516
+ }
517
+ // ── Restore ──────────────────────────────────────────────────────────
518
+ //
519
+ // `BaseDeviceProvider.onRestoreDevices` default impl iterates
520
+ // `savedDevices`, looks up the right class via `deviceClasses`,
521
+ // and calls `kernel.devices.create()`. Each created device fires
522
+ // its `onCreated` lifecycle hook (see `RtspCamera.onCreated()`)
523
+ // which publishes streams to the broker — no per-provider override
524
+ // needed here.
525
+ // ── Internal — re-publish every RtspCamera to the broker ────────────
526
+ async republishAll() {
527
+ const all = await this.ctx.kernel.devices?.getAll() ?? [];
528
+ let published = 0;
529
+ for (const dev of all) {
530
+ if (!(dev instanceof RtspCamera)) continue;
531
+ try {
532
+ await dev.publishToBroker();
533
+ published++;
534
+ } catch (err) {
535
+ this.ctx.logger.debug("publishToBroker threw during republish", {
536
+ tags: { deviceId: dev.id },
537
+ meta: { error: err instanceof Error ? err.message : String(err) }
538
+ });
539
+ }
540
+ }
541
+ if (published > 0) {
542
+ this.ctx.logger.info("Re-published RTSP streams to stream-broker", {
543
+ meta: { published, total: all.length }
544
+ });
545
+ }
546
+ }
547
+ }
4
548
  export {
5
- RtspProviderAddon
549
+ RtspCamera as R,
550
+ RtspProviderAddon,
551
+ rtspCameraSchema as r
6
552
  };
7
- //# sourceMappingURL=addon.mjs.map
553
+ //# sourceMappingURL=addon.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
1
+ {"version":3,"file":"addon.mjs","sources":["../src/rtsp-camera.ts","../src/addon.ts"],"sourcesContent":["import { z } from \"zod\";\nimport { BaseDevice, DeviceType, DeviceFeature, classifyStreams, hydrateSchema, snapshotCapability, maskUrlCredentials } from \"@camstack/types\";\nimport type {\n ConfigUISchema,\n ConfigUISchemaWithValues,\n DeviceContext,\n ICameraDevice,\n InferNativeProvider,\n StreamMetadata,\n StreamQuality,\n StreamSourceEntry,\n} from \"@camstack/types\";\n\n/**\n * Stable, provider-assigned ids within a (deviceId) scope.\n *\n * `stream-1/2/3` map 1:1 with the operator-visible input slots in the\n * creation form. Once assigned at create time (or via legacy migration)\n * the id is permanent — editing a URL keeps its slot. Separating the id\n * from the quality lets the stream-broker decide the (high/mid/low)\n * mapping from probed metadata without the driver having to re-label.\n */\nconst streamEntrySchema = z.object({\n id: z.string().regex(/^stream-\\d+$/).describe(\"Stable slot id (stream-1/2/3)\"),\n url: z.string().describe(\"RTSP stream URL\"),\n profileHint: z.enum(['high', 'mid', 'low']).optional().describe(\"Provider-suggested profile — advisory only\"),\n resolution: z\n .object({ width: z.number().int().positive(), height: z.number().int().positive() })\n .optional()\n .describe(\"Probed resolution, if known\"),\n});\n\nexport type RtspStreamEntry = z.infer<typeof streamEntrySchema>\n\n/**\n * Accepts both the new `{id, url, profileHint?, resolution?}` shape AND\n * the legacy `{label, url}` shape. Legacy entries get rewritten to the\n * new shape at parse time so the rest of the driver can treat storage\n * uniformly. The constructor then persists the new shape back to disk\n * in `migrateLegacyStreamsIfNeeded` so subsequent parses are already\n * on the new shape.\n */\nconst streamsField = z.preprocess(\n (raw: unknown): unknown => {\n if (!Array.isArray(raw)) return raw\n return (raw as readonly unknown[]).map((entry, i): unknown => {\n if (!entry || typeof entry !== 'object') return entry\n const o = entry as Record<string, unknown>\n if (typeof o['id'] === 'string') return entry\n if (typeof o['label'] === 'string' && typeof o['url'] === 'string') {\n return {\n id: `stream-${i + 1}`,\n url: o['url'],\n profileHint: o['label'],\n }\n }\n return entry\n })\n },\n z.array(streamEntrySchema).min(1),\n)\n\nexport const rtspCameraSchema = z.object({\n // `name` is now base meta (DB column on `device-meta`, not in this\n // hardware-config schema). Read via `this.name` (resolved by\n // `BaseDevice` from `ctx.deviceMeta.name`); mutated via\n // `kernel.devices.setName(id, name)`.\n streams: streamsField.describe(\"Published RTSP streams\"),\n snapshotUrl: z.string().default(\"\").describe(\"Snapshot URL (optional)\"),\n username: z.string().default(\"\").describe(\"Username\"),\n password: z.string().default(\"\").describe(\"Password\"),\n});\n\n/**\n * Legacy storage shape: `[{ label: 'high'|'mid'|'low', url }]`.\n * Migrated to the new `[{id, url, profileHint}]` shape on construct\n * via `config.setAll`. The conversion is permanent — after one boot\n * every camera row is on the new shape.\n */\nconst legacyStreamSchema = z.object({\n label: z.enum(['high', 'mid', 'low']),\n url: z.string(),\n})\n\nexport class RtspCamera\n extends BaseDevice<typeof rtspCameraSchema>\n implements ICameraDevice\n{\n readonly type = DeviceType.Camera as const;\n features = [] as const satisfies readonly DeviceFeature[];\n\n constructor(ctx: DeviceContext) {\n super(\n ctx,\n rtspCameraSchema,\n { type: DeviceType.Camera },\n );\n // Online state is now firmware/broker-event driven only — BaseDevice\n // seeds `device-status` slice with online=false, and the provider's\n // stream-health aggregator (`BaseDeviceProvider.subscribeStreamHealth`)\n // flips it true on the first `stream.online` event.\n this.migrateLegacyStreamsIfNeeded(ctx.persistedConfig ?? {})\n this.registerSnapshotProvider();\n }\n\n /**\n * Detect legacy `{label, url}` stream shapes and reshape to\n * `{id, url, profileHint}`. Persists via `config.setAll` once, so\n * subsequent boots find the new shape and this branch is inert.\n *\n * Runs fire-and-forget — the constructor can't await. The reshape is\n * additive (ids are newly minted) so a failed persist is recoverable\n * on next boot.\n */\n private migrateLegacyStreamsIfNeeded(initialData: Record<string, unknown>): void {\n const raw = initialData['streams']\n if (!Array.isArray(raw)) return\n // All entries already have `id` → new shape; nothing to do.\n if (raw.every((s) => s && typeof s === 'object' && typeof (s as Record<string, unknown>)['id'] === 'string')) {\n return\n }\n const migrated: RtspStreamEntry[] = []\n let nextIdx = 1\n for (const entry of raw) {\n const parsed = legacyStreamSchema.safeParse(entry)\n if (!parsed.success) continue\n migrated.push({\n id: `stream-${nextIdx++}`,\n url: parsed.data.url,\n profileHint: parsed.data.label,\n })\n }\n if (migrated.length === 0) return\n const snapshotUrl = this.config.get('snapshotUrl') ?? ''\n const username = this.config.get('username') ?? ''\n const password = this.config.get('password') ?? ''\n this.ctx.logger.info('Migrating legacy {label,url} streams to {id,url,profileHint}', {\n tags: { stableId: this.stableId },\n meta: { streamCount: migrated.length },\n })\n void this.config\n .setAll({ streams: migrated, snapshotUrl, username, password })\n .catch((err: unknown) => {\n this.ctx.logger.warn('Legacy stream migration failed', {\n tags: { stableId: this.stableId },\n meta: { error: err instanceof Error ? err.message : String(err) },\n })\n })\n }\n\n /**\n * Publish every configured stream to the system stream-broker as a\n * `pull-rtsp` cam stream. Idempotent — the broker's publishCameraStream\n * entry is keyed by `(deviceId, camStreamId)` so callers may invoke\n * this at will (e.g. on stream-broker restart). Fire-and-forget safe.\n */\n /**\n * Lifecycle hook fired by the kernel after registration. Publishes\n * the camera's streams to the broker — the kernel doesn't know\n * about the broker, but the camera does. Best-effort: if the\n * broker isn't ready, the provider's `system.ready-state`\n * subscription re-publishes on the next ready fire.\n */\n override async onActivate(): Promise<void> {\n await this.publishToBroker().catch((err: unknown) => {\n this.ctx.logger.warn('publishToBroker on activate failed — will retry on broker ready', {\n meta: { error: err instanceof Error ? err.message : String(err) },\n })\n })\n }\n\n async publishToBroker(): Promise<void> {\n const streams = this.config.get('streams')\n for (const [i, s] of streams.entries()) {\n try {\n await this.ctx.api.streamBroker.publishCameraStream.mutate({\n deviceId: this.id,\n camStreamId: s.id,\n kind: 'pull-rtsp',\n url: s.url,\n ...(s.resolution ? { resolution: s.resolution } : {}),\n label: `Stream ${i + 1}`,\n })\n } catch (err: unknown) {\n this.ctx.logger.warn('publishCameraStream failed', {\n tags: { deviceId: this.id, camStreamId: s.id },\n meta: { error: err instanceof Error ? err.message : String(err) },\n })\n }\n }\n }\n\n /**\n * Thin compat shim: reflects the stored `{id, url, profileHint?}`\n * streams into the legacy `StreamSourceEntry` shape so\n * `ICameraDevice`-aware consumers (device-manager legacy paths,\n * device-cap-proxy fallback) still find data. The broker itself does\n * NOT read this anymore — it consumes `publishCameraStream`. This\n * shim goes away when every consumer migrates (C5/C7).\n */\n async getStreamSources(): Promise<readonly StreamSourceEntry[]> {\n const streams = this.config.get('streams')\n return streams.map((s): StreamSourceEntry => ({\n id: s.id,\n label: s.profileHint ?? s.id,\n protocol: 'rtsp' as const,\n url: s.url,\n ...(s.profileHint ? { profileHint: s.profileHint } : {}),\n ...(s.resolution ? { resolution: s.resolution } : {}),\n }))\n }\n\n override async removeDevice(): Promise<void> {\n this.ctx.logger.info('Removing RTSP camera', { meta: { stableId: this.stableId } });\n const streams = this.config.get('streams')\n await Promise.all(\n streams.map((s) =>\n this.ctx.api.streamBroker.retractCameraStream\n .mutate({ deviceId: this.id, camStreamId: s.id })\n .catch((err: unknown) => {\n this.ctx.logger.warn('retractCameraStream failed', {\n tags: { deviceId: this.id, camStreamId: s.id },\n meta: { error: err instanceof Error ? err.message : String(err) },\n })\n }),\n ),\n )\n }\n\n // ── Driver-authored settings UI ────────────────────────────────────────\n //\n // Streams are stored as `Array<{id, url, profileHint?, resolution?}>`\n // but the settings UI exposes them as a `multiple` probe field of raw\n // URLs — the form only cares about the ordered list of URLs, the ids\n // and classification live in storage. Values are projected from\n // storage into the flat form keys referenced by the schema.\n\n override getSettingsUISchema(): ConfigUISchemaWithValues {\n const schema: ConfigUISchema = {\n sections: [\n // The `name` field is now base meta — rendered by the\n // device-manager's own settings contribution (with the\n // location field), NOT by the driver's hardware-config\n // schema. Driver UIs only carry technical knobs.\n {\n id: 'streams',\n title: 'RTSP Streams',\n description: 'Provide 1–3 RTSP URLs for this camera. After save, each URL is probed and the stream-broker assigns the (high / mid / low) profiles by resolution (highest pixel count → high). Input slot order in this form does not affect the broker profile mapping.',\n columns: 1,\n fields: [\n {\n type: 'probe',\n key: 'streams',\n label: 'RTSP stream URL',\n required: true,\n placeholder: 'rtsp://user:pass@host:554/Streaming/Channels/101',\n inputType: 'url',\n multiple: {\n min: 1,\n max: 3,\n addLabel: 'Add stream',\n itemLabel: 'Stream ${n}',\n itemDefault: '',\n },\n },\n ],\n },\n {\n id: 'snapshot',\n title: 'Snapshot',\n columns: 1,\n fields: [\n { type: 'probe', key: 'snapshotUrl', label: 'Snapshot URL', description: 'Optional HTTP(S) endpoint that returns a JPEG still.', placeholder: 'http://host/ISAPI/Streaming/channels/101/picture', inputType: 'url' },\n ],\n },\n {\n id: 'credentials',\n title: 'Credentials',\n description: 'Optional. If the RTSP URL already contains user:pass@ you can leave these empty.',\n columns: 2,\n fields: [\n { type: 'text', key: 'username', label: 'Username' },\n { type: 'password', key: 'password', label: 'Password', showToggle: true },\n ],\n },\n ],\n }\n return hydrateSchema(schema, this.collectFormValues())\n }\n\n /**\n * Project stored config into the flat UI keys referenced by the schema.\n * Form only sees URLs — the internal id/profileHint/resolution stay\n * invisible to the operator (the broker decides profiles from probed\n * metadata at save time).\n */\n private collectFormValues(): Record<string, unknown> {\n const stored = this.config.get('streams')\n // Preserve the slot order from storage (`stream-1`, `stream-2`, …).\n const sorted = [...stored].sort((a, b) => a.id.localeCompare(b.id, 'en', { numeric: true }))\n const urls = sorted.map((s) => s.url).filter((u) => u.length > 0)\n return {\n streams: urls,\n snapshotUrl: this.config.get('snapshotUrl'),\n username: this.config.get('username'),\n password: this.config.get('password'),\n }\n }\n\n override async applySettingsPatch(patch: Record<string, unknown>): Promise<void> {\n const current = this.collectFormValues()\n const merged: Record<string, unknown> = { ...current, ...patch }\n const pickStr = (k: string): string => (typeof merged[k] === 'string' ? (merged[k] as string).trim() : '')\n\n const next: Record<string, unknown> = {}\n\n const rawStreams = merged['streams']\n const incomingUrls: string[] = Array.isArray(rawStreams)\n ? rawStreams\n .filter((s): s is string => typeof s === 'string')\n .map((s) => s.trim())\n .filter((s) => s.length > 0)\n : []\n\n if (incomingUrls.length === 0) {\n // UI cleared every slot — keep current streams so zod's min(1)\n // invariant holds and the camera keeps streaming until the next\n // save with real URLs.\n next.streams = this.config.get('streams')\n } else {\n const probed = await this.probeAndClassify(incomingUrls.slice(0, 3))\n next.streams = probed\n }\n\n next.snapshotUrl = pickStr('snapshotUrl')\n next.username = pickStr('username')\n next.password = typeof merged['password'] === 'string' ? merged['password'] : this.config.get('password')\n\n await this.config.setAll(next)\n\n // Storage now has the new stream list — re-publish so the broker\n // sees any URL/resolution changes. `publishCameraStream` is\n // idempotent for the same camStreamId so slots that didn't change\n // are free no-ops.\n await this.publishToBroker()\n }\n\n /**\n * Probe a list of RTSP URLs through the kernel stream-probe and return\n * the new `{id, url, profileHint?, resolution?}` entries. Ids are\n * assigned `stream-1/2/3` in input order so the UI's slot ordering is\n * preserved across edits. The broker is in charge of the actual\n * (high/mid/low) profile assignment via `computeInitialAssignment`.\n */\n private async probeAndClassify(urls: readonly string[]): Promise<RtspStreamEntry[]> {\n const streamProbe = this.ctx.streamProbe\n if (!streamProbe) {\n this.ctx.logger.warn(\n '[rtsp-edit] ctx.streamProbe unavailable — falling back to input-order profile assignment',\n { meta: { streamCount: urls.length } },\n )\n const sequence: ReadonlyArray<StreamQuality> =\n urls.length === 1 ? ['high']\n : urls.length === 2 ? ['high', 'low']\n : ['high', 'mid', 'low']\n return urls.map((url, i): RtspStreamEntry => ({\n id: `stream-${i + 1}`,\n url,\n profileHint: sequence[i]!,\n }))\n }\n\n const probed = await Promise.all(\n urls.map(async (url) => {\n try {\n const metadata = await streamProbe.probe(url, { force: false })\n return { url, metadata }\n } catch (err) {\n this.ctx.logger.warn(\n '[rtsp-edit] probe failed — landing in the lowest tier via classifyStreams',\n { meta: { url: maskUrlCredentials(url), error: err instanceof Error ? err.message : String(err) } },\n )\n const emptyMetadata: StreamMetadata = {}\n return { url, metadata: emptyMetadata }\n }\n }),\n )\n const classified = classifyStreams(probed)\n // Each classified entry carries a quality tag. We preserve the\n // input order for the slot id and use the tag as `profileHint`;\n // resolution comes from the probed metadata (matched by url).\n const metadataByUrl = new Map(probed.map((p) => [p.url, p.metadata]))\n const classifiedByUrl = new Map(classified.map((c) => [c.url, c]))\n return urls.map((url, i): RtspStreamEntry => {\n const tag = classifiedByUrl.get(url)\n const meta = metadataByUrl.get(url)\n const entry: RtspStreamEntry = { id: `stream-${i + 1}`, url }\n if (tag) entry.profileHint = tag.quality\n if (meta?.width && meta?.height) entry.resolution = { width: meta.width, height: meta.height }\n return entry\n })\n }\n\n // ── Native capabilities ────────────────────────────────────────────────\n\n private registerSnapshotProvider(): void {\n const snapshotUrl = (this.config.get(\"snapshotUrl\") ?? \"\").trim();\n if (!snapshotUrl) return;\n\n const provider: InferNativeProvider<typeof snapshotCapability> = {\n getSnapshot: async ({ deviceId }) => {\n if (deviceId !== this.id) {\n throw new Error(`RtspCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);\n }\n const buf = await this.fetchHttpSnapshot(snapshotUrl);\n if (buf.length === 0) return null;\n return { base64: buf.toString(\"base64\"), contentType: \"image/jpeg\" };\n },\n invalidateCache: async () => {\n /* no-op — caching is the snapshot orchestrator's concern */\n },\n };\n this.ctx.registerNativeCap(snapshotCapability, provider);\n }\n\n private async fetchHttpSnapshot(url: string): Promise<Buffer> {\n const username = (this.config.get(\"username\") ?? \"\").trim();\n const password = (this.config.get(\"password\") ?? \"\").trim();\n const headers: Record<string, string> = {};\n if (username || password) {\n const creds = Buffer.from(`${username}:${password}`).toString(\"base64\");\n headers[\"authorization\"] = `Basic ${creds}`;\n }\n const res = await fetch(url, { headers });\n if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);\n const ab = await res.arrayBuffer();\n return Buffer.from(ab);\n }\n}\n","import type {\n ConfigUISchema,\n CreateDeviceSpec,\n DeviceConstructor,\n FieldProbeResult,\n IDevice,\n ProviderRegistration,\n StreamQuality,\n SystemEvent,\n} from '@camstack/types'\nimport { BaseDeviceProvider, DeviceType, classifyStreams, EventCategory, maskUrlCredentials } from '@camstack/types'\nimport { RtspCamera, rtspCameraSchema, type RtspStreamEntry } from './rtsp-camera.js'\n\nfunction getString(obj: Record<string, unknown>, key: string): string {\n const v = obj[key]\n return typeof v === 'string' ? v : ''\n}\n\nfunction getStringArray(obj: Record<string, unknown>, key: string): readonly string[] {\n const v = obj[key]\n if (!Array.isArray(v)) return []\n return v\n .filter((s): s is string => typeof s === 'string')\n .map((s) => s.trim())\n .filter((s) => s.length > 0)\n}\n\n/**\n * Build the creation form schema. Hand-written ConfigUISchema instead of\n * converting from Zod — this lets us expose probe-typed fields (live\n * ffprobe \"Test\" button) that the automatic converter doesn't emit.\n *\n * `streams` is a single probe field rendered as a `multiple` array —\n * 1..3 instances; the backend probes each via the kernel's\n * `StreamProbeService` and publishes them to the stream-broker, which\n * decides the (high / mid / low) profile mapping by resolution.\n */\nfunction buildCreationFormSchema(): ConfigUISchema {\n return {\n sections: [\n {\n id: 'identity',\n title: 'Camera',\n columns: 1,\n fields: [\n { type: 'text', key: 'name', label: 'Name', required: true, placeholder: 'Living room' },\n ],\n },\n {\n id: 'streams',\n title: 'RTSP Streams',\n description: 'Provide 1–3 RTSP URLs for this camera. Each URL is probed and published to the stream-broker; the broker assigns the (high / mid / low) profiles by resolution — you do not need to label the streams manually.',\n columns: 1,\n fields: [\n {\n type: 'probe',\n key: 'streams',\n label: 'RTSP stream URL',\n required: true,\n placeholder: 'rtsp://user:pass@host:554/Streaming/Channels/101',\n inputType: 'url',\n multiple: {\n min: 1,\n max: 3,\n addLabel: 'Add stream',\n itemLabel: 'Stream ${n}',\n itemDefault: '',\n },\n },\n ],\n },\n {\n id: 'snapshot',\n title: 'Snapshot',\n columns: 1,\n fields: [\n { type: 'probe', key: 'snapshotUrl', label: 'Snapshot URL', description: 'Optional HTTP(S) endpoint that returns a JPEG still.', placeholder: 'http://host/ISAPI/Streaming/channels/101/picture', inputType: 'url' },\n ],\n },\n {\n id: 'credentials',\n title: 'Credentials',\n description: 'Optional. If the RTSP URL already contains user:pass@ you can leave these empty.',\n columns: 2,\n fields: [\n { type: 'text', key: 'username', label: 'Username' },\n { type: 'password', key: 'password', label: 'Password', showToggle: true },\n ],\n },\n ],\n }\n}\n\n/**\n * Re-publish every RtspCamera's streams to the broker whenever the\n * broker emits `system.ready-state` state=ready. Handles boot-order\n * races (broker starts after provider) and broker restart recovery.\n * At normal runtime, `onCreateDevice` publishes eagerly so new cameras\n * don't have to wait for the next ready fire.\n */\nfunction isStreamBrokerReady(event: SystemEvent): boolean {\n if (event.category !== 'system.ready-state') return false\n const data = event.data as Record<string, unknown>\n return data['capName'] === 'stream-broker' && data['state'] === 'ready'\n}\n\nexport class RtspProviderAddon extends BaseDeviceProvider {\n protected readonly addonId = 'provider-rtsp'\n protected readonly providerName = 'RTSP'\n protected readonly deviceClasses: Partial<Record<DeviceType, DeviceConstructor<IDevice>>> = {\n [DeviceType.Camera]: RtspCamera,\n }\n\n constructor() { super({}) }\n\n protected override async onInitialize(): Promise<ProviderRegistration[]> {\n const regs = await super.onInitialize()\n // Autonomous online tracking — mirror `state.cameraStreams.online`\n // (written by the stream-broker on every health flip) into\n // `device.online` for every RTSP-owned device. RTSP has no firmware\n // liveness signal, so the broker's view is the canonical source.\n // Reolink, which has its own firmware push events, doesn't subscribe.\n this.subscribe(\n { category: EventCategory.DeviceStateChanged },\n (event) => {\n const data = event.data as { deviceId?: number; capName?: string; slice?: { online?: boolean } }\n if (data.capName !== 'camera-streams') return\n const deviceId = data.deviceId\n if (typeof deviceId !== 'number') return\n const registry = this.ctx.kernel.deviceRegistry\n if (!registry) return\n if (registry.getAddonId(deviceId) !== this.addonId) return\n const device = registry.getById(deviceId)\n if (!device) return\n const online = data.slice?.online === true\n if (device.online !== online) device.online = online\n },\n )\n this.subscribe(\n { category: EventCategory.SystemReadyState },\n (event) => {\n if (!isStreamBrokerReady(event)) return\n void this.republishAll().catch((err: unknown) => {\n this.ctx.logger.warn('Failed to re-publish RTSP streams after broker ready', {\n meta: { error: err instanceof Error ? err.message : String(err) },\n })\n })\n },\n )\n return regs\n }\n\n // ── Creation ─────────────────────────────────────────────────────────\n\n protected async onGetCreationSchema(type: DeviceType): Promise<ConfigUISchema | null> {\n if (type !== DeviceType.Camera) return null\n return buildCreationFormSchema()\n }\n\n protected async onCreateDevice(type: DeviceType, config: Record<string, unknown>): Promise<CreateDeviceSpec> {\n if (type !== DeviceType.Camera) {\n throw new Error(`RTSP provider does not support device type: ${type}`)\n }\n\n const name = getString(config, 'name').trim()\n if (!name) throw new Error('Camera name is required')\n\n const rawStreams = getStringArray(config, 'streams')\n if (rawStreams.length === 0) throw new Error('At least one RTSP stream URL is required')\n if (rawStreams.length > 3) throw new Error('At most 3 RTSP stream URLs are supported')\n\n const streamProbe = this.ctx.kernel.streamProbe\n if (!streamProbe) throw new Error('ctx.kernel.streamProbe unavailable — cannot probe streams')\n const probed = await Promise.all(\n rawStreams.map(async (url) => {\n try {\n const metadata = await streamProbe.probe(url, { force: false })\n return { url, metadata, ok: true as const }\n } catch (err) {\n this.ctx.logger.warn(\n '[rtsp-create] probe failed — publishing without probed metadata',\n { meta: { url: maskUrlCredentials(url), error: err instanceof Error ? err.message : String(err) } },\n )\n return { url, metadata: undefined, ok: false as const }\n }\n }),\n )\n\n const classified = classifyStreams(\n probed.map((p) => ({ url: p.url, metadata: p.metadata ?? {} })),\n )\n const hintByUrl = new Map<string, StreamQuality>(classified.map((c) => [c.url, c.quality]))\n\n const streams: RtspStreamEntry[] = probed.map((p, i): RtspStreamEntry => {\n const entry: RtspStreamEntry = { id: `stream-${i + 1}`, url: p.url }\n const hint = hintByUrl.get(p.url)\n if (hint) entry.profileHint = hint\n if (p.metadata?.width && p.metadata?.height) {\n entry.resolution = { width: p.metadata.width, height: p.metadata.height }\n }\n return entry\n })\n\n const parsed = rtspCameraSchema.parse({\n streams,\n snapshotUrl: getString(config, 'snapshotUrl'),\n username: getString(config, 'username'),\n password: getString(config, 'password'),\n })\n\n return {\n meta: { type: DeviceType.Camera, name },\n config: parsed,\n }\n }\n\n // ── Field probing ────────────────────────────────────────────────────\n\n override async testCreationField(input: {\n type: DeviceType\n key: string\n value: unknown\n }): Promise<FieldProbeResult> {\n if (input.type !== DeviceType.Camera) {\n return { status: 'error', error: `Unsupported device type: ${input.type}` }\n }\n const streamProbe = this.ctx.kernel.streamProbe\n if (!streamProbe) {\n return { status: 'error', error: 'ctx.kernel.streamProbe unavailable — cannot reach kernel probe' }\n }\n return streamProbe.probeField(input.key, input.value)\n }\n\n // ── Restore ──────────────────────────────────────────────────────────\n //\n // `BaseDeviceProvider.onRestoreDevices` default impl iterates\n // `savedDevices`, looks up the right class via `deviceClasses`,\n // and calls `kernel.devices.create()`. Each created device fires\n // its `onCreated` lifecycle hook (see `RtspCamera.onCreated()`)\n // which publishes streams to the broker — no per-provider override\n // needed here.\n\n // ── Internal — re-publish every RtspCamera to the broker ────────────\n\n private async republishAll(): Promise<void> {\n const all = (await this.ctx.kernel.devices?.getAll()) ?? []\n let published = 0\n for (const dev of all as readonly IDevice[]) {\n if (!(dev instanceof RtspCamera)) continue\n try {\n await dev.publishToBroker()\n published++\n } catch (err) {\n this.ctx.logger.debug('publishToBroker threw during republish', {\n tags: { deviceId: dev.id },\n meta: { error: err instanceof Error ? err.message : String(err) },\n })\n }\n }\n if (published > 0) {\n this.ctx.logger.info('Re-published RTSP streams to stream-broker', {\n meta: { published, total: all.length },\n })\n }\n }\n}\n"],"names":[],"mappings":"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"}