ONVIF client for all ONVIF devices with TypeScript types.
- TypeScript + Promise API
- Typed ONVIF/WSDL interfaces (26.06)
- Profiles S, T, G, M, C, A
- WS-Discovery, WS-Security, Digest (MD5 / SHA-1 / SHA-256 / SHA-512-256)
- Lazy-loaded service modules
- Built to work around real-camera quirks across vendors
Note
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 onvifOne build covers CommonJS, ESM, and TypeScript (require / import + .d.ts).
ESM import loads the same CJS build via exports.import - examples: Quick start.
Subpaths: onvif/compatibility, onvif/compatibility/promises (callback / Promise Cam
wrappers), onvif/interfaces/* (WSDL types, e.g. onvif/interfaces/onvif).
To pin the previous major line explicitly:
npm install onvif@previousAPI reference (TypeDoc): https://agsh.github.io/onvif/ -
start from the Onvif class.
WSDL types: import type { MediaUri } from 'onvif/interfaces/onvif'.
After connect(), call methods on lazy namespaces (onvif.media, onvif.ptz, ...;
events is eager) - see Services. Also exported:
Discovery.
Longer notes in innerDocs/ (GitHub; not in the npm tarball):
- Start here: guides.md, troubleshooting.md, connection.md, auth.md
- Also: events.md, errors.md, migration.md, quirks.md, performance.md, resources.md, soap-xml.md, vendor-extensions.md, testing.md
Call connect() before service methods (or pass autoConnect: true). Call close() when done.
Same pattern works with require('onvif') (CommonJS).
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();
console.log(await onvif.device.getDeviceInformation());
await onvif.close();RTSP preview + Socket.IO / FFmpeg demo: rtsp.socketio.ptz.js.
333702629-e816fed6-067a-4f77-b3f5-ccd9d5ff1310.webm
- Typed WSDL interfaces (26.06) and TypeDoc API
- Lazy service namespaces; eager
events- performance.md - Auth: WS-Security + HTTP Digest - auth.md
- WS-Discovery - Discovery
- Media profile bag and non-camera profiles (C / A / G / M) - Connection, Beyond cameras
- Typed errors, vendor
xs:any, SOAP helpers - see Deep docs
Address the device with url or hostname / port / path / useSecure, plus
username / password. Other common flags:
timeout- HTTP socket timeout for ordinary SOAP (ms; default 10000). PullMessages uses its own longer timeout (pull wait + margin)useWSSecurity- SOAP PasswordDigest headers (defaulttrue; HTTP Digest still applies; see auth.md)autoConnect- callconnect()from the constructor (defaultfalse)agent- customhttp(s).Agent, proxy agent, orfalse(see resources.md)preserveAddress- rewrite XAddr host/port to the constructor address (NAT / loopback); seepreserveAddresssecureOptions- TLS options when using HTTPSdefaultMedia- default forconnect({ media })when omitted (Discovery may setfalse)
Full field list: OnvifOptions.
Call 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. connect() is not a login check - call device.getDeviceInformation() afterwards if you
need to verify username/password early. Details:
connection.md.
After media discovery, omitted profileToken / source tokens default from the token bag
onvif.activeMediaProfile (tokens only; codec/fps live on onvif.defaultProfile). Switch with
setActiveMediaProfile(profileToken):
await onvif.setActiveMediaProfile('ProfileToken_2');
const stream = await onvif.media.getStreamUri({ protocol: 'RTSP' });Details: connection.md.
Profiles S / T
are the video path above (media / media2 / ptz / imaging). The same Onvif client also
covers access control, recording, and analytics - without treating every device as a camera.
Profile C / A
(door / access): skip Media probing. Discovery sets defaultMedia: false when ProbeMatch scopes
look access-only (no Streaming/T); or pass { media: false } yourself. After connect, omit-token
DoorControl / AccessControl calls use activeDoorToken / activeAccessPointToken.
await onvif.connect({ media: false });
console.log(onvif.activeDoorToken, onvif.uri.doorcontrol);
await onvif.doorControl.unlockDoor(); // uses activeDoorToken
await onvif.setActiveDoor('DoorToken_2');Recipe: guides.md / door.unlock.js.
Profile G (recording / replay / search): connect
still probes Media when present. Defaults: activeRecordingToken /
activeRecordingJobToken. See
guides.md /
replay.uri.js.
Profile M (analytics): onvif.analytics /
onvif.analyticsDevice - analytics.rules.js.
Entity tokens: connection.md.
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:
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.
Informational progress uses status (NIC enumeration and each Probe send) - useful when
nothing appears on a multi-homed host.
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));
Discovery.on('status', (msg) => console.log('discovery', msg));
// Found interface eth0
// Sending IPv4 from 192.168.1.10 (eth0) to 239.255.255.250:3702
const devices = await Discovery.probe({
timeout: 5000,
// device: 'eth0', // bind to one NIC (default: every address on every NIC)
// type: 'udp6', // optional family filter; omit = IPv4 + IPv6
// address: '192.168.1.50', port: 3702, // directed unicast Probe
// types default to ['tds:Device']; pass [] to match any WS-Discovery type
// 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();
}What probe does (beyond a single-interface IPv4 multicast):
- Dual-stack by default - Probe from every non-loopback IPv4 and IPv6 address on every NIC
(
239.255.255.250/FF02::C, port3702). Passtype: 'udp4' | 'udp6'to limit the family; omittypefor both. - IPv6 link-local bind - zone id is attached for
fe80::binds (%eth0on Unix, numeric%scopeidon Windows). - Multi-homed / directed Probe - optional
device(one NIC), oraddress+portfor a unicast Probe still sent from each selected local address. statusdiagnostics -Discovery.on('status', ...)gets human-readable lines while interfaces are listed and Probes are sent (not errors). See the sample below.- Dedup & filtering - devices are keyed by ProbeMatch
EndpointReference(same NVT on IPv4+IPv6 -> one result);RelatesTo/ MessageID drops replies meant for another client's Probe;types/scopesnarrow the Probe; ProbeMatch scopes setdefaultMedia(falsefor access-only Profile C/A without Streaming/T). - Other options -
timeout,bufferSize,listeningPort,resolve: false(raw ProbeMatch instead ofOnvif).
Runnable sample (includes status):
discovery.js.
Methods take typed ONVIF options. Returns are unwrapped - What methods return.
Task recipes: guides.md. Method details:
TypeDoc (start at Onvif).
Namespaces (lazy except events):
- Core -
device,events - Video (S / T) -
media,media2,ptz,imaging,deviceIO - Recording (G) -
recording,replay,search,receiver - Access (C / A) -
doorControl,accessControl,credential,accessRules,schedule - Analytics (T / M) -
analytics,analyticsDevice - Other -
display,actionEngine,thermal,provisioning,uplink,appManagement,advancedSecurity(experimental)
SOAP replies are nested (GetFooResponse -> Foo -> ...). This library unwraps them to the
simplest useful value:
- One useful field -> that value alone (
systemReboot()->"Rebooting";media2.getStreamUri()->"rtsp://...";createDoor()-> token string). - Several fields -> one plain object (
getDeviceInformation();media.getStreamUri()->MediaUriwith.uri). - Lists -> always an array, even for one item (
getProfiles(),getDoors({ token }), ...). - Paginated
*Listmethods keep{ items?, nextStartReference? }(or door/access list shape).
const stream = await onvif.media.getStreamUri({ protocol: 'RTSP' });
console.log(stream.uri); // Media 1.0 -> MediaUri
const uri = await onvif.media2.getStreamUri({ protocol: 'RtspUnicast' });
console.log(uri); // Media 2 -> string
const doors = await onvif.doorControl.getDoors({ token: ['Door_1'] });
console.log(doors.length); // always an arrayRule of thumb: strip SOAP nesting; scalars/objects when that is all there is; arrays for
repeating elements; keep a wrapper only for paging (*List) or multi-field values.
First onvif.on('event', ...) starts a global pull-point (Events is eager). Use eventError
for pull / reconnect failures; remove listeners or close() to stop. For topic filters or another
pull-point, use Subscription and unsubscribe() yourself (close() does not tear those down).
import { Onvif, Subscription } from 'onvif';
onvif.on('event', (msg) => console.log(msg.topic?._));
onvif.on('eventError', (err) => console.error(err));
const sub = new Subscription(onvif, {
filter: { topicExpression: [{ expression: 'tns1:RuleEngine/CellMotionDetector/Motion' }] },
});
sub.on('data', (msg) => console.log('motion', msg.topic?._));
await sub.subscribe();More: events.md, guides.md, events.with.filter.ts.
xs:any / xs:anyAttribute via xsany and $. Parse gives camelCase and a wire tree on
obj[xsany]. Setters serialize [xsany] only. Details:
vendor-extensions.md.
build / parseSOAPString, onvif.request(), requestContext:
soap-xml.md.
Typed failures (OnvifTimeoutError, OnvifTransportError, OnvifParseError,
OnvifResponseError, OnvifSoapFaultError, OnvifAuthError, OnvifValidationError):
errors.md.
Full catalog: troubleshooting.md. Quirks: quirks.md.
- Auth / Unauthorized after connect -
connect()is not a login check; callgetDeviceInformation(). See troubleshooting Auth + auth.md. - Empty profiles / no RTSP - media skipped, wrong bag, or Media2-only. Access-only C/A: Beyond cameras. Else troubleshooting Media / RTSP.
- RTSP host wrong; SOAP OK with
preserveAddress- flag does not rewrite RTSP body URLs. See troubleshooting +preserveAddress. - PTZ AbsoluteMove Fault - check SupportedPTZSpaces; prefer ContinuousMove.
- Events stop / process exit -
eventError; alwayssub.on('error')on own Subscription. - Discovery finds nothing -
Discovery.on('status'); bind NIC / family. - Timeout ~10 s / process never exits - raise
timeout;close()/unsubscribe().
Cheat-sheet (or keep Cam via onvif/compatibility / .../promises):
Cam->Onvif(+await connect()/close())- Flat
cam.getStreamUri(...)->onvif.media.getStreamUri(...),onvif.ptz.*, ... - Unwrapped returns - What methods return
activeSource->activeMediaProfile+defaultProfilefor codec/fps
Checklist and diffs: migration.md. Older line: branch v0.x.
Samples in examples/:
- device.info.js - device info, hostname, scopes, services
- media.stream.js - profiles, RTSP + snapshot URIs
- media.snapshot.ts - snapshot URI + HTTP Digest download
- media2.profiles.js - Profile T Media2 profiles / encoders
- imaging.settings.js - imaging settings and options
- analytics.rules.js - analytics rules and modules
- deviceio.relay.js - DeviceIO relays / digital inputs
- door.unlock.js - Profile C/A door unlock / lock
- rtsp.socketio.ptz.js - RTSP preview + PTZ over Socket.IO
- ptz.nodes.js - GetNodes / SupportedPTZSpaces
- ptz.keyboard.js - keyboard ContinuousMove + presets
- discovery.js - WS-Discovery (includes
status) - scan.ip.range.js - IP range scan
- proxy.socks5.js - SOCKS5 proxy
- https.digest.mjs - HTTPS + Digest mock (ESM)
- events.with.filter.ts - topic-filtered events
- osd.js - on-screen text overlays
- replay.uri.js - Profile G replay URI
- compatibility.cjs / compatibilityPromises.cjs - Cam layer
Tested against many vendors (issues + lab), including ACTi, Amcrest, Avigilon, Axis, Bosch, Canon, Dahua, D-Link, Foscam, Hanwha / Wisenet, Hikvision, Panasonic, Pelco, Reolink, Sony, TP-Link / Tapo, Uniview, Vivotek, and others. Known quirks: quirks.md.
Report manufacturer / model / 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 support and real-camera testing, and to everyone who filed issues over the years.