ONVIF client for all ONVIF devices with TypeScript types.
Coming from the classic Cam API (0.x / 0.8)? You can stay on
npm install onvif@previous, use the onvif/compatibility layer with this package, or browse
branch v0.x.
Requires Node.js 18+. The published package ships compiled build/ (CommonJS + .d.ts),
README, LICENSE, and CHANGELOG - no need to clone this repository for normal use.
npm install onvif
const { Onvif } = require('onvif'); // CommonJS
import { Onvif } from 'onvif'; // ESM / TypeScript (Node loads the CJS build)
Subpaths: onvif/compatibility, onvif/compatibility/promises (callback / Promise Cam wrappers).
To pin the previous major line explicitly:
npm install onvif@previous
API reference (TypeDoc): https://agsh.github.io/onvif/ -
start from the Onvif class.
After connect(), call methods on lazy-loaded service namespaces (onvif.media, onvif.ptz, ...; events is eager) -
see Services. Also exported: Discovery.
Call connect() before service methods (or pass autoConnect: true). Call close() when you no longer need the connection.
import { Onvif } from 'onvif';
const onvif = new Onvif({
url: 'http://192.168.1.13:8000/onvif/device_service',
username: 'admin',
password: 'admin',
});
// or: { hostname, port, path?, useSecure? }
await onvif.connect();
const info = await onvif.device.getDeviceInformation();
console.log(info);
await onvif.close();
const { Onvif } = require('onvif');
(async () => {
const onvif = new Onvif({
url: 'http://192.168.1.13:8000/onvif/device_service',
username: 'admin',
password: 'admin',
});
await onvif.connect();
console.log(await onvif.device.getDeviceInformation());
await onvif.close();
})();
Node.js - works with plain JavaScript (require / import). TypeScript is optional;
types ship with the package.
ONVIF with FFmpeg, RTSP and Socket.IO (http://localhost:6147):
sudo apt install ffmpeg
npm install onvif socket.io rtsp-ffmpeg
const server = require('http').createServer((req, res) =>
res.end(`
<!DOCTYPE html><body>
<canvas width='640' height='480' />
<script src="/socket.io/socket.io.js"></script><script>
const socket = io(), ctx = document.getElementsByTagName('canvas')[0].getContext('2d');
socket.on('data', (data) => {
const img = new Image;
const url = URL.createObjectURL(new Blob([new Uint8Array(data)], {type: 'application/octet-binary'}));
img.onload = () => {
URL.revokeObjectURL(url, {type: 'application/octet-binary'});
ctx.drawImage(img, 100, 100);
};
img.src = url;
});
</script></body></html>`),
);
const { Onvif } = require('onvif');
const io = require('socket.io')(server);
const rtsp = require('rtsp-ffmpeg');
server.listen(6147);
const onvif = new Onvif({ username: 'username', password: 'password', hostname: '192.168.0.116', port: 2020 });
(async () => {
await onvif.connect();
const input = (await onvif.media.getStreamUri({ protocol: 'RTSP' })).uri.replace(
'://',
`://${onvif.username}:${onvif.password}@`,
);
const stream = new rtsp.FFMpeg({ input, resolution: '320x240', quality: 3 });
io.on('connection', (socket) => {
const pipeStream = socket.emit.bind(socket, 'data');
stream.on('disconnect', () => stream.removeListener('data', pipeStream)).on('data', pipeStream);
});
setInterval(
() =>
onvif.ptz.absoluteMove({
position: {
x: Math.random() * 2 - 1,
y: Math.random() * 2 - 1,
zoom: Math.random(),
},
}),
3000,
);
})().catch(console.error);
EventEmitter - innerDocs/events.mdxs:any / xs:anyAttribute) - innerDocs/vendor-extensions.mdbuild / parseSOAPString / request / requestContext) - innerDocs/soap-xml.mdactiveMediaProfile / setActiveMediaProfile) - see ConnectionCall connect() before most methods. It synchronizes time (for WS-Security), discovers service URLs
via GetServices with IncludeCapability: false (or GetCapabilities if that fails), and - when
Media is available - loads profiles and video sources. Media1 is preferred; Media2 GetProfiles runs
only if Media1 is missing or fails. Pass { media: false } to skip media probing (door stations,
reachability checks). Discovery.probe sets defaultMedia: false on access-only devices
(Profile C/A without Streaming/T in ProbeMatch); connect() honors that when media is omitted.
Override with { media: true | false }. For capability blocks from GetServices, call
device.getServices({ includeCapability: true }) after connect.
On success, connect() emits connect and returns the instance. A failed connect() rejects after
clearing prior discovery state (safe to retry). Media probe failures emit warn but still complete
connect. Details: innerDocs/connection.md.
activeMediaProfile)After media discovery, connect() builds a token bag - onvif.activeMediaProfile - from one
selected Media Profile (typically the first usable profile per video source). Multi-source devices
also fill onvif.activeMediaProfiles (one bag per video source); slot 0 is the process-wide
default until you switch. Service methods that omit profileToken / configurationToken /
videoSourceToken read defaults from the current bag (media, media2, ptz, analytics,
imaging, deviceIO, thermal, ...).
The bag holds tokens only. Codec / resolution / fps stay on the full profile object
onvif.defaultProfile (same selection as the bag). Deprecated aliases: activeSource /
activeSources (same objects). Rebuild after mutating profiles on the device with
getActiveMediaProfiles().
Switch the current pipeline with setActiveMediaProfile(profileToken) (must exist in
onvif.profiles; validated against videoSources when those were loaded):
await onvif.connect();
console.log(onvif.profiles.map((p) => p.token));
// e.g. ['ProfileToken_1', 'ProfileToken_2'] - main vs sub stream
await onvif.setActiveMediaProfile('ProfileToken_2');
const stream = await onvif.media.getStreamUri({ protocol: 'RTSP' });
// uses ProfileToken_2 / its encoder without passing tokens
// one-off override still works:
await onvif.media.getStreamUri({
protocol: 'RTSP',
profileToken: 'ProfileToken_1',
});
Same video source, different codecs (H.264 vs H.265): cameras usually expose that as
two Media Profiles that share one videoSource, each with its own
videoEncoderConfiguration.encoding. Pick the profile by encoding, then switch the bag
(codec metadata lives on defaultProfile, not on the token bag):
await onvif.connect();
for (const p of onvif.profiles) {
const enc = p.videoEncoderConfiguration;
console.log(p.token, enc?.encoding, enc?.resolution);
}
const h265 = onvif.profiles.find(
(p) => p.videoEncoderConfiguration?.encoding === 'H265',
);
if (!h265) throw new Error('no H265 profile on this device');
await onvif.setActiveMediaProfile(h265.token);
console.log(onvif.defaultProfile?.videoEncoderConfiguration?.encoding); // H265
const uri = (await onvif.media.getStreamUri({ protocol: 'RTSP' })).uri;
// RTSP for the H265 pipeline; PTZ / imaging still use the same video source tokens
Switch back the same way (encoding === 'H264'). If both codecs sit on one profile and you
must change the encoder config on the device, that is a Media setVideoEncoderConfiguration
flow - not setActiveMediaProfile.
How the bag is built, multi-source devices, and more examples: innerDocs/connection.md#active-media-profile-token-bag.
preserveAddress (NAT / wrong XAddr host or port)Devices often return service or subscription XAddrs that you cannot reach as written -
http://127.0.0.1/... behind a vendor proxy, an internal LAN IP while you connect through
NAT or a port-forward, or a different port than the one in the constructor / url.
By default (preserveAddress: false) ordinary SOAP still works: TCP always goes to the
constructor hostname:port (or host/port from url); from each XAddr only the path
(and query) is used. Set preserveAddress: true when the full URL must be rewritten to
that constructor host/port (path/query kept) - needed for SOAP a:To and for PullMessages /
Renew / Unsubscribe, which dial the subscription Address as options.url. Effective ports
are compared (empty URL.port means 80/443). This does not rewrite RTSP/snapshot URI
strings inside Media response bodies.
const onvif = new Onvif({
hostname: '203.0.113.10', // public / NAT address you can reach
port: 8080, // forwarded port (camera may advertise :80 in XAddr)
username: 'admin',
password: 'admin',
preserveAddress: true,
});
When finished, call await onvif.close() to stop the pull-point subscription (if any) and release
HTTP keep-alive sockets. Without close(), idle connections can keep a Node process from exiting.
Safe to call more than once. Call await onvif.connect() again to reopen after close().
Agents, renew/reconnect timers, and custom Subscription teardown:
innerDocs/resources.md.
Find ONVIF devices on the LAN with WS-Discovery (Discovery.probe). Each match is an
Onvif instance (set credentials, then connect()).
probe() always resolves (possibly []); bad replies and socket issues emit error only.
import { Discovery } from 'onvif';
Discovery.on('device', (cam, rinfo) => {
console.log('found', rinfo.address, cam.hostname, cam.xaddrs?.map((u) => u.href));
});
Discovery.on('error', (err) => console.error('discovery', err));
const devices = await Discovery.probe({
timeout: 5000,
// device: 'eth0', // bind to a NIC when needed
// types: ['dn:NetworkVideoTransmitter'],
// scopes: ['onvif://www.onvif.org/Profile/T'],
});
for (const cam of devices) {
if (!('connect' in cam)) continue;
cam.username = 'admin';
cam.password = 'admin';
await cam.connect();
console.log(await cam.device.getDeviceInformation());
await cam.close();
}
Options include timeout, device (interface), types / scopes filters, bufferSize, and
IPv4/IPv6. RelatesTo / MessageID matching avoids mixing Probe replies. Runnable sample:
discovery.js.
Methods take typed ONVIF request options and return response data. Some helpers accept more convenient
fields (for example dateTime?: Date on SetSystemDateAndTimeExtended).
| Service | Namespace | Profile |
|---|---|---|
| Device | onvif.device |
Core (all) |
| Media | onvif.media |
S |
| Media2 | onvif.media2 |
T |
| PTZ | onvif.ptz |
S, T |
| Events | onvif.events |
Core (all) |
| Imaging | onvif.imaging |
S, T |
| Analytics | onvif.analytics |
T, M |
| AnalyticsDevice | onvif.analyticsDevice |
M |
| Recording | onvif.recording |
G |
| Replay | onvif.replay |
G |
| Search | onvif.search |
G |
| Receiver | onvif.receiver |
G |
| DeviceIO | onvif.deviceIO |
S, T, C |
| Display | onvif.display |
- |
| ActionEngine | onvif.actionEngine |
- |
| Thermal | onvif.thermal |
- |
| DoorControl | onvif.doorControl |
C, A |
| AccessControl | onvif.accessControl |
C, A |
| Credential | onvif.credential |
A, C |
| AccessRules | onvif.accessRules |
A |
| Schedule | onvif.schedule |
A |
| Provisioning | onvif.provisioning |
- |
| AdvancedSecurity | onvif.advancedSecurity |
- |
Return unwrapping
getDeviceInformation() -> info object, systemReboot() -> message string)media.getStreamUri() / getSnapshotUri() -> MediaUri with .uri, invalidAfterConnect, ...)getAccessPoints, getDoors, ...) - unwrapped to an array ([] when empty)*List methods - keep the ONVIF list response object ({ accessPoint?, nextStartReference? }, ...)onvif.on('event', (msg) => console.log(msg));
Pull-point, WS-BaseNotification, filters, and push: innerDocs/events.md.
ONVIF xs:any / xs:anyAttribute extension points are exposed via xsany and $.
After parse you get a camelCase view and a wire tree on obj[xsany]. Setters serialize [xsany] only.
Details: innerDocs/vendor-extensions.md.
build / parseSOAPString, public onvif.request(), and per-call requestContext request/response
capture: innerDocs/soap-xml.md.
This package uses the Onvif API. Existing Cam apps can keep working via
onvif/compatibility / onvif/compatibility/promises - see
innerDocs/migration.md.
Older line docs: branch v0.x.
Samples in the examples folder:
digestAuth / getDigestHeaders)Compatibility-layer demos: compatibility.cjs, compatibilityPromises.cjs.
Tested and quirk-fixed against real devices from many vendors (closed GitHub issues and lab use), including 360 Vision Technology Predator, ACTi, Amcrest, Avigilon, Axis, Bosch, Canon, Dahua, D-Link, Foscam, Hanwha / Wisenet, Hikvision, Panasonic, Pelco, Reolink, Sony, Sricam, TP-Link / Tapo, Tyco, Uniview, Vivotek, and others.
Report your device and firmware via the compatibility form.
console.log(await onvif.device.getDeviceInformation());
{
"manufacturer": "tp-link",
"model": "Tapo C220",
"firmwareVersion": "1.4.4 Build 260515 Rel.24570n",
"serialNumber": "7461572b",
"hardwareId": 1
}
Thanks to HappyTimeSoft for HappyTime ONVIF Server used in integration tests, to @RogerHardiman for ongoing support and real-camera testing, and to everyone who filed issues over the years.