ExOxvif
ExOxvif controls ONVIF IP cameras from Elixir. It wraps oxvif, an async Rust ONVIF client, in a Rustler NIF.
| Service | Functions |
|---|---|
| Discovery | ExOxvif.discover/1 (WS-Discovery multicast) |
| Device | info, clock, services, scopes |
| Media | profiles, RTSP stream URI, snapshot URI |
| PTZ | absolute, relative and continuous move, stop, status, presets, home, configurations, nodes and spaces |
| Imaging | focus move, stop, status |
| Events | pull-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
:timeoutoption (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 inhandle_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.