# ExOxvif

[![Hex.pm](https://img.shields.io/hexpm/v/ex_oxvif.svg)](https://hex.pm/packages/ex_oxvif)
[![CI](https://github.com/cortfritz/ex_oxvif/actions/workflows/ci.yml/badge.svg)](https://github.com/cortfritz/ex_oxvif/actions/workflows/ci.yml)

ExOxvif controls ONVIF IP cameras from Elixir. It wraps
[oxvif](https://github.com/smiti1642/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

```elixir
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

```elixir
{: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

```sh
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](https://www.conventionalcommits.org/).
To install the hook:

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

Releasing is described in [RELEASING.md](RELEASING.md).

## License

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