Skip to content

onvif: Major overhaul of ONVIF support (discovery, reconnect, focus, imaging, presets fix) - #312

Open
jonata wants to merge 2 commits into
glikely:mainfrom
jonata:onvif-enhancements
Open

onvif: Major overhaul of ONVIF support (discovery, reconnect, focus, imaging, presets fix)#312
jonata wants to merge 2 commits into
glikely:mainfrom
jonata:onvif-enhancements

Conversation

@jonata

@jonata jonata commented May 12, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR makes the experimental ONVIF driver actually usable end-to-end against real-world cameras (notably budget Xiongmai/XM-chipset firmwares that are loose with the spec), and adds WS-Discovery as the primary entry path so users no longer have to type host/port by hand.

It's a single commit on purpose for ease of pulling, but I'm happy to split into smaller patches if that's preferred — the changes group cleanly along the section boundaries below.

Bug fixes in the existing driver

  • Integer truncation in absoluteMove() / relativeMove(). Both took int pan/tilt/zoom, silently casting the normalized doubles to 0 or 1. Every absolute/relative move was effectively (0, 0, 0).
  • Redundant Basic auth header dropped. sendRequest() unconditionally set an Authorization: Basic ... header even though the WS-UsernameToken inside the SOAP envelope already authenticates. Some firmwares rejected the duplicate as ambiguous.
  • Default port 889980 — the more common ONVIF default.
  • Empty-URL guard. sendRequest() no longer pushes a request with an empty URL (which can happen when an operation fires before GetCapabilities has populated the per-service XAddrs). Avoids spamming Protocol "" is unknown once per click.
  • Per-request transfer timeout (10s). Without this, failures hung on the OS TCP timeout (~2 min). 10s is generous for an ONVIF SOAP exchange.
  • Unescaped & in SOAP body text sanitized before parsing. Many cameras emit &protocol= / &channel= in stream URIs. Python's ElementTree tolerates it; Qt's QDomDocument rejects the whole document. We replace any & not already starting a known entity with &.
  • Preset save was broken. SetPresetResponse carries the camera-assigned PresetToken, but the old code threw it away — so memory_recall() later had no token and silently did nothing. Now we remember which slot triggered the SetPreset and store the returned token there. memory_reset() also clears the stale local token after RemovePreset.

Reliability improvements

  • Clock-skew handling. connectCamera() now begins with GetSystemDateAndTime (an unauthenticated call per spec), computes the offset between camera and host clocks, and adjusts every WS-Security Created timestamp by it. Cameras with bad NTP no longer reject our calls as out-of-window timestamps. If the time probe errors out, ensureCapabilitiesRequested() makes sure GetCapabilities still runs so the device doesn't stay un-initialized.
  • Stale XAddr host override. Some firmwares (Xiongmai/hsoap is the prolific offender) advertise their static/DHCP IP in WS-Discovery XAddrs and in GetCapabilities responses even when reached via a different address (NAT, multi-homed, IP changed since boot). We now override the host of any XAddr that doesn't match the responder/configured host — both in the WS-Discovery parse and in handleGetCapabilitiesResponse. This is what ODM and most other ONVIF clients do.
  • Connection status + auto-reconnect. PTZOnvif now calls setConnected() on every success/failure, so the dock's red/green indicator reflects reality. After 3 consecutive failures (~15s), the device is marked disconnected and the next status-timer tick restarts the full connect chain from GetSystemDateAndTime — so a camera reboot or a network blip recovers without user intervention.
  • 5s GetStatus polling keeps the cached pan/tilt/zoom position in sync. The same timer drives the reconnect retry when disconnected.

New features

WS-Discovery + selection dialog

Adding an ONVIF device now pops a dialog that:

  • Sends a SOAP-over-UDP Probe to 239.255.255.250:3702 from every suitable interface (multi-homed hosts find cameras on every LAN, not just the default-route one).
  • Lists discovered cameras with host / port / manufacturer / model parsed from Scopes.
  • Has a manual-add row for cameras the multicast probe can't reach (different subnet, multicast-blocked LAN, etc.).
  • Fetches per-profile RTSP stream URIs via GetCapabilitiesGetProfilesGetStreamUri, with the same clock-skew handling as the runtime driver and a clear SOAP-fault surface so auth/credential failures produce readable errors instead of "Failed to parse SOAP response".
  • Supports anonymous probing for cameras that don't require auth (empty password ⇒ skip the WS-Security header entirely).
  • Has a stream selector for which profile's URL to use when auto-creating a Media Source.
  • Optionally auto-creates an OBS Media Source from the picked stream URI, with the user's credentials embedded as Basic auth so it survives the per-session token expiry that many firmwares use.

Per-device settings

  • Media profile combo — pick which profile's ProfileToken PTZ commands target. Persists across reloads.
  • Speed multiplier slider (0.1–10.0) — for cameras that accept ONVIF velocities above the spec maximum.

Imaging service

  • Continuous focus moves + AutoFocusMode toggle (MANUAL/AUTO), wired through PTZDevice::focus_changed. Only fires on cameras that advertise an Imaging XAddr.
  • White balance mode combo (AUTO/MANUAL).

PTZ service

  • SetHomePosition via right-click on the dock's Home button. Gated by a new virtual supportsSetHome() (default false), so VISCA/Pelco/USB devices show no menu and their behavior is unchanged — only ONVIF overrides it to true.

Files

File Change
src/onvif-discovery.{cpp,hpp} New — OnvifDiscovery (WS-Discovery probe), OnvifMediaProbe (stream-URI fetcher), OnvifDiscoveryDialog (UI)
src/ptz-onvif.{cpp,hpp} All the fixes and the new features above
src/settings.cpp New Add-Device flow routes through the discovery dialog and optionally creates a Media Source
src/ptz-device.hpp New virtuals pantilt_set_home() and supportsSetHome() (default no-op/false)
src/ptz-controls.{cpp,hpp} Right-click handler on the dock's Home button
data/locale/en-GB.ini New strings for the discovery dialog, profile combo, white balance, speed boost, set-home action
CMakeLists.txt Adds the new discovery sources under ENABLE_ONVIF

Test plan

  • Manually tested against a Xiongmai (XM530_RF50X30_8M, hsoap/2.8, ONVIF 17.12) camera: WS-Discovery finds it, manual entry works for cross-subnet, RTSP URIs come back, Media Source auto-creates and plays, pan/tilt/zoom respond, presets save and recall, status dot turns green, auto-reconnect recovers after the camera reboots.
  • Tested against a from-scratch Python ONVIF simulator covering WS-Discovery, Device/Media/PTZ/Imaging services, presets, and Set/GotoHomePosition.
  • SetHomePosition: not all firmwares implement it (the spec lets cameras silently accept-but-not-store). When the camera supports it, the saved position is recalled by a subsequent single-click on Home.
  • No changes to VISCA / Pelco / USB code paths; their behavior is unchanged. Verified the new supportsSetHome() default keeps the new menu hidden for those protocols.

@glikely

glikely commented May 12, 2026

Copy link
Copy Markdown
Owner

Amazing! Thanks for working on this. Yes, please split the change into logical, bisectable commits. That will make the changes a lot easier to review. I'm also in the midst of some major rework to the device management, and it will be easy to reconcile with that work if the changes are in smaller pieces.

@jonata
jonata force-pushed the onvif-enhancements branch from 326d124 to 56a05e4 Compare May 12, 2026 13:49
@jonata

jonata commented May 12, 2026

Copy link
Copy Markdown
Contributor Author

Done — force-pushed the branch as 15 focused, bisectable commits, each with a Signed-off-by:

  1. onvif: Use double for absoluteMove and relativeMove arguments
  2. onvif: Drop redundant Basic Authorization header
  3. onvif: Default port to 80 instead of 8899
  4. onvif: Guard empty URLs and add 10s transfer timeout
  5. onvif: Sanitize unescaped ampersands in SOAP responses
  6. onvif: Capture camera-assigned PresetToken in SetPresetResponse
  7. onvif: Synchronize WS-Security timestamps with camera clock
  8. onvif: Override stale XAddr host in GetCapabilities response
  9. onvif: GetStatus polling + connection status + auto-reconnect
  10. onvif: Add media profile selection UI
  11. onvif: Per-device speed multiplier for non-spec-compliant cameras
  12. onvif: Add Imaging service for focus and white balance
  13. ptz-device, onvif, ptz-controls: SetHomePosition via right-click on Home
  14. onvif: Add WS-Discovery probe and selection dialog
  15. onvif: Auto-create OBS Media Source from discovered RTSP stream

Each commit builds on its own; the final tree is identical to what was in the previous squashed version. Happy to reorder, split further, or drop any of these to make merging easier alongside your device-management rework.

@glikely glikely left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great series, thank you! I've merged the first three commits as they were flawless. I've got a nitpick on where formatting changes to keep clang-format happy are applied, and there is a change to add the override keyword that is a coding fix that should be committed separately from the feature. Fix those things up and I'll merge the rest of the commits minus the last one.

I really appreciate the detailed commit messages. It made it a lot easier to review and understand the code.

The last commit I want to think about a bit more. I'm experimenting with a major overhaul on how PTZ instances are created by making them source filters, which would conflict with your change to auto-create the ONVIF source. You can leave that feature in the series, and I'll make a decision later on whether I merge it.

Comment thread src/ptz-onvif.cpp
Comment thread src/ptz-onvif.cpp
Comment thread src/ptz-onvif.hpp
@jonata
jonata force-pushed the onvif-enhancements branch from 56a05e4 to f2edecc Compare May 13, 2026 13:26
@jonata

jonata commented May 13, 2026

Copy link
Copy Markdown
Contributor Author

Pushed the requested changes:

  • Squashed the two formatting hunks back into their parent commits — the regex one-liner is back in onvif: Sanitize unescaped ampersands in SOAP responses and the obs_properties_add_list(...) wrap is back in onvif: Add media profile selection UI.
  • Split the override additions into their own commit (ptz: Annotate PTZ driver virtual overrides with the override keyword) and extended it to every driver as you suggested: PTZOnvif, PTZVisca, PTZViscaOverIP, PTZViscaOverTCP, PTZViscaSerial, PTZPelco, PTZUSBCam. The SetHomePosition commit is now just the new virtuals + right-click handler.

Branch is rebased on top of the new main (so the three already-merged commits are dropped). Sounds good on holding the last commit for the source-filter rework — happy to revisit once you've landed it.

@jonata

jonata commented May 13, 2026

Copy link
Copy Markdown
Contributor Author

About the last commit, sure. By now it's just adding a media source with the RTSP url, based on the ONVIF discovery.
Thanks!!

@jonata
jonata force-pushed the onvif-enhancements branch from f2edecc to b683c28 Compare May 13, 2026 13:37
@glikely

glikely commented May 16, 2026

Copy link
Copy Markdown
Owner

Tested against a from-scratch Python ONVIF simulator covering WS-Discovery, Device/Media/PTZ/Imaging services, presets, and Set/GotoHomePosition.

Have you published the simulator code? I'd like to make use of it.

@jonata

jonata commented May 17, 2026

Copy link
Copy Markdown
Contributor Author

It's a silly simulator, that would print PTZ values in a colorbar on rstp.
onvif-sim.zip

@glikely

glikely commented May 17, 2026

Copy link
Copy Markdown
Owner

That's pretty useful. Do you want to add it to the repo? Perhaps in /scripts. I've got a really poor VISCA simulator in there too. They can be friends

@glikely

glikely commented May 18, 2026

Copy link
Copy Markdown
Owner

Thanks for adding the script. I've pulled it into main.

I've left the last two patches adding the discovery features from mainline for the moment. I'm experimenting with making PTZDevices be source filters instead of being managed independently, which would mess with the discover code for ONVIF. If the filters approach works the way I hope it will, then I'll get you to look at the tree and see how the ONVIF discovery can be adapted for the new model. Give me a week or two to mess with it some more.

In the mean time, please take a look at the proc_handler api pull request. I would appreciate your review and testing to make sure I haven't broken anything. I'm attending a conference at the moment and away from my test hardware.

#310

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR significantly expands the experimental ONVIF support by adding WS-Discovery-based camera discovery + a selection dialog, plus wiring the new discovery flow into the “Add PTZ device” UI and build system.

Changes:

  • Add a new ONVIF discovery / media-probe implementation (OnvifDiscovery, OnvifMediaProbe) and a Qt selection dialog (OnvifDiscoveryDialog).
  • Update the settings “Add ONVIF” flow to use the discovery dialog and optionally auto-create an OBS Media Source from a probed RTSP URI.
  • Add supporting build / localization / tooling updates (CMake sources, locale strings, scripts unignored, ONVIF emulator script).

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/settings.cpp Routes “Add ONVIF” through the discovery dialog and optionally creates an OBS media source from the selected RTSP URI.
src/onvif-discovery.hpp Declares the WS-Discovery probe, media probe, and selection dialog APIs.
src/onvif-discovery.cpp Implements WS-Discovery probing, async SOAP media probing, and the camera selection dialog UI.
scripts/onvifemu.py Adds a minimal ONVIF emulator for manual end-to-end testing.
data/locale/en-GB.ini Adds new UI strings for the ONVIF discovery dialog and related settings.
CMakeLists.txt Adds the new ONVIF discovery sources under ENABLE_ONVIF.
.gitignore Un-ignores /scripts so the new emulator/tooling script is tracked.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/onvif-discovery.cpp
Comment thread src/onvif-discovery.cpp
Comment thread src/onvif-discovery.cpp
Comment thread src/onvif-discovery.cpp Outdated
Comment on lines +249 to +251
QUrl s(scope);
QString path = s.path();
QString value = QUrl::fromPercentEncoding(s.path().toUtf8());
Comment thread src/onvif-discovery.cpp Outdated
Comment on lines +851 to +857
lines << QString("Host: %1").arg(m_selected.host);
lines << QString("Port: %1").arg(m_selected.port);
if (!m_selected.manufacturer.isEmpty())
lines << QString("Manufacturer: %1").arg(m_selected.manufacturer);
if (!m_selected.model.isEmpty())
lines << QString("Model: %1").arg(m_selected.model);
if (!m_selected.location.isEmpty())
Comment thread src/onvif-discovery.cpp
@glikely

glikely commented Jun 2, 2026

Copy link
Copy Markdown
Owner

I've made progress on the plugin refactoring. Your remaining patches still applies and builds cleanly, but you'll probably want to check to make sure I haven't broken anything. Please take a look at the main branch.

I've also allowed Copilot to do a review. It's AI, so take the results with a grain of salt, but there are some valid comments in there.

I'm still looking at making PTZ devices filters, so that PTZ devices are always associated with a source, instead of something that is loosly associated afterwards. That change will impact both of your remaining patches. The autodiscovery patch will need to be reworked because there won't necessarily be an 'add' button in the settings dialog anymore. However, what you could do is add an autodetect button or a discovered cameras combo box to the ONVIF get_properties() function. That would give you the hook for opening the dialog to choose a discovered camera. That is a change that could be made now. However, I'm happy if you want to make that change in a follow up patch.

The auto-create OBS Media Source patch is more difficult. When PTZ devices are setup as filters then the OBS Source needs to be in place first, so you'll need to trigger the source creation somewhere else. It could be a button in the settings dialog I suppose. Needs some thought.

Regardless, please rebase onto latest mainline, make sure all is still working, and refresh this PR

jonata added 2 commits June 6, 2026 12:32
Adds a first-class entry path that replaces manual host/port typing.
Selecting "ONVIF (experimental)" from the Add Device menu now opens an
OnvifDiscoveryDialog that:

- Sends a SOAP-over-UDP Probe to the WS-Discovery multicast group
  (239.255.255.250:3702) from every up + multicast-capable IPv4
  interface, so multi-homed hosts find cameras on every LAN, not just
  the default-route one.

- Lists discovered cameras in a table with host/port/manufacturer/model
  parsed from the response's Scopes section.

- Picks the responder's source address when a camera's XAddrs all
  point at hosts we can't reach — a common Xiongmai/budget-cam quirk
  where the device advertises its DHCP-assigned IP even when actually
  reached via NAT or a different subnet. This mirrors what ODM does.

- Has a "Don't see your camera? Add manually" row so cameras the
  multicast probe can't reach (a different routed network, a
  multicast-blocked switch, etc.) can be added by hand.

- Fetches per-profile RTSP stream URIs via an OnvifMediaProbe helper
  (GetSystemDateAndTime for clock sync, GetCapabilities, GetProfiles,
  GetStreamUri-per-profile), surfacing SOAP faults so credential or
  configuration failures produce a readable error instead of "Failed
  to parse SOAP response". Empty password ⇒ probe without a
  WS-Security header at all, matching the way ODM treats anonymous
  cameras.

- Sanitizes unescaped `&` in SOAP responses the same way PTZOnvif
  does, so Xiongmai-style GetStreamUri URLs with `&channel=` /
  `&protocol=` don't blow up Qt's QDomDocument parser.

- Has a 10s transferTimeout on every probe request, so the dialog
  doesn't sit on the OS TCP timeout when a camera goes offline.

- Saves the offending response body to /tmp/onvif-probe-bad-response.xml
  when a SOAP parse fails, so the failure is debuggable from a single
  user report.

On accept, settings.cpp creates the PTZ device pre-filled with the
discovered host / port / credentials. Auto-creating an OBS Media
Source from the picked stream URI is intentionally deferred to a
follow-up patch — this commit is just the discovery + selection flow.

Signed-off-by: Jonatã Bolzan Loss <jonata@jonata.org>
Builds on the previous WS-Discovery dialog patch: once the user picks
a camera, also offer to drop an FFmpeg-based Media Source into the
current scene pointing at the camera's RTSP stream.

The dialog grows a "Also create an OBS Media Source for this camera's
stream" checkbox (default checked) and a stream picker that lists every
RTSP URI returned by GetStreamUri so the user can choose Main vs Sub
before clicking "Use Selected Camera".

On accept, settings.cpp:
- Takes the URI from the picker (falls back to the first usable URI),
- Embeds the dialog's username/password as Basic auth into the URL so
  the source still works after the per-session token in the camera's
  reply expires,
- Creates an "ffmpeg_source" with restart_on_activate and hw_decode,
- Adds it to the currently active scene.

If the user unchecks the box, only the PTZ device is created. If
GetStreamUri came back empty (anonymous probe was rejected, or the
camera has no streamable profile), the Media Source step silently
no-ops.

Signed-off-by: Jonatã Bolzan Loss <jonata@jonata.org>
@jonata
jonata force-pushed the onvif-enhancements branch from 0c750c0 to 2c49994 Compare June 6, 2026 15:32
@jonata

jonata commented Jun 6, 2026

Copy link
Copy Markdown
Contributor Author

Refreshed:

  • Rebased onto current main. The scripts: Add an ONVIF camera emulator commit was correctly dropped since you already merged it as f1d41ea (working trees match). The two remaining patches still apply and build cleanly against the new device-management refactor — confirmed locally.
  • Squashed Copilot-flagged fixes into onvif: Add WS-Discovery probe and selection dialog:
    • Cancel in-flight OnvifMediaProbe on Rescan and when the table selection is cleared, so a late reply can't populate the dialog for a stale camera.
    • Replaced the fixed /tmp/onvif-probe-bad-response.xml dump with a QTemporaryFile under QStandardPaths::TempLocation — no more world-readable / race-prone path, and the user-facing error now reports the actual filename used.
    • Removed dead value local in the WS-Discovery scope parser.
    • Wrapped the detail-pane labels (Host:, Port:, Manufacturer:, Model:, Location:, UUID:, Device service:, Types:, Scopes:) in obs_module_text(); reused PTZ.ONVIF.Discovery.Col.* for the first four and added 5 new PTZ.ONVIF.Discovery.Detail.* entries.
    • Dropped the if (user.isEmpty() && pass.isEmpty()) return; guard so anonymous cameras get probed. OnvifMediaProbe already omits the WS-Security header when the password is empty, so this is now consistent with the PR description.
  • Re: the autodetect-button-in-get_obs_properties() suggestion — happy to do that as a follow-up patch once the source-filter rework settles, as you offered.

clang-format and gersemi pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants