8000 GitHub - agsh/onvif: ONVIF client for all ONVIF devices with TypeScript types. Supports Profiles S/T/G/M/C/A, WS-Discovery, WS-Security/Digest, Events, Media/Media2, PTZ, Recording/Replay, and a 0.x compatibility layer. · GitHub
[go: up one dir, main page]

Skip to content
agshPublic

About

ONVIF client for all ONVIF devices with TypeScript types. Supports Profiles S/T/G/M/C/A, WS-Discovery, WS-Security/Digest, Events, Media/Media2, PTZ, Recording/Replay, and a 0.x compatibility layer.

Topics

Resources

Stars

795 stars

Watchers

42 watching

Forks

Repository files navigation

ONVIF

Coverage Status

ONVIF client for all ONVIF devices with TypeScript types.

  • TypeScript + Promise API
  • Typed ONVIF/WSDL interfaces
  • Profiles S, T, G, M, C, A
  • WS-Discovery, WS-Security, Digest (MD5 / SHA-1 / SHA-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.

ONVIF

Installation

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

Documentation

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.

Quick start

Call connect() before service methods (or pass autoConnect: true). Call close() when you no longer need the connection.

TypeScript / ESM

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();

CommonJS

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();
})();

Example project

Note

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):

333702629-e816fed6-067a-4f77-b3f5-ccd9d5ff1310.webm

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. => {
      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);

Features

Connection

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. Details: innerDocs/connection.md.

Default Media Profile (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.

Discovery

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 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();
}

Options include timeout, device (interface), types / scopes filters, bufferSize, and IPv4/IPv6. RelatesTo / MessageID matching avoids mixing Probe replies. Runnable sample: discovery.js.

Services

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

  • Single useful value - unwrapped (e.g. getDeviceInformation() -> info object, systemReboot() -> message string)
  • Multi-field value - keep the object. Prefer .uri on stream/snapshot results:
    • media.getStreamUri() / media.getSnapshotUri() -> MediaUriResult (uri always; Media 1.0 also has invalidAfterConnect / invalidAfterReboot / timeout)
    • When media2Support is set, those media.* helpers route through Media2 and return { uri } only (validity fields are not synthesized). Force Media 1.0 with onvif.media2Support = false
    • Direct media2.getStreamUri() / media2.getSnapshotUri() always return { uri }
  • By-token getters (getAccessPoints, getDoors, ...) - unwrapped to an array ([] when empty)
  • Paginated *List methods - keep the ONVIF list response object ({ accessPoint?, nextStartReference? }, ...)

Events

onvif.on('event', (msg) => console.log(msg));

Pull-point, WS-BaseNotification, filters, and push: innerDocs/events.md.

Vendor XML

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.

SOAP XML

build / parseSOAPString, public onvif.request(), and per-call requestContext request/response capture: innerDocs/soap-xml.md.

Migration from v0.x

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.

Examples

Samples in the examples folder:

Compatibility-layer demos: compatibility.cjs, compatibilityPromises.cjs.

Device compatibility

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

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.

OBEY!

About

ONVIF client for all ONVIF devices with TypeScript types. Supports Profiles S/T/G/M/C/A, WS-Discovery, WS-Security/Digest, Events, Media/Media2, PTZ, Recording/Replay, and a 0.x compatibility layer.

Topics

Resources

Stars

795 stars

Watchers

42 watching

Forks

Releases

Used by

Contributors

Languages

0