A software PTZ controller for up to 4 OBSBOT Tail Air / Tail 2 cameras, usable from Mac, iPad and iPhone — like a hardware PTZ joystick controller, but on your screens.
Project home: https://github.com/thorsheim/vPTZcontroller
It has two parts:
- The bridge — a small Node.js server that must run on a machine on the same network as the cameras (your Mac, or a Raspberry Pi). Browsers cannot send UDP packets, and VISCA-over-IP is a UDP protocol, so this bridge does the camera talking.
- The control surface — a touch-optimized web app served by the bridge. Open it in Safari/Chrome on any device on your network, and add it to your iPad/iPhone home screen so it behaves like a real app.
Features: camera switching, group mode (drive several cameras at once), variable-speed joystick (speed scales with how far you push), slow/normal/fast speed banks plus a fine slider, zoom at variable speed, presets 1–9 (save and recall, per camera or per group), focus control (auto / manual / one-push / near / far), home position, live low-latency previews of each camera via RTSP, online/offline status per camera, and full keyboard control on the Mac.
For each camera:
- Connect it to your network — Ethernet is strongly recommended over Wi-Fi for control responsiveness and preview stability.
- Give it a fixed IP address: either set a static IP on the camera, or (easier) create a DHCP reservation in your router so the camera always gets the same IP. Write the IPs down.
- In the OBSBOT app, enable VISCA over IP (the default port is 52381 — leave it unless you have a reason not to).
- (Optional, for previews) Enable RTSP on the camera (Media Transmission →
RTSP) and copy the exact
rtsp://…URL the app shows. You'll paste this into the controller's settings.
NDI note: your NDI streams are untouched by this app. Keep using NDI into OBS/vMix/etc. for program video. The controller's previews use RTSP because browsers can't decode NDI; quality is deliberately preview-grade so four streams stay light. See "NDI monitoring" below.
-
Install Node.js from https://nodejs.org — click the big green download button, open the
.pkgfile, and click through the installer like any Mac app. That's the only thing to install. -
Download the project: on the GitHub page click Code → Download ZIP and unzip it somewhere permanent (e.g. your home folder) — or
git clone https://github.com/thorsheim/vPTZcontroller.gitif you're comfortable with the terminal. Then double-clickStart PTZ Controller.command. A window opens, the bridge starts (installing its one small dependency automatically on first run), and the controller appears in your browser. Keep that window open while you use the controller; close it to stop the bridge.First launch only: macOS may block it because it was downloaded from the internet. If so, right-click the file → Open → Open (you only have to do this once). On newer macOS versions the allow button may instead be in System Settings → Privacy & Security.
If the launcher says Node.js isn't installed, it opens nodejs.org for you — install it, then double-click the launcher again.
(Terminal alternative, if you prefer:
cd ~/vPTZcontroller && npm install && node server.js.) -
(Optional, for previews) The video previews need ffmpeg. The easiest way to get it on a Mac is Homebrew (https://brew.sh): install Homebrew, then
brew install ffmpeg. Skip this for now if you like — every control works without it; you just won't see video in the app. -
The Terminal prints the address to open, e.g.
http://192.168.1.20:8080. Leave that Terminal window open while you use the controller — closing it stops the bridge.- On the Mac: open
http://localhost:8080. - On iPad/iPhone: open the
http://<mac-ip>:8080address in Safari, tap Share → Add to Home Screen. It launches full-screen like a native app.
- On the Mac: open
-
Click the ⚙️ gear, enter each camera's name + IP (+ RTSP URL if you want previews), tick Enabled, and save.
If macOS asks whether Node may accept incoming network connections, allow it.
To start the bridge automatically at login, add a Login Item for a tiny script,
or use pm2: npm install -g pm2 && pm2 start server.js --name ptz && pm2 save && pm2 startup.
-
Switch cameras: tap a camera card at the top. Green dot = camera responding.
-
Group mode: tap Group mode, then tap cameras to add/remove them from the group (amber outline). Every command — joystick, zoom, presets, home — now goes to all grouped cameras simultaneously. All selects every enabled camera.
-
Joystick: drag the pad. Direction = pan/tilt, distance from center = speed, scaled by the Slow/Normal/Fast bank or the fine slider. Release = instant stop.
-
Presets: tap 1–9 to recall. Tap Save mode then a slot to store the current position into that slot. (Presets are stored in the cameras, per camera.)
-
Zoom / Focus near / Focus far: press and hold; release stops.
-
Preview: shows the selected camera. Toggle off to save bandwidth.
-
SD / HQ: the button next to the preview toggle switches between light preview quality (480p, ~8 fps — fine for framing) and high-quality monitoring (1080p, ~20 fps from the same camera stream). HQ uses noticeably more bandwidth and CPU on the bridge machine, so use it one camera at a time. Both are tunable in
config.json(previewandpreviewHQ).
| Key | Action |
|---|---|
| Arrow keys | Pan/tilt (combinable, at current speed) |
+ / - (hold) |
Zoom tele / wide |
1–9 |
Recall preset |
Shift+1–9 |
Save preset |
Alt/Option+1–4 |
Switch active camera |
g |
Toggle group mode |
h |
Home position |
The bridge has to run somewhere — but it does not have to be a Mac. The same code runs on a Raspberry Pi (any model with Ethernet; a Pi Zero 2 W works):
sudo apt install -y nodejs npm ffmpeg git
git clone https://github.com/thorsheim/vPTZcontroller.git
cd vPTZcontroller && npm install
npm start # or use pm2 as above to run it permanentlyLeave the Pi powered near your router and your iPad/iPhone controller works with no Mac in sight. Until you add a Pi, iPad-only sessions can fall back to the official OBSBOT app (control only, one camera at a time).
The bridge exposes a plain HTTP API, so any Stream Deck action that can open a URL works — the built-in System → Website action (untick "open in browser" where available), or better, a web-request plugin like API Ninja from the Stream Deck store, which fires GET requests silently.
Point buttons at URLs like these (replace the IP with your bridge machine's IP):
http://192.168.1.20:8080/api/action?do=preset&cams=1&slot=3 recall preset 3 on cam 1
http://192.168.1.20:8080/api/action?do=preset&cams=1,2&slot=1 preset 1 on cams 1+2 together
http://192.168.1.20:8080/api/action?do=setpreset&cams=1&slot=3 save current shot to slot 3
http://192.168.1.20:8080/api/action?do=home&cams=all all cameras to home
http://192.168.1.20:8080/api/action?do=focus&cams=2&action=onepush one-push AF on cam 2
http://192.168.1.20:8080/api/action?do=stop&cams=all emergency stop everything
cams is a comma list of camera numbers (1–4) or all for every enabled
camera. The same API works from Bitfocus Companion, shell scripts, or anything
that can make an HTTP request.
Full NDI decoding in a browser isn't possible (NDI needs a native SDK), which is why previews use RTSP — that was the "preview quality is fine" trade-off we chose. If you later want true NDI multiview on the Mac, run a free NDI monitor (e.g. NDI Tools' Studio Monitor) alongside this controller; on iPad there are NDI monitor apps on the App Store. A future native Swift version of this app could embed the NDI SDK directly.
- Camera dot stays red: check the IP, that VISCA over IP is enabled on the
camera, and that camera + bridge are on the same subnet. Test with
ping <camera-ip>from the bridge machine. - No preview: is ffmpeg installed on the bridge machine? Is RTSP enabled on the camera and the URL exactly as shown in the OBSBOT app? Watch the bridge console — ffmpeg errors are printed there.
- Tilt speed feels the same as pan speed on Tail Air: known firmware quirk — the camera ignores the tilt-speed byte and uses the pan speed for both axes.
- Controls lag on Wi-Fi cameras: use Ethernet for the cameras if at all possible; Wi-Fi congestion affects both control and preview.
- Port 8080 already in use: start with another port:
PORT=8090 npm start.
server.js the bridge (VISCA over IP + WebSocket + preview relay)
public/index.html the entire control surface UI
public/manifest.webmanifest, sw.js, icon.png PWA bits for Add to Home Screen
config.json created on first run; your camera list lives here
Security note: the bridge has no authentication — anyone on your LAN can open the controller. That's usually fine on a home/production network; don't port-forward it to the internet.

