Skip to content
 
 

Repository files navigation

📸 Virtual Onvif Proxy (Fork)

Simple docker container to add any RTSP stream into UniFi Protect 5+

Maintained by mountaindude (Ptarmigan Labs).


Why this fork?

This is a fork of p10tyr/rtsp-to-onvif by Piotr Kula, which itself was forked from daniela-hase/onvif-server by Daniela Hasenbring. The original repository (Piotr's) had not been updated for several years.

Background is that I have a few "Axis Companion Bullet Mini LE" cameras (link) that are nice and inexpensive, but they are not ONVIF compliant. Instead their firmware is locked down so they only work with Axis' own hardware/video recorders.

I wanted to use them with UniFi Protect, which requires ONVIF cameras. This tool allows me to wrap the RTSP stream from the camera in an ONVIF interface so that Protect can adopt it.
There are for sure other tools that do this, but I wanted a small solution that I can understand how it works and that runs as a Docker container. Having full control over the source code and dependencies is a bonus.

So, this fork was created to:

  • Security audit all source code and dependencies before running in production
  • Update dependencies to resolve 8 npm vulnerabilities (2 critical, 3 high, 2 moderate, 1 low)
  • Fix code bugs (process.exit, this.listen binding, shell injection hardening)
  • Improve Docker setup (pinned Alpine version, HEALTHCHECK, compose build directive)
  • Fix CI (dependabot config, workflow capitalization bug)
  • Unit tests (basic coverage for config-tools.js and onvif-server.js)
  • Build and publish new Docker images under ptarmiganlabs/rtsp-to-onvif (Docker Hub + GitHub Container Registry)
  • Long term maintenance (Make it easy to update dependencies, automatic version numbering, rebuild, and publish new images)

Security disclaimer

This fork serves my own needs. I have a well isolated network and a small number of cameras.
The tool could certainly benefit from additional security hardening (CORS, better credentials handling, etc.), so please review the code and test it in a safe environment before using it in production or exposing it to the internet.

Also, I have no way to test every camera model, and I cannot guarantee that this software will work for you.

Use at your own risk.

Security audit results

A security audit was performed as part of this fork. The codebase is small (~670 lines of JavaScript) and was reviewed in its entirety.

Verdict: Not malicious, as far as I can tell. No outbound callbacks, no data exfiltration, no telemetry, no arbitrary code download/execution. All network operations are local (WS-Discovery multicast, TCP proxy, DHCP). All URLs are either ONVIF/W3C namespace constants or constructed from the user's own config file.

Dependencies updated

Package Old version New version Reason
soap 1.1.5 1.9.3 Resolved xmldom, lodash, xml-crypto, form-data, formidable, axios CVEs
yaml 2.6.1 2.9.0 Stack overflow fix (GHSA-48c2-rrv3-qjmp)
node-uuid 1.4.8 replaced with uuid ^11.0.0 Deprecated package; crypto-secure RNG instead of Math.random()

The above is the status as of July 2026. Dependencies and versions are subject to change in future releases. See the package.json file for the current versions.

Code fixes

Bug File Fix
exit(-1) throws ReferenceError src/config-tools.js Changed to process.exit(-1)
this.listen loses context in HTTP server src/onvif-server.js Wrapped in arrow function to preserve this binding
Shell injection via config values src/config-tools.js Replaced execSync string interpolation with spawnSync arg arrays

These are code changes done as part of the initial onboarding of the repository. Additional code updates may be made in future releases as needed.

Docker improvements

  • Bumped and pinned base image to node:24-alpine3.24 (reproducible builds)
  • Added HEALTHCHECK with curl
  • Uncommented restart: unless-stopped

These are changes done as part of the initial onboarding of the repository. Additional updates may be made in future releases as needed.

CI fixes

  • Fixed .github/dependabot.yml — empty package-ecosystem: ""npm, docker, github-actions
  • Fixed .github/workflows/docker_publish.yml${{Github.run_number}}${{ github.run_number }} (case-sensitive)
  • Updated workflow to publish to both Docker Hub and GitHub Container Registry
  • Removed Docker Build Cloud config from upstream author

These are changes done as part of the initial onboarding of the repository. Additional updates may be made in future releases as needed.

My camera setup

I'm using an Axis Companion Bullet Mini LE (link to AXIS) at 10.11.12.13 (made-up IP) with credentials <username> / <password>. If your camera ships with default credentials, change them if possible.

Finding the RTSP path

The RTSP path was found at Home Assistant forums and tested with ffprobe:

ffprobe "rtsp://<username>:<password>@10.11.12.13/axis-media/media.amp?videocodec=h264&Axis-Orig-Sw=true"

Output confirmed:

  • Codec: H264 (High profile)
  • Resolution: 1920x1080
  • Framerate: 30 fps

Snapshot authentication issue

The snapshot endpoint (/axis-cgi/jpg/image.cgi) was tested with various curl auth options (-u, --digest, --anyauth) but none returned a valid JPEG — the camera likely requires a specific digest auth handshake that curl couldn't negotiate.

This is not a problem for UniFi Protect. The live RTSP stream works, and Protect shows the video stream. The snapshot path is still included in the config for completeness.

Working config.yaml

This is the exact config running in production:

onvif:
  - name: AxisCompanionBulletMiniLE
    dev: ens18
    target:
      hostname: 10.11.12.13
      ports:
        rtsp: 554
        snapshot: 80
    highQuality:
      rtsp: /axis-media/media.amp?videocodec=h264&Axis-Orig-Sw=true
      snapshot: /axis-cgi/jpg/image.cgi
      width: 1920
      height: 1080
      framerate: 30
      bitrate: 4096
      quality: 4
    ports:
      server: 8081
      rtsp: 8554
      snapshot: 8282

Notes:

  • dev: ens18 — the VM's network interface (find yours with ip addr on the Linux host)
  • ports.snapshot: 8282 — changed from the default 8080 because it was already in use on the host
  • mac and uuid are intentionally omitted — auto-generated on first run and written back to the file
  • Credentials (root/pass) are not in the config file — enter them in Unifi Protect during adoption

Quick start

⚠️ Linux host required

This container requires network_mode: "host" and CAP_NET_ADMIN to create virtual network interfaces (macvlan) and participate in ONVIF multicast discovery. These features only work on Linux hosts — they are not supported on macOS or Windows Docker Desktop. Use a Linux server, VM, or LXC container.

Option A: Use a pre-built image (recommended)

  1. Download the files you need and configure

    mkdir rtsp-to-onvif && cd rtsp-to-onvif
    wget https://raw.githubusercontent.com/ptarmiganlabs/rtsp-to-onvif/refs/heads/main/compose.yaml
    wget https://raw.githubusercontent.com/ptarmiganlabs/rtsp-to-onvif/refs/heads/main/config.example.yaml
    cp config.example.yaml config.yaml
    nano config.yaml    # edit with your camera details
  2. Pull and run

    sudo docker compose up

Watch the logs for:

  • CONFIG: UUIDv4 - <generated>
  • CONFIG: MAC - <generated>
  • SERVER: <camera name> - HTTP listening on <ip>:8081
  • PROXY: 8554 --> <camera ip>:554

Option B: Build from source

  1. Clone the repository

    git clone https://github.com/ptarmiganlabs/rtsp-to-onvif.git
    cd rtsp-to-onvif
  2. Create your config

    cp config.example.yaml config.yaml
    nano config.yaml    # edit with your camera details
  3. Build and run with Docker Compose

    sudo docker compose up --build

    Or build manually then run:

    docker build -t ptarmiganlabs/rtsp-to-onvif:latest .
    # Or use the included build script:
    ./build-docker.sh
    docker run --network host --cap-add NET_ADMIN -v $(pwd)/config.yaml:/onvif.yaml ptarmiganlabs/rtsp-to-onvif:latest

Watch the logs for:

  • CONFIG: UUIDv4 - <generated>
  • CONFIG: MAC - <generated>
  • SERVER: <camera name> - HTTP listening on <ip>:8081
  • PROXY: 8554 --> <camera ip>:554

Adopt in Unifi Protect

Go to Protect → Devices → Add Device. The camera should appear. Adopt it and enter your camera's credentials.

Switch to detached mode

Once confirmed working, stop with Ctrl+C and run:

sudo docker compose up -d

Reduce log verbosity (optional)

Debug logging is enabled by default. To disable, edit compose.yaml and set DEBUG: 0:

    environment:
      DEBUG: 0

Then restart: sudo docker compose up -d

Useful commands

# View logs when running detached
sudo docker compose logs -f

# Stop the container
sudo docker compose down

# Rebuild after pulling code updates (if building from source)
git pull && sudo docker compose up --build -d

# Clean up stale virtual interfaces if the container didn't exit cleanly
sudo ip link del dev rtsp2onvif_0

Multiple cameras with ARP issues

If multiple cameras show wrong MAC/IP in Protect, run on the host before starting:

sudo sysctl -w net.ipv4.conf.all.arp_ignore=1
sudo sysctl -w net.ipv4.conf.all.arp_announce=2

To persist across reboots:

echo 'net.ipv4.conf.all.arp_ignore=1' | sudo tee /etc/sysctl.d/99-onvif.conf
echo 'net.ipv4.conf.all.arp_announce=2' | sudo tee -a /etc/sysctl.d/99-onvif.conf

Config file reference

  • You just need to supply the bare minimum for each camera
  • MAC addresses auto-generated with Unicast LAA prefix 1A:11:B0 and random NIC address
  • UUID addresses auto-generated (UUIDv4)
  • IPv4 comes from your DHCP server

ℹ️ NOTE

This file will be overwritten during automatic configuration so comments will be lost. Keep a backup of your commented version.

No username or passwords required here!

Minimal required fields per camera:

Field Purpose Example
name Display name in ONVIF consumer (letters only, no spaces) AxisCompanionBulletMiniLE
dev Host network interface for virtual IP (find via ip addr) ens18
target.hostname Your camera's IP address 10.11.12.13
target.ports.rtsp Camera's RTSP port 554
target.ports.snapshot Camera's HTTP port for snapshots 80
highQuality.rtsp RTSP stream path /axis-media/media.amp?videocodec=h264
highQuality.snapshot Snapshot URL path /axis-cgi/jpg/image.cgi
highQuality.width / height Video resolution 1920 / 1080
highQuality.framerate FPS 30
highQuality.bitrate Video bitrate in kb/s 4096
highQuality.quality Quality (leave at 4) 4
ports.server / rtsp / snapshot Virtual server ports (change if conflicts) 8081 / 8554 / 8282

Auto-generated (don't set these initially):

  • mac — auto-created with LAA prefix 1A:11:B0:XX:XX:XX, IP assigned via DHCP
  • uuid — auto-generated UUIDv4 (ONVIF device identifier)

Docker image

Images are published to two registries:

Registry Image
Docker Hub ptarmiganlabs/rtsp-to-onvif:latest
GitHub Container Registry ghcr.io/ptarmiganlabs/rtsp-to-onvif:latest

The compose.yaml pulls the pre-built image from Docker Hub by default. To use the GitHub Container Registry image instead, comment out the Docker Hub image: line and uncomment the GHCR line in compose.yaml.

Supported architectures

Images are published as a multi-arch manifest for:

  • linux/amd64 (x86_64)
  • linux/arm64 (Raspberry Pi 4/5, AWS Graviton, Apple Silicon hosts)

Docker automatically pulls the image matching your host's architecture.

ℹ️ A local ./build-docker.sh build produces only your host's architecture (single-platform), not the multi-arch manifest.

Building from source

To build the image locally instead of pulling a pre-built one:

With Docker Compose — add build: . to compose.yaml and run:

sudo docker compose up --build

Manually with Docker:

docker build -t ptarmiganlabs/rtsp-to-onvif:latest .
# Or use the included build script:
./build-docker.sh

Then run with the built image:

docker run --network host --cap-add NET_ADMIN -v $(pwd)/config.yaml:/onvif.yaml ptarmiganlabs/rtsp-to-onvif:latest


Original README (from p10tyr/rtsp-to-onvif)

Preserved as-is for reference. The sections below are from the upstream repository and may contain outdated information, original typos, and references to the original Docker image name (kulasolutions/rtsp-to-onvif).


📸 Virtual Onvif Proxy

Simple docker container to add any RTSP stream into UniFi Protect 5+

This is a continuation from the simple virtual ONVIF proxy that was originally released by Daniela Hase.

This repository has added features such as ...

  • Making it a pure docker appliance. Pull-And-Run™
  • Only deals with RSTP to ONVIF proxies
  • Auto creates MAC addresses and registers IPv4 with DHCP
  • more to come...

What can you adopt?

  • Adopt IP camera --> RTSP (h264) --> Protect
  • Adopt Raspberry Pi Camera --> uv4l --> RTSP (h264) --> Protect
  • Adopt Analog --> NVR --> RTSP (h264) --> Protect
  • Adopt WebCam --> go2rtc --> RTSP (h264) --> Protect
  • Adopt ... Anything RTSP --> Protect

IP camera --> RTSP (h264) --> Protect

image

Analog! --> NVR --> RTSP (h264) --> Protect

analog-dvr-rtsp

🧾 Getting Started

In a few steps you will have everything needed to run container first time. This will auto confiugre IP's for you. If you want more control over MAC's and IP's scroll down to Router Setup

Docker compose

Create a directory locally where you will keep your compose and config files.

  1. Create a directory and change into it
  • mkdir rtsp-to-onvif and cd rtsp-to-onvif
  1. Download the compose.yaml file
  • wget https://raw.githubusercontent.com/p10tyr/rtsp-to-onvif/refs/heads/release/compose.yaml
  1. Download the config.example.yaml and clone it
  • wget https://raw.githubusercontent.com/p10tyr/rtsp-to-onvif/refs/heads/release/config.example.yaml
  • cp config.example.yaml config.yaml
  1. Edit and configure your cameras
  • nano config.yaml
  1. Run compose in attached mode and check for any messages.
  • sudo docker compose up
  1. If you see the cameras show up in Protect then you can run docker in detached mode (or use Dockge, Portainer, etc...)
  • sudo docker compose up -d

Config file

  • You just need to supply the bare minimum for each camera
  • Autoconfigure MAC addresses all use Unicast LAA prefix 1A:11:B0 and the NIC address will be random
  • UUID addresses will be added automatically
  • IPv4 will come from your DHCP server

ℹ️ NOTE

This file will be overwritten during automatic configuration so comments will be lost.

No username or passwords required here!

onvif:
  - name: BulletCam                               # A user define named that will show up in the consumer device. Use letters only, no spaces or special characters
    dev: enp2s0 #eth0                             # Network interface to add virtual IP's too. use ip addr to find your name
    target:
      hostname: 192.168.1.187                      # Your cameras IPv4 address
      ports:
        rtsp: 554                                  # Your cameras RTSP port. Typically 554
        snapshot: 80                               # Cameras non https port for snapshots
    highQuality:
      rtsp: /Streaming/Channels/101/                    # The RTSP Path
      snapshot: /ISAPI/Streaming/Channels/101/picture   # Snapshot path - not working yet
      width: 2048                                       # The Video Width
      height: 1536                                      # The Video Height
      framerate: 15                                     # The Video Framerate/FPS
      bitrate: 3072                                     # The Video Bitrate in kb/s
      quality: 4                                        # Quality, leave this as 4 for the high quality stream.
    ports:                                              # Virtual server ports. No need to change these unles you run into port already in use problems
      server: 8081
      rtsp: 8554
      snapshot: 8080
    #mac - automatically added here and IP comes from DHCP- Add your own if you know what you doing
    #uuid - ONVIF ID - automatically added here. If you change it Protect will think its a different camera

Credits

Thank you Daniela Hase for relasing the original script to the public! Original repository https://github.com/daniela-hase/onvif-server

It has truly inspired me and gave me so many ideas! That is why I had to fork your original repo so that I could develop this further to be a docker appliance.

Unifi Protect

Tested on Unifi Protect 5.0.40+

Once the device shows up in protect, make sure the correct MAC address is assigned to the IP before adopting. You can then adopt it and provide the username and password that are set on the real RTSP device.

Known Limitations

"Third-party features such as analytics, audio playback, and pan-tilt-zoom (PTZ) control are not supported." - Unify Support

  • Seems to only support recording normal/high profile h264 video streams at the moment
  • Your luck with h265 may vary
  • Scrubbing does not seem to work? Possibly depends on the h264 implementaion on the camera
  • Snapshot not implemented yet. Hope it works.
  • HighProfile support only for now - You can supply LowProfile but that shows up as an extra camera.

⚒️ Roadmap

  • Simplyfy docker - DONE
    • Only run in Docker - DONE
    • Auto virtual MAC registrations - DONE
    • Register with DCHP - DONE
    • More debug messages - DONE
  • Learn about the ONVIF Profile S
    • Implement snapshot functionality?
    • Implement some other features?

🛜 Docker and Docker Compose

Debug is enabled byu default in compose.yaml Once you have setup complete you can disable it.

compose.yaml file

You don't really have to change anything in this file. It has all the settings and permsions required to make it just work.

Some properties

  • volumnes: ./config.yaml:/onvif.yaml - where your config file is. Next step
  • cap_add: NET_ADMIN - Required to create virtual networks based on config file
  • environment: DEBUG:1 - Uncommnet if you need more debug logs to show up

Router setup

ONVIF discovery works by using MAC addresses. If you are happy with DHCP you can skip this step

If you really static reservations - Do that BEFORE running the container.

Add static reservatations using LAA MAC's

  • MAC's starting with x2:xx:xx:xx:xx:xx,x6:xx:xx:xx:xx:xx,xA:xx:xx:xx:xx:xx and xE:xx:xx:xx:xx:xx are Locally Administered Addresses (LAA)

Virtual ONVIF 1

  • MAC 0A:00:00:00:00:51
  • IP 192.168.51

Virtual ONVIF 2

  • MAC 0A:00:00:00:00:52
  • IP 192.168.52

Konwn problems

Usuaully mulitple camera will just work out the box with the same server ports working for each virtual IP

If you seem to have problems like

  • MAC Addresses not showing properly for multiple cameras in Protect
  • Port numbers in use error during startup
  • MAC shows the wrong IP

Generally depends from OS to OS. Eg in Ubuntu 22.

You need to run these commands to allow virtual interface max advertising - but you still need a differnt port per virtual IP

sudo sysctl -w net.ipv4.conf.all.arp_ignore=1
sudo sysctl -w net.ipv4.conf.all.arp_announce=2

Other stuff

Misc notes


Remove a virutal IP on the host without rebooting sudo ip link del dev rtsp2onvif_<number>


Wrapping an RTSP Stream

This tool is used to create ONVIF devices from regular RTSP streams by creating the following configuration.

Cameras before ONVIF had all kinds of weird and wonderful implemenations

You will have to find out the stream and snapshot details with your own research, by seraching the web for URLS. You should verify the stream using VLC and the snapshot URL using a browser.

Things to look out for

  • http is enabled (for snapshots)
  • if snapshot is not working, try admin account. some cameras are like that
  • rtsp is enabled ideally on port 554

RTSP Example: Assume you have this RTSP stream:

rtsp://192.168.1.32:554/Streaming/Channels/101/
       \__________/ \_/\______________________/
            |       Port    |
         Hostname           |
                          Path

If your RTSP url does not have a port it uses the default port 554.

Your RTSP url may contain a username and password - those should NOT be included in the config file. Instead you will have to enter them in the software that you plan on consuming this Onvif camera in, for example during adoption in Unifi Protect.

Next you need to figure out the resolution and framerate for the stream. If you don't know them, you can use VLC to open the RTSP stream and check the Media Information (Window -> Media Information) for the "Video Resolution" and "Frame rate" on the "Codec Details" page, and the "Stream bitrate" on the "Statistics" page. The bitrate will fluctuate quite a bit most likely, so just pick a number that is close to it (e.g. 1024, 2048, 4096 ..).

You can either randomly change a few numbers of the UUID, or use a UUIDv4 generator1.

If you have a separate low-quality RTSP stream available, fill in the information for the lowQuality section above but this shows up as a seperate camera in unify.

Note

Since we don't provide a snapshot url you will onyl see the Onvif logo in certain places in Unifi Protect where it does not show the livestream.

Footnotes

  1. UUIDv4 Generator

About

Self hosted Docker container for virtual RTSP to ONVIF streams for UniFi Protect 5+

Topics

Resources

Security policy

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages