src.nth.io/

summaryrefslogtreecommitdiff
path: root/scrypted/scrypted-viewport.ts
diff options
context:
space:
mode:
authorLuke Hoersten <[email protected]>2026-06-14 10:57:01 -0500
committerLuke Hoersten <[email protected]>2026-06-14 10:57:01 -0500
commit1ba5250c96644440226ad71922f40c7eaa4ef649 (patch)
tree2e7b4262d1aa5528b224b6d8a176b7d19a91a986 /scrypted/scrypted-viewport.ts
parent52f4105aa059b9d7310d80cfb86abe844f7be05a (diff)
scrypted/script: auto-populate viewport host via mDNS
The host field is now optional in "+ Add Device". The script tries dns.promises.lookup("viewport-<name>.local") via the OS resolver (Bonjour on macOS, nss-mdns on Linux, host networking on Docker) on: - every registerViewport call (plugin start, child instantiation, settings change, periodic 5-min refresh), - right after createDevice so a fresh viewport's host field is populated by the time the operator opens its settings page. On a successful lookup that differs from the stored host, the resolved IP is written back to the child's storage. POST /config and POST /frame then use the resolved value. Falls back gracefully to the operator-entered host (with a one-line warning if both are empty). A per-viewport "Auto-resolve via mDNS" toggle (default on) opts out — useful for cross-VLAN setups or hosts where mDNS doesn't reach. scrypted/README.md adds a "How mDNS auto-resolve works" section covering the per-OS resolver requirements and the Docker host- networking note.
Diffstat (limited to 'scrypted/scrypted-viewport.ts')
-rw-r--r--scrypted/scrypted-viewport.ts58
1 files changed, 54 insertions, 4 deletions
diff --git a/scrypted/scrypted-viewport.ts b/scrypted/scrypted-viewport.ts
index 7d10a57..e2c5ad6 100644
--- a/scrypted/scrypted-viewport.ts
+++ b/scrypted/scrypted-viewport.ts
@@ -50,6 +50,8 @@ import sdk, {
SettingValue,
} from "@scrypted/sdk";
+import { promises as dns } from "dns";
+
const { systemManager, endpointManager, mediaManager, deviceManager } = sdk;
// Tuning constants. Frame interval is also exposed on the parent's
@@ -59,6 +61,22 @@ const REREGISTER_INTERVAL_MS = 5 * 60_000;
const HTTP_TIMEOUT_MS = 1_000;
const DEFAULT_IDLE_TIMEOUT_MS = 60_000;
const DEFAULT_BRIGHTNESS = 80;
+const MDNS_LOOKUP_TIMEOUT_MS = 1_500;
+
+// Resolve `viewport-<name>.local` via the OS resolver (Bonjour on macOS,
+// nss-mdns on Linux, host networking on Docker). Returns null on failure or
+// timeout — caller falls back to the operator-entered host.
+async function lookupMdns(hostname: string): Promise<string | null> {
+ try {
+ const lookup = dns.lookup(hostname, { family: 4 });
+ const timeout = new Promise<null>(r => setTimeout(() => r(null), MDNS_LOOKUP_TIMEOUT_MS));
+ const result = await Promise.race([lookup, timeout]);
+ if (!result) return null;
+ return (result as { address: string }).address || null;
+ } catch {
+ return null;
+ }
+}
// ============================================================================
// Child: one viewport binding
@@ -83,17 +101,27 @@ class Viewport extends ScryptedDeviceBase implements Settings {
const v = this.storage.getItem("brightness");
return v ? Math.max(0, Math.min(100, parseInt(v, 10) || 0)) : DEFAULT_BRIGHTNESS;
}
+ get mdnsAuto(): boolean {
+ return this.storage.getItem("mdns_auto") !== "false"; // default true
+ }
async getSettings(): Promise<Setting[]> {
return [
{
key: "host",
title: "IP or hostname",
- description: "Viewport's address on the LAN. Update if DHCP renumbers (or use a DHCP reservation).",
+ description: "Viewport's address on the LAN. Auto-resolved via mDNS when the toggle below is on; otherwise set manually.",
placeholder: "192.168.1.42",
value: this.host,
},
{
+ key: "mdns_auto",
+ title: "Auto-resolve via mDNS",
+ description: "Look up viewport-<name>.local via the OS resolver on every register and overwrite the host field with the discovered IP. Disable for cross-VLAN setups or hosts where mDNS resolution doesn't work.",
+ type: "boolean",
+ value: this.mdnsAuto,
+ },
+ {
key: "cameraId",
title: "Camera",
description: "Camera whose events drive this viewport's wake/sleep, and whose snapshots get streamed.",
@@ -231,8 +259,9 @@ class ScryptedViewportProvider extends ScryptedDeviceBase
},
{
key: "host",
- title: "IP or hostname",
- placeholder: "192.168.1.42",
+ title: "IP or hostname (optional)",
+ description: "Leave blank to auto-resolve via mDNS (viewport-<name>.local). Set manually if mDNS doesn't reach this network.",
+ placeholder: "auto-resolve via mDNS",
},
{
key: "cameraId",
@@ -270,6 +299,15 @@ class ScryptedViewportProvider extends ScryptedDeviceBase
this.childIds = [...this.childIds, nativeId];
this.console.log(`created viewport "${name}" (${nativeId})`);
+
+ // Fire-and-forget mDNS resolve so the host field is auto-populated
+ // by the time the operator opens the new device's settings page.
+ // getDevice() above already attempted a register; this catches the
+ // case where the operator left host blank and mDNS resolution was
+ // the only way to fill it.
+ const child = this.viewports.get(nativeId);
+ if (child) this.registerViewport(child).catch(() => {});
+
return nativeId;
}
@@ -314,8 +352,20 @@ class ScryptedViewportProvider extends ScryptedDeviceBase
}
}
+ private async refreshHostFromMdns(v: Viewport): Promise<void> {
+ if (!v.mdnsAuto || !v.name) return;
+ const ip = await lookupMdns(`viewport-${v.name}.local`);
+ if (!ip || ip === v.host) return;
+ this.console.log(`mDNS: "${v.name}" host "${v.host || "(empty)"}" -> "${ip}"`);
+ v.storage.setItem("host", ip);
+ }
+
private async registerViewport(v: Viewport) {
- if (!v.host) return;
+ await this.refreshHostFromMdns(v);
+ if (!v.host) {
+ this.console.warn(`register "${v.name}" skipped — no host (set one manually or check mDNS)`);
+ return;
+ }
try {
await this.postJSON(`http://${v.host}/config`, {
viewport: v.name,