ViewtronServer
Overview
ViewtronServer is an HTTP server that listens for webhook events from Viewtron IP cameras and NVRs. When a camera detects something — a license plate, face, intrusion, line crossing, etc. — it sends an HTTP POST with XML data to your server. ViewtronServer parses the XML and emits a ViewtronEvent object.
Built on Node.js EventEmitter, so you subscribe to events with the standard .on() pattern. The server handles all Viewtron camera connection requirements automatically: HTTP/1.1 persistent connections, keepalive timeouts, XML success replies, and connected camera tracking.
const { ViewtronServer } = require('viewtron-sdk');
const server = new ViewtronServer({ port: 5050 });
server.on('event', (event, clientIP) => {
console.log(`[${event.category}] ${event.eventDescription} from ${clientIP}`);
});
server.start();
Constructor
const server = new ViewtronServer(options);
| Option | Type | Default | Description |
|---|---|---|---|
port | number | 5050 | TCP port to listen on. Use 0 to let the OS assign an available port. |
maxBodySize | number | 5242880 | Maximum POST body size in bytes (5 MB). Requests exceeding this are dropped. Camera posts with images can be large, but anything over 5 MB is likely malformed. |
onEvent | function | undefined | Shorthand for server.on('event', fn) |
onConnect | function | undefined | Shorthand for server.on('connect', fn) |
onRaw | function | undefined | Shorthand for server.on('raw', fn) |
The callback options are convenience shortcuts. These two are equivalent:
// Using constructor options
const server = new ViewtronServer({
port: 5050,
onEvent: (event, clientIP) => { /* ... */ },
onConnect: (clientIP) => { /* ... */ },
});
// Using EventEmitter .on()
const server = new ViewtronServer({ port: 5050 });
server.on('event', (event, clientIP) => { /* ... */ });
server.on('connect', (clientIP) => { /* ... */ });
Methods
start()
Start the HTTP server. Returns a Promise that resolves when the server is listening.
start(): Promise<{ port: number, ip: string }>
The resolved object contains:
port— the actual port the server is listening on (useful when you passport: 0)ip— the LAN IP address of the machine (first non-internal IPv4 interface)
const server = new ViewtronServer({ port: 5050 });
const { port, ip } = await server.start();
console.log(`Listening on http://${ip}:${port}`);
// Output: Listening on http://192.168.1.100:5050
If the port is already in use, the Promise rejects with an EADDRINUSE error:
try {
await server.start();
} catch (err) {
if (err.code === 'EADDRINUSE') {
console.error(`Port ${server.port} is already in use`);
}
}
stop()
Stop the server gracefully. Returns a Promise that resolves when the server has closed all connections.
stop(): Promise<void>
await server.stop();
console.log('Server stopped');
Safe to call even if the server was never started — resolves immediately in that case.
Events (EventEmitter)
ViewtronServer extends Node.js EventEmitter. Subscribe to events with .on(), .once(), or pass callback options in the constructor.
'event'
server.on('event', (event, clientIP) => { })
The primary event. Emitted once for each parsed detection event.
event— a ViewtronEvent object with properties likecategory,plateNumber,eventType, etc.clientIP—string, the IP address of the camera or NVR (IPv4, with::ffff:prefix stripped)
Emitted for all detection types:
- LPR (license plate recognition)
- Face detection
- Intrusion / line crossing / region entry / region exit
- Loitering, illegal parking
- Target counting (by line or area)
- Video metadata
Not emitted for:
- Keepalive heartbeats (cameras send these every ~30 seconds)
- Alarm status on/off messages
- Traject (smart tracking data) — filtered at the server level
server.on('event', (event, clientIP) => {
if (event.category === 'lpr') {
console.log(`Plate: ${event.plateNumber} from ${clientIP}`);
}
});
Unlike the Python SDK, the Node.js server filters traject data before it reaches the 'event' listener. Traject posts are high-volume (multiple per second per camera) and are dropped at the request handler level. If you need traject data, use the ViewtronEvent parser directly — see ViewtronEvent.
'connect'
server.on('connect', (clientIP) => { })
Emitted the first time a camera sends any message from a new IP address. Each camera IP is tracked — the event fires once per IP, not on every keepalive.
Triggers on both keepalive heartbeats and real detection events, whichever arrives first.
server.on('connect', (clientIP) => {
console.log(`Camera connected: ${clientIP}`);
});
'raw'
server.on('raw', (xml, clientIP) => { })
Emitted with the raw XML string before parsing. Useful for debugging or logging raw camera data.
xml—string, the raw XML body from the HTTP POSTclientIP—string, the IP address of the camera or NVR
Filtering: 'raw' is not emitted for:
- Traject data — high-volume tracking data that would flood raw logging
- Alarm status messages —
alarmStatusInfopayloads with no detection data - Keepalive heartbeats
- Non-XML bodies
- Bodies without an
\<?xmldeclaration
const fs = require('fs');
server.on('raw', (xml, clientIP) => {
fs.appendFileSync(`raw_${clientIP}.xml`, xml + '\n---\n');
});
'listening'
server.on('listening', ({ port, ip }) => { })
Emitted when the server starts listening. Contains the same \{ port, ip \} object returned by start().
server.on('listening', ({ port, ip }) => {
console.log(`Server ready at http://${ip}:${port}`);
});
server.start();
'error'
server.on('error', (err) => { })
Emitted on server errors. This includes both Node.js HTTP server errors and XML parsing errors from individual requests.
server.on('error', (err) => {
console.error('Server error:', err.message);
});
If you don't attach an 'error' listener, unhandled errors will throw (standard Node.js EventEmitter behavior). Always attach an error handler in production.
Properties
port
server.port: number
The port the server is listening on. Set from the constructor option, then updated to the actual port after start() resolves (relevant when using port: 0).
maxBodySize
server.maxBodySize: number
Maximum POST body size in bytes. Requests exceeding this limit are silently dropped (connection destroyed).
connectedCameras
server.connectedCameras: Map<string, Date>
A Map of connected camera IPs to their first-seen timestamps. Populated automatically as cameras send keepalives or detection events.
server.connectedCameras.forEach((firstSeen, ip) => {
console.log(`${ip} connected since ${firstSeen.toISOString()}`);
});
// Check if a specific camera is connected
if (server.connectedCameras.has('192.168.1.108')) {
console.log('Camera is online');
}
// Number of connected cameras
console.log(`${server.connectedCameras.size} cameras connected`);
The map is never cleared automatically. Cameras are added on first contact and stay in the map for the lifetime of the server. If you need to detect disconnections, implement your own timeout logic based on the last-seen time.
Camera Connection Requirements
ViewtronServer handles these automatically, but they're useful to understand when troubleshooting:
- HTTP/1.1 persistent connections — cameras use
Connection: keep-aliveand maintain a single long-lived TCP connection. The server setskeepAliveTimeout = 60000(60 seconds) because cameras heartbeat every 30 seconds. Node.js defaults to 5 seconds, which silently kills connections between heartbeats.headersTimeoutis set to 65 seconds to stay above the keepalive timeout. - XML success response — every POST receives a
\<status>success\</status>XML reply immediately (before the body is even read). This tells the camera the connection is alive. - Keepalive heartbeats — cameras send a POST every ~30 seconds. Empty body, explicit
\<messageType>keepalive\</messageType>, or\<deviceInfo>without\<smartType>(IPC keepalive). All three forms are recognized and handled silently. - Reboot after config changes — after changing HTTP Post settings on a camera or NVR, reboot the device for changes to take effect. The camera caches connection settings and won't reconnect properly without a reboot.
For instructions on configuring cameras to send events, see the HTTP Post Setup guide.
Common Patterns
Basic Event Listener
The minimal setup — listen for events and log them:
const { ViewtronServer } = require('viewtron-sdk');
const server = new ViewtronServer({ port: 5050 });
server.on('event', (event, clientIP) => {
console.log(`[${event.category}] ${event.eventDescription} from ${clientIP}`);
if (event.category === 'lpr') {
console.log(` Plate: ${event.plateNumber}`);
console.log(` Group: ${event.plateGroup}`);
}
});
server.on('connect', (clientIP) => {
console.log(`Camera connected: ${clientIP}`);
});
server.on('error', (err) => {
console.error('Server error:', err.message);
});
server.start().then(({ port, ip }) => {
console.log(`Listening on http://${ip}:${port}`);
});
Category Routing
Dispatch events to separate handler functions based on their category:
function handleLPR(event, clientIP) {
console.log(`Plate: ${event.plateNumber} (${event.plateGroup}) from ${clientIP}`);
if (event.vehicle) {
console.log(` Vehicle: ${event.vehicle.color} ${event.vehicle.brand} ${event.vehicle.model}`);
}
}
function handleIntrusion(event, clientIP) {
console.log(`Intrusion: ${event.eventType} target=${event.targetType} from ${clientIP}`);
}
function handleFace(event, clientIP) {
if (event.face) {
console.log(`Face: age=${event.face.age} sex=${event.face.sex} from ${clientIP}`);
}
}
const handlers = {
lpr: handleLPR,
intrusion: handleIntrusion,
face: handleFace,
};
server.on('event', (event, clientIP) => {
const handler = handlers[event.category];
if (handler) {
handler(event, clientIP);
}
});
Saving Images to Disk
Most detection events include snapshot images. The sourceImageBytes and targetImageBytes properties return decoded Buffers:
const fs = require('fs');
const path = require('path');
server.on('event', (event, clientIP) => {
if (event.category === 'lpr' && event.hasImages) {
const dir = 'captures';
fs.mkdirSync(dir, { recursive: true });
const source = event.sourceImageBytes;
if (source) {
fs.writeFileSync(path.join(dir, `${event.plateNumber}_full.jpg`), source);
}
const target = event.targetImageBytes;
if (target) {
fs.writeFileSync(path.join(dir, `${event.plateNumber}_crop.jpg`), target);
}
}
});
Running Alongside Express
ViewtronServer runs its own HTTP server on a dedicated port. Run it alongside an Express app for a web dashboard:
const express = require('express');
const { ViewtronServer } = require('viewtron-sdk');
// Express app on port 3000
const app = express();
const recentEvents = [];
app.get('/events', (req, res) => {
res.json(recentEvents.slice(-50));
});
app.get('/cameras', (req, res) => {
const cameras = [];
viewtron.connectedCameras.forEach((firstSeen, ip) => {
cameras.push({ ip, firstSeen });
});
res.json(cameras);
});
app.listen(3000, () => console.log('Web UI on http://localhost:3000'));
// ViewtronServer on port 5050
const viewtron = new ViewtronServer({ port: 5050 });
viewtron.on('event', (event, clientIP) => {
recentEvents.push({
category: event.category,
type: event.eventType,
description: event.eventDescription,
plate: event.plateNumber || undefined,
camera: clientIP,
time: new Date().toISOString(),
});
});
viewtron.start().then(({ port, ip }) => {
console.log(`Viewtron server on http://${ip}:${port}`);
});
Async/Await Startup
Use async/await for clean startup and shutdown:
const { ViewtronServer } = require('viewtron-sdk');
async function main() {
const server = new ViewtronServer({ port: 5050 });
server.on('event', (event, clientIP) => {
console.log(`[${event.category}] ${event.eventDescription}`);
});
const { port, ip } = await server.start();
console.log(`Listening on http://${ip}:${port}`);
// Graceful shutdown on Ctrl+C
process.on('SIGINT', async () => {
console.log('\nShutting down...');
await server.stop();
process.exit(0);
});
}
main().catch(console.error);
Related Pages
- ViewtronEvent — parsing events returned by the server
- LPR Events — license plate recognition event details
- Intrusion Events — intrusion and line crossing event details
- Face Detection Events — face detection event details
- Counting Events — target counting event details
- Images — working with event snapshot images