A cross-platform ONVIF device simulator written in Go. Supports CLI, TUI, and GUI modes, making it easy to test ONVIF clients without real hardware.
v0.4.0 is the first release to implement every ONVIF Profile S v1.3 device-side mandatory feature — discovery, capabilities, system + network configuration, user handling, media profile/encoder/source/metadata configuration, H.264/H.265/MJPEG over RTSP, and WS-BaseNotification pull-point + push event handling.
- ONVIF Profile S (camera streaming simulation)
- Multiple interface modes: CLI, TUI, GUI
- Embedded RTSP server — point each profile at a local mp4 and the simulator loops it as the live stream
- Configure multiple streams (main, sub)
- Profile S (v1.3) — every device-side mandatory feature implemented as of v0.4.0; see
doc/for the bundled Profile S Specification PDF, and Profile S conformance validation below for the validation harness.
Linux / macOS
curl -fsSL https://github.com/GyeongHoKim/onvif-simulator/releases/latest/download/install.sh | bashWindows (PowerShell)
iex (irm https://github.com/GyeongHoKim/onvif-simulator/releases/latest/download/install.ps1)After installation, the onvif-simulator command will be available in your terminal.
Download the installer for your platform from the Releases page:
| Platform | File |
|---|---|
| Windows | onvif-simulator-gui-amd64-installer.exe |
| macOS | onvif-simulator-gui-darwin-amd64.dmg |
| Linux | onvif-simulator-gui-linux-amd64.AppImage |
Run the installer and follow the on-screen instructions.
The Raspberry Pi build channel embeds the mtxrpicam capture helper from
bluenviron/mediamtx so a Pi Camera
Module can drive an ONVIF device on Raspberry Pi OS without any extra setup.
The build is CLI/TUI only — there is no Pi GUI binary.
| Pi model | OS arch | Release archive |
|---|---|---|
| Pi Zero / 2 / 3 | 32-bit | onvif-simulator-rpi_<version>_linux_arm.tar.gz |
| Pi 3 / 4 / 5 (64) | 64-bit | onvif-simulator-rpi_<version>_linux_arm64.tar.gz |
Each archive contains a single onvif-simulator-rpi binary.
The install.sh one-liner above auto-detects Raspberry Pi via
/proc/device-tree/model and pulls the matching rpi archive — running it on a
Pi installs the rpi build as onvif-simulator with no extra flags. Set
ONVIF_SIMULATOR_CHANNEL=default (or =rpi) to override detection.
To install manually, download the matching archive from the
Releases page and
configure a profile with "kind": "rpicam":
See onvif-simulator.example.rpi.json for a worked example. The default build
channel (Linux/macOS/Windows × amd64/arm64) does not carry mtxrpicam and
rejects kind=rpicam profiles at startup with a clear error message.
Third-party software notice for the Raspberry Pi channel: see
NOTICE.
After install.sh has placed the binary at /usr/local/bin/onvif-simulator
and you have copied the rpi example config into place (see
Configuration), register the simulator as a system
service so it starts at boot and restarts on failure.
Create /etc/systemd/system/onvif-simulator.service:
[Unit]
Description=ONVIF Simulator
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=pi
ExecStart=/usr/local/bin/onvif-simulator serve
Restart=on-failure
RestartSec=3
[Install]
WantedBy=multi-user.targetThen enable and start it:
sudo systemctl daemon-reload
sudo systemctl enable --now onvif-simulator.service
systemctl status onvif-simulator.service
journalctl -u onvif-simulator.service -f # tail logsNotes:
- Replace
User=piwith the account that owns the config file. The user must be in thevideogroup forkind=rpicamprofiles to access the camera (the defaultpiaccount on Raspberry Pi OS already is). - The simulator reads
onvif-simulator.jsonfrom that user's XDG config directory (~/.config/onvif-simulator/onvif-simulator.jsonon Linux). To pin a different path, changeExecStartto/usr/local/bin/onvif-simulator serve -config /etc/onvif-simulator.json. - To apply config changes, edit the JSON and run
sudo systemctl restart onvif-simulator.service.
Run a single virtual device directly from the command line.
# Start a virtual device with default settings(cannot customize in CLI mode)
onvif-simulator serve
# List available options
onvif-simulator serve --helpInteractive terminal UI for managing:
- Device Service
- change device information
- Media Service
- change stream uri
- Event Service
- trigger motion detection
onvif-simulator tuiDownload and run the installer for your platform from the Releases page. The GUI provides a native window with a web-based interface for full graphical management of virtual devices.
The features are the same as the TUI mode.
onvif-simulator embeds its own RTSP server. For each profile, point media_file_path at a local H.264/H.265 mp4 file; the simulator loops it and serves an RTSP stream at rtsp://<host>:<rtsp_port>/<profile token>, which is what GetStreamUri returns.
The config file is named onvif-simulator.json and is auto-created on first run at the OS-standard user config directory:
| OS | Path |
|---|---|
| macOS | ~/Library/Application Support/onvif-simulator/onvif-simulator.json |
| Linux | $XDG_CONFIG_HOME/onvif-simulator/onvif-simulator.json (falls back to ~/.config/onvif-simulator/...) |
| Windows | %AppData%\onvif-simulator\onvif-simulator.json (typically C:\Users\<you>\AppData\Roaming\...) |
To override the path for a single run, pass -config /path/to/onvif-simulator.json to the CLI. As a fallback for ad-hoc use and tests, Load also accepts ./onvif-simulator.json in the working directory when no path has been set.
Two example files ship in every release:
onvif-simulator.example.json— default channel (file-backed profiles).onvif-simulator.example.rpi.json— Raspberry Pi channel (rpicam profile). Use this on a Pi.
To start from the bundled example, create the user config directory if it does not exist, then copy the example file to the path for your OS (see the table above) or into the working directory. Both examples must be renamed to onvif-simulator.json at the destination — that is the only filename the simulator reads.
# Linux: respects XDG_CONFIG_HOME when set, otherwise ~/.config
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/onvif-simulator"
mkdir -p "$CONFIG_DIR"
cp onvif-simulator.example.json "$CONFIG_DIR/onvif-simulator.json"
# macOS
mkdir -p "$HOME/Library/Application Support/onvif-simulator"
cp onvif-simulator.example.json "$HOME/Library/Application Support/onvif-simulator/onvif-simulator.json"
# Or keep it in the working directory for quick experiments (no extra directory needed)
cp onvif-simulator.example.json onvif-simulator.jsonRaspberry Pi — recommended on the rpi build channel. Substitute the rpi example file:
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/onvif-simulator"
mkdir -p "$CONFIG_DIR"
cp onvif-simulator.example.rpi.json "$CONFIG_DIR/onvif-simulator.json"Windows (PowerShell) — %AppData% expands to your roaming profile directory (see table above):
New-Item -ItemType Directory -Force -Path "$env:APPDATA\onvif-simulator" | Out-Null
Copy-Item onvif-simulator.example.json "$env:APPDATA\onvif-simulator\onvif-simulator.json"
# Or working directory:
Copy-Item onvif-simulator.example.json onvif-simulator.jsonMinimal required fields:
{
"version": 1,
"device": {
"uuid": "urn:uuid:00000000-0000-4000-8000-000000000001",
"manufacturer": "ONVIF Simulator",
"model": "SimCam-100",
"serial": "SN-0001"
},
"network": {
"http_port": 8080
},
"media": {
"profiles": [
{
"name": "main",
"token": "profile_main",
"media_file_path": "/absolute/path/to/main.mp4"
}
]
}
}(network.rtsp_port is optional; omit it to use the default 8554.)
Optional sections (all fields shown in onvif-simulator.example.json):
| Section | Purpose |
|---|---|
auth |
Enable HTTP Digest / WS-UsernameToken / JWT authentication and manage users. |
runtime |
Persist Device Management runtime state: discovery_mode, hostname, dns, default_gateway, network_protocols, system_date_and_time. Written by ONVIF Set* operations; editing manually sets the initial value. |
events |
Configure the Event Service: max_pull_points, subscription_timeout (Go duration, e.g. "1h"), and the topics list (name + enabled flag). |
Notes:
network.rtsp_portis optional and defaults to8554when omitted or set to0; it must differ fromhttp_port.media_file_pathmust be an absolute path to an mp4 with an H.264 or H.265 video track.- When
media_file_pathis set,encoding,width,height, andfpsare read from the file at startup (via probe) and replace the in-memory profile values for the running simulator;bitrateandgop_lengthare not derived from the file and still come from the config for ONVIF. Persisted JSON values for the probe-derived fields are only used as fallback display data when the simulator is stopped.
If you don't have a sample clip handy, generate one with ffmpeg:
ffmpeg -y -f lavfi -i testsrc=duration=10:size=1280x720:rate=30 \
-c:v libx264 -pix_fmt yuv420p sample.mp4v0.4.0 advertises ONVIF Profile S v1.3 device-side mandatory features. The compliance posture is verified at two levels:
The test/e2e package drives a running simulator with use-go/onvif and exercises every Profile S mandatory operation (§7.1 auth — §7.13 metadata), the WS-BaseNotification pull-point + push event handlers, MJPEG advertising (JPEG instance count + resolutions), and the embedded RTSP playback path.
# Terminal 1 — start the simulator pointed at an H.264/H.265 mp4 (see Configuration)
onvif-simulator serve
# Terminal 2 — run the SOAP-level suite. ONVIF_HOST/USERNAME/PASSWORD override defaults.
just e2eThe suite passes against the bundled onvif-simulator.example.json once media_file_path is set to an absolute path. CI runs it for every PR.
The ONVIF Device Test Tool is the canonical conformance suite. It is Windows-only and is not run automatically — execute it locally before tagging a Profile S–claiming release:
- Start the simulator on the test machine (or a reachable host) with
onvif-simulator serve. Make sure the configuredmedia_file_pathpoints at an H.264 (or H.265) mp4 — the JPEG output is synthesised on the fly from the same source. - In DTT, Add Device by IP, run Discover if WS-Discovery is reachable, and select the Profile S test pool.
- Capture the DTT log into
doc/conformance/dtt-v0.4.0.log(or attach to the release notes). Profile S–mandatory test cases must all returnPassed; investigate anyFailed/Skippedrow before a release.
Test machine specifics, expected runtime, and known limitations live in doc/conformance/README.md (created on first run).
Install mise and let it provision the required toolchain:
mise installThis installs Go 1.26.2, golangci-lint 2.11.4, and Node.js 24.15.0 (needed for GUI via Wails).
On Windows, just rpicam-fetch, just ffmpeg-fetch, and the cli-rpi-* recipes run the PowerShell ports under scripts/ (fetch-mtxrpicam.ps1, fetch-ffmpeg.ps1) via pwsh. Install PowerShell 7+ and make sure pwsh is on your PATH, then confirm with pwsh -Version.
For GUI development, also install the Wails CLI:
go install github.com/wailsapp/wails/v2/cmd/wails@v2.12.0This matches the version pinned in the release workflow.
The repo uses just as its task runner. Install it once via any of:
mise install # picks up the version pinned in mise.toml
brew install just # macOS / Linuxbrew
scoop install just # Windows (scoop)
cargo install just # any platform with RustThen:
git clone https://github.com/GyeongHoKim/onvif-simulator.git
cd onvif-simulator
go mod tidy
cp onvif-simulator.example.json onvif-simulator.json # fill in your RTSP URIs
just setup # install git hooks and commitlintThe legacy Makefile is a thin deprecation wrapper that simply forwards every target to the matching just recipe; new work should call just <recipe> directly. Run just --list to see everything available.
| Command | Description |
|---|---|
just setup |
Install git hooks and commitlint (run once after cloning) |
just cli |
Build the CLI/TUI binary |
just gui |
Build the GUI binary (requires Wails) |
just format |
Run golangci-lint fmt across all packages |
just lint |
Run golangci-lint |
just clean |
Remove build artifacts |
# CLI / TUI
go run . serve
go run .
# GUI (requires Wails)
wails devMIT
{ "name": "main", "token": "profile_main", "kind": "rpicam", "rpicam": { "camera_id": 0, "width": 1920, "height": 1080, "fps": 30, "bitrate": 4000000, "idr_period": 60 } }