Skip to content

Repository files navigation

ONVIF

Coverage Status

ONVIF client protocol implementation for Node.js.

Tip

This page describes the 1.x version of the ONVIF library written in TypeScript. The 1.x version is currently in the release candidate stage. If you are looking for the README for the stable 0.x version, please see branch v0.x

The default npm installation still uses version 0.x. If you want to try this new version, install it with:

npm install onvif@rc

A TypeScript-first ONVIF client for Node.js with typed WSDL interfaces, Promise-based APIs, and broad service coverage:

  • Core media & PTZ — (Profiles S, T) device info, Media / Media2 profiles, stream URIs, imaging, presets, continuous and absolute moves
  • Events — (Profile M) pull-point and WS-BaseNotification with topic filters and EventEmitter integration
  • Recording & replay — (Profile G) NVR search, recordings, and replay URIs
  • Physical access — (Profiles C, A) DoorControl, AccessControl, Credential, AccessRules, Schedule
  • More services — (Profile T) Analytics, DeviceIO, Display, Action Engine, Thermal, Provisioning, AdvancedSecurity
  • Discovery & auth — WS-Discovery on the LAN; WS-Security and Digest (MD5 / SHA-1 / SHA-256)

Works server-side on Node.js 18+, tested on GitHub Actions.

ONVIF

About

This is a new version of the ONVIF library. The previous version was written in JavaScript, while this version is written in TypeScript and includes interfaces describing ONVIF data structures.

At the moment, all the methods from v0.8 have been implemented in the new Onvif API, and a v0.x compatibility layer is available for existing projects.

Tip

The main 1.x API uses the Onvif class with service modules (onvif.device, onvif.media, onvif.ptz, …). For v0.8 migration, import the separate compatibility modules (not part of require('onvif')): require('onvif/compatibility') for callbacks or require('onvif/compatibility/promises') for async/await (both export Cam and Discovery).

The documentation for the new library was generated with TypeDoc and is available here:

Thanks a lot for your interest!
I will be happy to answer any questions and hear your feedback.


Features

  • TypeScript interfaces for the latest ONVIF WSDL specification generated by onvif-generate-interfaces to provide code completion and type checking in the IDE for the requested and returned values
  • Complete documentation
  • Tests using the real ONVIF server from HappyTimeSoft
  • Event support: pull-point, base ws-notification, filters, EventEmitter inheritance. See below
  • Lazy loading of ONVIF service modules — see Performance / lazy loading
  • Authentication with WS-Security and Digest (MD5, SHA-1, SHA-256), also Advanced Security (experimental)
  • WS-Discovery support for finding devices on the local network
  • Full: Device, Events, Media, Media2, PTZ, Imaging, Analytics, AnalyticsDevice, Recording, Replay, Search, Receiver, DeviceIO, Display, Action Engine, Thermal, DoorControl, AccessControl, Credential, AccessRules, Schedule, Provisioning, AdvancedSecurity support.

    Not yet implemented (interfaces only, from ONVIF Network Interface Specifications): AuthenticationBehavior, Application Management (appmgmt), Uplink, FederatedSearch

  • Improved error handling
  • Compatible with the original API structure
  • Optional v0.x layer (not in the main export): require('onvif/compatibility') for callbacks, require('onvif/compatibility/promises') for Promises (both export Cam and Discovery)

Connection

Before you can use most library methods, call connect() on your Onvif instance. This method performs the initial handshake with the device and fills internal state so later SOAP requests are authenticated and routed to the correct service endpoints.

connect() runs the following steps in order:

  1. Time synchronizationgetSystemDateAndTime() is called first. ONVIF WS-Security authentication includes a timestamp in the nonce digest, so the client must know the offset between its own clock and the device clock (timeShift). The library tries an unauthenticated request first, as the ONVIF spec allows, and retries with credentials when the device requires authentication (some Panasonic and Digital Barriers models behave this way).
  2. Service discovery — the library calls GetServices (the modern ONVIF approach introduced with Profile T) from a small connection helper, without loading the full device module. If that fails on older devices, it falls back to GetCapabilities. Both methods populate onvif.uri with the URLs for media, PTZ, events, replay, and other services that subsequent requests use.
  3. Media configuration (only when the device advertises a Media service) — GetProfiles and GetVideoSources run in parallel, then getActiveSources() matches each video source to a suitable media profile. This sets activeSource, defaultProfile, and defaultProfiles, including encoder settings and PTZ configuration. Devices without video (for example Profile C door stations) skip this step and still complete connect() successfully.

On success, connect() emits a connect event and returns the Onvif instance. Pass autoConnect: true in the constructor to run this automatically after instantiation.

TypeScript

import { Onvif } from 'onvif';

const onvif = new Onvif({ hostname: '192.168.1.13', port: 8000, username: 'admin', password: 'admin' });
await onvif.connect();
const info = await onvif.device.getDeviceInformation();
console.log(info);

CommonJS

const { Onvif } = require('onvif');

(async () => {
  const onvif = new Onvif({ hostname: '192.168.1.13', port: 8000, username: 'admin', password: 'admin' });
  await onvif.connect();
  const info = await onvif.device.getDeviceInformation();
  console.log(info);
})();

ESM (.mjs or "type": "module")

import { Onvif } from 'onvif';

const onvif = new Onvif({ hostname: '192.168.1.13', port: 8000, username: 'admin', password: 'admin' });
await onvif.connect();
const info = await onvif.device.getDeviceInformation();
console.log(info);

Performance / lazy loading

The 1.x package is organized so you only pay for the ONVIF services you actually use.

Startup footprint

require('onvif') / import { Onvif } from 'onvif' loads a core set of modules (client, connection helpers, events, discovery, utils) — on the order of ~100 KiB of compiled JS. Large service implementations such as device, media, media2, ptz, recording, or advancedsecurity are not pulled in at import time.

Rough sizes of a few compiled service files (illustrative; exact numbers change with releases):

Module Approx. size
Core (onvif + connection + events + utils + …) ~95 KiB
media.js ~78 KiB
media2.js ~70 KiB
device.js ~39 KiB
Remaining service modules combined ~200+ KiB

Eagerly loading every service would put the initial JS footprint well over 400 KiB. With lazy loading, a typical camera client that only uses Device + Media + PTZ loads those modules on first use instead of at process start.

How loading works

  • Service namespaces (onvif.device, onvif.media, onvif.ptz, onvif.thermal, …) are lazy proxies. The corresponding module is loaded the first time you call a method on it (for example await onvif.ptz.getNodes()).
  • connect() uses dedicated helpers in connection.ts for the handshake SOAP (GetServices / GetCapabilities, Media GetProfiles / GetVideoSources). It does not load the full device / media / media2 class modules. Profiles and video sources are stored on the Onvif instance (onvif.profiles, onvif.videoSources); when Media is later loaded, it reuses that cache.
  • Events is constructed eagerly (needed for onvif.on('event', …)). Everything else stays deferred.
  • The main package entry exports service classes as TypeScript types only, so CommonJS require('onvif') does not force-load Recording, Thermal, and similar modules just because they appear in the type surface.

Practical tips

import { Onvif } from 'onvif';

const onvif = new Onvif({ hostname: '192.168.1.13', username: 'admin', password: 'admin' });
await onvif.connect(); // handshake only — no full Media/Device class modules yet

const info = await onvif.device.getDeviceInformation(); // loads device.js on first use
const uri = await onvif.media.getStreamUri({ protocol: 'RTSP' }); // loads media.js on first use
// onvif.thermal is never loaded unless you call it

If you only need Discovery or Events, you can avoid Media entirely: Profile C / door-control style devices complete connect() without a Media service, and unused namespaces stay unloaded for the lifetime of the process.


Feedback

If you like the library, please fill out this form so we know which devices it supports. There are a lot of devices, and not all of them correctly support the ONVIF specification. We're trying our best to make it work with as many devices as possible, so your feedback is important to us.

@RogerHardiman tested this lib on a test bed with 5 x Axis, 2 x Bosch, 1 x Canon, 2 x Hanwha, 4 x HikVision, 1 x Panasonic, 2 x Sony and 2 x unknown vendor cameras. There is a mix of PTZ and Fixed cameras and a mix of Pre-Profile, Profile S, Profile G and Profile T devices.

But we want to learn about as wide a range of devices as possible. So yes, please leave your feedback, it is important

Just run console.log(await onvif.device.getDeviceInformation());, you will get something like this:

{
  "manufacturer": "tp-link",
  "model": "Tapo C220",
  "firmwareVersion": "1.4.4 Build 260515 Rel.24570n",
  "serialNumber": "7461572b",
  "hardwareId": 1
}

and put it here with your comments please: https://docs.google.com/forms/d/e/1FAIpQLSfXsVZv802YFDISGCZaLaJaC_isw2wKQpJ11UurvgO5veYzUw/viewform


Migration from v0.x

Version 1.x introduces a new typed Onvif API. Compatibility modules are not re-exported from require('onvif') — import them explicitly: onvif/compatibility (callbacks) or onvif/compatibility/promises (async/await).

Callbacks (onvif/compatibility)

const { Cam, Discovery } = require('onvif/compatibility');

const cam = new Cam(
  { hostname: '192.168.1.13', port: 8000, username: 'admin', password: 'admin' },
  (error) => {
    if (error) throw error;
    cam.getDeviceInformation((err, info) => {
      if (err) throw err;
      console.log(info);
    });
  },
);

See compatibility.cjs.

Promises (onvif/compatibility/promises)

const { Cam, Discovery } = require('onvif/compatibility/promises');

const cam = new Cam({ hostname: '192.168.1.13', port: 8000, username: 'admin', password: 'admin' });

(async () => {
  await cam.connect();
  console.log(await cam.getDeviceInformation());
})();

See compatibilityPromises.cjs.

The promisified Cam wraps the callback implementation: no auto-connect (call await cam.connect()), methods return Promises, getters and EventEmitter APIs are forwarded, and _cam exposes the underlying instance.

Both compatibility entry points also export Discovery (callback or Promise probe), matching v0.x usage. Discovered cams include xaddrs (all ProbeMatch XAddrs as URL[]).

Compatibility notes (known differences vs v0.8)

The Cam surface from v0.8 is largely covered. Remaining behavioral differences:

  • gotoPreset accepts both { presetToken } (ONVIF / 1.x) and the v0 alias { preset } (sent as PresetToken)
  • rawResponse may omit the statusCode second argument that v0 emitted
  • setNTP(options) mutates the passed options object (fills NTPManual) — same as v0.x Callbacks match v0.x (err, data, xml?): the third argument is the raw SOAP response XML from the underlying request (Onvif.lastResponseXml). getPresets / cam.presets follow token → preset (duplicate names kept; 0.8.1+ intent). Note: published onvif@0.8.2 still returns name → preset from the getPresets callback while storing token → preset on cam.presets — compatibility aligns both with the token-keyed shape.

Examples

located in the Examples Folder on the Github

Tip

Not all of them were reworked for version 1.x.

  • compatibility.cjs - v0.x callback API (require('onvif/compatibility')): connect and print getDeviceInformation
  • compatibilityPromises.cjs - v0.x Promise API (require('onvif/compatibility/promises')): same, with async/await
  • events.with.filter.ts - ONVIF Events. With filters, pull-point, push-sub subscriptions
  • example.js - Move camera to a pre-defined position then server the RTSP URL up via a HTTP Server. Click on the RTSP address in a browser to open the video (if you have the VLC plugin installed)
  • example2.js - takes an IP address range, scans the range for ONVIF devices (brute force scan) and displays information about each device found including make and model and RTSP URLs For Profile S Cameras and Encoders it displays the default RTSP address For Profile G Recorders it displays the RTSP address of the first recording
  • example3.js - reads the command line cursor keys and sends PTZ commands to the Camera
  • example4.js - uses Discovery to find cameras on the local network
  • example5.js - connect to a camera via SOCKS proxy. Note SSH includes a SOCKS proxy so you can use this example to connect to remote cameras via SSH
  • example6.js - ONVIF Events. Example can be switched btween using Pull Point Subscriptions and using Base Subscribe with a built in mini HTTP Server
  • example7.js - legacy v0.x Promise example (for 1.x use compatibilityPromises.cjs instead)
  • example8.js - example setting OSD On Screen Display. (also uses Promises API)

Events

Common approach

To subscribe to all events using pull-point subscription you can just use .on() method, since the Onvif class inherits from the EventEmitter class.

const onvif = new Onvif();
function eventHandler(msg) {
  console.log(msg);
  onvif.off('event');
}
onvif.on('event', eventHandler);

Subscription class

If you need to subscribe to events, you can use the Subscription class. This class is for the specific subscriptions, for example, when we need to subscribe to events from the camera with the filters. or add some more subscriptions than the common one. It uses the pull-point subscription. It inherits from EventEmitter. And emits two events: data and error. To use it you need to call subscribe() method. And to stop the device subscription and remove all listeners you need to call unsubscribe() method.

The first and the only one argument for data is the NotificationMessage object. And an error raised only when the connection to the device is lost.

await cam.connect();
const sub = new Subscription(cam, {
  filter: {
    topicExpression: [
      {
        expression: 'tns1:RuleEngine/CellMotionDetector/Motion',
        dialect: 'http://www.onvif.org/ver10/tev/topicExpression/ConcreteSet',
      },
    ],
  },
});
sub.on('data', async (data) => {
  console.log(new Date().toLocaleTimeString(), 'motion', data.topic._, data.message.message.data);
  await sub.unsubscribe();
});
await sub.subscribe();

For a full interactive example, see events.with.filter.ts.

This class is used internally by the Onvif class for the common event listener.

Push WS-BaseNotification

With push (WS-BaseNotification), the camera sends event notifications to an HTTP endpoint you host, instead of you polling the device.

To use it: start an HTTP server reachable from the camera, call subscribe with that URL as the consumer reference, keep the subscription alive with renew before it expires, and call unsubscribe when you are done. Method signatures are in the Events class documentation. A working flow is shown in events.with.filter.ts — the HTTP server at lines 50–65, and subscribe / unsubscribe at lines 143–164.


Interfaces

Interfaces are generated according to the latest version of the ONVIF specification.

All methods accept options defined by the ONVIF specification and return data from the corresponding <method_name>Response.

For example, the getCapabilities method accepts a single argument of type GetCapabilities and returns a result of type Capabilities.

Below is the internal structure of the GetCapabilitiesResponse type:

export interface GetCapabilitiesResponse {
  /** Capability information. */
  capabilities?: Capabilities;
}

class Device {
  // ...

  async getCapabilities(options?: GetCapabilities): Promise<Capabilities> {
    // ...
  }

  // ...
}

In general, the library tries to avoid returning objects that contain only a single property.

In some cases, where native JavaScript types are more convenient, interfaces are extended with additional fields.

For example:

  • SetSystemDateAndTime
  • SetSystemDateAndTimeExtended

The extended version adds a more convenient field:

export interface SetSystemDateAndTimeExtended extends SetSystemDateAndTime {
  /**
   * Javascript Date object to use instead of UTCDateTime
   */
  dateTime?: Date;
  // ...
}

Support for xs:any

The ONVIF specifications include numerous extension points, which presents a challenge:

  • on one hand, we want simple and convenient interfaces
  • on the other hand, we need a unified mechanism for handling undocumented vendor-specific data

This data is usually provided through:

<xs:any namespace="##any" processContents="lax" minOccurs="0" maxOccurs="unbounded"/>

This mechanism is important for:

  • backward compatibility
  • forward compatibility
  • XML ↔ JavaScript transformation in ONVIF get/set methods

Example

Suppose we have an ElementItem structure defined in:

Example schema:

<xs:element name="ElementItem" minOccurs="0" maxOccurs="unbounded">
    <xs:annotation>
        <xs:documentation>Complex value structure.</xs:documentation>
    </xs:annotation>

    <xs:complexType>
        <xs:sequence>
            <xs:any namespace="##any" processContents="lax">
                <xs:annotation>
                    <xs:documentation>
                        XML tree containing the element value as defined
                        in the corresponding description.
                    </xs:documentation>
                </xs:annotation>
            </xs:any>
        </xs:sequence>

        <xs:attribute name="Name" type="xs:string" use="required">
            <xs:annotation>
                <xs:documentation>Item name.</xs:documentation>
            </xs:annotation>
        </xs:attribute>
    </xs:complexType>
</xs:element>

This becomes the following autogenerated TypeScript interface:

export interface ElementItem {
  /** Item name. */
  name: string;

  /** XML tree containing the element value as defined in the corresponding description. */
  [key: string]: unknown;
}

Real-World XML Example

For example, in MetadataConfiguration:

<Parameters>
    <ElementItem>
        <Name>elementItem1</Name>

        <Param1>
            <Data>42</Data>
        </Param1>

        <Param2>param2</Param2>
    </ElementItem>
</Parameters>

After parsing, the object looks like this:

{
  elementItem : [{
    name    : 'elementItem1',

    param1  : {
      data : 42
    },

    param2  : 'param2',

    __any__ : {
      'Name'   : ['elementItem1'],

      'Param1' : [{
        'Data' : ['42']
      }],

      'Param2' : 'param2'
    }
  }]
}

This object contains:

  • the required name field
  • parsed xs:any fields (param1, param2)
  • the raw __any__ field

The __any__ field contains the original unprocessed object returned by xml2js


Why Keep __any__?

A reasonable question is:

Why keep the raw XML structure?

The answer is simple.

When configuring ONVIF devices, extensions, or vendor-specific parameters, we often do not know how to serialize a clean JavaScript object back into the correct XML structure. At the same time, we still want to work with the data in a convenient way.

So when modifying device configuration (for example using setMetadataConfiguration), follow two simple rules:

1. Modify known fields directly

elementItem[0].name = 'hello'

2. Modify unknown/vendor-specific fields inside __any__

elementItem[0].__any__.Param2 = 'hi'

This structure can then be automatically converted back into the appropriate SOAP XML.


Tests

All tests are written using Jest.

Run them with:

npm test

The tests use happytime-onvif-server as a test device, including integration suites for the v0.x compatibility layer (onvif/compatibility and onvif/compatibility/promises).

Golden suite __tests__/compatibility.golden.test.ts runs the same scenarios against npm onvif@0.8.2 (onvif-v0) and the master compatibility Cam, comparing callback args (err, data, xml), key result fields, post-connect properties, and rawRequest / rawResponse events.

Thanks to HappyTimeSoft for providing the opportunity to test the full ONVIF specification.

Products are available here:


2_0YMEc2JsheGS0HvU0AM4cv0Lvey7tzzGCWzNHTLoMkFECz0USvK4RmZEa4Fnk8pJAYqXE5qx-qtECccJSD5LQNmPtzwt2a43eEfLAPrfQEMth4zwCsVeEO1-zvTszMxJ9pk93n0Fsj0eHynN709rTqLgRnizjgXL7hCKyEm0T4ZyL3ZyRglVPINRhbK2PW

About

A TypeScript-first ONVIF client for Node.js with typed WSDL services, lazy loading, WS-Security/Digest authentication, WS-Discovery, Events, Media/Media2, PTZ, Recording/Replay and a drop-in compatibility layer for the legacy 0.x API.

Topics

Resources

Contributing

Stars

785 stars

Watchers

44 watching

Forks

Releases

Used by

Contributors

Languages