Skip to main content

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);
OptionTypeDefaultDescription
portnumber5050TCP port to listen on. Use 0 to let the OS assign an available port.
maxBodySizenumber5242880Maximum 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.
onEventfunctionundefinedShorthand for server.on('event', fn)
onConnectfunctionundefinedShorthand for server.on('connect', fn)
onRawfunctionundefinedShorthand 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 pass port: 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 like category, plateNumber, eventType, etc.
  • clientIPstring, 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}`);
}
});
note

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.

  • xmlstring, the raw XML body from the HTTP POST
  • clientIPstring, 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 — alarmStatusInfo payloads with no detection data
  • Keepalive heartbeats
  • Non-XML bodies
  • Bodies without an \&lt;?xml declaration
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-alive and maintain a single long-lived TCP connection. The server sets keepAliveTimeout = 60000 (60 seconds) because cameras heartbeat every 30 seconds. Node.js defaults to 5 seconds, which silently kills connections between heartbeats. headersTimeout is set to 65 seconds to stay above the keepalive timeout.
  • XML success response — every POST receives a \&lt;status>success\&lt;/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 \&lt;messageType>keepalive\&lt;/messageType>, or \&lt;deviceInfo> without \&lt;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);