Simple docker container to add any RTSP stream into UniFi Protect 5+
Maintained by mountaindude (Ptarmigan Labs).
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)
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.
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.
| 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.
| 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.
- Bumped and pinned base image to
node:24-alpine3.24(reproducible builds) - Added
HEALTHCHECKwithcurl - 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.
- Fixed
.github/dependabot.yml— emptypackage-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.
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.
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
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.
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: 8282Notes:
dev: ens18— the VM's network interface (find yours withip addron the Linux host)ports.snapshot: 8282— changed from the default8080because it was already in use on the hostmacanduuidare 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
⚠️ Linux host requiredThis container requires
network_mode: "host"andCAP_NET_ADMINto 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.
-
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
-
Pull and run
sudo docker compose up
Watch the logs for:
CONFIG: UUIDv4 - <generated>CONFIG: MAC - <generated>SERVER: <camera name> - HTTP listening on <ip>:8081PROXY: 8554 --> <camera ip>:554
-
Clone the repository
git clone https://github.com/ptarmiganlabs/rtsp-to-onvif.git cd rtsp-to-onvif -
Create your config
cp config.example.yaml config.yaml nano config.yaml # edit with your camera details -
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>:8081PROXY: 8554 --> <camera ip>:554
Go to Protect → Devices → Add Device. The camera should appear. Adopt it and enter your camera's credentials.
Once confirmed working, stop with Ctrl+C and run:
sudo docker compose up -dDebug logging is enabled by default. To disable, edit compose.yaml and set DEBUG: 0:
environment:
DEBUG: 0Then restart: sudo docker compose up -d
# 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_0If 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=2To 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- You just need to supply the bare minimum for each camera
- MAC addresses auto-generated with Unicast LAA prefix
1A:11:B0and 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 prefix1A:11:B0:XX:XX:XX, IP assigned via DHCPuuid— auto-generated UUIDv4 (ONVIF device identifier)
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.
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.shbuild produces only your host's architecture (single-platform), not the multi-arch manifest.
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 --buildManually with Docker:
docker build -t ptarmiganlabs/rtsp-to-onvif:latest .
# Or use the included build script:
./build-docker.shThen run with the built image:
docker run --network host --cap-add NET_ADMIN -v $(pwd)/config.yaml:/onvif.yaml ptarmiganlabs/rtsp-to-onvif:latestOriginal 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).
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
Analog! --> NVR --> RTSP (h264) --> Protect
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
Create a directory locally where you will keep your compose and config files.
- Create a directory and change into it
mkdir rtsp-to-onvifandcd rtsp-to-onvif
- Download the compose.yaml file
wget https://raw.githubusercontent.com/p10tyr/rtsp-to-onvif/refs/heads/release/compose.yaml
- Download the config.example.yaml and clone it
wget https://raw.githubusercontent.com/p10tyr/rtsp-to-onvif/refs/heads/release/config.example.yamlcp config.example.yaml config.yaml
- Edit and configure your cameras
nano config.yaml
- Run compose in attached mode and check for any messages.
sudo docker compose up
- 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
- You just need to supply the bare minimum for each camera
- Autoconfigure MAC addresses all use Unicast LAA prefix
1A:11:B0and 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 cameraThank 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.
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.
- 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?
Debug is enabled byu default in compose.yaml Once you have setup complete you can disable it.
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 stepcap_add: NET_ADMIN- Required to create virtual networks based on config fileenvironment: DEBUG:1- Uncommnet if you need more debug logs to show up
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:xxandxE:xx:xx:xx:xx:xxare 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
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=2Misc 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 |
PathIf 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.

