obsbot-mcp 0.1.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/LICENSE +21 -0
- package/README.md +170 -0
- package/dist/capture/ffmpeg-args.d.ts +17 -0
- package/dist/capture/ffmpeg-args.js +48 -0
- package/dist/capture/ffmpeg-args.js.map +1 -0
- package/dist/capture/manager.d.ts +60 -0
- package/dist/capture/manager.js +159 -0
- package/dist/capture/manager.js.map +1 -0
- package/dist/codec/commands.d.ts +63 -0
- package/dist/codec/commands.js +185 -0
- package/dist/codec/commands.js.map +1 -0
- package/dist/codec/crc.d.ts +2 -0
- package/dist/codec/crc.js +11 -0
- package/dist/codec/crc.js.map +1 -0
- package/dist/codec/encoding.d.ts +7 -0
- package/dist/codec/encoding.js +8 -0
- package/dist/codec/encoding.js.map +1 -0
- package/dist/codec/frame.d.ts +20 -0
- package/dist/codec/frame.js +73 -0
- package/dist/codec/frame.js.map +1 -0
- package/dist/codec/opcodes.d.ts +32 -0
- package/dist/codec/opcodes.js +509 -0
- package/dist/codec/opcodes.js.map +1 -0
- package/dist/codec/types.d.ts +13 -0
- package/dist/codec/types.js +2 -0
- package/dist/codec/types.js.map +1 -0
- package/dist/device/manager.d.ts +9 -0
- package/dist/device/manager.js +20 -0
- package/dist/device/manager.js.map +1 -0
- package/dist/device/session.d.ts +22 -0
- package/dist/device/session.js +37 -0
- package/dist/device/session.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/framing.d.ts +31 -0
- package/dist/mcp/framing.js +45 -0
- package/dist/mcp/framing.js.map +1 -0
- package/dist/mcp/ready.d.ts +31 -0
- package/dist/mcp/ready.js +71 -0
- package/dist/mcp/ready.js.map +1 -0
- package/dist/mcp/render.d.ts +3 -0
- package/dist/mcp/render.js +14 -0
- package/dist/mcp/render.js.map +1 -0
- package/dist/mcp/server.d.ts +3 -0
- package/dist/mcp/server.js +49 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +12 -0
- package/dist/mcp/tools.js +580 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/transport/helper-process.d.ts +39 -0
- package/dist/transport/helper-process.js +140 -0
- package/dist/transport/helper-process.js.map +1 -0
- package/dist/transport/transport.d.ts +62 -0
- package/dist/transport/transport.js +8 -0
- package/dist/transport/transport.js.map +1 -0
- package/dist/transport/windows.d.ts +34 -0
- package/dist/transport/windows.js +68 -0
- package/dist/transport/windows.js.map +1 -0
- package/native/prebuilt/win32-x64/obsbot-helper.exe +0 -0
- package/package.json +33 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Michael Jordan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# obsbot-mcp
|
|
2
|
+
|
|
3
|
+
A cross-platform [Model Context Protocol](https://modelcontextprotocol.io) server that controls an
|
|
4
|
+
**OBSBOT Tiny 2** camera over its standard UVC/USB interface — pan/tilt/roll the gimbal, zoom, AI
|
|
5
|
+
subject tracking, focus/exposure/white-balance/image controls, HDR and field-of-view, plus snapshot,
|
|
6
|
+
preview, and recording — without any vendor SDK.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install obsbot-mcp
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## MCP client configuration
|
|
15
|
+
|
|
16
|
+
Add a stdio server entry pointing at the installed binary (or directly at `dist/index.js`):
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"mcpServers": {
|
|
21
|
+
"obsbot": {
|
|
22
|
+
"command": "obsbot-mcp"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
If you're running from a local checkout instead of an npm install, point `command`/`args` at
|
|
29
|
+
`node` and the built entry point instead:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"obsbot": {
|
|
35
|
+
"command": "node",
|
|
36
|
+
"args": ["path/to/obsbot-mcp/dist/index.js"]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Debug / diagnostics tools
|
|
43
|
+
|
|
44
|
+
By default the server advertises only the normal control surface. Pass `--debug` to additionally
|
|
45
|
+
expose the diagnostics surface — the `obsbot_probe` tool (raw XU byte get/set/query) and the
|
|
46
|
+
`raw` 60-byte status block on `obsbot_get_status`:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"mcpServers": {
|
|
51
|
+
"obsbot": {
|
|
52
|
+
"command": "node",
|
|
53
|
+
"args": ["path/to/obsbot-mcp/dist/index.js", "--debug"]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
With the installed binary, use `"command": "obsbot-mcp"` and `"args": ["--debug"]`.
|
|
60
|
+
|
|
61
|
+
## Tools
|
|
62
|
+
|
|
63
|
+
### Device & power
|
|
64
|
+
|
|
65
|
+
| Tool | Parameters | Description |
|
|
66
|
+
|------|------------|-------------|
|
|
67
|
+
| `obsbot_list_devices` | — | List connected OBSBOT-compatible video capture devices. |
|
|
68
|
+
| `obsbot_set_run_status` | `state`: `"run" \| "sleep"` | Wake (`"run"`) or sleep the camera/gimbal. |
|
|
69
|
+
| `obsbot_get_status` | — | Read the live status block: `{ awake, hdr, aiMode, trackSpeed }`. Under `--debug`, also returns the raw 60-byte block as hex. |
|
|
70
|
+
|
|
71
|
+
### Gimbal (PTZ)
|
|
72
|
+
|
|
73
|
+
| Tool | Parameters | Description |
|
|
74
|
+
|------|------------|-------------|
|
|
75
|
+
| `obsbot_ptz_move_angle` | `yaw`, `pitch`, `roll` (degrees, `roll` defaults `0`) | Move the gimbal to an absolute angle. Positive yaw pans to the camera's left, positive pitch tilts down. Yaw clamped to `[-150, 150]`, pitch to `[-90, 90]`. Absolute 1:1 degrees, hardware-verified. |
|
|
76
|
+
| `obsbot_ptz_move_speed` | `yaw`, `pitch`, `roll` (deg/s, `roll` defaults `0`), `autoStopMs` (default `800`) | Drive the gimbal at a speed, then auto-stop after `autoStopMs` so it can't run away. Same yaw/pitch sign convention as `move_angle`. |
|
|
77
|
+
| `obsbot_gimbal_recenter` | — | Recenter the gimbal (return to home position). |
|
|
78
|
+
| `obsbot_gimbal_position` | — | Read the gimbal's current absolute `{ yaw, pitch }` in degrees via standard UVC Pan/Tilt. May lag a move still in progress. |
|
|
79
|
+
|
|
80
|
+
### Zoom
|
|
81
|
+
|
|
82
|
+
| Tool | Parameters | Description |
|
|
83
|
+
|------|------------|-------------|
|
|
84
|
+
| `obsbot_zoom_absolute` | `ratio` (`1.0`–`2.0`) | Set absolute zoom ratio, clamped to `[1.0, 2.0]`. |
|
|
85
|
+
| `obsbot_zoom_speed` | `ratio` (`1.0`–`2.0`), `speed` (default `0`) | Zoom to a ratio at a chosen speed: `0` = device default, `1`–`10` slow→fast, `255` = maximum. |
|
|
86
|
+
|
|
87
|
+
### AI tracking
|
|
88
|
+
|
|
89
|
+
| Tool | Parameters | Description |
|
|
90
|
+
|------|------------|-------------|
|
|
91
|
+
| `obsbot_ai_tracking` | `enabled` (bool), `mode` (default `"normal"`) | Enable/disable AI subject tracking and choose framing: `normal \| upper-body \| close-up \| headless \| lower-body`. Polls status and returns `{ verified, matched }` (`matched:false` = no subject tracked yet). |
|
|
92
|
+
| `obsbot_ai_track_speed` | `speed`: `"standard" \| "sport"` | Set the tracking-speed preset (Center's Standard/Sport): `standard` (slower follow) or `sport` (snappier). |
|
|
93
|
+
| `obsbot_face_focus` | `enabled` (bool) | Enable or disable face-priority autofocus. |
|
|
94
|
+
|
|
95
|
+
### Image & lens
|
|
96
|
+
|
|
97
|
+
| Tool | Parameters | Description |
|
|
98
|
+
|------|------------|-------------|
|
|
99
|
+
| `obsbot_fov` | `fov`: `"wide" \| "medium" \| "narrow"` | Set the field of view: wide (86°), medium (78°), narrow (65°). |
|
|
100
|
+
| `obsbot_hdr` | `enabled` (bool) | Toggle HDR/WDR imaging on or off. |
|
|
101
|
+
| `obsbot_focus` | `mode`: `"auto" \| "manual"`, `position` (`0`–`100`, default `50`) | `auto` = continuous autofocus; `manual` = set the focus motor to `position` (near→far). |
|
|
102
|
+
| `obsbot_exposure` | `mode`: `"auto" \| "manual"`, `level` (`0`–`100`, default `50`) | `auto` = auto-exposure; `manual` = set `level` (0 darkest → 100 brightest). |
|
|
103
|
+
| `obsbot_white_balance` | `mode`: `"auto" \| "manual"`, `temperature` (Kelvin, default `5000`) | `auto` = auto white balance; `manual` = set a colour temperature (clamped to device range). |
|
|
104
|
+
| `obsbot_image_control` | `control`, `level` (`0`–`100`) | Adjust `brightness \| contrast \| hue \| saturation \| sharpness \| gain \| backlight-compensation`; `level` maps onto the device range. |
|
|
105
|
+
|
|
106
|
+
### Capture
|
|
107
|
+
|
|
108
|
+
| Tool | Parameters | Description |
|
|
109
|
+
|------|------------|-------------|
|
|
110
|
+
| `obsbot_snapshot` | `maxDim` (`256`–`1920`, default `1024`), `quality` (`1`–`100`, default `80`), `settleMs` (default `600`), `source` (default `"device"`) | Grab one still frame and return it as an image (for framing/lighting/exposure checks). `source`: `device \| virtual \| ndi`. |
|
|
111
|
+
| `obsbot_record_start` | `durationSec` (optional), `audio` (default `true`), `outputPath` (optional), `source` (default `"device"`) | Start recording to MP4. Open-ended recordings auto-stop after 60 min; audio uses the OBSBOT mic; defaults under `Videos/OBSBOT`. Returns a `sessionId`. **Needs ffmpeg.**¹ |
|
|
112
|
+
| `obsbot_preview_start` | `source` (default `"device"`) | Open a live preview window. Returns a `sessionId`. **Needs ffplay.**¹ |
|
|
113
|
+
| `obsbot_capture_stop` | `sessionId` | Stop a recording or preview session (recordings are finalized gracefully). |
|
|
114
|
+
| `obsbot_capture_list` | — | List active recording/preview sessions. |
|
|
115
|
+
|
|
116
|
+
### Diagnostics (`--debug` only)
|
|
117
|
+
|
|
118
|
+
| Tool | Parameters | Description |
|
|
119
|
+
|------|------------|-------------|
|
|
120
|
+
| `obsbot_probe` | `mode`: `"get" \| "set" \| "query"`, plus `selector`, `length`, `hex`, `opcode`, `payloadHex` | RE/diagnostics only — raw XU byte get/set and framed table queries. Advertised only under `--debug`. |
|
|
121
|
+
|
|
122
|
+
¹ `record`/`preview` shell out to **ffmpeg**/**ffplay** (install: `winget install Gyan.FFmpeg`
|
|
123
|
+
on Windows, `brew install ffmpeg` on macOS, `apt install ffmpeg` on Linux). `snapshot` does **not**
|
|
124
|
+
need ffmpeg — it grabs the frame through the native helper.
|
|
125
|
+
|
|
126
|
+
## Supported platforms
|
|
127
|
+
|
|
128
|
+
- **Windows x64** — supported today. The native helper is built from source in `native/windows/`
|
|
129
|
+
(CMake + MSVC); the published npm package ships a prebuilt binary so end users need no toolchain.
|
|
130
|
+
- **Linux / macOS** — not yet implemented. The design is platform-agnostic (see [`PROTOCOL.md`](./PROTOCOL.md));
|
|
131
|
+
adding support means writing an equivalent native helper for each OS's UVC control APIs
|
|
132
|
+
(`V4L2` on Linux, `AVFoundation`/`IOKit` on macOS) behind the same JSON-RPC-over-stdio contract used by
|
|
133
|
+
the Windows helper. Contributions welcome.
|
|
134
|
+
|
|
135
|
+
## No proprietary SDK
|
|
136
|
+
|
|
137
|
+
This project speaks the camera's USB protocol directly through the OS's standard UVC driver stack and
|
|
138
|
+
does **not** use, link, bundle, or ship any vendor SDK. See [`PROTOCOL.md`](./PROTOCOL.md) for the
|
|
139
|
+
protocol reference (frame format, checksum, command table).
|
|
140
|
+
|
|
141
|
+
## How it works
|
|
142
|
+
|
|
143
|
+
The camera exposes two independent control surfaces, both reachable through the OS's standard UVC
|
|
144
|
+
(USB Video Class) driver stack — this project never talks to the USB device directly, so the OS keeps
|
|
145
|
+
mediating access and the camera remains usable as a normal webcam at the same time commands are sent:
|
|
146
|
+
|
|
147
|
+
- **Standard UVC controls** — zoom (`CT_ZOOM_ABSOLUTE`), focus and exposure (`IAMCameraControl`),
|
|
148
|
+
gimbal position readback (UVC Pan/Tilt), and the image controls plus white balance
|
|
149
|
+
(`IAMVideoProcAmp`) — are the camera's built-in UVC properties, driven via DirectShow on Windows.
|
|
150
|
+
- **Vendor commands** — gimbal moves, recenter, wake/sleep, AI tracking, HDR, and field of view —
|
|
151
|
+
are sent through the camera's UVC Extension Unit, driven via `IKsControl::KsProperty` against the
|
|
152
|
+
XU's topology node on Windows.
|
|
153
|
+
|
|
154
|
+
Both are issued through a small native helper process (`obsbot-helper.exe` on Windows) that the Node
|
|
155
|
+
server spawns and talks to over a line-delimited JSON-RPC protocol on stdin/stdout. The helper is the
|
|
156
|
+
only platform-specific piece; the codec (frame encoding, CRC-16/USB checksum, command table), transport
|
|
157
|
+
abstraction, device manager, and MCP tool definitions are all pure TypeScript/JavaScript and shared across
|
|
158
|
+
platforms.
|
|
159
|
+
|
|
160
|
+
## Verifying against real hardware
|
|
161
|
+
|
|
162
|
+
`scripts/e2e.mjs` drives the built stack (`dist/`) against a physically connected camera: it wakes the
|
|
163
|
+
device, zooms in, pans the gimbal, recenters, zooms back out, and puts the camera to sleep, with a short
|
|
164
|
+
pause and console log before each step so a human can watch it happen. **This moves the physical gimbal —
|
|
165
|
+
only run it under supervision:**
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
npm run build
|
|
169
|
+
node scripts/e2e.mjs
|
|
170
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type CaptureSource = "device" | "virtual" | "ndi";
|
|
2
|
+
export interface DshowDevices {
|
|
3
|
+
video: string[];
|
|
4
|
+
audio: string[];
|
|
5
|
+
}
|
|
6
|
+
export declare function parseDshowDevices(stderr: string): DshowDevices;
|
|
7
|
+
export declare function resolveVideoName(devices: DshowDevices, source: CaptureSource): string | undefined;
|
|
8
|
+
export declare function resolveAudioName(devices: DshowDevices): string | undefined;
|
|
9
|
+
export declare function buildRecordArgs(o: {
|
|
10
|
+
videoName: string;
|
|
11
|
+
audioName?: string;
|
|
12
|
+
durationSec: number;
|
|
13
|
+
outputPath: string;
|
|
14
|
+
}): string[];
|
|
15
|
+
export declare function buildPreviewArgs(o: {
|
|
16
|
+
videoName: string;
|
|
17
|
+
}): string[];
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// ffmpeg -f dshow -list_devices prints one line per device on stderr:
|
|
2
|
+
// [dshow @ ..] "Friendly Name" (video)
|
|
3
|
+
// The name is the first quoted span; the trailing "(video)"/"(audio)" is the type.
|
|
4
|
+
export function parseDshowDevices(stderr) {
|
|
5
|
+
const video = [];
|
|
6
|
+
const audio = [];
|
|
7
|
+
for (const line of stderr.split(/\r?\n/)) {
|
|
8
|
+
const m = line.match(/"(.+)" \((video|audio)\)\s*$/);
|
|
9
|
+
if (!m)
|
|
10
|
+
continue;
|
|
11
|
+
if (m[2] === "video")
|
|
12
|
+
video.push(m[1]);
|
|
13
|
+
else
|
|
14
|
+
audio.push(m[1]);
|
|
15
|
+
}
|
|
16
|
+
return { video, audio };
|
|
17
|
+
}
|
|
18
|
+
export function resolveVideoName(devices, source) {
|
|
19
|
+
if (source === "virtual")
|
|
20
|
+
return devices.video.find((n) => /OBSBOT Virtual Camera/i.test(n));
|
|
21
|
+
if (source === "ndi")
|
|
22
|
+
return devices.video.find((n) => /NDI Webcam/i.test(n));
|
|
23
|
+
return devices.video.find((n) => /OBSBOT Tiny 2/i.test(n) && !/Virtual/i.test(n));
|
|
24
|
+
}
|
|
25
|
+
export function resolveAudioName(devices) {
|
|
26
|
+
return devices.audio.find((n) => /OBSBOT.*Mic/i.test(n));
|
|
27
|
+
}
|
|
28
|
+
export function buildRecordArgs(o) {
|
|
29
|
+
const input = o.audioName
|
|
30
|
+
? `video=${o.videoName}:audio=${o.audioName}`
|
|
31
|
+
: `video=${o.videoName}`;
|
|
32
|
+
return [
|
|
33
|
+
"-hide_banner", "-loglevel", "warning", "-f", "dshow",
|
|
34
|
+
"-i", input,
|
|
35
|
+
"-t", String(o.durationSec),
|
|
36
|
+
"-c:v", "libx264", "-pix_fmt", "yuv420p",
|
|
37
|
+
...(o.audioName ? ["-c:a", "aac"] : []),
|
|
38
|
+
"-y", o.outputPath,
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
export function buildPreviewArgs(o) {
|
|
42
|
+
return [
|
|
43
|
+
"-hide_banner", "-loglevel", "warning", "-f", "dshow",
|
|
44
|
+
"-i", `video=${o.videoName}`,
|
|
45
|
+
"-window_title", "OBSBOT preview",
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=ffmpeg-args.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ffmpeg-args.js","sourceRoot":"","sources":["../../src/capture/ffmpeg-args.ts"],"names":[],"mappings":"AASA,sEAAsE;AACtE,yCAAyC;AACzC,mFAAmF;AACnF,MAAM,UAAU,iBAAiB,CAAC,MAAc;IAC9C,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QACzC,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAAC;QACrD,IAAI,CAAC,CAAC;YAAE,SAAS;QACjB,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,OAAO;YAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;;YAClC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACxB,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,OAAqB,EACrB,MAAqB;IAErB,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,wBAAwB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7F,IAAI,MAAM,KAAK,KAAK;QAAE,OAAO,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9E,OAAO,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AACpF,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,OAAqB;IACpD,OAAO,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,CAK/B;IACC,MAAM,KAAK,GAAG,CAAC,CAAC,SAAS;QACvB,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,UAAU,CAAC,CAAC,SAAS,EAAE;QAC7C,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,EAAE,CAAC;IAC3B,OAAO;QACL,cAAc,EAAE,WAAW,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO;QACrD,IAAI,EAAE,KAAK;QACX,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC;QAC3B,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS;QACxC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACvC,IAAI,EAAE,CAAC,CAAC,UAAU;KACnB,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,CAAwB;IACvD,OAAO;QACL,cAAc,EAAE,WAAW,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO;QACrD,IAAI,EAAE,SAAS,CAAC,CAAC,SAAS,EAAE;QAC5B,eAAe,EAAE,gBAAgB;KAClC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { spawn as realSpawn } from "node:child_process";
|
|
2
|
+
import { type CaptureSource } from "./ffmpeg-args.js";
|
|
3
|
+
export interface CaptureSession {
|
|
4
|
+
id: string;
|
|
5
|
+
kind: "record" | "preview";
|
|
6
|
+
pid: number;
|
|
7
|
+
source: CaptureSource;
|
|
8
|
+
outputPath?: string;
|
|
9
|
+
durationSec?: number;
|
|
10
|
+
startedAtIso: string;
|
|
11
|
+
}
|
|
12
|
+
/** Expected operational failure (surfaced to the user as text, not thrown to the client). */
|
|
13
|
+
export declare class CaptureError extends Error {
|
|
14
|
+
constructor(message: string);
|
|
15
|
+
}
|
|
16
|
+
export declare class FfmpegMissingError extends CaptureError {
|
|
17
|
+
constructor();
|
|
18
|
+
}
|
|
19
|
+
interface FsLike {
|
|
20
|
+
existsSync: (p: string) => boolean;
|
|
21
|
+
mkdirSync: (p: string, opts: {
|
|
22
|
+
recursive: boolean;
|
|
23
|
+
}) => void;
|
|
24
|
+
}
|
|
25
|
+
interface Deps {
|
|
26
|
+
spawn?: typeof realSpawn;
|
|
27
|
+
clock?: () => string;
|
|
28
|
+
hasBinary?: (name: string) => boolean;
|
|
29
|
+
fs?: FsLike;
|
|
30
|
+
}
|
|
31
|
+
export declare class CaptureManager {
|
|
32
|
+
private readonly spawn;
|
|
33
|
+
private readonly clock;
|
|
34
|
+
private readonly hasBinary;
|
|
35
|
+
private readonly fs;
|
|
36
|
+
private readonly sessions;
|
|
37
|
+
private devices?;
|
|
38
|
+
private seq;
|
|
39
|
+
constructor(deps?: Deps);
|
|
40
|
+
private ensureFfmpeg;
|
|
41
|
+
private probeDevices;
|
|
42
|
+
private timestampName;
|
|
43
|
+
startRecord(o: {
|
|
44
|
+
source: CaptureSource;
|
|
45
|
+
durationSec?: number;
|
|
46
|
+
audio: boolean;
|
|
47
|
+
outputPath?: string;
|
|
48
|
+
}): Promise<CaptureSession>;
|
|
49
|
+
startPreview(o: {
|
|
50
|
+
source: CaptureSource;
|
|
51
|
+
}): Promise<CaptureSession>;
|
|
52
|
+
stop(id: string): Promise<{
|
|
53
|
+
kind: "record" | "preview";
|
|
54
|
+
outputPath?: string;
|
|
55
|
+
graceful: boolean;
|
|
56
|
+
}>;
|
|
57
|
+
list(): CaptureSession[];
|
|
58
|
+
stopAll(): void;
|
|
59
|
+
}
|
|
60
|
+
export {};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { spawn as realSpawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { parseDshowDevices, resolveVideoName, resolveAudioName, buildRecordArgs, buildPreviewArgs, } from "./ffmpeg-args.js";
|
|
6
|
+
/** Expected operational failure (surfaced to the user as text, not thrown to the client). */
|
|
7
|
+
export class CaptureError extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "CaptureError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class FfmpegMissingError extends CaptureError {
|
|
14
|
+
constructor() {
|
|
15
|
+
super("Recording/preview needs ffmpeg and ffplay, which aren't installed. " +
|
|
16
|
+
"Install with: winget install Gyan.FFmpeg (Windows) / brew install ffmpeg (mac) / " +
|
|
17
|
+
"apt install ffmpeg (Linux).");
|
|
18
|
+
this.name = "FfmpegMissingError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const OPEN_ENDED_CAP_SEC = 3600;
|
|
22
|
+
const GRACEFUL_STOP_MS = 5000;
|
|
23
|
+
function defaultHasBinary(name) {
|
|
24
|
+
const r = spawnSync(name, ["-version"], { stdio: "ignore" });
|
|
25
|
+
return !r.error;
|
|
26
|
+
}
|
|
27
|
+
export class CaptureManager {
|
|
28
|
+
spawn;
|
|
29
|
+
clock;
|
|
30
|
+
hasBinary;
|
|
31
|
+
fs;
|
|
32
|
+
sessions = new Map();
|
|
33
|
+
devices;
|
|
34
|
+
seq = 0;
|
|
35
|
+
constructor(deps = {}) {
|
|
36
|
+
this.spawn = deps.spawn ?? realSpawn;
|
|
37
|
+
this.clock = deps.clock ?? (() => new Date().toISOString());
|
|
38
|
+
this.hasBinary = deps.hasBinary ?? defaultHasBinary;
|
|
39
|
+
this.fs = deps.fs ?? { existsSync, mkdirSync };
|
|
40
|
+
}
|
|
41
|
+
ensureFfmpeg() {
|
|
42
|
+
if (!this.hasBinary("ffmpeg") || !this.hasBinary("ffplay")) {
|
|
43
|
+
throw new FfmpegMissingError();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
probeDevices() {
|
|
47
|
+
if (this.devices)
|
|
48
|
+
return Promise.resolve(this.devices);
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const child = this.spawn("ffmpeg", ["-hide_banner", "-f", "dshow", "-list_devices", "true", "-i", "dummy"]);
|
|
51
|
+
let err = "";
|
|
52
|
+
child.stderr?.on("data", (d) => { err += d.toString(); });
|
|
53
|
+
child.on("error", (e) => reject(new CaptureError(`failed to run ffmpeg for device probe: ${e.message}`)));
|
|
54
|
+
child.on("close", () => {
|
|
55
|
+
this.devices = parseDshowDevices(err);
|
|
56
|
+
resolve(this.devices);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
timestampName() {
|
|
61
|
+
// clock() -> "2026-07-13T09:33:58.000Z"; make obsbot-YYYYMMDD-HHMMSS.mp4
|
|
62
|
+
const iso = this.clock();
|
|
63
|
+
const compact = iso.replace(/[-:]/g, "").replace("T", "-").replace(/\..*$/, "");
|
|
64
|
+
return `obsbot-${compact}.mp4`;
|
|
65
|
+
}
|
|
66
|
+
async startRecord(o) {
|
|
67
|
+
this.ensureFfmpeg();
|
|
68
|
+
const devices = await this.probeDevices();
|
|
69
|
+
const videoName = resolveVideoName(devices, o.source);
|
|
70
|
+
if (!videoName)
|
|
71
|
+
throw new CaptureError(`no '${o.source}' video source found (is OBSBOT Center / NDI running?)`);
|
|
72
|
+
let audioName;
|
|
73
|
+
if (o.audio) {
|
|
74
|
+
audioName = resolveAudioName(devices);
|
|
75
|
+
if (!audioName)
|
|
76
|
+
throw new CaptureError("OBSBOT microphone not found; retry with audio:false for a silent clip");
|
|
77
|
+
}
|
|
78
|
+
const outputPath = o.outputPath ?? join(homedir(), "Videos", "OBSBOT", this.timestampName());
|
|
79
|
+
if (o.outputPath && this.fs.existsSync(o.outputPath)) {
|
|
80
|
+
throw new CaptureError(`output file already exists: ${o.outputPath}`);
|
|
81
|
+
}
|
|
82
|
+
this.fs.mkdirSync(dirname(outputPath), { recursive: true });
|
|
83
|
+
const durationSec = o.durationSec ?? OPEN_ENDED_CAP_SEC;
|
|
84
|
+
const args = buildRecordArgs({ videoName, audioName, durationSec, outputPath });
|
|
85
|
+
const child = this.spawn("ffmpeg", args, { stdio: ["pipe", "ignore", "ignore"] });
|
|
86
|
+
const session = {
|
|
87
|
+
id: `cap${++this.seq}`, kind: "record", pid: child.pid ?? -1,
|
|
88
|
+
source: o.source, outputPath, durationSec, startedAtIso: this.clock(),
|
|
89
|
+
};
|
|
90
|
+
this.sessions.set(session.id, { session, child });
|
|
91
|
+
child.once("exit", () => this.sessions.delete(session.id));
|
|
92
|
+
child.on("error", () => this.sessions.delete(session.id));
|
|
93
|
+
return session;
|
|
94
|
+
}
|
|
95
|
+
async startPreview(o) {
|
|
96
|
+
this.ensureFfmpeg();
|
|
97
|
+
const devices = await this.probeDevices();
|
|
98
|
+
const videoName = resolveVideoName(devices, o.source);
|
|
99
|
+
if (!videoName)
|
|
100
|
+
throw new CaptureError(`no '${o.source}' video source found (is OBSBOT Center / NDI running?)`);
|
|
101
|
+
const args = buildPreviewArgs({ videoName });
|
|
102
|
+
const child = this.spawn("ffplay", args, { stdio: "ignore" });
|
|
103
|
+
const session = {
|
|
104
|
+
id: `cap${++this.seq}`, kind: "preview", pid: child.pid ?? -1,
|
|
105
|
+
source: o.source, startedAtIso: this.clock(),
|
|
106
|
+
};
|
|
107
|
+
this.sessions.set(session.id, { session, child });
|
|
108
|
+
child.once("exit", () => this.sessions.delete(session.id));
|
|
109
|
+
child.on("error", () => this.sessions.delete(session.id));
|
|
110
|
+
return session;
|
|
111
|
+
}
|
|
112
|
+
async stop(id) {
|
|
113
|
+
const entry = this.sessions.get(id);
|
|
114
|
+
if (!entry)
|
|
115
|
+
throw new CaptureError(`no such capture session: ${id}`);
|
|
116
|
+
const { session, child } = entry;
|
|
117
|
+
let graceful = true;
|
|
118
|
+
if (session.kind === "record") {
|
|
119
|
+
// 'q' on stdin is ffmpeg's clean stop (finalizes the MP4 moov atom).
|
|
120
|
+
graceful = await new Promise((resolve) => {
|
|
121
|
+
let done = false;
|
|
122
|
+
const timer = setTimeout(() => { if (!done) {
|
|
123
|
+
done = true;
|
|
124
|
+
child.kill();
|
|
125
|
+
resolve(false);
|
|
126
|
+
} }, GRACEFUL_STOP_MS);
|
|
127
|
+
child.once("exit", () => { if (!done) {
|
|
128
|
+
done = true;
|
|
129
|
+
clearTimeout(timer);
|
|
130
|
+
resolve(true);
|
|
131
|
+
} });
|
|
132
|
+
try {
|
|
133
|
+
child.stdin?.write("q");
|
|
134
|
+
}
|
|
135
|
+
catch { /* already gone */ }
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
child.kill();
|
|
140
|
+
}
|
|
141
|
+
this.sessions.delete(id);
|
|
142
|
+
return { kind: session.kind, outputPath: session.outputPath, graceful };
|
|
143
|
+
}
|
|
144
|
+
list() {
|
|
145
|
+
return [...this.sessions.values()].map((e) => e.session);
|
|
146
|
+
}
|
|
147
|
+
// Hard-kill every child. Wired to server shutdown so nothing orphans; this is
|
|
148
|
+
// last-resort cleanup, not a graceful stop.
|
|
149
|
+
stopAll() {
|
|
150
|
+
for (const { child } of this.sessions.values()) {
|
|
151
|
+
try {
|
|
152
|
+
child.kill();
|
|
153
|
+
}
|
|
154
|
+
catch { /* ignore */ }
|
|
155
|
+
}
|
|
156
|
+
this.sessions.clear();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manager.js","sourceRoot":"","sources":["../../src/capture/manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { VendorFrame, RunState } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* RE/diagnostics: build ANY opcode-table entry as a V3 frame with an arbitrary
|
|
4
|
+
* payload. Used by the obsbot_probe tool to exercise unmapped GET/query opcodes
|
|
5
|
+
* (e.g. AI_GET_QUICK_STATUS) while reverse-engineering the feedback surface.
|
|
6
|
+
*/
|
|
7
|
+
export declare const encodeVendorProbe: (name: string, payload: Buffer) => VendorFrame;
|
|
8
|
+
export declare const encodeSetRunStatus: (state: RunState) => VendorFrame;
|
|
9
|
+
export declare const encodePtzMoveAngle: (yaw: number, pitch: number, roll: number) => VendorFrame;
|
|
10
|
+
export declare const encodePtzMoveSpeed: (yaw: number, pitch: number, roll: number) => VendorFrame;
|
|
11
|
+
export declare const encodeRecenter: () => VendorFrame;
|
|
12
|
+
export declare const zoomRatioToUnits: (ratio: number, min: number, max: number) => number;
|
|
13
|
+
export type AiTrackMode = "human-normal" | "human-full-body" | "human-half-body" | "human-close-up" | "human-auto-view" | "animal-normal" | "animal-close-up" | "animal-auto-view";
|
|
14
|
+
export declare const AI_TRACK_MODES: AiTrackMode[];
|
|
15
|
+
/** Enable AI tracking in a specific framing mode (AI_SET_AI_TRACK_MODE, cmd 0x6f). */
|
|
16
|
+
export declare const encodeAiTrackEnable: (mode: AiTrackMode) => VendorFrame;
|
|
17
|
+
/** Disable AI tracking of the current target (AI_SET_CANCEL_TARGET, cmd 0x6e — no payload). */
|
|
18
|
+
export declare const encodeAiTrackDisable: () => VendorFrame;
|
|
19
|
+
/** Enable auto group-framing (AI_SET_AUTO_GROUP, cmd 0x70 — 8 zero bytes). */
|
|
20
|
+
export declare const encodeAiGroupEnable: () => VendorFrame;
|
|
21
|
+
/** Disable auto group-framing (AI_CANCEL_AUTO_GROUP, cmd 0x71 — no payload). */
|
|
22
|
+
export declare const encodeAiGroupDisable: () => VendorFrame;
|
|
23
|
+
export type AiTrackSpeed = "standard" | "sport";
|
|
24
|
+
export declare const AI_TRACK_SPEEDS: AiTrackSpeed[];
|
|
25
|
+
export declare const encodeAiTrackSpeed: (speed: AiTrackSpeed) => VendorFrame;
|
|
26
|
+
export declare const encodeZoomWithSpeed: (ratioX100: number, speed: number) => VendorFrame;
|
|
27
|
+
export declare const encodeFaceFocus: (enable: boolean) => VendorFrame;
|
|
28
|
+
/** Request the current face-priority autofocus state (CAM_GET_FACE_FOCUS, cmd 0x35c2). */
|
|
29
|
+
export declare const encodeGetFaceFocus: () => VendorFrame;
|
|
30
|
+
export interface FaceFocusState {
|
|
31
|
+
enabled: boolean;
|
|
32
|
+
}
|
|
33
|
+
/** Decode a CAM_GET_FACE_FOCUS reply payload: int32le enable flag at offset 0. */
|
|
34
|
+
export declare const decodeFaceFocus: (payload: Buffer) => FaceFocusState;
|
|
35
|
+
export declare const UVC_XU_SELECTOR = 6;
|
|
36
|
+
export type FovType = "wide" | "medium" | "narrow";
|
|
37
|
+
export declare const FOV_TYPES: FovType[];
|
|
38
|
+
export declare const encodeFov: (fov: FovType) => Buffer;
|
|
39
|
+
export declare const encodeHdr: (on: boolean) => Buffer;
|
|
40
|
+
export type AiFramingMode = "normal" | "upper-body" | "close-up" | "headless" | "lower-body";
|
|
41
|
+
export declare const AI_FRAMING_MODES: AiFramingMode[];
|
|
42
|
+
export declare const encodeAiTracking: (on: boolean, mode?: AiFramingMode) => Buffer;
|
|
43
|
+
export declare const CAMERA_CONTROL_PAN = 0;
|
|
44
|
+
export declare const CAMERA_CONTROL_TILT = 1;
|
|
45
|
+
export declare const CAMERA_CONTROL_FOCUS = 6;
|
|
46
|
+
export declare const CAMERA_CONTROL_EXPOSURE = 4;
|
|
47
|
+
export declare const VIDEO_PROCAMP_WHITE_BALANCE = 7;
|
|
48
|
+
export declare const UVC_FLAG_AUTO = 1;
|
|
49
|
+
export declare const UVC_FLAG_MANUAL = 2;
|
|
50
|
+
export type ImageControl = "brightness" | "contrast" | "hue" | "saturation" | "sharpness" | "backlight-compensation" | "gain";
|
|
51
|
+
export declare const IMAGE_CONTROL_PROP: Record<ImageControl, number>;
|
|
52
|
+
export declare const IMAGE_CONTROLS: ImageControl[];
|
|
53
|
+
/** Map a 0..100 percentage onto an inclusive [min,max] device-unit range. */
|
|
54
|
+
export declare const percentToRange: (pct: number, min: number, max: number) => number;
|
|
55
|
+
export type AiModeStatus = "no-tracking" | "normal" | "upper-body" | "close-up" | "headless" | "lower-body" | "desk" | "whiteboard" | "hand" | "group" | "unknown";
|
|
56
|
+
export type TrackSpeedStatus = "standard" | "sport" | "unknown";
|
|
57
|
+
export interface CameraStatus {
|
|
58
|
+
awake: boolean;
|
|
59
|
+
hdr: boolean;
|
|
60
|
+
aiMode: AiModeStatus;
|
|
61
|
+
trackSpeed: TrackSpeedStatus;
|
|
62
|
+
}
|
|
63
|
+
export declare const decodeStatus: (block: Buffer) => CameraStatus;
|