obsbot-mcp 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -20
- package/dist/build-info.json +5 -0
- package/dist/device/manager.d.ts +8 -0
- package/dist/device/manager.js +37 -4
- package/dist/device/manager.js.map +1 -1
- package/dist/geometry/aim.d.ts +24 -13
- package/dist/geometry/aim.js +26 -13
- package/dist/geometry/aim.js.map +1 -1
- package/dist/ipc/build-id.d.ts +25 -0
- package/dist/ipc/build-id.js +60 -0
- package/dist/ipc/build-id.js.map +1 -0
- package/dist/ipc/client.d.ts +36 -0
- package/dist/ipc/client.js +106 -4
- package/dist/ipc/client.js.map +1 -1
- package/dist/ipc/coordinator.d.ts +91 -12
- package/dist/ipc/coordinator.js +293 -57
- package/dist/ipc/coordinator.js.map +1 -1
- package/dist/ipc/gate.d.ts +14 -0
- package/dist/ipc/gate.js +45 -0
- package/dist/ipc/gate.js.map +1 -0
- package/dist/ipc/owner.d.ts +27 -1
- package/dist/ipc/owner.js +67 -4
- package/dist/ipc/owner.js.map +1 -1
- package/dist/ipc/protocol.d.ts +30 -0
- package/dist/ipc/protocol.js +40 -0
- package/dist/ipc/protocol.js.map +1 -1
- package/dist/ipc/rendezvous.d.ts +37 -11
- package/dist/ipc/rendezvous.js +143 -30
- package/dist/ipc/rendezvous.js.map +1 -1
- package/dist/mcp/server.js +40 -12
- package/dist/mcp/server.js.map +1 -1
- package/dist/tail2/api.d.ts +218 -0
- package/dist/tail2/api.js +267 -0
- package/dist/tail2/api.js.map +1 -0
- package/dist/tail2/mdns.d.ts +48 -0
- package/dist/tail2/mdns.js +216 -0
- package/dist/tail2/mdns.js.map +1 -0
- package/dist/tail2/registry.d.ts +100 -0
- package/dist/tail2/registry.js +200 -0
- package/dist/tail2/registry.js.map +1 -0
- package/dist/tail2/snapshot.d.ts +54 -0
- package/dist/tail2/snapshot.js +157 -0
- package/dist/tail2/snapshot.js.map +1 -0
- package/dist/tail2/tools.d.ts +7 -0
- package/dist/tail2/tools.js +370 -0
- package/dist/tail2/tools.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/native/prebuilt/darwin-arm64/obsbot-helper +0 -0
- package/native/prebuilt/darwin-x64/obsbot-helper +0 -0
- package/native/prebuilt/linux-x64/obsbot-helper +0 -0
- package/native/prebuilt/win32-x64/obsbot-helper.exe +0 -0
- package/package.json +4 -1
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import dgram from "node:dgram";
|
|
2
|
+
import { networkInterfaces } from "node:os";
|
|
3
|
+
const GROUP = "224.0.0.251";
|
|
4
|
+
const MDNS_PORT = 5353;
|
|
5
|
+
const MAC_RE = /^[0-9a-f]{2}(:[0-9a-f]{2}){5}$/;
|
|
6
|
+
const isUsefulHost = (h) => h !== "" && h !== "0.0.0.0" && h !== "255.255.255.255";
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// DNS packet parsing — just enough for mDNS response packets: questions are
|
|
9
|
+
// skipped, answer names are decoded (compression pointers included), and TXT
|
|
10
|
+
// records are unpacked into their length-prefixed strings. SRV/A/AAAA are
|
|
11
|
+
// tolerated structurally but only TXT carries the Tail 2 digest.
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
/** Decode a (possibly compressed) DNS name. Returns the name and the offset
|
|
14
|
+
* just past it in the ORIGINAL section — after a compression pointer the
|
|
15
|
+
* record's fixed fields continue right after the pointer itself. */
|
|
16
|
+
function readName(buf, off) {
|
|
17
|
+
const labels = [];
|
|
18
|
+
let jumped = false;
|
|
19
|
+
let end = off;
|
|
20
|
+
let pos = off;
|
|
21
|
+
let jumps = 0;
|
|
22
|
+
while (true) {
|
|
23
|
+
if (pos >= buf.length)
|
|
24
|
+
return { name: "", end: buf.length };
|
|
25
|
+
const len = buf[pos];
|
|
26
|
+
if (len === 0) {
|
|
27
|
+
if (!jumped)
|
|
28
|
+
end = pos + 1;
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
if ((len & 0xc0) === 0xc0) {
|
|
32
|
+
if (pos + 1 >= buf.length)
|
|
33
|
+
return { name: "", end: buf.length };
|
|
34
|
+
const ptr = ((len & 0x3f) << 8) | buf[pos + 1];
|
|
35
|
+
if (!jumped)
|
|
36
|
+
end = pos + 2;
|
|
37
|
+
if (++jumps > 32)
|
|
38
|
+
return { name: "", end: buf.length }; // pointer loop
|
|
39
|
+
pos = ptr;
|
|
40
|
+
jumped = true;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (pos + 1 + len > buf.length)
|
|
44
|
+
return { name: "", end: buf.length };
|
|
45
|
+
labels.push(buf.subarray(pos + 1, pos + 1 + len).toString("latin1"));
|
|
46
|
+
pos += 1 + len;
|
|
47
|
+
}
|
|
48
|
+
return { name: labels.join("."), end };
|
|
49
|
+
}
|
|
50
|
+
/** Unpack one packet's TXT strings; other record types are skipped by RDLENGTH. */
|
|
51
|
+
function txtStringsOf(buf) {
|
|
52
|
+
const strings = [];
|
|
53
|
+
if (buf.length < 12)
|
|
54
|
+
return strings;
|
|
55
|
+
const qd = buf.readUInt16BE(4);
|
|
56
|
+
const an = buf.readUInt16BE(6);
|
|
57
|
+
let pos = 12;
|
|
58
|
+
for (let i = 0; i < qd; i++) {
|
|
59
|
+
const { end } = readName(buf, pos);
|
|
60
|
+
pos = end + 4; // QTYPE + QCLASS
|
|
61
|
+
if (pos > buf.length)
|
|
62
|
+
return strings;
|
|
63
|
+
}
|
|
64
|
+
for (let i = 0; i < an; i++) {
|
|
65
|
+
const { end } = readName(buf, pos);
|
|
66
|
+
pos = end;
|
|
67
|
+
if (pos + 10 > buf.length)
|
|
68
|
+
return strings;
|
|
69
|
+
const type = buf.readUInt16BE(pos);
|
|
70
|
+
const rdLength = buf.readUInt16BE(pos + 8);
|
|
71
|
+
const rdata = buf.subarray(pos + 10, pos + 10 + rdLength);
|
|
72
|
+
pos += 10 + rdLength;
|
|
73
|
+
if (type === 16) {
|
|
74
|
+
// TXT RDATA: a sequence of <1-byte length><bytes> strings.
|
|
75
|
+
let p = 0;
|
|
76
|
+
while (p < rdata.length) {
|
|
77
|
+
const l = rdata[p];
|
|
78
|
+
if (p + 1 + l > rdata.length)
|
|
79
|
+
break;
|
|
80
|
+
strings.push(rdata.subarray(p + 1, p + 1 + l).toString("utf8"));
|
|
81
|
+
p += 1 + l;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return strings;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Extract a Tail 2 announcement from one mDNS packet, or null for anything
|
|
89
|
+
* else on the wire. Detection is by the digest's own shape — TXT strings that
|
|
90
|
+
* JSON-parse into an object carrying a `ble_mac` (the registry's identity
|
|
91
|
+
* MAC) plus a reachable IP — so other devices' mDNS traffic parses to nothing
|
|
92
|
+
* and registers nothing. `srcIp` is where the packet came from; it becomes a
|
|
93
|
+
* host of last resort and a live-reachability hint.
|
|
94
|
+
*/
|
|
95
|
+
export function parseTail2Announcement(buf, srcIp) {
|
|
96
|
+
const merged = {};
|
|
97
|
+
for (const s of txtStringsOf(buf)) {
|
|
98
|
+
if (!s.startsWith("{"))
|
|
99
|
+
continue;
|
|
100
|
+
try {
|
|
101
|
+
const v = JSON.parse(s);
|
|
102
|
+
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
103
|
+
Object.assign(merged, v);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Someone else's TXT string that merely looks like JSON — ignore.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const mac = typeof merged.ble_mac === "string" ? merged.ble_mac.toLowerCase() : "";
|
|
111
|
+
if (!MAC_RE.test(mac))
|
|
112
|
+
return null;
|
|
113
|
+
const hosts = [];
|
|
114
|
+
for (const h of [merged.wired_ip, merged.wireless_ip]) {
|
|
115
|
+
if (typeof h === "string" && isUsefulHost(h) && !hosts.includes(h))
|
|
116
|
+
hosts.push(h);
|
|
117
|
+
}
|
|
118
|
+
if (srcIp && isUsefulHost(srcIp) && !hosts.includes(srcIp))
|
|
119
|
+
hosts.push(srcIp);
|
|
120
|
+
if (hosts.length === 0)
|
|
121
|
+
return null;
|
|
122
|
+
let name = mac;
|
|
123
|
+
if (typeof merged.device_name === "string" && /^[0-9a-f]+$/i.test(merged.device_name)) {
|
|
124
|
+
const decoded = Buffer.from(merged.device_name, "hex").toString("utf8");
|
|
125
|
+
if (decoded !== "")
|
|
126
|
+
name = decoded;
|
|
127
|
+
}
|
|
128
|
+
return { mac, name, hosts };
|
|
129
|
+
}
|
|
130
|
+
/** Accumulates announcements across packets, keyed by MAC. Testable without
|
|
131
|
+
* sockets: push() buffers, results() the deduplicated list. */
|
|
132
|
+
export function createAnnouncementCollector() {
|
|
133
|
+
const byMac = new Map();
|
|
134
|
+
return {
|
|
135
|
+
push(buf, srcIp) {
|
|
136
|
+
const a = parseTail2Announcement(buf, srcIp);
|
|
137
|
+
if (!a)
|
|
138
|
+
return;
|
|
139
|
+
const existing = byMac.get(a.mac);
|
|
140
|
+
if (existing) {
|
|
141
|
+
for (const h of a.hosts)
|
|
142
|
+
if (!existing.hosts.includes(h))
|
|
143
|
+
existing.hosts.push(h);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
byMac.set(a.mac, a);
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
results() {
|
|
150
|
+
return [...byMac.values()];
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Listen for Tail 2 announcements for `ms` milliseconds (the camera repeats
|
|
156
|
+
* every few seconds, so 5s is ample) and return what was heard. Never throws:
|
|
157
|
+
* a bind failure or a network without multicast yields an empty list, and the
|
|
158
|
+
* caller falls back to the HTTP sweep.
|
|
159
|
+
*/
|
|
160
|
+
export function listenForTail2Announcements(ms = 5000) {
|
|
161
|
+
return new Promise((resolve) => {
|
|
162
|
+
const collector = createAnnouncementCollector();
|
|
163
|
+
let socket = null;
|
|
164
|
+
let finished = false;
|
|
165
|
+
const finish = () => {
|
|
166
|
+
if (finished)
|
|
167
|
+
return;
|
|
168
|
+
finished = true;
|
|
169
|
+
clearTimeout(timer);
|
|
170
|
+
if (socket) {
|
|
171
|
+
socket.removeAllListeners();
|
|
172
|
+
try {
|
|
173
|
+
socket.dropMembership(GROUP);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// Already gone.
|
|
177
|
+
}
|
|
178
|
+
socket.close();
|
|
179
|
+
}
|
|
180
|
+
resolve(collector.results());
|
|
181
|
+
};
|
|
182
|
+
const timer = setTimeout(finish, ms);
|
|
183
|
+
try {
|
|
184
|
+
socket = dgram.createSocket({ type: "udp4", reuseAddr: true });
|
|
185
|
+
socket.on("message", (buf, rinfo) => {
|
|
186
|
+
collector.push(buf, rinfo.address);
|
|
187
|
+
});
|
|
188
|
+
socket.on("error", finish);
|
|
189
|
+
socket.bind(MDNS_PORT, () => {
|
|
190
|
+
const s = socket;
|
|
191
|
+
try {
|
|
192
|
+
s.addMembership(GROUP);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// Default-interface join failed; explicit per-interface joins below.
|
|
196
|
+
}
|
|
197
|
+
for (const addrs of Object.values(networkInterfaces())) {
|
|
198
|
+
for (const a of addrs ?? []) {
|
|
199
|
+
if (a.family !== "IPv4" || a.internal)
|
|
200
|
+
continue;
|
|
201
|
+
try {
|
|
202
|
+
s.addMembership(GROUP, a.address);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
// One unusable interface must not cost the others.
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
finish();
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=mdns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mdns.js","sourceRoot":"","sources":["../../src/tail2/mdns.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Tail2Api } from "./api.js";
|
|
2
|
+
import { type Tail2Announcement } from "./mdns.js";
|
|
3
|
+
/**
|
|
4
|
+
* MAC-keyed registry of attached OBSBOT Tail 2 cameras.
|
|
5
|
+
*
|
|
6
|
+
* The Tiny 2's DeviceManager discovers cameras over USB and identifies them
|
|
7
|
+
* by serial; the Tail 2 reports no USB serial number in either of its USB-C
|
|
8
|
+
* modes (MTP or UVC — TAIL2-PROTOCOL.md §1, §11), so a Tail
|
|
9
|
+
* 2's identity is its MAC, exactly as OBSBOT Center treats it. Entries are
|
|
10
|
+
* created by probing a host with the discovery handshake (GET device_info,
|
|
11
|
+
* then WS hello — this module only needs the first half) and can be added
|
|
12
|
+
* four ways:
|
|
13
|
+
*
|
|
14
|
+
* - the OBSBOT_TAIL2_HOSTS environment variable (comma-separated hosts),
|
|
15
|
+
* for a stable, known network;
|
|
16
|
+
* - the obsbot_tail2_scan tool, which listens for the camera's mDNS
|
|
17
|
+
* announcements (~5s) and falls back to sweeping the local /24(s);
|
|
18
|
+
* - the camera's own periodic mDNS announcement, which names its MAC, its
|
|
19
|
+
* name and both its IPs — no probe needed, the camera just said so;
|
|
20
|
+
* - resolve()/get() with an explicit host, which registers on first use.
|
|
21
|
+
*
|
|
22
|
+
* "Registered" does NOT mean "verified reachable right now" — entries persist
|
|
23
|
+
* across a camera going offline, and the next operation against its API fails
|
|
24
|
+
* with the actionable unreachable error rather than a mystery. Re-registration
|
|
25
|
+
* by MAC is idempotent and merges new candidate hosts into the entry.
|
|
26
|
+
*/
|
|
27
|
+
export interface Tail2Entry {
|
|
28
|
+
mac: string;
|
|
29
|
+
name: string;
|
|
30
|
+
/** Every host this camera has answered on (wired and wireless IPs). */
|
|
31
|
+
hosts: string[];
|
|
32
|
+
api: Tail2Api;
|
|
33
|
+
}
|
|
34
|
+
/** Mirrors DeviceManager's contract: same shapes, same caller expectations. */
|
|
35
|
+
export declare class AmbiguousTail2Error extends Error {
|
|
36
|
+
readonly available: string[];
|
|
37
|
+
constructor(available: string[]);
|
|
38
|
+
}
|
|
39
|
+
export declare class Tail2Registry {
|
|
40
|
+
private readonly byMac;
|
|
41
|
+
private readonly probeTimeoutMs;
|
|
42
|
+
private readonly makeApi;
|
|
43
|
+
constructor(opts?: {
|
|
44
|
+
probeTimeoutMs?: number;
|
|
45
|
+
makeApi?: (host: string) => Tail2Api;
|
|
46
|
+
});
|
|
47
|
+
/**
|
|
48
|
+
* Seed from `OBSBOT_TAIL2_HOSTS` (comma-separated; blank entries ignored).
|
|
49
|
+
* The opts passthrough exists for tests; production calls it bare.
|
|
50
|
+
*/
|
|
51
|
+
static fromEnv(env?: Record<string, string | undefined>, opts?: {
|
|
52
|
+
probeTimeoutMs?: number;
|
|
53
|
+
makeApi?: (host: string) => Tail2Api;
|
|
54
|
+
}): Tail2Registry;
|
|
55
|
+
/**
|
|
56
|
+
* Probe one host; register it if it answers as a Tail 2. Returns the entry,
|
|
57
|
+
* or null when nothing Tail-2-shaped answers there (wrong device, plain
|
|
58
|
+
* HTTP server, or silence) — a scan probes hundreds of addresses and
|
|
59
|
+
* "not here" is the normal outcome, not an error.
|
|
60
|
+
*/
|
|
61
|
+
addHost(host: string): Promise<Tail2Entry | null>;
|
|
62
|
+
/** Resolve the `camera` selector: MAC, device_name, or host — case-insensitive. */
|
|
63
|
+
resolve(camera?: string): Promise<Tail2Entry>;
|
|
64
|
+
list(): Tail2Entry[];
|
|
65
|
+
/**
|
|
66
|
+
* Register straight from the camera's own mDNS announcement — no probe.
|
|
67
|
+
* The digest carries the MAC, the name and both IPs, and the packet arrived
|
|
68
|
+
* seconds ago, so it is better evidence than a probe that has not run. The
|
|
69
|
+
* API binds to the wired IP first (the wireless IP is often the camera's
|
|
70
|
+
* own AP subnet, 192.168.55.x, unroutable from here).
|
|
71
|
+
*/
|
|
72
|
+
addAnnouncement(a: Tail2Announcement): Tail2Entry;
|
|
73
|
+
/**
|
|
74
|
+
* Discovery: listen for the camera's mDNS announcements (its own control
|
|
75
|
+
* channel — it multicasts a device digest every few seconds), and fall back
|
|
76
|
+
* to the HTTP subnet sweep only when nothing was heard, for networks where
|
|
77
|
+
* multicast is filtered. `OBSBOT_TAIL2_MDNS=0` skips the listen.
|
|
78
|
+
*/
|
|
79
|
+
scan(opts?: {
|
|
80
|
+
mdnsMs?: number;
|
|
81
|
+
listen?: (ms: number) => Promise<Tail2Announcement[]>;
|
|
82
|
+
sweep?: boolean;
|
|
83
|
+
subnets?: string[];
|
|
84
|
+
concurrency?: number;
|
|
85
|
+
}): Promise<Tail2Entry[]>;
|
|
86
|
+
/**
|
|
87
|
+
* Sweep every IPv4 /24 this machine belongs to (excluding loopback) for
|
|
88
|
+
* Tail 2 cameras, registering what answers. The mDNS listen in scan()
|
|
89
|
+
* covers every network the camera's announcements can reach; this sweep is
|
|
90
|
+
* the fallback for the rest — an HTTP GET of /camera/sdk/device_info per
|
|
91
|
+
* candidate address, the same probe OBSBOT Center's discovery uses, so the
|
|
92
|
+
* traffic pattern is one the camera expects to see. Bounded concurrency
|
|
93
|
+
* keeps the sweep from socket-bombing the host; per-address timeouts keep
|
|
94
|
+
* one slow device from stretching the scan.
|
|
95
|
+
*/
|
|
96
|
+
scanSubnet(opts?: {
|
|
97
|
+
concurrency?: number;
|
|
98
|
+
subnets?: string[];
|
|
99
|
+
}): Promise<Tail2Entry[]>;
|
|
100
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { networkInterfaces } from "node:os";
|
|
2
|
+
import { Tail2Api } from "./api.js";
|
|
3
|
+
import { listenForTail2Announcements } from "./mdns.js";
|
|
4
|
+
/** Mirrors DeviceManager's contract: same shapes, same caller expectations. */
|
|
5
|
+
export class AmbiguousTail2Error extends Error {
|
|
6
|
+
available;
|
|
7
|
+
constructor(available) {
|
|
8
|
+
super(`multiple Tail 2 cameras registered; specify one of: ${available.join(", ")}`);
|
|
9
|
+
this.name = "AmbiguousTail2Error";
|
|
10
|
+
this.available = available;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const NO_CAMERA_HINT = `no Tail 2 camera is registered. Add one via the obsbot_tail2_scan tool (sweeps the ` +
|
|
14
|
+
`local network), or set OBSBOT_TAIL2_HOSTS to its address(es), or pass its host as the ` +
|
|
15
|
+
`camera parameter on any Tail 2 tool`;
|
|
16
|
+
export class Tail2Registry {
|
|
17
|
+
byMac = new Map();
|
|
18
|
+
probeTimeoutMs;
|
|
19
|
+
makeApi;
|
|
20
|
+
constructor(opts = {}) {
|
|
21
|
+
this.probeTimeoutMs = opts.probeTimeoutMs ?? 2500;
|
|
22
|
+
this.makeApi =
|
|
23
|
+
opts.makeApi ?? ((host) => new Tail2Api({ baseUrl: host, timeoutMs: this.probeTimeoutMs }));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Seed from `OBSBOT_TAIL2_HOSTS` (comma-separated; blank entries ignored).
|
|
27
|
+
* The opts passthrough exists for tests; production calls it bare.
|
|
28
|
+
*/
|
|
29
|
+
static fromEnv(env = process.env, opts = {}) {
|
|
30
|
+
const reg = new Tail2Registry(opts);
|
|
31
|
+
const hosts = (env.OBSBOT_TAIL2_HOSTS ?? "")
|
|
32
|
+
.split(",")
|
|
33
|
+
.map((h) => h.trim())
|
|
34
|
+
.filter((h) => h !== "");
|
|
35
|
+
for (const h of hosts)
|
|
36
|
+
void reg.addHost(h);
|
|
37
|
+
// addHost is async; seeding is best-effort and races first use harmlessly
|
|
38
|
+
// (resolve() re-probes nothing, but a tool call coming in before the probe
|
|
39
|
+
// finishes simply doesn't see the entry yet and reports "not registered").
|
|
40
|
+
return reg;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Probe one host; register it if it answers as a Tail 2. Returns the entry,
|
|
44
|
+
* or null when nothing Tail-2-shaped answers there (wrong device, plain
|
|
45
|
+
* HTTP server, or silence) — a scan probes hundreds of addresses and
|
|
46
|
+
* "not here" is the normal outcome, not an error.
|
|
47
|
+
*/
|
|
48
|
+
async addHost(host) {
|
|
49
|
+
const api = this.makeApi(host);
|
|
50
|
+
let info;
|
|
51
|
+
try {
|
|
52
|
+
info = await api.info();
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
if (typeof info.mac !== "string" || info.mac === "" || !info.device_name)
|
|
58
|
+
return null;
|
|
59
|
+
const mac = info.mac.toLowerCase();
|
|
60
|
+
const existing = this.byMac.get(mac);
|
|
61
|
+
if (existing) {
|
|
62
|
+
// Same camera reachable at another address (wired + wireless): keep both.
|
|
63
|
+
if (!existing.hosts.includes(host))
|
|
64
|
+
existing.hosts.push(host);
|
|
65
|
+
return existing;
|
|
66
|
+
}
|
|
67
|
+
const entry = { mac, name: info.device_name, hosts: [host], api };
|
|
68
|
+
this.byMac.set(mac, entry);
|
|
69
|
+
return entry;
|
|
70
|
+
}
|
|
71
|
+
/** Resolve the `camera` selector: MAC, device_name, or host — case-insensitive. */
|
|
72
|
+
async resolve(camera) {
|
|
73
|
+
if (camera === undefined || camera === "") {
|
|
74
|
+
const all = [...this.byMac.values()];
|
|
75
|
+
if (all.length === 1)
|
|
76
|
+
return all[0];
|
|
77
|
+
if (all.length > 1)
|
|
78
|
+
throw new AmbiguousTail2Error(all.map((e) => e.mac));
|
|
79
|
+
throw new Error(NO_CAMERA_HINT);
|
|
80
|
+
}
|
|
81
|
+
const want = camera.toLowerCase();
|
|
82
|
+
for (const e of this.byMac.values()) {
|
|
83
|
+
if (e.mac === want || e.name.toLowerCase() === want)
|
|
84
|
+
return e;
|
|
85
|
+
if (e.hosts.some((h) => h.toLowerCase() === want))
|
|
86
|
+
return e;
|
|
87
|
+
}
|
|
88
|
+
// Not registered — but if the selector IS a reachable host, register it.
|
|
89
|
+
// This makes "camera: 192.168.0.132" work on a cold server with no env
|
|
90
|
+
// config and no scan, which is the fastest path from install to control.
|
|
91
|
+
const byHost = await this.addHost(camera);
|
|
92
|
+
if (byHost)
|
|
93
|
+
return byHost;
|
|
94
|
+
const available = [...this.byMac.values()].map((e) => e.mac);
|
|
95
|
+
throw new Error(`unknown Tail 2 camera "${camera}"; available: ` +
|
|
96
|
+
(available.length ? available.join(", ") : "(none registered — run obsbot_tail2_scan)") +
|
|
97
|
+
`. If "${camera}" is a host, nothing Tail 2 answered there.`);
|
|
98
|
+
}
|
|
99
|
+
list() {
|
|
100
|
+
return [...this.byMac.values()];
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Register straight from the camera's own mDNS announcement — no probe.
|
|
104
|
+
* The digest carries the MAC, the name and both IPs, and the packet arrived
|
|
105
|
+
* seconds ago, so it is better evidence than a probe that has not run. The
|
|
106
|
+
* API binds to the wired IP first (the wireless IP is often the camera's
|
|
107
|
+
* own AP subnet, 192.168.55.x, unroutable from here).
|
|
108
|
+
*/
|
|
109
|
+
addAnnouncement(a) {
|
|
110
|
+
const mac = a.mac.toLowerCase();
|
|
111
|
+
const existing = this.byMac.get(mac);
|
|
112
|
+
if (existing) {
|
|
113
|
+
for (const h of a.hosts)
|
|
114
|
+
if (!existing.hosts.includes(h))
|
|
115
|
+
existing.hosts.push(h);
|
|
116
|
+
return existing;
|
|
117
|
+
}
|
|
118
|
+
const entry = {
|
|
119
|
+
mac,
|
|
120
|
+
name: a.name,
|
|
121
|
+
hosts: [...a.hosts],
|
|
122
|
+
api: this.makeApi(a.hosts[0] ?? mac),
|
|
123
|
+
};
|
|
124
|
+
this.byMac.set(mac, entry);
|
|
125
|
+
return entry;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Discovery: listen for the camera's mDNS announcements (its own control
|
|
129
|
+
* channel — it multicasts a device digest every few seconds), and fall back
|
|
130
|
+
* to the HTTP subnet sweep only when nothing was heard, for networks where
|
|
131
|
+
* multicast is filtered. `OBSBOT_TAIL2_MDNS=0` skips the listen.
|
|
132
|
+
*/
|
|
133
|
+
async scan(opts = {}) {
|
|
134
|
+
let heard = [];
|
|
135
|
+
// An injected listener (tests) always runs; the env kill-switch governs
|
|
136
|
+
// only the default one.
|
|
137
|
+
const listen = opts.listen ?? (process.env.OBSBOT_TAIL2_MDNS === "0" ? null : listenForTail2Announcements);
|
|
138
|
+
if (listen) {
|
|
139
|
+
try {
|
|
140
|
+
heard = await listen(opts.mdnsMs ?? 5000);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
heard = []; // listening is best-effort; the sweep still covers it
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const found = new Map();
|
|
147
|
+
for (const a of heard) {
|
|
148
|
+
const e = this.addAnnouncement(a);
|
|
149
|
+
found.set(e.mac, e);
|
|
150
|
+
}
|
|
151
|
+
if (heard.length > 0 || opts.sweep === false)
|
|
152
|
+
return [...found.values()];
|
|
153
|
+
const swept = await this.scanSubnet({ subnets: opts.subnets, concurrency: opts.concurrency });
|
|
154
|
+
for (const e of swept)
|
|
155
|
+
found.set(e.mac, e);
|
|
156
|
+
return [...found.values()];
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Sweep every IPv4 /24 this machine belongs to (excluding loopback) for
|
|
160
|
+
* Tail 2 cameras, registering what answers. The mDNS listen in scan()
|
|
161
|
+
* covers every network the camera's announcements can reach; this sweep is
|
|
162
|
+
* the fallback for the rest — an HTTP GET of /camera/sdk/device_info per
|
|
163
|
+
* candidate address, the same probe OBSBOT Center's discovery uses, so the
|
|
164
|
+
* traffic pattern is one the camera expects to see. Bounded concurrency
|
|
165
|
+
* keeps the sweep from socket-bombing the host; per-address timeouts keep
|
|
166
|
+
* one slow device from stretching the scan.
|
|
167
|
+
*/
|
|
168
|
+
async scanSubnet(opts = {}) {
|
|
169
|
+
const concurrency = opts.concurrency ?? 32;
|
|
170
|
+
// Overridable for tests (and for pinning a scan to one network when this
|
|
171
|
+
// machine straddles several); default is every non-internal IPv4 /24.
|
|
172
|
+
const subnets = new Set(opts.subnets ?? []);
|
|
173
|
+
if (subnets.size === 0) {
|
|
174
|
+
for (const addrs of Object.values(networkInterfaces())) {
|
|
175
|
+
for (const a of addrs ?? []) {
|
|
176
|
+
if (a.family !== "IPv4" || a.internal)
|
|
177
|
+
continue;
|
|
178
|
+
subnets.add(a.address.split(".").slice(0, 3).join("."));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const candidates = [];
|
|
183
|
+
for (const s of subnets) {
|
|
184
|
+
for (let i = 1; i <= 254; i++)
|
|
185
|
+
candidates.push(`${s}.${i}`);
|
|
186
|
+
}
|
|
187
|
+
// addHost returns the entry for every host that ANSWERED as a Tail 2 —
|
|
188
|
+
// including already-registered cameras — so the result is "reachable on
|
|
189
|
+
// this sweep", not "new since last time".
|
|
190
|
+
const found = new Map();
|
|
191
|
+
for (let i = 0; i < candidates.length; i += concurrency) {
|
|
192
|
+
const results = await Promise.all(candidates.slice(i, i + concurrency).map((h) => this.addHost(h)));
|
|
193
|
+
for (const e of results)
|
|
194
|
+
if (e)
|
|
195
|
+
found.set(e.mac, e);
|
|
196
|
+
}
|
|
197
|
+
return [...found.values()];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/tail2/registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
/**
|
|
3
|
+
* Pull one still frame from a Tail 2's video output with ffmpeg.
|
|
4
|
+
*
|
|
5
|
+
* Primary source: the camera's SRT listener (the one streaming output this
|
|
6
|
+
* firmware actually serves; TAIL2-PROTOCOL.md §9) — requires Center's SRT
|
|
7
|
+
* toggle, exclusive with NDI. Because that toggle is a human-in-the-GUI step
|
|
8
|
+
* and this server is driven by AI agents that should stay autonomous, the
|
|
9
|
+
* fallback layer grabs from the NDI Tools "NDI Webcam" DirectShow device
|
|
10
|
+
* instead (Windows only): if the bridge is running and pointed at the
|
|
11
|
+
* camera's NDI feed, ffmpeg can read it directly and no Center trip is
|
|
12
|
+
* needed. When neither source is live, `snapshotOptions()` reports what it
|
|
13
|
+
* found and what each path costs, so the calling agent can pick.
|
|
14
|
+
*
|
|
15
|
+
* Note Center only allows changing streaming settings while the output is
|
|
16
|
+
* OFF, so probes landing in a configuration window see a dead port — not a
|
|
17
|
+
* startup delay (an earlier "minutes to bind" claim here was wrong and is
|
|
18
|
+
* retracted).
|
|
19
|
+
*/
|
|
20
|
+
export interface SrtSnapshotOpts {
|
|
21
|
+
maxDim?: number;
|
|
22
|
+
quality?: number;
|
|
23
|
+
port?: number;
|
|
24
|
+
streamId?: string;
|
|
25
|
+
/** Socket I/O budget for ffmpeg, ms. Generous: SRT handshake + first keyframe. */
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
}
|
|
28
|
+
export interface SrtSnapshot {
|
|
29
|
+
mime: string;
|
|
30
|
+
width: number;
|
|
31
|
+
height: number;
|
|
32
|
+
base64: string;
|
|
33
|
+
}
|
|
34
|
+
export declare class SrtNotEnabledError extends Error {
|
|
35
|
+
constructor();
|
|
36
|
+
}
|
|
37
|
+
export declare function srtSnapshot(host: string, opts?: SrtSnapshotOpts, spawnImpl?: typeof spawn): Promise<SrtSnapshot>;
|
|
38
|
+
/**
|
|
39
|
+
* Grab a frame from the NDI Tools "NDI Webcam" DirectShow device (Windows
|
|
40
|
+
* only). NOT used by the snapshot tool automatically: on the reference
|
|
41
|
+
* machine (2026-09-26) the bridge's user-configured output slots delivered
|
|
42
|
+
* uniformly black frames to ffmpeg (YAVG=YMAX=16) while other slots hung,
|
|
43
|
+
* and probing the devices disrupts the app. Kept for manual/verified use —
|
|
44
|
+
* if a slot is confirmed live (e.g. previews correctly in OBS), this works.
|
|
45
|
+
* Never force-kill the Webcam app: that corrupts its output-slot config.
|
|
46
|
+
*/
|
|
47
|
+
export declare function ndiWebcamSnapshot(opts?: {
|
|
48
|
+
maxDim?: number;
|
|
49
|
+
quality?: number;
|
|
50
|
+
timeoutMs?: number;
|
|
51
|
+
}, spawnImpl?: typeof spawn): Promise<SrtSnapshot | null>;
|
|
52
|
+
/** Standard NDI Tools install location, for the options report. */
|
|
53
|
+
export declare const NDI_WEBCAM_EXE = "C:\\Program Files\\NDI\\NDI 6 Tools\\Webcam\\Webcam.exe";
|
|
54
|
+
export declare const ndiToolsInstalled: () => boolean;
|