UNPKG

ts-onvif

Version:

Client to ONVIF devices

436 lines (312 loc) 15.6 kB
# ONVIF [![Coverage Status](https://raw.githubusercontent.com/agsh/onvif/refs/heads/gh-pages-debug/badges/coverage.svg)](https://github.com/agsh/onvif/tree/v1) ONVIF client protocol implementation for Node.js. > [!TIP] > This page describes the future 1.x version of the ONVIF library written in TypeScript. > If you are looking for the README for the stable 0.x version, please see > [branch v0.x](https://github.com/agsh/onvif/tree/v0.x) > > The default npm installation still uses version 0.x. If you want to try this new version, install it with: > > ```shell > npm install onvif@alpha > ``` This is a wrapper for the ONVIF protocol that allows you to: - get information about your NVT (Network Video Transmitter) device and its media sources - control PTZ (pan-tilt-zoom) movements - manage presets - detect devices on your network - control events - retrieve information about your NVR (Network Video Recorder) Profile G device - obtain a list of recordings The library runs on Node.js and works server-side. [![ONVIF](https://github.com/user-attachments/assets/f58fb3c8-6bf6-406c-bcc7-883c1da33c5d)](http://onvif.org) ## 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. > [!WARNING] > The library may have some method signatures changed; it is in alpha testing. > The documentation for the new library was generated with TypeDoc and is available here: - https://htmlpreview.github.io/?https://github.com/agsh/onvif/blob/v1/docs/index.html Code that uses the old version of the library (`0.8.x`) should work with the compatibility class: - https://github.com/agsh/onvif/blob/v1/src/compatibility/cam.ts > [!WARNING] > Compatibility mode is currently unsupported. 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](https://github.com/agsh/onvif-generate-interfaces) to provide code completion and type checking in the IDE for the requested and returned values - Complete [documentation](https://agsh.github.io/onvif/) - Tests using the real [ONVIF server](https://www.happytimesoft.com/products/onvif-server/index.html) from HappyTimeSoft - Event support: pull-point, base ws-notification, filters, EventEmitter inheritance. See below - Lazy import of service modules: if, for example, a thermal service is not used, it will not be loaded. - 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](https://www.onvif.org/profiles/specifications/)): > AuthenticationBehavior, Application Management (`appmgmt`), Uplink, FederatedSearch - Improved error handling - Compatible with the original API structure --- # 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 synchronization** — `getSystemDateAndTime()` 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 tries `device.getServices()`, the modern ONVIF approach introduced with Profile T. If that fails on older devices, it falls back to `device.getCapabilities()`. Both methods populate `onvif.uri` with the URLs for media, PTZ, events, replay, and other services that subsequent requests use. 3. **Media configuration** — `media.getProfiles()` and `media.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, so you can start streaming or controlling the camera without extra setup. 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 ```ts 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 ```js 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"`) ```js 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); ``` # Examples located in the Examples Folder on the Github > [!TIP] > Not all of them were reworked for version 1.x. * [example.js](https://github.com/agsh/onvif/blob/master/examples/events.with.filter.ts) - ONVIF Events. With filters, pull-point, push-sub subscriptions * [example.js](https://github.com/agsh/onvif/blob/master/examples/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](https://github.com/agsh/onvif/blob/master/examples/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](https://github.com/agsh/onvif/blob/master/examples/example3.js) - reads the command line cursor keys and sends PTZ commands to the Camera * [example4.js](https://github.com/agsh/onvif/blob/master/examples/example4.js) - uses Discovery to find cameras on the local network * [example5.js](https://github.com/agsh/onvif/blob/master/examples/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](https://github.com/agsh/onvif/blob/master/examples/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](https://github.com/agsh/onvif/blob/master/examples/example7.js) - example using a Promise API. It uses 'promisify' to convert the ONVIF Library to return promises and uses Await to wait for responses * [example8.js](https://github.com/agsh/onvif/blob/master/examples/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. ```ts 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. ```ts 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](https://github.com/agsh/onvif/blob/v1/examples/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](https://htmlpreview.github.io/?https://github.com/agsh/onvif/blob/v1/docs/classes/Events.html). A working flow is shown in [events.with.filter.ts](https://github.com/agsh/onvif/blob/v1/examples/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](https://github.com/onvif/specs). 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: ```ts 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: ```ts 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: ```xml <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: - https://github.com/onvif/specs/blob/9cdc78685cc9ddd80099a7a9cb9ada035dd0d5eb/wsdl/ver10/schema/onvif.xsd#L6611 Example schema: ```xml <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: ```ts 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`: ```xml <Parameters> <ElementItem> <Name>elementItem1</Name> <Param1> <Data>42</Data> </Param1> <Param2>param2</Param2> </ElementItem> </Parameters> ``` After parsing, the object looks like this: ```js { 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](https://github.com/Leonidas-from-XIV/node-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 ```js elementItem[0].name = 'hello' ``` ### 2. Modify unknown/vendor-specific fields inside `__any__` ```js 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: ```shell npm test ``` The tests use [happytime-onvif-server](https://github.com/agsh/happytime-onvif-server) as a test device. Thanks to [HappyTimeSoft](https://www.happytimesoft.com/index.html) for providing the opportunity to test the full ONVIF specification. Products are available here: - https://www.happytimesoft.com/product.html