Hex.pm CI

ExOxvif controls ONVIF IP cameras from Elixir. It wraps oxvif, an async Rust ONVIF client, in a Rustler NIF.

ServiceFunctions
DiscoveryExOxvif.discover/1 (WS-Discovery multicast)
Deviceinfo, clock, services, scopes
Mediaprofiles, RTSP stream URI, snapshot URI
PTZabsolute, relative and continuous move, stop, status, presets, home, configurations, nodes and spaces
Imagingfocus move, stop, status
Eventspull-point create, pull, renew, unsubscribe

Authentication uses WS-Security UsernameToken (PasswordDigest) with HTTP Digest fallback. Clock offsets are synced from the device by default.

Installation

def deps do
  [
    {:ex_oxvif, "~> 0.1"}
  ]
end

Precompiled NIFs are downloaded for these targets:

  • macOS (aarch64, x86_64)
  • Linux glibc (aarch64, x86_64)
  • Linux musl (aarch64, x86_64)

To compile from source instead, add {:rustler, "~> 0.37"} to your deps and set EX_OXVIF_BUILD=1. This needs a Rust toolchain of 1.91 or later.

Usage

{:ok, [camera | _]} = ExOxvif.discover()

{:ok, session} =
  ExOxvif.connect(hd(camera.xaddrs), username: "admin", password: "secret")

{:ok, info} = ExOxvif.Device.info(session)
{:ok, [profile | _]} = ExOxvif.Media.profiles(session)
{:ok, %{uri: rtsp}} = ExOxvif.Media.stream_uri(session, profile.token)

# Pan, tilt and zoom in the device's generic spaces
:ok = ExOxvif.PTZ.absolute_move(session, profile.token, pan: 0.25, tilt: -0.1, zoom: 0.5)
{:ok, status} = ExOxvif.PTZ.status(session, profile.token)

# Presets
{:ok, gate} = ExOxvif.PTZ.set_preset(session, profile.token, name: "gate")
:ok = ExOxvif.PTZ.goto_preset(session, profile.token, gate)

# Focus (imaging takes the video source token)
:ok = ExOxvif.Imaging.move(session, profile.video_source_token, {:continuous, 0.3})
:ok = ExOxvif.Imaging.stop(session, profile.video_source_token)

# Events
{:ok, sub} = ExOxvif.Events.create_pull_point(session, termination: 60_000)
{:ok, messages} = ExOxvif.Events.pull_messages(session, sub, wait: 5_000)

Every call returns {:ok, value}, :ok, or {:error, %ExOxvif.Error{}}. %ExOxvif.Error{} is an exception, so you can raise it. Its :kind separates transport failures, HTTP status codes, SOAP faults and parse errors.

Degrees and PTZ spaces

ONVIF PTZ positions are normalised: pan and tilt run over -1.0..1.0, and zoom over 0.0..1.0. How those values map to degrees depends on the camera. ExOxvif.PTZ.nodes/1 lists the coordinate spaces a camera supports, and ExOxvif.PTZ.configurations/1 gives its configured limits. A pointing application usually also needs per-camera calibration: the mount heading and the pan and tilt range in degrees.

How the NIF behaves

Each function is a NIF that returns immediately. The ONVIF request runs on a two-thread Tokio runtime inside the native library. When it finishes, the result is sent to the calling process, which is waiting in a receive. Consequences:

  • No BEAM scheduler, dirty or otherwise, blocks on camera I/O. A camera that hangs only delays the process that called it.
  • Calls from many processes run concurrently.
  • A Rust panic becomes {:error, %ExOxvif.Error{kind: :panic}}. It does not crash the VM.
  • Each call takes a :timeout option (default 15 s). oxvif already bounds every HTTP exchange at 10 s, so the timeout rarely fires. If it does, a late reply can still arrive as an {:ex_oxvif_reply, id, result} message, so a GenServer making calls should ignore that message in handle_info/2.

Development

mix deps.get
mix test                       # builds the NIF with oxvif's mock camera
mix test --include multicast   # also runs the WS-Discovery test

The test suite runs against oxvif's in-process mock ONVIF camera. That code is compiled only into test builds (the mock-server cargo feature) and never ships in release artifacts.

Commits follow Conventional Commits. To install the hook:

cp scripts/hooks/commit-msg .git/hooks/commit-msg

Releasing is described in RELEASING.md.

License

MIT. oxvif is also MIT-licensed and copyright its authors.