This document provides a comprehensive, step-by-step walkthrough for installing, configuring, and running a Node.js–based UVC PTZ (pan/tilt/zoom) agent on remote machines. Once deployed, the agent exposes a secure HTTP API to control any UVC-compliant USB camera attached to the host.
- Overview
- Prerequisites
- Node.js & npm
- Build Tools
- Network Requirements
- Project Setup
- Creating the Directory
- Initializing npm
- Installing Dependencies
- Agent Implementation
- File Structure
- index.js Explained
- Express Server
- Authentication Middleware
- UVCControl Integration
- API Endpoints
- Configuration & Environment
- Environment Variables
- Securing Credentials
- Running & Testing
- Local Launch
- Healthcheck & Capabilities Endpoint
- Making Control Requests
- Daemonizing the Agent
- Linux (systemd)
- Windows (NSSM)
- Cross-Platform (PM2)
- Network Security
- TLS/HTTPS with Reverse Proxy
- VPN or SSH Tunnel
- Central Server Integration
- Example Axios Client
- Error Handling
- Troubleshooting & Tips
- Common Build Errors
- USB Permission Issues
- Handling Multiple Cameras
- Appendix
- Useful Commands
- References
The UVC PTZ Agent is a lightweight Node.js service that wraps the uvc-control library to expose camera pan/tilt/zoom controls over HTTP. Deploy one agent per machine where a UVC camera is attached. Your central server can then remotely pan, tilt, or zoom the camera by sending authenticated HTTP requests.
- Version: Node.js v14 or newer.
- Installation:
- Windows/Mac: Download and install from nodejs.org.
- Linux (Debian/Ubuntu):
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash - sudo apt-get install -y nodejs
Native addons require C/C++ build tools and libusb headers:
-
Windows:
- Install Visual Studio Build Tools with the “Desktop development with C++” workload.
-
Linux:
sudo apt-get update sudo apt-get install -y build-essential libusb-1.0-0-dev pkg-config
-
macOS:
xcode-select --install
- Port Access: Decide on an HTTP port (default 3000). Ensure inbound TCP on that port is allowed by firewall or VPN.
- Security: Plan to secure the agent endpoint via Basic Auth, HTTPS, or VPN.
On the remote host:
mkdir ~/uvc-agent && cd ~/uvc-agentnpm init -yThis creates package.json with defaults.
npm install express uvc-control basic-auth- express: HTTP server framework
- uvc-control: UVC PTZ bindings
- basic-auth: Minimal Basic Auth parser
uvc-agent/
├── index.js
├── package.json
└── .env # optional for environment variables
Below is the full code for index.js with inline comments.
#!/usr/bin/env node
const express = require('express'); // Web server
const basicAuth = require('basic-auth'); // HTTP Basic Auth helper
const UVCControl = require('uvc-control'); // Camera PTZ library
const app = express();
app.use(express.json()); // parse JSON bodies
// ---- CONFIGURATION ----
const PORT = process.env.PORT || 3000;
const AUTH_USER = process.env.AUTH_USER || 'admin';
const AUTH_PASS = process.env.AUTH_PASS || 'secret';
// ---- AUTHENTICATION MIDDLEWARE ----
app.use((req, res, next) => {
const creds = basicAuth(req);
if (!creds || creds.name !== AUTH_USER || creds.pass !== AUTH_PASS) {
res.set('WWW-Authenticate', 'Basic realm="PTZ Agent"');
return res.status(401).send('Authentication required');
}
next();
});
// ---- INITIALIZE CAMERA ----
const cam = new UVCControl();
// ---- HEALTHCHECK ----
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
// ---- GET CAMERA CAPABILITIES ----
app.get('/caps', async (req, res) => {
try {
const caps = await cam.range(); // list of control ranges
res.json(caps);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ---- SET A CONTROL ----
app.post('/control', async (req, res) => {
const { control, value } = req.body;
if (!control || typeof value !== 'number') {
return res.status(400).json({ error: 'Provide {control: string, value: number}' });
}
try {
await cam.set(control, value);
res.json({ control, value, result: 'ok' });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// ---- START SERVER ----
app.listen(PORT, () => {
console.log(`🛠️ UVC Agent listening on port ${PORT}`);
});Key Points:
- Basic Auth secures every endpoint.
/healthlets you monitor uptime./capsreturns supported PTZ ranges (min/max values)./controlsets any UVC control by name.
Use a .env file or your shell to set:
AUTH_USER=agent01
AUTH_PASS=veryStrongPassword!
PORT=4000
Then load via your shell or a tool like dotenv if desired.
- Never commit passwords to Git.
- Use OS keyrings or container secrets for production.
npm install # ensure deps
node index.js # runs agentcurl -u agent01:veryStrongPassword! http://localhost:4000/health
# → { "status":"ok" }
curl -u agent01:veryStrongPassword! http://localhost:4000/caps
# → JSON listing pan, tilt, zoom rangescurl -u agent01:veryStrongPassword! -X POST http://localhost:4000/control -H "Content-Type: application/json" -d '{"control":"zoom_absolute","value":150}'Expect {"control":"zoom_absolute","value":150,"result":"ok"} and camera movement.
- Create
/etc/systemd/system/uvc-agent.service:[Unit] Description=UVC PTZ Agent After=network.target [Service] Type=simple User=YOUR_USER WorkingDirectory=/home/YOUR_USER/uvc-agent ExecStart=/usr/bin/node index.js Environment=AUTH_USER=agent01 Environment=AUTH_PASS=veryStrongPassword! Environment=PORT=4000 Restart=always RestartSec=5 [Install] WantedBy=multi-user.target
- Reload & start:
sudo systemctl daemon-reload sudo systemctl enable uvc-agent sudo systemctl start uvc-agent
- Download NSSM and unzip.
- Install service:
.\nssm.exe install UvcAgent- Application Path:
C:\Program Files\nodejs\node.exe - Arguments:
C:\path\to\uvc-agent\index.js - Env variables in NSSM GUI (
AUTH_USER,AUTH_PASS,PORT).
- Application Path:
- Start the
UvcAgentservice via Services.msc.
npm install -g pm2
pm2 start index.js --name uvc-agent \
--env AUTH_USER=agent01 --env AUTH_PASS=... --env PORT=4000
pm2 save
pm2 startup # follow printed instructions- Terminate TLS with Nginx or Caddy:
server { listen 443 ssl; server_name agent.example.com; ssl_certificate /etc/letsencrypt/live/agent.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/agent.example.com/privkey.pem; location / { proxy_pass http://localhost:4000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }
- SSH reverse tunnel from agent to central:
ssh -N -R 5001:localhost:4000 central-user@central.example.com
- Central then calls
http://localhost:5001/control.
Example using Axios in Node.js:
const axios = require('axios');
async function panTiltZoom(agentHost, ctrl, val) {
try {
const res = await axios.post(
`https://${agentHost}/control`,
{ control: ctrl, value: val },
{ auth: { username: 'agent01', password: 'veryStrongPassword!' } }
);
console.log('PTZ result:', res.data);
} catch (err) {
console.error('Error controlling camera:', err.message);
}
}
// Usage:
panTiltZoom('agent-A.example.com', 'absolute_pan_tilt', 100);Error Handling:
- Network errors: retry with exponential backoff.
- Authentication failures: verify credentials and time sync.
- Unsupported controls: check
/capsfirst.
- Build fails on Windows: ensure Visual C++ Build Tools installed, run
npm install --global --production windows-build-tools. - Permission denied on Linux: add your user to the
videogroup:sudo usermod -aG video $USER newgrp video - Multiple cameras: pass a device index to UVCControl:
const cam = new UVCControl('/dev/video1');
- Control names: run
cam.range()to list exactly supported names.
- List UVC controls on Linux:
v4l2-ctl --list-ctrls
- Test HTTP endpoint:
http --auth agent01:pw --json POST http://localhost:4000/control control=zoom_absolute value:=150
End of Document.