OConnector C++ library
v1.0.0
Table of contents
- Disclaimer
- Overview
- Versions
- Library files
- OConnector class description
- OConnector class declaration
- OConnector constructor
- OConnector destructor
- getVersion method
- declareVideoSource method
- declareVideoEncoder method
- declareMetadata method
- declareAnalytics method
- declareActuator method
- declarePtz method
- declareProfile method
- declareEvent method
- declareRelayOutput method
- declareDigitalInput method
- redeclareVideoSource method
- redeclareVideoEncoder method
- redeclarePtz method
- init method
- start method
- stop method
- isFactoryState method
- setAvailable method
- reportState method
- reflect method
- reflectPreset method
- removeReflectedPreset method
- notify method
- IMetadataController class description
- Data structures
- The address a client is told
- TLS
- Users, access classes and factory state
- Key encryption key
- Threading rules for host callbacks
- Metadata contract with the video service
- Build and connect to your project
- Example
- Portability
Disclaimer
ONVIF® is a registered trademark of ONVIF, Inc. The ONVIF name is used here only in a descriptive manner to indicate that this library is intended to implement aspects of the ONVIF® specifications for interoperability. This project is not affiliated with, endorsed by, or sponsored by ONVIF, Inc. or its members. No ONVIF® conformance or certification is claimed. Any conformance claim can only be made by a finished product that has successfully completed ONVIF’s official conformance process and received a valid Declaration of Conformance (DoC) from ONVIF. No claim is made to the exclusive right to use “ONVIF” apart from the technical reference shown in this library’s documentation and code.
Overview
OConnector is a C++17 library that implements the device side of ONVIF® for a host application. It sits between ONVIF® clients and the code of the host: the library owns the protocol, the transport and the cryptography, and hands every client request to the host as a typed callback for the part of the answer only the host knows. The library implements the ONVIF® Profile S, Profile T and Profile M specifications.
OConnector is general-purpose software: it is not tied to any sector or application, and the developer of a program decides what the program does with it.
The library owns its ports and connections, implements WS-Discovery, performs every cryptographic operation through OpenSSL (by way of the CryptoProvider library) and can push data to clients without a request. It is the control plane only: it returns stream addresses and never carries stream bytes, no detection or analytics data enters it, and it opens no outgoing connection - a push notification is composed by the library and carried by the host.
A host works in three steps. It declares everything the device can ever offer - video sources, video encoders, metadata streams, analytics engines, the mechanisms that move and the PTZ nodes over them, media profiles, event topics, relay outputs and digital inputs. It initialises the connector with a Config and its Handlers. It starts it. From then on the library serves the protocol on a thread of its own and calls back into the host. The set of declared entities is fixed once init has run; what changes at runtime is availability (setAvailable), the state the host reports (reportState, reflect) and the events it raises (notify). Declare the maximum a platform can ever have and mask what is not there; what a piece of hardware can never do is simply not declared. A declared capability a client can change - sensor modes, overlays, rotation, imaging controls - must be applicable through the hooks, because the declaration is what a client reads.
The library serves 183 ONVIF® operations across eight services. Authentication (HTTP Digest and WS-UsernameToken), the access classes, the user store, TLS, HTTP keep-alive and chunked bodies and the XML input limits apply to all of them. An operation the device does not serve is answered env:Receiver / ter:ActionNotSupported.
| Service | Prefix | Operations |
|---|---|---|
| Device Management | tds | 45 |
| Media2 | tr2 | 29 |
| Media ver10 | trt | 49 |
| Imaging | timg | 11 |
| PTZ | tptz | 20 |
| Event | tev, wsnt | 8 |
| Device IO | tmd | 7 |
| Analytics | tan | 14 |
Which operations a device registers follows from its declarations and its configuration, and is decided again by every init:
- the PTZ service exists only where a PTZ node was declared, and
GetServicesnames it only then; inside it the preset, home and auxiliary operations exist only where a node declares those families (PTZ §5.1.1, see declarePtz); GetServicesnames the Analytics service only wheredeclareAnalyticswas called;- the three imaging preset operations of Imaging §5.2 exist only where an
ImagingDeclcarries a preset; wsnt:Subscribeexists only whereConfig::maxNotificationProducersis above zero (Core §9.3.2);- the relay operations exist only where
declareRelayOutputwas called, and the digital input operations only wheredeclareDigitalInputwas called - their configuration pair only for an input that offers an idle state to configure.
Everything else is registered whatever was declared: it answers an empty list where nothing was declared, and ter:ActionNotSupported or a service-specific fault where a declaration an operation needs is missing.
Every name is declared in namespace rapidpixel. OConnector.h and IMetadataController.h end with a compatibility block that makes the names reachable as cr::onvif as well: qualified names, using-declarations, namespace aliases and deriving from cr::onvif::IMetadataController work. using namespace cr::onvif; makes every name of rapidpixel visible, the names of other RapidPixel libraries included, so prefer qualified names or using-declarations. Write a forward declaration, a definition, a specialisation or an argument-dependent hook (operator<<, to_json) for a library name in rapidpixel: in cr::onvif it is a second entity or does not compile, and a hook there is found only where cr::onvif is visible to ordinary lookup at the call - never by a JSON serialiser’s to_json - and where it is missed, a generic overload in scope is called without a diagnostic. OConnectorTypes.h included alone declares no cr::onvif.
Versions
Table 1 - Library versions.
| Version | Release date | What’s new |
|---|---|---|
| 0.1.0 | 17.08.2026 | - First library version. - Builds stand alone or inside a parent CMake project; the version in OConnectorVersion.h. |
| 0.2.0 | 17.08.2026 | - Public types: vocabulary types, declarations, Config, Handlers and IMetadataController. |
| 0.3.0 | 18.08.2026 | - Public interface separated from the implementation. - HTTP listener, SOAP envelope, ONVIF® faults and a hardened XML reader. - GetSystemDateAndTime and WS-Discovery Probe, Hello and Bye. |
| 0.4.0 | 19.08.2026 | - Standard crypto mode on a private OpenSSL context; TLS served on a second port.- HTTP chunked bodies, keep-alive and pipelining; HTTP Digest authentication. - Full XML reader and writer: CDATA, QName values, xs:any passed through verbatim. |
| 0.5.0 | 20.08.2026 | - WS-UsernameToken authentication with a replay cache; passwords sealed in a secret store. - Users with access classes checked for each operation, and a factory state. - Bounds against malformed requests and Probe floods; the crypto mode is fixed at first use. |
| 0.6.0 | 02.09.2026 | - Breaking: each hook takes a Caller first; LogSink is void(const Diagnostic&) with a stable DiagnosticCode.- 33 more Device Management operations: capabilities, clock, reboot, factory default, users, scopes, network, TLS. - declareActuator, declarePtz, Config::bindAddress, Config::authSchemes; Set checked against the schemas. |
| 0.7.0 | 06.09.2026 | - Breaking: getStreamUri/getSnapshot take StreamRequest/SnapshotRequest; Snapshot has URI lifetime fields.- 120 more operations (161 in all): Media2, ver10, Imaging, PTZ, events, Device IO; declareEvent, notify(); hooks validateVideoSource/applyVideoSource.- GetStreamUri waits for the host’s confirmation, at most Config::republishTimeout; Config::streamingTransports, authFreeAttempts, authBackoff. |
| 0.8.0 | 07.09.2026 | - What discovery announces: Config::announcedAddress, announcedPort, announcedTlsPort; Config::discoveryPort.- Handlers::multicastStreaming: StartMulticastStreaming and StopMulticastStreaming reach the host.- PtzSpacesDecl::extraSpaces, PtzDecl::defaultSpeed, defaultTimeout; tptz:SetConfiguration accepts its read-back. |
| 0.9.0 | 11.09.2026 | - Fixes to follow the ONVIF® specifications: PTZ compatible configurations, multicast groups, subscription references, metadata-only profiles. - tptz:SetConfiguration reaches the host: PtzConfig, validatePtzConfiguration and applyPtzConfiguration.- ImagingConfig::backlightCompensationLevel and wideDynamicRangeLevel. |
| 0.10.0 | 14.09.2026 | - Fixes to follow the ONVIF® specifications: network, PTZ status spaces, GetCapabilities categories, OSD options, Probe scopes.- NetworkAddress::origin; a host answering RebootNeeded=false makes the device announce itself again.- name on VideoSourceConfig, VideoEncoderConfig, MetadataConfig; discovery mode kept in Config::stateDirectory. |
| 0.11.0 | 14.09.2026 | - declareRelayOutput and declareDigitalInput: eight Device IO operations (169 in all), three hooks, trigger topics.- WS-Discovery rate limits: a global read budget and a per-network answer budget. - PullMessages may wait up to Config::maxTerminationTime plus one minute; a client’s POSIX time zone kept verbatim. |
| 0.11.1 | 15.09.2026 | - Fixes to follow the ONVIF® specifications: tr2:ConfigurationSet order, prefixes in a message content filter. |
| 0.12.0 | 16.09.2026 | - Profile M: declareAnalytics(AnalyticsDecl) and the Analytics service, fourteen operations (183 in all).- Optional hooks validateAnalytics, applyAnalytics and synchronizeMetadata.- GetStreamUri waits for the metadata stream as well; EventDecl::keyItems and Event::keyItems. |
| 0.12.1 | 16.09.2026 | - Fixes to follow the ONVIF® specifications: analytics topic prefixes, tan:SampleFrame in GetSupportedMetadata, empty module parameters. |
| 0.13.0 | 21.09.2026 | - setAvailable masks entities in media and PTZ; redeclareVideoSource, redeclareVideoEncoder and redeclarePtz.- OConnector::reflect and ConfigReflection: report a value the device changed by itself.- Host-owned presets: PtzDecl::presets, reflectPreset, removeReflectedPreset; Config::onUsersChanged. |
| 0.14.0 | 24.09.2026 | - Config::onScopesChanged: the configurable scopes as a client left them.- Handlers::ptzStorePreset: the host names the token of a preset a client stores.- Handlers::getTimeSettings: GetSystemDateAndTime answered from the time settings the host keeps. |
| 0.15.0 | 28.09.2026 | - Handlers::synchronizeVideoEncoder; without it SetSynchronizationPoint of a profile with an encoder is refused.- PTZ operations follow setAvailable; a kept DaylightSavings flag is reported only with a kept time zone. |
| 0.15.1 | 29.09.2026 | - A multicast group, set or declared, must be an address in 224.0.0.0/4 or ff00::/8, without prefix, port or zone. |
| 0.16.0 | 29.09.2026 | - ConfigReflection::osds: report the overlays a video source draws. |
| 0.17.0 | 30.09.2026 | - Cryptography through CryptoProvider, which the library needs to build. - init refuses TLS together with the FIPS mode; the crypto mode is held once per process. |
| 0.17.1 | 02.10.2026 | - Two presets of one node may share a name. - A subscription is refused only past twice the announced number. |
| 0.18.0 | 02.10.2026 | - init calls CryptoProvider::init(Config::cryptoMode) and relays its reason, CryptoProvider::lastError().- CryptoProvider 1.1.1 or later, checked at compile time. - Credentials compared in constant time; secrets erased after use. |
| 0.19.0 | 12.10.2026 | - Config::defaultTerminationTime (1 minute), pullPointCeiling (4) and notificationProducerCeiling (0).- Config::maxNtpServers and Config::zeroConfiguration, both off by default.- SystemLogging announced only where Handlers::getSystemLog is installed. |
| 0.20.0 | 12.10.2026 | - AnalyticsDecl::initialModules and initialRules: the analytics instances in force at start.- A fixed type may be seeded; no client operation adds or removes an instance of it. |
| 0.21.0 | 13.10.2026 | - WS-Discovery messages repeated as SOAP-over-UDP 1.1 says: Config::multicastRepeat and Config::unicastRepeat.- A Probe or Resolve refused by the answer budget is answered when it is sent again.- stop() waits for the repeated Bye copies, up to 750 ms with the defaults. |
| 0.22.0 | 15.10.2026 | - A subscription is addressed by <service address>/subscription/<identifier>, with no wsa:ReferenceParameters.- The generic GetVideoSourceConfigurationOptions lists every available video source. |
| 0.23.0 | 19.10.2026 | - CryptoProvider 1.3.0 or later, checked at compile time and built with -Werror by the top-level CMakeLists.txt.- Mode::Standard is refused when the crypto module came up as FIPS. |
| 0.23.1 | 25.10.2026 | - No sub-repositories: CryptoProvider is cloned next to the library or added by the parent project first; OpenSSL 3.0+. - Hardening: bounds on XML, SOAP and filter input, state files mode 0600, sockets close-on-exec. - Behaviour: stateDirectory must be writable; a failed Result<T>::value() aborts; IMetadataController constructors protected; bad time limits refused. |
| 1.0.0 | 29.10.2026 | - Breaking: names in namespace rapidpixel; cr::onvif kept as a compatibility namespace.- CryptoProvider 1.4.0 or later; Config::cryptoFips passed to it.- First public version. |
Library files
The library is supplied as source code only. The user is given a set of files in the form of a CMake project (repository). The repository structure is shown below:
CMakeLists.txt ------------------- Main CMake file of the library.
.gitignore ----------------------- Git ignore rules.
README.md ------------------------ This file: versions, interface, data structures, build.
src ------------------------------ Folder with library source code.
CMakeLists.txt --------------- CMake file of the library.
OConnector.h ----------------- Main library header file, the public interface.
OConnectorTypes.h ------------ Public vocabulary types and declarations.
IMetadataController.h -------- Metadata control interface of the video service.
OConnectorVersion.h ---------- Header file with library version.
OConnectorVersion.h.in ------- CMake service file to generate version header.
OConnector.cpp --------------- Forwarding implementation of the interface.
Impl ------------------------- Folder with the library implementation, not part of the API.
Diagnostic.h ------------- Escaping of received bytes in a log line; erasing buffers that hold secrets.
LockHierarchy.h ---------- Lock levels and the debug check of their order.
OConnectorImpl.h --------- Implementation class header file.
OConnectorImpl.cpp ------- Implementation class C++ file.
Auth --------------------- Folder with the authentication schemes.
Attempts.h ----------- Failed-credential accounting per source address.
Attempts.cpp --------- Attempt accounting, backoff and the lockout window.
CertificateStore.h --- TLS material of the device, on disk under the state directory.
CertificateStore.cpp - Certificate store implementation, 0600 files and an atomic write.
Digest.h ------------- HTTP digest challenges, nonces and verification.
Digest.cpp ----------- HTTP digest implementation, RFC 7616.
SecretStore.h -------- Encrypted store of the recoverable secrets; state file I/O.
SecretStore.cpp ------ Secret store implementation, AES-256-GCM at rest.
UsernameToken.h ------ WS-UsernameToken rules, replay cache and window.
UsernameToken.cpp ---- Username token implementation, Profile S.
UserStore.h ---------- Users, levels, roles and the access classes.
UserStore.cpp -------- User store implementation and factory state.
Generated ---------------- Folder with the code generated from the ONVIF® schemas.
DeviceTypes.h -------- Device Management types, generated from the ONVIF® schemas.
DeviceCodec.h -------- Parsers and serialisers of those types, generated.
DeviceCodec.cpp ------ Parser and serialiser implementation, generated.
Discovery ---------------- Folder with the WS-Discovery role of the device.
WsDiscovery.h -------- Probe and Resolve matching, message shapes, the UDP socket.
WsDiscovery.cpp ------ WS-Discovery Target Service implementation.
Dispatch ----------------- Folder with the operation routing of the library.
Dispatcher.h --------- Table of the operations the device serves.
Dispatcher.cpp ------- Operation routing implementation.
OperationTable.h ----- Services of the library and the diagnostic of a claimed profile.
OperationTable.cpp --- Operation table building, done by init.
TokenResolver.h ------ Entity tokens and the policy for one that names nothing.
TokenResolver.cpp ---- Token resolution implementation.
Events ------------------- Folder with the event service behind the ONVIF® operations.
Filter.h ------------- The topic filter and the message content filter, both grammars.
Filter.cpp ----------- Filter parsing, the ceilings and the matching of one event.
PropertyStore.h ------ Last value of every property event, replayed to new subscriptions.
PropertyStore.cpp ---- Property keying, the ceiling and the replay walk.
Subscriptions.h ------ Pull points, who owns one, the message queues and the expiry.
Subscriptions.cpp ---- Subscription identifiers, delivery and expiry.
TopicSet.h ----------- The topic set of GetEventProperties, built from the declarations.
TopicSet.cpp --------- Topic tree writing and the two location lists of the response.
Http --------------------- Folder with the HTTP front of the library.
Framing.h ------------ Request framing rules and the size limits.
Framing.cpp ---------- Request framing implementation.
Listener.h ----------- TCP listener and the event loop of the library.
Listener.cpp --------- Event loop implementation.
TlsChannel.h --------- One TLS connection, driven step by step by the loop.
TlsChannel.cpp ------- TLS connection implementation.
Model -------------------- Folder with the object model of the device.
CommandDebouncer.h --- Coalescing window of the outgoing configuration commands.
CommandDebouncer.cpp - Coalescing implementation and the merge of one burst.
ConfigStore.h -------- Configuration values in force, their generations and the readiness gate.
ConfigStore.cpp ------ Seeding from the declaration, confirmation and the readiness deadline.
EntityStore.h -------- Declared entities, their tokens, availability and resolution.
EntityStore.cpp ------ Entity store implementation and the per-node composition.
OptionsBuilder.h ----- Get*Options responses, generated from the declarations.
OptionsBuilder.cpp --- Options generation implementation, one writer per response.
PtzActuators.h ------- PTZ nodes, the mechanisms behind them and the axis rules.
PtzActuators.cpp ----- Axis masking and the split of one command per actuator.
StateTracker.h ------- Generations, what the hardware confirmed and the deadlines.
StateTracker.cpp ----- Confirmation, discarding a stale one and the deadline sweep.
TokenGenerator.h ----- Issuer of the tt:ReferenceToken of one uniqueness scope.
TokenGenerator.cpp --- Token issuing implementation and the stableId rules.
Validator.h ---------- Incoming Set values against the declarations that announced them.
Validator.cpp -------- Validation implementation, adaptation and the apply classes.
Services ----------------- Folder with the ONVIF® operations themselves.
CertOps.h ------------ Loading, listing and deleting the TLS material.
CertOps.cpp ---------- Certificate checks and responses.
DeviceInfo.h --------- Device identity, capabilities and the services answer.
DeviceInfo.cpp ------- Information and capability implementation.
DeviceIoOps.h -------- Device IO operations and their Device Management twins.
DeviceIoOps.cpp ------ The listings, the relay and input operations and the capability.
EventOps.h ----------- The event service, its capabilities and the event properties.
EventOps.cpp --------- Event service capabilities and event properties.
ImagingOps.h --------- The Imaging operations, addressed by video source and not by profile.
ImagingOps.cpp ------- Imaging settings, options, focus moves and the focus status.
Media2Ops.h ---------- The Media2 operations, the addresses and the service capabilities.
Media2Ops.cpp -------- Profiles, configurations, addresses, multicast, OSDs and sensor modes.
MediaOps.h ----------- The Media ver10 operations of Profile S, over the same store.
MediaOps.cpp --------- Profiles, configurations, addresses, MJPEG options, OSDs and modes.
AnalyticsOps.h ------- The fourteen operations of the Analytics service, ver20.
AnalyticsOps.cpp ----- Their bodies: the types, the instances, the options and the samples.
AnalyticsRegistry.h -- What the Analytics service reaches the declared engines through.
AnalyticsView.h ------ The analytics vocabulary the Analytics and media services share.
MediaRegistry.h ------ What the media services reach the profiles and configurations through.
NetworkOps.h --------- Host name, DNS, NTP, interfaces, protocols and the address filter.
NetworkOps.cpp ------- Network operation checks and responses.
PtzOps.h ------------- The PTZ operations, announced only where a node was declared.
PtzOps.cpp ----------- Nodes, configurations, the three moves, Stop and the status.
ScopeOps.h ----------- The scopes of the device and the discovery mode.
ScopeOps.cpp --------- Scope operation checks and responses.
ServiceSupport.h ----- What every service does at both ends of an operation.
SystemOps.h ---------- Clock setting, reboot, factory default and the log.
SystemOps.cpp -------- System operation implementation and the POSIX zone check.
SystemTime.h --------- Device clock and the GetSystemDateAndTime answer.
SystemTime.cpp ------- Device clock implementation.
UserOps.h ------------ The users of the device and the digest hashing algorithms.
UserOps.cpp ---------- User operation checks and responses.
Soap --------------------- Folder with the SOAP layer of the library.
Addressing.h --------- WS-Addressing headers.
Addressing.cpp ------- Addressing reader and writer implementation.
Envelope.h ----------- Request envelope rules and the response scaffolding.
Envelope.cpp --------- Envelope reader implementation.
Fault.h -------------- ONVIF® fault vocabulary and the fault writer.
Fault.cpp ------------ Fault mapping and serialisation.
Namespaces.h --------- The namespaces and prefixes a device message is written in.
Xml ---------------------- Folder with the XML front of the library.
Codec.h -------------- Lexical layer the generated codec reads values through.
Codec.cpp ------------ Lexical conversions and element text reading.
PullParser.h --------- XML pull reader and its input limits.
PullParser.cpp ------- XML pull reader implementation.
Writer.h ------------- XML writer.
Writer.cpp ----------- XML writer implementation.
internal --------------------- Folder with internal seams, not part of the API.
TestSeam.h --------------- Socket-free entry point for the library's own tests, off by default.
A host includes OConnector.h (which includes OConnectorTypes.h and IMetadataController.h) and links the OConnector target. Only the src folder is an include path of the target, and a host needs nothing under src/Impl or src/internal: the headers of src and the built archive are all it needs. OConnectorVersion.h is written by the configure step from OConnectorVersion.h.in.
OConnector class description
OConnector class declaration
The OConnector class is declared in the OConnector.h file. Class declaration:
class OConnector
{
public:
/// Class constructor. Opens nothing.
OConnector();
/// Class constructor installing the diagnostics sink.
explicit OConnector(LogSink log);
/// Class destructor. Stops a running service.
~OConnector();
/// The connector is neither copyable nor movable.
OConnector(const OConnector&) = delete;
OConnector& operator=(const OConnector&) = delete;
OConnector(OConnector&&) = delete;
OConnector& operator=(OConnector&&) = delete;
/// Get string of the current library version.
static std::string getVersion();
/// Declare a video source.
EntityId declareVideoSource(const VideoSourceDecl& decl);
/// Declare a video encoder.
EntityId declareVideoEncoder(const VideoEncoderDecl& decl);
/// Declare a metadata stream.
EntityId declareMetadata(const MetadataDecl& decl);
/// Declare an analytics engine.
EntityId declareAnalytics(const AnalyticsDecl& decl);
/// Declare a mechanism that physically moves.
EntityId declareActuator(const ActuatorDecl& decl);
/// Declare one PTZ node.
EntityId declarePtz(const PtzDecl& decl);
/// Declare the initial composition of a media profile.
EntityId declareProfile(const ProfileDecl& decl);
/// Declare one event topic.
EntityId declareEvent(const EventDecl& decl);
/// Declare one relay output.
EntityId declareRelayOutput(const RelayOutputDecl& decl);
/// Declare one digital input.
EntityId declareDigitalInput(const DigitalInputDecl& decl);
/// Replace the declaration of a video source nothing is offering.
bool redeclareVideoSource(EntityId id, const VideoSourceDecl& decl);
/// Replace the declaration of a video encoder nothing is offering.
bool redeclareVideoEncoder(EntityId id, const VideoEncoderDecl& decl);
/// Replace the declaration of a PTZ node nothing is offering.
bool redeclarePtz(EntityId id, const PtzDecl& decl);
/// Initialise the service.
bool init(const Config& config, Handlers handlers,
IMetadataController* metadata = nullptr);
/// Start serving: open the ports and announce the device.
bool start();
/// Stop serving: announce the departure and close the ports.
void stop();
/// Report whether no user is configured.
bool isFactoryState() const;
/// Set the availability of a declared entity.
void setAvailable(EntityId id, bool available,
std::string_view reason = {});
/// Report hardware state after an application.
void reportState(EntityId id, const StateUpdate& state, Generation gen);
/// Report a value the device changed by itself.
void reflect(EntityId id, const ConfigReflection& values);
/// Add or replace a preset the host stores.
bool reflectPreset(EntityId node, const PtzPresetDecl& preset);
/// Remove a preset the host no longer stores.
bool removeReflectedPreset(EntityId node, const std::string& token);
/// Raise an event without a request.
bool notify(const Event& event);
private:
/// Pointer to the connector implementation.
OConnectorImpl* m_impl{nullptr};
};
Lifecycle: declare every entity the device will ever have, then init, then start; stop ends the serving, and init and start may follow it again. Which method may be called from which thread is stated once, under Threading rules for host callbacks.
OConnector constructor
The constructors open nothing: the ports and the loop thread belong to start. Constructors declaration:
OConnector();
explicit OConnector(LogSink log);
| Parameter | Description |
|---|---|
| log | Diagnostics sink, a std::function<void(const Diagnostic&)> (see Diagnostic struct). Copied. Empty behaves like the default constructor. |
Install the sink here to have declarations refused before init reported; a connector constructed without one reports them nowhere. Config::log then serves everything from init onwards, and an empty Config::log leaves this sink in place. The copy and move constructors and assignment operators are deleted.
OConnector destructor
Destructor declaration:
~OConnector();
The destructor stops a running service as stop does and blocks the same way; a connector that is not running is destroyed at once. It then overwrites the passwords held in memory and frees the TLS context with its key. It deletes no file at rest (see Key encryption key). Where a connector may be destroyed from is stated under Threading rules for host callbacks.
getVersion method
The getVersion() method returns a string of the current version of the library, in the format “Major.Minor.Patch” (the OCONNECTOR_VERSION macro of OConnectorVersion.h). It never fails. Method declaration:
static std::string getVersion();
This method can be used without an OConnector class instance, from any thread at any time:
std::cout << "OConnector class version: " << OConnector::getVersion() << std::endl;
Console output:
OConnector class version: 1.0.0
declareVideoSource method
The declareVideoSource(…) method declares one video source: a sensor with its frame geometry, rotation, sensor modes, imaging and OSD surface and encoder instance limits. Method declaration:
EntityId declareVideoSource(const VideoSourceDecl& decl);
| Parameter | Description |
|---|---|
| decl | Capabilities of the source, see Declaration structures. Copied; it need not outlive the call. |
The rules every declare method shares:
- A declare method is accepted only before the first
initthat succeeded. Afterwards - a stopped connector included - it is refused asDiagnosticCode::DeclarationRefused, because the set of declared entities is fixed from there on. redeclareVideoSource, redeclareVideoEncoder and redeclarePtz replace the declaration of one entity in place. - A declaration that breaks a rule of its structure is refused, with the reason on the sink installed by the constructor.
- A declaration that stands but could not be taken exactly as written, such as a
stableIdthat cannot be used, is reported asDiagnosticCode::DeclarationAdapted. - The connector serialises concurrent declarations; their order decides the generated tokens.
The encoder instances of all video sources together may not exceed 24 (EncoderInstanceLimitsDecl::total). A source that declares selectable sensor modes, overlays or rotation while either Handlers::validateVideoSource or Handlers::applyVideoSource is empty is reported at init as DiagnosticCode::VideoSourceControlMissing: those operations answer ter:ActionNotSupported.
Returns: entity id of the source, or OCONNECTOR_INVALID_ENTITY_ID if the declaration was refused.
declareVideoEncoder method
The declareVideoEncoder(…) method declares one video encoder over a declared video source. The main and the sub stream of one sensor are two encoders. Method declaration:
EntityId declareVideoEncoder(const VideoEncoderDecl& decl);
| Parameter | Description |
|---|---|
| decl | Capabilities of the encoder: its video source (VideoEncoderDecl::source, declared already), its codecs, bitrate modes and multicast group. A multicast group, if any, must be an address in 224.0.0.0/4 or ff00::/8 (MulticastDecl::address), without prefix, port or zone. Copied. |
The rules of declareVideoSource apply.
Returns: entity id of the encoder, or OCONNECTOR_INVALID_ENTITY_ID if the declaration was refused.
declareMetadata method
The declareMetadata(…) method declares one metadata stream: the filters, compressions and multicast group a tt:MetadataConfiguration may carry (see Metadata contract with the video service). Method declaration:
EntityId declareMetadata(const MetadataDecl& decl);
| Parameter | Description |
|---|---|
| decl | Capabilities of the stream. A multicast group, if any, must be a multicast address, as for an encoder. Copied. |
The rules of declareVideoSource apply. A metadata stream names no video source, so it does not follow a camera that is masked: the host masks it itself (see setAvailable).
Returns: entity id of the stream, or OCONNECTOR_INVALID_ENTITY_ID if the declaration was refused.
declareAnalytics method
The declareAnalytics(…) method declares one analytics engine and its VideoAnalyticsConfiguration (Profile M). Method declaration:
EntityId declareAnalytics(const AnalyticsDecl& decl);
| Parameter | Description |
|---|---|
| decl | The engine: the module and rule types a client may instantiate (AnalyticsDecl::modules, AnalyticsDecl::rules), their parameters and limits, and the instances in force at start (AnalyticsDecl::initialModules, AnalyticsDecl::initialRules). Copied. |
What is declared is what a client may ask for; no detection enters the library. Declaring one is what makes GetServices name the Analytics service; without it a Profile M claim is reported as incomplete. The ParentTopic of each message a type describes joins the topic set.
The instances in force at start are empty by default, so a client creates every one. A seed is judged as a client’s Create is, except that a fixed type may be seeded - the only way an instance of a fixed type exists, since no client operation adds or removes one - and the XML of each element item has to read in full. The seed is not persisted: a new connector starts from it again, while stop() and start() keep the set in force. An analytics engine names no video source, so the host masks it itself when its camera goes.
Returns: entity id of the engine, or OCONNECTOR_INVALID_ENTITY_ID if the declaration was refused.
declareActuator method
The declareActuator(…) method declares one mechanism that physically moves: a pan/tilt platform or a zoom lens. Method declaration:
EntityId declareActuator(const ActuatorDecl& decl);
| Parameter | Description |
|---|---|
| decl | The mechanism. ActuatorDecl::name is for the diagnostics of the host and never for a client; an empty one is refused. ActuatorDecl::reports declares what the state record of the mechanism can carry. |
An actuator is not an ONVIF® entity: it carries no token and no client can name it. It is declared so that a PTZ node can reference it and setAvailable can address it, and one actuator may serve several nodes: four cameras on one pan/tilt platform, each with its own zoom, are four declarePtz calls naming one shared panTilt and four different zoom mechanisms, and a pan/tilt command from any of the four is one Handlers::ptzMove call on the platform. The library does not serialise or arbitrate those commands; the Caller of every call says who asked.
The flags of PtzStatusDecl in ActuatorDecl::reports are read exactly as declared, and nothing is declared by default: a platform with no position feedback declares panTiltStatusSupported and leaves panTiltPositionSupported false, and a position reported against it is then dropped with a diagnostic, so GetStatus omits the field.
Returns: entity id of the actuator, or OCONNECTOR_INVALID_ENTITY_ID if the declaration was refused.
declarePtz method
The declarePtz(…) method declares one ONVIF® PTZ node over a video source and the actuators that move it. Call it once per node. Method declaration:
EntityId declarePtz(const PtzDecl& decl);
| Parameter | Description |
|---|---|
| decl | The node: its video source (PtzDecl::videoSource), its pan/tilt and zoom actuators (PtzDecl::panTilt, PtzDecl::zoom), its spaces, its timeout range, its defaults, and its preset, home and auxiliary families. Copied. |
An axis with no actuator does not exist for a client: leave panTilt unset and the node advertises no pan/tilt space and refuses every pan/tilt command; leave zoom unset and the same holds for zoom. A fixed camera with a motorised lens is a node with zoom and no panTilt. One camera on a pan/tilt platform it shares with other cameras (see declareActuator), with a zoom of its own:
// The connector and the camera of the Example, before init.
OConnector connector([](const Diagnostic& diagnostic) { std::cout << diagnostic.message << std::endl; });
VideoSourceDecl camera;
camera.nativeResolution = Resolution{1920, 1080};
camera.maxFrameRate = 30.0F;
camera.boundsRange.widthRange = Range<int>{1920, 1920};
camera.boundsRange.heightRange = Range<int>{1080, 1080};
const EntityId sourceId = connector.declareVideoSource(camera);
ActuatorDecl platform;
platform.name = "pan and tilt of the mast"; // For the diagnostics of the host, never for a client.
platform.reports.panTiltPositionSupported = true;
platform.reports.panTiltStatusSupported = true; // What its state record may carry, and nothing else.
ActuatorDecl lens;
lens.name = "zoom of camera 1"; // Its own mechanism, one per camera.
lens.reports.zoomPositionSupported = true;
lens.reports.zoomStatusSupported = true;
const EntityId platformId = connector.declareActuator(platform); // Shared with every camera on the mast.
const EntityId lensId = connector.declareActuator(lens);
if (sourceId == OCONNECTOR_INVALID_ENTITY_ID || platformId == OCONNECTOR_INVALID_ENTITY_ID ||
lensId == OCONNECTOR_INVALID_ENTITY_ID)
{
return false; // Refused; the reason has already reached the sink.
}
PtzDecl ptz;
ptz.videoSource = sourceId; // Which camera this node belongs to.
ptz.panTilt = platformId;
ptz.zoom = lensId;
// The space URIs are the ones the PTZ service specification defines; the library holds no
// constants for them, so a host writes the string it means.
ptz.spaces.continuousPanTilt = PtzSpace2DDecl{
"http://www.onvif.org/ver10/tptz/PanTiltSpaces/VelocityGenericSpace", {-1.0F, 1.0F}, {-1.0F, 1.0F}};
ptz.spaces.continuousZoom =
PtzSpace1DDecl{"http://www.onvif.org/ver10/tptz/ZoomSpaces/VelocityGenericSpace", {-1.0F, 1.0F}};
const EntityId nodeId = connector.declarePtz(ptz);
if (nodeId == OCONNECTOR_INVALID_ENTITY_ID)
{
return false;
}
ProfileDecl profile;
profile.name = "camera 1";
profile.videoSource = sourceId;
profile.ptz = nodeId; // The NODE id, never a video source id.
if (connector.declareProfile(profile) == OCONNECTOR_INVALID_ENTITY_ID)
{
return false;
}
Spaces. The eight optional members of PtzSpacesDecl are the default space of each kind of move: what an omitted @space stands for and what tt:PTZConfiguration names. A node that moves absolutely has to declare the generic position space of each axis it moves that way - PanTiltSpaces/PositionGenericSpace (PTZ §5.7.1.1) and ZoomSpaces/PositionGenericSpace (PTZ §5.7.1.3) - as the default or as an extra; declarePtz does not check it. Every entry of PtzSpacesDecl::extraSpaces is one more space of the kind it names - Profile T §7.21, for one, expects both PositionGenericSpace and SphericalPositionSpaceDegrees for an absolute pan/tilt move:
PtzDecl ptz; // A node, filled in before declarePtz is called.
// The default: what an omitted @space stands for, and what tt:PTZConfiguration names.
ptz.spaces.absolutePanTilt = PtzSpace2DDecl{
"http://www.onvif.org/ver10/tptz/PanTiltSpaces/PositionGenericSpace", {-1.0F, 1.0F}, {-1.0F, 1.0F}};
// One more, beside it. Profile T §7.21 asks for this one as well.
PtzExtraSpaceDecl spherical;
spherical.kind = PtzSpaceKind::AbsolutePanTilt;
spherical.uri = "http://www.onvif.org/ver10/tptz/PanTiltSpaces/SphericalPositionSpaceDegrees";
spherical.xRange = Range<float>{-180.0F, 180.0F};
spherical.yRange = Range<float>{-90.0F, 90.0F}; // Required on a pan/tilt kind, refused on a 1D one.
ptz.spaces.extraSpaces.push_back(spherical);
GetNode, GetNodes and GetConfigurationOptions then list both, the default first; the Default*Space elements of GetConfiguration name the default alone. A move that writes an extra’s URI in @space is judged against that space and reaches Handlers::ptzMove carrying it; a move that writes none is judged against the default; a URI the node declares for neither, or for another kind, is ter:SpaceNotSupported. A Speed is judged the same way, with the fault of the operation: AbsoluteMove, RelativeMove, ContinuousMove and GotoPreset answer a Speed in a space the node does not declare with ter:SpaceNotSupported and a value out of range with ter:InvalidSpeed, while GotoHomePosition answers ter:InvalidSpeed for both. An axis with no speed space at all is ter:InvalidSpeed on all five. Refused at the declaration: an extra of a kind whose default member is unset, a URI declared twice, and an extra whose axes do not match its kind (yRange is required on the pan/tilt kinds and refused on the others).
Defaults. PTZ §5.2.1 obliges a node that moves absolutely or relatively to state default speeds, and a node that moves continuously to state a default timeout. PtzDecl::defaultSpeed is a PtzVector whose panTiltSpace and zoomSpace name the speed space of each axis (empty: the default speed space, written as no @space), and PtzDecl::defaultTimeout is a Duration:
PtzDecl ptz; // A node, filled in before declarePtz is called.
PtzVector defaultSpeed;
defaultSpeed.pan = 0.5F;
defaultSpeed.tilt = 0.5F; // tt:Vector2D needs both; half of one is refused.
defaultSpeed.panTiltSpace = "http://www.onvif.org/ver10/tptz/PanTiltSpaces/GenericSpeedSpace";
ptz.defaultSpeed = defaultSpeed; // tt:PTZConfiguration/DefaultPTZSpeed.
ptz.timeoutRange = Range<Duration>{std::chrono::seconds(1), std::chrono::seconds(60)};
ptz.defaultTimeout = std::chrono::seconds(5); // Inside that window, or the declaration is refused.
Refused at the declaration: a speed space the node declares for no speed axis, a component outside the range of its space, a pan without its tilt, a speed with no component, and a timeout outside PtzDecl::timeoutRange. A node that moves and leaves a default unset is served and reported at init as DiagnosticCode::PtzDefaultsMissing, one line per node; the speed is checked per axis, so a node that pans and zooms with a speed for pan and tilt alone is reported too.
The PTZ configuration. tptz:GetConfigurations, tptz:GetConfiguration, tptz:GetCompatibleConfigurations, the tt:PTZConfiguration of trt:GetProfiles/trt:GetProfile and the tr2:PTZ of tr2:GetProfiles write the same configuration. tptz:SetConfiguration accepts that document back: it compares every element the device answers with the declaration and refuses only one that differs. An element the device answers nothing for (PanTiltLimits, ZoomLimits, a ramp attribute) is ter:ConfigModify on presence alone. A client’s change reaches Handlers::validatePtzConfiguration and Handlers::applyPtzConfiguration (see Handlers struct).
Presets, home position and auxiliary commands. PtzDecl::maximumPresets, PtzDecl::homeSupported and PtzDecl::auxiliaryCommands decide whether the operations exist at all (PTZ §5.1.1). A node left at the defaults offers none of SetPreset, GetPresets, GotoPreset, RemovePreset, GotoHomePosition, SetHomePosition and SendAuxiliaryCommand; a node with a preset ceiling offers all four preset operations, and the ceiling announced as MaximumNumberOfPresets is the one that refuses a further SetPreset with ter:TooManyPresets. A SendAuxiliaryCommand naming a command the node does not list is refused, never forwarded. The home position is a preset with one unnamed slot (PTZ §5.5): PresetAction::SetHome and PresetAction::GotoHome reach Handlers::ptzPreset with PresetOp::token empty, and the host keeps the position. A node that declares PtzDecl::fixedHomePosition is answered env:Receiver / ter:Action / ter:CannotOverwriteHome (HTTP 500) for SetHomePosition without the host being asked. PtzDecl::presets seeds the preset list (see reflectPreset).
Movement and status. AbsoluteMove, RelativeMove, ContinuousMove and Stop are split over the mechanisms behind the node. GetStatus answers from what reportState recorded for those mechanisms, or from Handlers::ptzGetStatus where the host installs it, whose answer goes on the wire as it is. A composed position is expressed in the default absolute spaces of the node asked about (PTZ §5.3.7): the library maps each axis linearly from the range of the space the sample names to the range of the node’s default space, both as that node declares them, so two nodes over one mechanism each read the sample in their own space. A node that declares no absolute space of the axis, and a sample with an empty @space (read as the space in force), travel unconverted; a sample in a space the node does not declare for that axis, or a pair of ranges that are not finite intervals, travel unconverted and are reported at Warning as DiagnosticCode::PtzStatusSpaceNotConverted, once per read.
The declaration is also refused when the source is not a declared video source, when it carries a node already, when an actuator was not declared, when a space is declared for an axis that has no actuator, when an actuator is bound with no space to move in, and after init.
Returns: entity id of the node - the value ProfileDecl::ptz takes - or OCONNECTOR_INVALID_ENTITY_ID if the declaration was refused.
declareProfile method
The declareProfile(…) method declares the initial composition of a media profile. Method declaration:
EntityId declareProfile(const ProfileDecl& decl);
| Parameter | Description |
|---|---|
| decl | Initial composition. Every id it names has to be a declared entity of the kind of its member: ProfileDecl::ptz takes the id declarePtz returned, never a video source id. Copied. |
Profiles are owned by the library afterwards: CreateProfile, DeleteProfile and AddConfiguration are served by the library, which asks the host only Handlers::checkConflicts, for an operation that changes what a profile references. Config::maxProfiles bounds how many profiles a client can create. A profile with neither a video source nor a video encoder is a metadata stream of its own.
Returns: entity id of the profile, or OCONNECTOR_INVALID_ENTITY_ID if the declaration was refused.
declareEvent method
The declareEvent(…) method declares one event topic the device can raise. Method declaration:
EntityId declareEvent(const EventDecl& decl);
| Parameter | Description |
|---|---|
| decl | The event: its topic, whether it is a property, the tt:SimpleItemDescription items of its Source, Key and Data lists, and, for a vendor topic, its namespace and the namespace and schema locations. Copied. |
The declarations are the whole topic set: beside the topics declareEvent declares there are only tns1:Media/ProfileChanged and tns1:Media/ConfigurationChanged, which init declares, the two Device IO topics of declareRelayOutput and declareDigitalInput, and the ParentTopic of each message an analytics type describes. The set does not grow at runtime, so tev:GetEventPropertiesResponse/FixedTopicSet is TRUE. An event is not an ONVIF® entity: a client names it by its topic, and setAvailable has nothing to do with it.
EventDecl::property makes the message a property (tt:MessageDescription/@IsProperty), which a new subscription is told again (see notify). EventDecl::sourceItems, EventDecl::keyItems and EventDecl::dataItems describe the three item lists; an empty list writes no element. EventDecl::namespaceLocation and EventDecl::schemaLocation are the locations Core §9.7 asks a device to report beside the topic set.
A topic of the host’s own tree is served when EventDecl::topicNamespace binds its prefix (Core §9.6.1); every document that carries the topic then binds that prefix. Refused at the declaration: a prefix standing for nothing, a prefix or namespace the library’s own documents use, tns1 bound to anything but the ONVIF® topic namespace, two namespaces under one prefix, and two prefixes over one namespace.
The declaration is also refused for an empty topic, a topic with no namespace prefix, a topic declared twice, an item description missing its name or its type, and after init.
Returns: entity id of the declaration, or OCONNECTOR_INVALID_ENTITY_ID if it was refused.
declareRelayOutput method
The declareRelayOutput(…) method declares one relay output of the device (Device IO §5.6, Core §8.6). Method declaration:
EntityId declareRelayOutput(const RelayOutputDecl& decl);
| Parameter | Description |
|---|---|
| decl | The relay: what it can be set to (RelayOutputDecl::modes, the delay times as a range or as a discrete set), what it is set to now (the mode, the delay time and the idle state) and its stable token. Copied. |
Declaring a relay also declares the topic tns1:Device/Trigger/Relay (Device IO §5.10.2), once however many relays there are. No response of any service carries the state of a relay, so the host reports it with reportState and StateUpdate::logicalState (true meaning active), and the library raises the property out of it - which is also how a monostable relay reports its return to idle. A client triggers the relay through Handlers::setRelayOutputState and configures it through Handlers::setRelayOutputSettings.
The declaration is refused when no mode is offered, when the mode in force is not one of them, when Monostable is offered with no delay times, when the delay time in force is outside them, when the delay times are read as a range and are not exactly two values, and after init.
Returns: entity id of the relay, or OCONNECTOR_INVALID_ENTITY_ID if it was refused.
declareDigitalInput method
The declareDigitalInput(…) method declares one digital input of the device (Device IO §5.7). Method declaration:
EntityId declareDigitalInput(const DigitalInputDecl& decl);
| Parameter | Description |
|---|---|
| decl | The input: the idle states it offers (DigitalInputDecl::idleStates), the one in force (DigitalInputDecl::idleState) and its stable token. Copied. An empty list of idle states is an input that cannot be inverted, and leaves the configuration operations of Device IO §5.7.2 and §5.7.3 unregistered. |
Declaring an input also declares the topic tns1:Device/Trigger/DigitalInput (Device IO §5.10.1), on the terms of declareRelayOutput: the host reports the state with StateUpdate::logicalState and the library raises the property. A client configures the idle states through Handlers::setDigitalInputConfigurations.
The declaration is refused when the idle state in force is not one of the declared options, and after init.
Returns: entity id of the input, or OCONNECTOR_INVALID_ENTITY_ID if it was refused.
redeclareVideoSource method
The redeclareVideoSource(…) method replaces the declaration of a video source while nothing is offering it - for a camera slot that was empty at boot, or one a camera of another type is plugged into. Method declaration:
bool redeclareVideoSource(EntityId id, const VideoSourceDecl& decl);
| Parameter | Description |
|---|---|
| id | Id declareVideoSource returned. |
| decl | New capabilities of the source. Copied. |
Three kinds have a replacement call:
| Call | Replaces | May not change |
|---|---|---|
redeclareVideoSource(id, VideoSourceDecl) | what the sensor can do | the id, the token, the PTZ node over it |
redeclareVideoEncoder(id, VideoEncoderDecl) | what the encoder can do | the id, the token, the video source it reads |
redeclarePtz(id, PtzDecl) | what the node can do | the id, the token, its video source and its actuators |
What is required is that nothing offers the entity - no enumeration names it and no token resolves to it. An encoder and a PTZ node follow their video source, so one setAvailable masking the camera is enough to re-declare all three. Each call applies the rules of the matching declare method, and may be called before or after init, while the service runs.
What was tracked for the entity is dropped: the next Get answers the seed of the new declaration. A node’s presets are replaced by the new PtzDecl::presets. What has a token of its own - an OSD a client created - stays. stableId of the new declaration is ignored: the token does not change. A video source is judged against the Config::maxProfiles ceiling in force, so a swap may be refused encoder instances the same declaration would have been granted at boot.
A re-declaration does not change which operations the device serves - the operation table is built by init (see Overview) - so a host whose camera types differ declares the widest set at boot on every slot and masks what is not plugged in. A re-declaration announces nothing: once the entity is unmasked, the host raises the two Media topics, as setAvailable states.
Returns: TRUE if the declaration was replaced. FALSE, with the declaration in force left as it was, when the id names no declared entity (DiagnosticCode::UndeclaredEntity) or one of another kind, when the device is still offering it, or when the new declaration breaks a rule of the matching declare method (DiagnosticCode::DeclarationRefused).
redeclareVideoEncoder method
The redeclareVideoEncoder(…) method replaces the declaration of a video encoder nothing is offering, on the terms of redeclareVideoSource; the video source it names may not change. Method declaration:
bool redeclareVideoEncoder(EntityId id, const VideoEncoderDecl& decl);
| Parameter | Description |
|---|---|
| id | Id declareVideoEncoder returned. |
| decl | New capabilities of the encoder. Copied. |
Returns: TRUE if the declaration was replaced, or FALSE for the reasons redeclareVideoSource lists.
redeclarePtz method
The redeclarePtz(…) method replaces the declaration of a PTZ node nothing is offering, on the terms of redeclareVideoSource; the video source and the actuators it names may not change. Its presets are seeded from decl.presets - an empty list leaves none. A node that gains auxiliary commands this way is neither served nor advertised until the next init. Method declaration:
bool redeclarePtz(EntityId id, const PtzDecl& decl);
| Parameter | Description |
|---|---|
| id | Id declarePtz returned. |
| decl | New declaration of the node. Copied. |
Returns: TRUE if the declaration was replaced, or FALSE for the reasons redeclareVideoSource lists.
init method
The init(…) method installs the configuration and the host callbacks and builds the operation table out of the declarations. Method declaration:
bool init(const Config& config, Handlers handlers, IMetadataController* metadata = nullptr);
| Parameter | Description |
|---|---|
| config | Configuration, see Config struct. Copied. |
| handlers | Host callbacks, see Handlers struct. Moved. |
| metadata | Metadata control surface of the video service, may be nullptr. Stored and never called (see IMetadataController class description). Not owned. |
init works on the calling thread: it establishes the cryptographic mode, calls Config::provideKek, reads and writes the secret store and the state files, and builds the operation table. It opens no port and starts no thread. A connector that is initialised and not running may be initialised again: the new configuration and handlers replace the previous ones.
An empty Config::users (factory state) and an empty Config::profiles are accepted. What init refuses, each with its reason on the log sink as DiagnosticCode::InitRefused:
- Ports and addresses: a
Config::portorConfig::discoveryPortof 0, aConfig::multicastRepeatorConfig::unicastRepeatoutside whatUdpRepeatstates, aConfig::bindAddressthat is not a dotted quad, and aConfig::announcedAddressthat is neither an address nor a host name. - The event service: a negative
Config::minTerminationTime, aConfig::maxTerminationTimeabove 8760 hours, aConfig::defaultTerminationTimeoutside the two, aConfig::republishTimeoutabove 3 720 000 ms, and a ceiling below its announced number (Config::pullPointCeiling,Config::notificationProducerCeiling). - An empty
Config::authSchemes. - The secret store: a missing store or key provider, a key that is refused, and a store file that cannot be opened, read or written - see Key encryption key.
- The users: a roster that breaks a rule of Users, access classes and factory state.
- The cryptographic mode:
TlsConfig::enabledunderMode::Fips, and a mode that cannot be established. - The lifecycle: an
initwhile the service runs or while anotherinit,start()orstop()is in progress (see Threading rules for host callbacks). - An operation table this build cannot assemble, a defect of the library and never of the configuration.
The cryptographic mode. init calls CryptoProvider::init with Config::cryptoMode and Config::cryptoFips; a host never initialises CryptoProvider itself, and a refused mode refuses init with the reason of CryptoProvider::lastError(). The mode is process wide and held once: a second connector asking for the same mode is served, one asking for another mode is refused, and an init refused over the mode establishes nothing. See Dependencies for what each mode needs.
What a refused init leaves. The configuration, handlers and roster of an earlier init stay in force. Not undone: an established cryptographic mode; the secret store file, which a roster with empty passwords may have created or a partial seal may have written; the certificates/ folder of the state directory. The next init that succeeds re-seals the store to its own roster, so retry under the same key, or remove the store file before changing the key.
Returns: TRUE if the configuration is installed, or FALSE if it was refused, with the reason on the log sink of that configuration or, where it names none, on the sink installed at construction.
start method
The start() method starts serving: it binds the HTTP listener on Config::port and, with TLS enabled, the TLS listener on TlsConfig::port, opens WS-Discovery on Config::discoveryPort and creates the loop thread, which serves every request and calls every hook. The first Hello goes out from that thread. Method declaration:
bool start();
start() returns once the listeners are bound and the loop runs. A WS-Discovery socket that cannot be opened does not refuse the start: the device serves requests and is reported as DiagnosticCode::DiscoveryNotServed.
Returns: TRUE if the service is running. FALSE, with the reason on the log sink as DiagnosticCode::StartRefused, for a start before init or while the service runs or another init, start() or stop() is in progress; TLS enabled without a certificate or a private key that loads; a listener socket that cannot be created; a port that cannot be bound or listened on - taken, or on a Config::bindAddress this host does not carry; a loop thread the system refuses; and a stop() that arrived while it was binding. A refused start leaves nothing open and the configuration installed, so it may be started again.
stop method
The stop() method stops serving: it announces the departure (Bye) and closes the ports. It is idempotent. Method declaration:
void stop();
stop() blocks until the loop thread has ended and the copies of the Bye have been sent (Config::multicastRepeat): up to 750 ms with the defaults, and about 4 s with the largest delays init accepts. A stop() that arrives while another one is tearing the service down returns at once, and an init() or start() is refused until the first one has returned. A stop() from a handler does not wait; see Threading rules for host callbacks. On a connector that is not running it returns at once.
Returns: None.
isFactoryState method
The isFactoryState() method reports whether the device is in factory state: no user is configured (see Users, access classes and factory state). Method declaration:
bool isFactoryState() const;
Returns: TRUE if no user is configured - TRUE before init as well - or FALSE if at least one is.
setAvailable method
The setAvailable(…) method sets the availability of a declared entity, at any time, without rebuilding the object model. Method declaration:
void setAvailable(EntityId id, bool available, std::string_view reason = {});
| Parameter | Description |
|---|---|
| id | Entity to update. An id that names no declared entity changes nothing and is reported as DiagnosticCode::UndeclaredEntity. |
| available | TRUE if the entity can serve requests or FALSE if not. |
| reason | Text for the log sink, reported as DiagnosticCode::AvailabilityChanged; empty by default. Never sent to a client. |
An unavailable entity keeps its token and its configuration. What a masked entity stops being is offered:
- An enumeration filters, and a token that names a masked entity is answered by
Config::unknownToken, exactly as a token that names nothing:Rejectfaults (ter:NoProfileorter:NoConfig) andFallbackToDefaultanswers the first entity of the kind that is still available. Both media services observe it -trt:GetVideoSources,trt:GetProfiles,tr2:GetProfiles,tr2:GetOSDsand theGet<entity>Configurationsfamily - and so dotmd:GetVideoSourcesand PTZ.tr2:GetStreamUrion a masked profile neither answers an address nor callsHandlers::getStreamUri. A profile never advertises a masked configuration. - Deletion does not read the mask:
DeleteProfileaddressed to a masked profile deletes it. - A profile is masked when a mechanism that produces its stream is masked - its video source, and its video encoder where it has one. An encoder and a PTZ node follow the video source their declaration names, so
tptz:GetNodes,tptz:GetConfigurationsandtptz:GetCompatibleConfigurationsdrop a node whose camera is gone. A profile with neither a source nor an encoder follows its metadata configuration instead. - Two kinds do not follow a camera, and the host masks them itself: the metadata stream and the analytics engine. Unplugging a camera is
setAvailableon its video source, on the metadata stream declared for it and on any analytics engine over it, and on nothing else. - A masked PTZ node, analytics engine, or metadata stream under a profile that has video does not take the profile’s video away. Each is still left out of the enumeration of its own kind.
The PTZ availability rule. A masked node refuses no command: AbsoluteMove, RelativeMove, ContinuousMove, Stop and GotoPreset on the profile of a node masked directly - not derived from a masked camera - reach Handlers::ptzMove, Handlers::ptzStop and Handlers::ptzPreset as on an available one. A masked profile is a different door: the same operations addressed by a profile a gone camera has masked answer Config::unknownToken before any hook is called - Reject faults ter:NoProfile, and FallbackToDefault hands the command to the first available profile’s node. An operation addressed by a node token still describes the node: tptz:GetNode, tptz:GetConfiguration, tptz:GetConfigurationOptions and tptz:SetConfiguration answer about a node tptz:GetNodes has stopped listing, whether it was masked itself or went with its camera. Masking an actuator does not take pan and tilt away from the nodes above it: they stay listed, unchanged, and their commands still reach Handlers::ptzMove. On a device that installs no Handlers::ptzGetStatus, once no available mechanism behind a node has a sample, the UtcTime of tptz:GetStatus falls back to the instant of the answer, while the position and the moving flags of a masked mechanism are still answered from its last record.
The library raises no event for a mask or an unmask; the host does, through notify, on the two topics init declares for this purpose:
| Topic | Source items | Raised for |
|---|---|---|
tns1:Media/ProfileChanged | Token: the profile’s token | a profile that left or rejoined the enumerations |
tns1:Media/ConfigurationChanged | Token: the configuration’s token; Type: the tr2:ConfigurationEnumeration value of its kind | a configuration that left or rejoined them |
An entity that will never be there is not declared at all; setAvailable is for a mechanism that comes and goes. It calls no hook.
Returns: None.
reportState method
The reportState(…) method reports hardware state, and it is the only way state enters the library: the library polls nothing, and what it answers a client is what was reported here. Method declaration:
void reportState(EntityId id, const StateUpdate& state, Generation gen);
| Parameter | Description |
|---|---|
| id | Entity the state belongs to. An id that names no declared entity is reported as DiagnosticCode::UndeclaredEntity and nothing is stored or raised. |
| state | What the hardware reports (StateUpdate). An absent member leaves the stored one untouched. |
| gen | Generation being confirmed: the value handed to the matching apply* hook. Ignored where the report carries only a sample, which normally passes zero. |
A confirmation answers an application. A report carrying StateUpdate::running, StateUpdate::failure, StateUpdate::actualFrameRate or StateUpdate::actualBitrateKbps confirms the generation it names. A confirmation older than the last one accepted is discarded whole, the samples it carried included, and so is a generation the library never handed out; the log sink is told which. A bare acknowledgement has to carry running. An entity nothing has applied to sits at generation zero, so a confirmation for it names zero; a generation handed to an apply* hook is never zero. A confirmation that never arrives is reported as DiagnosticCode::ConfirmationDeadlineMissed, to the host and to no client. StateUpdate::running TRUE is also what opens the readiness gate GetStreamUri waits on (Config::republishTimeout).
A sample is never judged against a generation. StateUpdate::ptz carries the PTZ state of a mechanism and is read only for an actuator declareActuator returned; StateUpdate::focus carries the focus state of a lens and is read only for a video source whose ImagingDecl declares a focus move. Anything else is dropped with a DiagnosticCode::StateNotCarried warning. FocusStatus::moving is a bool: a source nobody has reported on answers IDLE. Clients read the samples back through tptz:GetStatus (without Handlers::ptzGetStatus) and timg:GetStatus.
StateUpdate::logicalState is raised, not stored: the state of a relay output or a digital input becomes the property event on tns1:Device/Trigger/Relay or tns1:Device/Trigger/DigitalInput, with the token of the entity as its source item. Reported before start(), it is dropped with a diagnostic, as notify would drop it.
It calls no hook.
Returns: None.
reflect method
The reflect(…) method tells the library a value the device changed by itself, which no client asked for - an operator changed the bitrate in the device’s own web interface, a lens moved its own white balance - so that Get answers what the device really does. Method declaration:
void reflect(EntityId id, const ConfigReflection& values);
| Parameter | Description |
|---|---|
| id | Entity the values are about: a video source for imaging and for the overlays, an encoder for the encoder. An id that names no declared entity is reported as DiagnosticCode::UndeclaredEntity. |
| values | What the device reports (ConfigReflection). A member for another kind than the id names is dropped with a DiagnosticCode::ReflectionNotCarried warning. |
It is not a confirmation: the value in force is rewritten and nothing else happens - no generation, no deadline, no hook, and the readiness gate untouched. The value is not judged against the declaration, so a value outside the declared range is shown, not clamped. ConfigReflection::imaging is read member by member (an absent member is “unknown”), and ConfigReflection::videoEncoder is taken whole, VideoEncoderConfig::multicast included; ImagingConfig::videoSource, VideoEncoderConfig::encoder and VideoEncoderConfig::name are never taken from the report.
It is ignored while an application is outstanding for that entity and inside its confirmation window, so a client’s Set followed by a Get answers the value just set. Such a drop is reported at Debug as DiagnosticCode::ReflectionNotCarried.
ConfigReflection::osds is the whole set of overlays a video source draws, and the one member that creates and deletes: an entry whose token names an overlay of that source rewrites it, every other entry - an empty token included - takes an overlay of that source no entry named, oldest first, an entry left over is created under a fresh token, and an overlay no entry took is deleted. A host that reports the same overlay every cycle keeps one token for it. Nothing is judged against VideoSourceDecl::osd. A report is dropped while a CreateOSD, SetOSD, DeleteOSD or video source Set of that source is outstanding. A report for a source with no OsdDecl, or one the OSD tokens of the run ran out under, is a ReflectionNotCarried warning.
It calls no hook.
Returns: None.
reflectPreset method
The reflectPreset(…) method adds or replaces a preset the host stores. The library answers tptz:GetPresets from a mirror of the host’s presets, and the host keeps it in step. Method declaration:
bool reflectPreset(EntityId node, const PtzPresetDecl& preset);
| Parameter | Description |
|---|---|
| node | Id declarePtz returned. |
| preset | The preset, with the token the host names it by (PtzPresetDecl::token, never empty), its name and its position. |
PtzDecl::presets is the seed of the mirror; an empty token, a repeated token and more presets than PtzDecl::maximumPresets are each a refused declaration. Afterwards reflectPreset and removeReflectedPreset keep the mirror in step: nothing is commanded and no hook is called. A token already in the node’s list is replaced in place.
Refused, as DiagnosticCode::PtzPresetReflectionRefused with the node as the diagnostic’s entity: an empty token (Error), a creation past the node’s MaximumNumberOfPresets (Warning), and an id that is not a declared PTZ node (Error). The token is not judged against tt:ReferenceToken, so keep it within 64 characters.
The token namespace is shared with a client’s tptz:SetPreset, which reaches Handlers::ptzPreset, or Handlers::ptzStorePreset where the host installs it and names the token itself. Without ptzStorePreset the library issues the token and never hands out one already in the node’s list. A token the library issued is never issued again; a token a seed or reflectPreset supplied may be, once it has left the list. A host that wants its tokens never reused uses a series the library’s own Preset<N> series cannot produce, such as PTZPresetToken_<N>.
Returns: TRUE if the mirror was updated, or FALSE - with the mirror left as it was - if the token is empty, the node is at its MaximumNumberOfPresets, or the id is not a declared PTZ node.
removeReflectedPreset method
The removeReflectedPreset(…) method removes a preset the host no longer stores from the mirror (see reflectPreset). GetPresets is then one entry short, and GotoPreset on the token answers ter:NoToken. Method declaration:
bool removeReflectedPreset(EntityId node, const std::string& token);
| Parameter | Description |
|---|---|
| node | Id declarePtz returned. |
| token | Token of the preset to remove. |
Returns: TRUE if the mirror was updated, or FALSE if the node or the token is unknown, reported at Error as DiagnosticCode::PtzPresetReflectionRefused.
notify method
The notify(…) method raises an event without a request. Method declaration:
bool notify(const Event& event);
| Parameter | Description |
|---|---|
| event | Event to raise: its topic, the entity it is about, its time, its Source, Key and Data items, whether it is a property and, for a property, its stage. Copied. |
TRUE means the event was accepted for delivery: the service is running, its topic is declared, and at least one subscription’s filter passes it and its queue had room. FALSE means it was not delivered: queue it and raise it again, never assume a client saw it. No subscription, a filter that excludes the topic, and a full queue (Config::maxMessagesPerSubscription) are ordinary reasons, each reported as DiagnosticCode::EventDropped; a topic nothing declared, and every event before start() and after stop(), is dropped with a Warning.
A property is answered again: the library keeps the last value of every property, keyed on its topic and source items, and replays it to every new subscription and on SetSynchronizationPoint with PropertyOperation="Initialized" (Core §9.4.2, §9.5). The first raise of a property is Initialized and a later one Changed, decided by the library. Deleted is the one stage the host says itself, in Event::stage (PropertyStage::Deleted); it removes the record, so nothing replays it. A pull point created with tev:ChangedOnly is not seeded, and a delete is not offered to it.
It calls no hook.
Returns: TRUE if the event was accepted for delivery or FALSE if it was dropped.
IMetadataController class description
IMetadataController class declaration
The IMetadataController interface class is declared in the IMetadataController.h file, which needs the vocabulary types only. Class declaration:
class IMetadataController
{
public:
/// Class destructor. The host owns the controller.
virtual ~IMetadataController() = default;
/// Put a metadata configuration in force.
virtual bool applyMetadataConfiguration(EntityId id,
const MetadataConfig& config,
Generation gen) = 0;
/// Close the current metadata document and open a new one.
virtual bool requestSynchronizationPoint(EntityId id) = 0;
/// Report whether a track is published and can be subscribed to.
virtual bool isTrackReady(EntityId id) const = 0;
protected:
/// Default constructor. The class can be used as a base only.
IMetadataController() = default;
/// Copy constructor.
IMetadataController(const IMetadataController& src) = default;
/// Move constructor.
IMetadataController(IMetadataController&& src) = default;
/// Copy assignment operator.
IMetadataController& operator=(const IMetadataController& src) = default;
/// Move assignment operator.
IMetadataController& operator=(IMetadataController&& src) = default;
};
Nothing in the library calls this interface. init accepts a controller and stores the pointer, but calls none of its methods and never deletes it, so passing one behaves as passing nullptr. The interface records the design of the boundary with the video service; what the two sides owe each other is stated in Metadata contract with the video service.
The constructors and the assignment operators are protected: an object is created, copied and moved only as an object of a class derived from IMetadataController.
| Method | What it stands for in the design |
|---|---|
applyMetadataConfiguration(id, config, gen) | Put an accepted SetMetadataConfiguration in force, echoing gen through reportState once the stream runs with it. The hook that is called is Handlers::applyMetadata. |
requestSynchronizationPoint(id) | Close the current metadata document and open a new one (Profile M §7.5.1). The hook that is called is Handlers::synchronizeMetadata. |
isTrackReady(id) | Report whether a track is published. The library answers this question with its own readiness gate (Config::republishTimeout) instead. |
Data structures
The data structures are declared in OConnectorTypes.h, except Config and Handlers, which are declared in OConnector.h. Each structure and member is documented at its declaration.
Config struct
Everything the library needs to start, passed to init once and copied. Structure declaration:
struct Config
{
uint16_t port{80};
std::string bindAddress;
std::string announcedAddress;
uint16_t announcedPort{0};
uint16_t announcedTlsPort{0};
uint16_t discoveryPort{3702};
UdpRepeat multicastRepeat;
UdpRepeat unicastRepeat{1, 50, 250, 500};
std::vector<User> users;
DiscoveryInfo discovery;
TlsConfig tls;
cr::crypto::Mode cryptoMode{cr::crypto::Mode::Standard};
cr::crypto::FipsConfig cryptoFips;
std::set<Profile> profiles;
std::set<AuthScheme> authSchemes{AuthScheme::HttpDigest,
AuthScheme::UsernameToken};
UnknownTokenPolicy unknownToken{UnknownTokenPolicy::Reject};
XmlLimits xmlLimits;
CommandDebouncePolicy debounce;
Duration republishTimeout{std::chrono::milliseconds(2000)};
StreamingTransportDecl streamingTransports;
bool nonAggregateRtspControl{false};
int maxNtpServers{0};
bool zeroConfiguration{false};
int maxProfiles{0};
int maxPullPoints{2};
int maxNotificationProducers{0};
Duration minTerminationTime{std::chrono::seconds(10)};
Duration maxTerminationTime{std::chrono::hours(1)};
Duration defaultTerminationTime{std::chrono::minutes(1)};
int pullPointCeiling{4};
int notificationProducerCeiling{0};
int maxMessagesPerSubscription{100};
int maxNotificationFailures{OCONNECTOR_NOTIFY_FAILURE_MAX};
int authFreeAttempts{OCONNECTOR_AUTH_FREE_ATTEMPTS};
Duration authBackoff{std::chrono::milliseconds(OCONNECTOR_AUTH_BACKOFF_MS)};
Duration authBackoffMax{
std::chrono::milliseconds(OCONNECTOR_AUTH_BACKOFF_MAX_MS)};
Duration authForget{std::chrono::milliseconds(OCONNECTOR_AUTH_FORGET_MS)};
std::string stateDirectory;
SecretStorePath secretStore;
std::function<Result<SecretKey>()> provideKek;
std::function<void(const std::vector<UserRecord>&)> onUsersChanged;
std::function<void(const std::vector<std::string>&)> onScopesChanged;
LogSink log;
};
Table 2 - Config fields.
| Field | Default | Description |
|---|---|---|
port | 80 | Port of the plain HTTP listener. 0 is refused by init. |
bindAddress | empty: every interface | Address the HTTP and TLS listeners bind; a value that is not a dotted quad is refused. WS-Discovery is not confined by it; see The address a client is told. |
announcedAddress | empty: bindAddress, then every non-loopback interface | Address Hello, Bye and ProbeMatches carry, and the fallback of a request with no usable Host. A device behind a proxy names the proxy here. A value that is neither a dotted quad nor a host name, or that carries a port, is refused. |
announcedPort | 0: port | Port the announcements carry; left out of the URI when it is 80. |
announcedTlsPort | 0: tls.port when TLS is enabled | TLS port the announcements carry; no https URI without TLS. |
discoveryPort | 3702 | UDP port of WS-Discovery; another value is for two devices on one host. 0 is refused. |
multicastRepeat | UdpRepeat{2, 50, 250, 500} | Copies of every Hello and Bye (SOAP-over-UDP 1.1): sent 1 + repeat times, the first delay between minDelayMs and maxDelayMs, doubled up to upperDelayMs. Refused: repeat outside 0 to 4, or delays not 0 <= minDelayMs <= maxDelayMs <= upperDelayMs <= 1000. |
unicastRepeat | UdpRepeat{1, 50, 250, 500} | Copies of every ProbeMatches and ResolveMatches, with the bounds of multicastRepeat. |
users | empty: factory state | The whole roster; see Users, access classes and factory state. |
discovery | empty DiscoveryInfo | Identity and scopes: endpointReference (empty: derived), the configurable scopes, manufacturer, model, firmwareVersion, serialNumber, hardwareId. Clients may change the configurable scopes; the profile, hardware and name scopes are fixed (ter:FixedScope). |
tls | disabled | See TLS. |
cryptoMode | cr::crypto::Mode::Standard | Cryptographic mode, established by init; see init and Dependencies. |
cryptoFips | default cr::crypto::FipsConfig | Under Mode::Fips only: the FIPS module and its configuration (moduleDir, configFile, requiredVersion, requireApprovedSettings, ignoreEnvironment, requireTlsEmsCheck, described by CryptoProvider). |
profiles | empty | Profiles the device claims: announced and diagnosed, never enforced (see below). |
authSchemes | {HttpDigest, UsernameToken} | Credentials the device accepts and announces. An empty set is refused. |
unknownToken | UnknownTokenPolicy::Reject | Answer to a token that names nothing or a masked entity: Reject faults; FallbackToDefault answers about the first available entity of the kind asked for and logs the substitution. |
xmlLimits | 1 MiB body, depth 64, 100 000 elements | Input limits. A larger body is answered HTTP 413; a deeper or larger document env:Sender, HTTP 400. |
debounce | 200 ms window, 1000 ms maximum delay | Coalescing of configuration changes (see below). Zero disables it. |
republishTimeout | 2000 ms | Longest wait of the readiness gate of GetStreamUri (see below). 0 disables the gate; above 3 720 000 ms is refused. |
streamingTransports | every member false | Transports the host’s streaming plane carries - rtpMulticast, rtpOverTcp, rtpOverRtspTcp - announced, unchecked, in the capabilities of both media services and of tds:GetCapabilities. Set what is true: clients choose transports from it. |
nonAggregateRtspControl | false | @NonAggregateControl of both media services: the streaming plane accepts a PLAY per media section of the SDP. Announced, unchecked. |
maxNtpServers | 0: no NTP | Longest NTP server list. At 0, GetNTP and SetNTP answer ter:ActionNotSupported and NTP time is refused with ter:NtpServerUndefined. A negative value is read, and announced, as zero. |
zeroConfiguration | false | TRUE: the host serves NetworkChange::ZeroConfiguration; FALSE: GetZeroConfiguration and SetZeroConfiguration answer ter:ActionNotSupported. Announced TRUE only where Handlers::getNetwork and Handlers::setNetwork are both installed. |
maxProfiles | 0: 24 | Most profiles the device holds, at most 24; a CreateProfile past it is refused. Announced as @MaximumNumberOfProfiles. |
maxPullPoints | 2 | Pull points announced and guaranteed. Profile T requires at least two. A negative value is read, and announced, as zero. |
maxNotificationProducers | 0 | Push subscriptions announced and guaranteed. At 0 the device does not push and wsnt:Subscribe is not registered; above it, install Handlers::deliverNotification. A negative value is read, and announced, as zero. |
minTerminationTime, maxTerminationTime, defaultTerminationTime | 10 s, 1 h and 1 min | Bounds of a subscription’s lifetime and of a Renew, and the lifetime of a request that names none (see below). Refused: a negative minimum, a maximum above 8760 h, a default outside the two. |
pullPointCeiling, notificationProducerCeiling | 4 and 0 | Where the device stops creating subscriptions. A ceiling below its announced number is refused. |
maxMessagesPerSubscription | 100 | Most messages queued for one subscription, answered as the MaxMessageLimit of a tev:PullMessagesFaultResponse; a further one is refused and notify answers FALSE. A negative value is read, and answered, as zero. |
maxNotificationFailures | 3 | Consecutive refused deliveries after which a push subscription is destroyed; 0 never destroys one. |
authFreeAttempts, authBackoff, authBackoffMax, authForget | 5, 1000 ms, 8000 ms, 10 min | Authentication back-off (see below). 0 is valid for each. |
stateDirectory | empty: no state is kept | Directory of the state kept across restarts (see below). It must be writable. |
secretStore | empty | Sealed file of the recoverable passwords. Mandatory with AuthScheme::UsernameToken, which is accepted by default; see Key encryption key. |
provideKek | empty | Supplier of the 32-byte key of secretStore. Mandatory with AuthScheme::UsernameToken; see Key encryption key. |
onUsersChanged | empty | Called with the roster, without passwords, after every change a client makes to it; see Users, access classes and factory state. |
onScopesChanged | empty | Called with the configurable scopes after every change a client makes to them; pass them back as discovery.scopes to keep them across a restart. |
log | empty | Diagnostics sink (see Diagnostic struct). A non-empty sink replaces the one of the constructor; an empty one keeps it. |
Readiness gate. A change that restarts a stream - a codec, an encoder profile, a resolution, a rotation or a mirror, a metadata compression, a multicast transport - closes the gate of that entity until the host confirms the generation with StateUpdate::running TRUE. GetStreamUri of either media service waits for the gates of the profile’s video source, encoder and metadata stream before it asks the host for an address. Past republishTimeout the address is answered anyway and DiagnosticCode::ConfirmationDeadlineMissed is reported.
Coalescing. Changes of one entity that arrive inside the quiet debounce.window are applied once, with the last value and generation; debounce.maxDelay releases a change under a burst that never pauses. A superseded generation never reaches the host, and applications leave in ascending generation order (DiagnosticCode::CommandsCoalesced, DiagnosticCode::CommandDelayCapped). It covers the video source, sensor mode, OSD, encoder and metadata configurations of both media services and the imaging settings and preset. PTZ moves and stops are never coalesced.
Subscriptions. A create past pullPointCeiling is refused with wsnt:UnableToCreatePullPointFault, a Subscribe past notificationProducerCeiling with wsnt:SubscribeCreationFailedFault. A lifetime outside the termination-time bounds is answered wsnt:UnacceptableInitialTerminationTimeFault (wsnt:UnacceptableTerminationTimeFault for a Renew) carrying the bounds. A client that neither pulls nor renews loses its subscription after defaultTerminationTime, and every PullMessages extends a pull point that named no lifetime by that much again, or by its Timeout where longer. A PullMessages may wait up to maxTerminationTime plus one minute.
Authentication back-off. Failures are counted per peer address, or per account name where no peer can be named. The first authFreeAttempts cost nothing, each further failure doubles a wait capped at authBackoffMax, and the count is forgotten after authForget of quiet. A correct credential is always admitted and clears the count.
Profiles. A claim is announced - one scope URI per profile in WS-Discovery: onvif://www.onvif.org/Profile/Streaming for Profile::S, onvif://www.onvif.org/Profile/T and onvif://www.onvif.org/Profile/M - and diagnosed: init reports each gap between the claim and what the device serves as a DiagnosticCode::ProfileGap warning (the authentication scheme, Device Management operations and services each profile mandates, and a Profile::S claim with maxNotificationProducers at zero). A claim never refuses anything. The library makes no claim of its own: a profile is announced only when the host lists it in Config::profiles, and a conformance claim is the matter of the finished product (see Disclaimer).
State directory. With stateDirectory empty the device keeps no state across restarts: the WS-Discovery instance identifier, the discovery mode a client set and the multicast streaming to resume after a reboot are lost (DiagnosticCode::MulticastStateNotRecorded), and the certificate operations answer ter:ActionNotSupported. A device without a reliable real-time clock should set it: otherwise a cold boot may announce a lower InstanceId, and clients discard its announcements as stale. The library keeps wsdd-instance, discovery-mode, multicast and certificates/ there - the last at mode 0700, holding one PEM certificate and one unencrypted PEM private key per record at mode 0600 - and writes every file through a temporary file renamed into place, never through a link. Point it at storage the device alone can read.
The structures of the other Config members:
struct UdpRepeat
{
int repeat{2};
int minDelayMs{50};
int maxDelayMs{250};
int upperDelayMs{500};
};
struct DiscoveryInfo
{
std::string endpointReference;
std::vector<std::string> scopes;
std::string manufacturer;
std::string model;
std::string firmwareVersion;
std::string serialNumber;
std::string hardwareId;
};
struct XmlLimits
{
size_t maxRequestBytes{OCONNECTOR_MAX_REQUEST_BYTES};
size_t maxDepth{OCONNECTOR_MAX_XML_DEPTH};
size_t maxElements{OCONNECTOR_MAX_XML_ELEMENTS};
};
struct CommandDebouncePolicy
{
Duration window{std::chrono::milliseconds(200)};
Duration maxDelay{std::chrono::milliseconds(1000)};
};
struct StreamingTransportDecl
{
bool rtpMulticast{false};
bool rtpOverTcp{false};
bool rtpOverRtspTcp{false};
};
TlsConfig is described under TLS, User and UserRecord under Users, access classes and factory state, and SecretStorePath and SecretKey under Key encryption key.
Declaration structures
Each declare method takes one structure that follows the ONVIF® schemas: the library writes every Get*Options answer out of it and judges every Set against it. stableId, where a structure has one, is what the ONVIF® token of the entity is derived from, and userData is a pointer the library stores and never reads.
Table 3 - declarations, in the order a host writes them.
| Call | Declares | Names |
|---|---|---|
declareVideoSource(VideoSourceDecl) | one sensor with its imaging and OSD surface | - |
declareVideoEncoder(VideoEncoderDecl) | one encoder; the main and the sub stream of one sensor are two encoders | its video source |
declareMetadata(MetadataDecl) | one metadata stream | - |
declareAnalytics(AnalyticsDecl) | one analytics engine and its VideoAnalyticsConfiguration | - |
declareActuator(ActuatorDecl) | one mechanism that physically moves | - |
declarePtz(PtzDecl) | one ONVIF® PTZ node and the mechanisms behind it | its video source, its pan/tilt actuator, its zoom actuator |
declareProfile(ProfileDecl) | one media profile | any of the above, each in the slot of its kind |
declareEvent(EventDecl) | one event topic of the topic set | - |
declareRelayOutput(RelayOutputDecl) | one relay output, and the topic tns1:Device/Trigger/Relay | - |
declareDigitalInput(DigitalInputDecl) | one digital input, and the topic tns1:Device/Trigger/DigitalInput | - |
Table 4 - public structures by family.
| Family | Structures |
|---|---|
| Vocabulary | EntityId (the id of a declared entity; OCONNECTOR_INVALID_ENTITY_ID is 0), Generation, Duration (milliseconds), TimePoint, Timestamp, Range<T>, Resolution, QName |
| Video source declaration | VideoSourceDecl, BoundsRangeDecl, RotateDecl, VideoSourceModeDecl, EncoderInstanceLimitsDecl, EncoderInstanceDecl, OsdDecl, OsdInstanceLimitsDecl, OsdTextDecl, OsdImageDecl, OsdColorDecl, ColorOptionsDecl, ColorDecl, ColorspaceRangeDecl |
Imaging declaration (inside VideoSourceDecl) | ImagingDecl, ExposureDecl, FocusDecl, WhiteBalanceDecl, ModeLevelDecl, ModeSupportDecl, NoiseReductionDecl, IrCutFilterAutoAdjustmentDecl, FocusMoveDecl, AbsoluteFocusDecl, RelativeFocusDecl, ContinuousFocusDecl, ImagingPresetDecl |
| Encoder and metadata declaration | VideoEncoderDecl, VideoEncoderCodecDecl, MulticastDecl, MetadataDecl, SensorDataDecl |
| PTZ declaration | ActuatorDecl, PtzStatusDecl, PtzDecl, PtzSpacesDecl, PtzSpace1DDecl, PtzSpace2DDecl, PtzExtraSpaceDecl, PtControlDirectionDecl, PtzPresetDecl, PtzVector |
| Profile and event declaration | ProfileDecl, EventDecl, SimpleItemDecl |
| Analytics declaration | AnalyticsDecl, AnalyticsTypeDecl, AnalyticsParameterDecl, ElementItemDecl, MessageDecl, ItemRange, AnalyticsInstance, SimpleItemValue, ElementItemValue |
| Device IO declaration | RelayOutputDecl, DigitalInputDecl |
| Configuration of the library | User, UserRecord, DiscoveryInfo, UdpRepeat, TlsConfig, StreamingTransportDecl, SecretStorePath, SecretKey, CommandDebouncePolicy, XmlLimits |
Settings handed to a validate*/apply* pair | VideoSourceConfig (with CaptureArea, OsdConfig, OsdColor), VideoEncoderConfig, ImagingConfig, MetadataConfig, PtzConfig, AnalyticsConfig; ProposedChange for checkConflicts |
| Commands and questions of the other hooks | SystemTime, TimeSettings, NetworkConfig, NetworkAddress, NetworkProtocolConfig, SystemLog, PtzMove, PtzStop, PtzStatus, PresetOp, AuxCommand, FocusMove, RelayOutputConfig, DigitalInputConfig, StreamRequest, SnapshotRequest, RequestOrigin, StreamUri, Snapshot, MulticastOp, Notification |
| From the host to the library | StateUpdate, FocusStatus, ConfigReflection, Event |
| Identity, answers and diagnostics | Caller, Principal, PtzOrigin, Fault, Result<T>, Diagnostic, LogSink |
Handlers struct
The host callbacks, a structure of std::function members moved into init. Structure declaration:
struct Handlers
{
std::function<Result<VideoSourceConfig>(const Caller&,
const VideoSourceConfig&)>
validateVideoSource;
std::function<void(const Caller&, const VideoSourceConfig&, Generation)>
applyVideoSource;
std::function<Result<VideoEncoderConfig>(const Caller&,
const VideoEncoderConfig&)>
validateVideoEncoder;
std::function<void(const Caller&, const VideoEncoderConfig&, Generation)>
applyVideoEncoder;
std::function<Result<ImagingConfig>(const Caller&, const ImagingConfig&)>
validateImaging;
std::function<void(const Caller&, const ImagingConfig&, Generation)>
applyImaging;
std::function<Result<MetadataConfig>(const Caller&, const MetadataConfig&)>
validateMetadata;
std::function<void(const Caller&, const MetadataConfig&, Generation)>
applyMetadata;
std::function<Result<AnalyticsConfig>(const Caller&,
const AnalyticsConfig&)>
validateAnalytics;
std::function<void(const Caller&, const AnalyticsConfig&, Generation)>
applyAnalytics;
std::function<void(const Caller&, EntityId)> synchronizeMetadata;
std::function<void(const Caller&, EntityId videoEncoder)>
synchronizeVideoEncoder;
std::function<Result<PtzConfig>(const Caller&, const PtzConfig&)>
validatePtzConfiguration;
std::function<void(const Caller&, const PtzConfig&, Generation)>
applyPtzConfiguration;
std::function<Result<void>(const Caller&, const SystemTime&)> setSystemTime;
std::function<Result<TimeMode>(const Caller&)> getTimeMode;
std::function<Result<TimeSettings>(const Caller&)> getTimeSettings;
std::function<Result<std::string>(const Caller&)> systemReboot;
std::function<Result<void>(const Caller&, ResetType)> factoryDefault;
std::function<Result<bool>(const Caller&, NetworkChange,
const NetworkConfig&)>
setNetwork;
std::function<Result<std::vector<NetworkConfig>>(const Caller&,
NetworkChange)>
getNetwork;
std::function<Result<SystemLog>(const Caller&, LogType)> getSystemLog;
std::function<Result<void>(const Caller&, const PtzMove&)> ptzMove;
std::function<Result<void>(const Caller&, const PtzStop&)> ptzStop;
std::function<Result<PtzStatus>(const Caller&)> ptzGetStatus;
std::function<Result<void>(const Caller&, const PresetOp&)> ptzPreset;
std::function<Result<std::string>(const Caller&, const PresetOp&)>
ptzStorePreset;
std::function<Result<void>(const Caller&, const AuxCommand&)> ptzAuxiliary;
std::function<Result<void>(const Caller&, EntityId, const FocusMove&)>
moveFocus;
std::function<Result<void>(const Caller&, EntityId)> stopFocus;
std::function<Result<void>(const Caller&, EntityId, RelayLogicalState)>
setRelayOutputState;
std::function<Result<void>(const Caller&, const RelayOutputConfig&)>
setRelayOutputSettings;
std::function<Result<void>(const Caller&,
const std::vector<DigitalInputConfig>&)>
setDigitalInputConfigurations;
std::function<Result<StreamUri>(const Caller&, const StreamRequest&)>
getStreamUri;
std::function<Result<Snapshot>(const Caller&, const SnapshotRequest&)>
getSnapshot;
std::function<Result<void>(const Caller&, const MulticastOp&)>
multicastStreaming;
std::function<Result<void>(const Caller&, const ProposedChange&)>
checkConflicts;
std::function<Result<void>(const Caller&, const Notification&)>
deliverNotification;
};
Every one of the 38 hooks takes a Caller first: who asked, what token they wrote, how it resolved and, for the PTZ hooks, the profile, camera, node and mechanism of the call. A setting is a validate/apply pair, an action is one hook per command and a question is one hook per question. Where and how hooks are called: Threading rules for host callbacks.
Table 5 - hooks.
| Hook | Kind | Called for | Empty |
|---|---|---|---|
validateVideoSource, applyVideoSource | setting | SetVideoSourceConfiguration, CreateOSD, SetOSD, DeleteOSD and SetVideoSourceMode of both media services; VideoSourceConfig::change says which of the three families arrived | ter:ActionNotSupported unless both are installed |
validateVideoEncoder, applyVideoEncoder | setting | SetVideoEncoderConfiguration of both media services | ter:ActionNotSupported unless both are installed |
validateImaging, applyImaging | setting | SetImagingSettings and the imaging SetCurrentPreset | ter:ActionNotSupported unless both are installed |
validateMetadata, applyMetadata | setting | SetMetadataConfiguration of both media services | ter:ActionNotSupported unless both are installed |
validateAnalytics, applyAnalytics | setting | the six writing operations of the Analytics service and trt:SetVideoAnalyticsConfiguration | refuses nothing |
validatePtzConfiguration, applyPtzConfiguration | setting | tptz:SetConfiguration | refuses nothing: the declaration settles the judgement |
synchronizeVideoEncoder | action | SetSynchronizationPoint of both media services, once per video encoder of the profile, first | a profile with an encoder is answered ter:ActionNotSupported, and neither half is called |
synchronizeMetadata | action | the same operations, once per metadata stream of the profile, after the video half | the metadata half is skipped; nothing is refused |
setSystemTime | action | SetSystemDateAndTime | ter:ActionNotSupported |
getTimeMode | question | GetSystemDateAndTime (PRE_AUTH) | TimeMode::Manual |
getTimeSettings | question | GetSystemDateAndTime (PRE_AUTH); supersedes getTimeMode where installed | answered through getTimeMode |
systemReboot | action | SystemReboot | ter:ActionNotSupported |
factoryDefault | action | SetSystemFactoryDefault | ter:ActionNotSupported |
setNetwork | action | SetNetworkInterfaces, SetNetworkProtocols, SetNetworkDefaultGateway, SetHostname, SetDNS, SetNTP, SetIPAddressFilter, SetZeroConfiguration | ter:ActionNotSupported |
getNetwork | question | the eight matching reading operations; GetHostname is PRE_AUTH | ter:ActionNotSupported |
getSystemLog | question | GetSystemLog | ter:ActionNotSupported, and SystemLogging announced FALSE |
ptzMove | action | AbsoluteMove, RelativeMove, ContinuousMove, once per actuator | ter:ActionNotSupported |
ptzStop | action | Stop, once per actuator | ter:ActionNotSupported |
ptzGetStatus | question | tptz:GetStatus | the status is composed from what reportState recorded |
ptzPreset | action | GotoPreset, RemovePreset, SetPreset, GotoHomePosition, SetHomePosition | ter:ActionNotSupported |
ptzStorePreset | action | SetPreset, replacing ptzPreset for it and naming the token | SetPreset reaches ptzPreset with a token of the library’s own |
ptzAuxiliary | action | SendAuxiliaryCommand | ter:ActionNotSupported |
moveFocus, stopFocus | action | timg:Move, timg:Stop, addressed by video source | ter:ActionNotSupported |
setRelayOutputState | action | SetRelayOutputState of Device IO and of Device Management | ter:ActionNotSupported |
setRelayOutputSettings | action | SetRelayOutputSettings of Device IO and of Device Management | ter:ActionNotSupported |
setDigitalInputConfigurations | action | tmd:SetDigitalInputConfigurations, the whole request in one call | ter:ActionNotSupported |
getStreamUri | question | GetStreamUri of both media services | ter:ActionNotSupported |
getSnapshot | question | GetSnapshotUri of both media services | ter:ActionNotSupported |
multicastStreaming | action | StartMulticastStreaming and StopMulticastStreaming of both media services, and the resumption at start | keeps the bookkeeping and refuses nothing; where an AutoStart group is declared, init reports DiagnosticCode::MulticastHookMissing |
checkConflicts | question | an operation that changes a configuration or what a profile references | no conflicts exist |
deliverNotification | action | every push subscription, from the loop’s own tick | nothing can be pushed; with Config::maxNotificationProducers above zero init reports DiagnosticCode::NotificationDeliveryMissing |
Settings. A validate* hook is synchronous and receives a value the library has already judged against the declaration. It may adapt it - an out-of-range bitrate is corrected rather than refused (Media2 §5.3.3) - and Get then answers the adapted value; validatePtzConfiguration adapts nothing. validateAnalytics receives the whole set of instances, never a delta, and what it returns is stored without being judged again, so a default it adds must sit inside the declared range. An apply* hook is asynchronous: it hands the work on, returns, and the host confirms later with reportState and the Generation it was given. An apply* hook that throws leaves its change in force and is reported as DiagnosticCode::HardwareRefusedConfiguration; correct the value with reflect. The library seeds the PTZ configuration from the declaration at every start, so a host that keeps a client’s change records it in applyPtzConfiguration; when it rebuilds its declaration from a changed default space, it moves the old default into PtzSpacesDecl::extraSpaces rather than declaring a URI twice.
The name of a configuration (VideoSourceConfig::name, VideoEncoderConfig::name, MetadataConfig::name, PtzConfig::name) travels through the same pair: it starts as the token, has at most 64 characters (ter:ConfigModify past it), may be empty, and nothing resolves a configuration by it. An OSD keeps its token as its name. Selecting an imaging preset (ImagingConfig::preset) travels through the imaging pair. MetadataConfig::eventFilter carries the event filter a client wrote, as one XML element of at most 8 192 bytes, stored and answered back but never enforced by the library.
System hooks. systemReboot and factoryDefault must schedule and return, never perform: the response is written after the hook returns, and Core §8.3.13 requires it to be sent before the device reboots. systemReboot returns the text of tds:SystemRebootResponse/Message; an empty text is answered with a neutral message of the library. The library clears nothing on factoryDefault - neither the roster nor a file: a host that resets the users stops the connector and calls init again with its factory Config::users. getTimeMode reports how the device keeps its clock. getTimeSettings, where installed, answers the mode, POSIX zone and DaylightSavings flag the host keeps without applying them; the library computes LocalDateTime from that zone (a zone it cannot interpret keeps the local time of the process), and the flag counts only beside a kept zone. These two, and getNetwork with NetworkChange::Hostname, are reached by anonymous clients.
Network hooks. The NetworkChange says which of the eight operations arrived. Members of NetworkConfig the change does not name carry nothing, and NetworkConfig::enabled and NetworkConfig::dhcp are empty when the request did not mention them. setNetwork returns RebootNeeded: FALSE means the settings are in force now, and the library announces the new address with a Hello; TRUE means at the next reboot. NetworkChange::Ntp arrives only where Config::maxNtpServers is positive, NetworkChange::ZeroConfiguration only where Config::zeroConfiguration is TRUE. getNetwork answers one element per interface for NetworkChange::Interfaces and exactly one for every other question.
Device IO hooks. setRelayOutputState triggers a relay; the client learns the result from the property the host raises with StateUpdate::logicalState. setRelayOutputSettings and setDigitalInputConfigurations are commands with nothing to confirm: the library records the value once the host has taken it, and their faults are answered synchronously. A mode or delay time the relay does not offer is env:Sender / ter:InvalidArgVal / ter:ModeError, and an idle state the input does not offer env:Sender / ter:InvalidArgVal / ter:SettingsInvalid, both decided by the library from the declaration before the host is asked; a refusal of the host is answered by its FaultCode. setDigitalInputConfigurations takes the whole request in one call: all or none (Device IO §5.7.3).
PTZ hooks. ptzMove and ptzStop are called once per actuator the request affects, each with that actuator’s components only; ptzStop carries the PanTilt and Zoom selectors of the request. ptzGetStatus, ptzPreset, ptzStorePreset and ptzAuxiliary are about the node. PTZ commands reach the host in arrival order. The refusals of SetPreset the library can decide (ter:TooManyPresets, ter:NoToken, ter:MovingPTZ) are made before ptzStorePreset is asked, and a token it returns that another preset of the node holds replaces that preset.
Stream addresses. getStreamUri and getSnapshot return a URI; the media path is not in the library. StreamRequest carries the profile, its video source (which camera), encoder and metadata stream, the media service that asked, the transport normalised and as written (StreamRequest::protocolAsWritten), the multicast and secure axes, and RequestOrigin, the address the client reached - an RtspOverHttp answer uses that port (Media2 §5.4.1). invalidAfterConnect, invalidAfterReboot and timeout of StreamUri and Snapshot are written on the ver10 path only, where a value the schema fixes is normalised and reported as DiagnosticCode::MediaAddressNormalised. An address longer than 128 octets is answered unchanged and reported as DiagnosticCode::MediaAddressOversized.
Synchronisation points. SetSynchronizationPoint of either media service calls synchronizeVideoEncoder with the encoder of the profile (add an I-frame now) and then synchronizeMetadata with its metadata stream (open a new document). Neither carries a Generation.
Multicast. The library records the profiles a client started in Config::stateDirectory, and the host sends the RTP packets. multicastStreaming is called once per change with a MulticastOp - profile, encoder, metadata stream, the group in force and the direction; a repeated Stop reaches nobody. What the last run left started is handed over after start() with MulticastOp::resumed set. A refusal of the host does not change the client’s answer and is reported as DiagnosticCode::MulticastCommandRefused.
Push notifications. deliverNotification is handed one finished HTTP request (Notification::request) and the address the client subscribed with (Notification::address). The host decides whether that address may be dialled, and sends the request from a thread of its own. A refused delivery is not retried and its messages are dropped; Config::maxNotificationFailures consecutive refusals destroy the subscription.
What a refusal becomes. A failed Result a hook answers is the answer the client gets: the fault chain of its FaultCode (Table 10), with Fault::reason as the Detail - so a reason must carry no secret. Two hooks put every refusal on a chain of their own: checkConflicts on env:Receiver / ter:Action / ter:ConfigurationConflict, and the change is not made; ptzGetStatus on env:Receiver / ter:Action / ter:NoStatus. getStreamUri maps OperationProhibited to env:Sender / ter:OperationProhibited / ter:StreamConflict, and its other codes follow Table 10. multicastStreaming and deliverNotification refuse nothing a client sees.
A hook must not throw. An exception out of a hook that serves a request is caught: the request is answered env:Receiver / ter:CriticalError (HTTP 500), reported as DiagnosticCode::HttpHandlerThrew, and the loop goes on. A getStreamUri that throws after waiting on the readiness gate is answered HTTP 503 (DiagnosticCode::HttpRequestDeferred); a multicastStreaming that throws while resuming is reported as DiagnosticCode::HttpLoopFailure and that profile is not resumed; an exception out of deliverNotification counts as a refusal.
Caller struct
The first argument of every hook: who asked, and what they named. Structures declaration:
struct Caller
{
Principal principal;
std::string token;
TokenResolutionOutcome outcome{TokenResolutionOutcome::Named};
std::string diagnostic;
std::optional<PtzOrigin> ptz;
};
struct Principal
{
std::string name;
AccessLevel level{AccessLevel::Anonymous};
};
struct PtzOrigin
{
EntityId profile{OCONNECTOR_INVALID_ENTITY_ID};
std::optional<EntityId> videoSource;
EntityId node{OCONNECTOR_INVALID_ENTITY_ID};
std::optional<EntityId> actuator;
};
Table 6 - Caller fields.
| Field | Description |
|---|---|
principal | Identity the request authenticated as: its name and its AccessLevel. An anonymous request - a PRE_AUTH operation such as GetSystemDateAndTime - is AccessLevel::Anonymous with an empty name, never an absent value. |
token | Token of the entity the client wrote, exactly as written. Empty for an operation that carries no token. |
outcome | How that token resolved: TokenResolutionOutcome::Named, or FellBack under UnknownTokenPolicy::FallbackToDefault. A token that names nothing faults before any hook runs under Reject, so there is no “unknown”. |
diagnostic | What the library has to say about the resolution, such as the substitution of a fallback; empty when the token named its entity exactly. |
ptz | Set for the six PTZ hooks, unset for every other: PtzOrigin::profile, PtzOrigin::videoSource (which camera; unset when the profile has none), PtzOrigin::node, and PtzOrigin::actuator, the mechanism this call moves (unset for the hooks about the node). Unset for validatePtzConfiguration and applyPtzConfiguration, which are addressed by a node token: PtzConfig::node names the node. |
Result class
Result<T> is what a hook answers: a value, or a fault. Result<void> is the specialisation of an answer that carries no value. Both are declared in OConnectorTypes.h.
Table 7 - Result members.
| Member | Description |
|---|---|
Result(const T& value), Result(T&& value) | A successful result carrying the value. Not in Result<void>, whose default constructor is the successful result. |
Result(FaultCode code, std::string reason = {}) | A failed result. reason becomes the Detail of the fault a client receives. |
Result(Fault fault) | A failed result out of a Fault (code and reason); the code must not be FaultCode::None. |
isOk(), explicit operator bool() | TRUE if the result is successful. |
value() | The value of a successful result. Calling it on a failed result aborts the process, in every build: check isOk() first. Not in Result<void>. |
fault() | The Fault of a failed result; FaultCode::None on a successful one. |
Result<StreamUri> refused(FaultCode::InvalidArgVal, "no stream on this profile");
if (!refused)
{
std::cout << "refused: " << refused.fault().reason << std::endl;
}
StreamUri address;
address.uri = "rtsp://192.0.2.10:554/camera1";
const Result<StreamUri> answered(address);
if (answered.isOk())
{
std::cout << "answered: " << answered.value().uri << std::endl;
}
Result<void> done;
std::cout << (done.isOk() ? "done" : "failed") << std::endl;
Diagnostic struct
What the log sink is handed: Config::log and the sink of the constructor are a LogSink, which is std::function<void(const Diagnostic&)>. Structure declaration:
struct Diagnostic
{
LogLevel level{LogLevel::Info};
DiagnosticCode code{DiagnosticCode::Unspecified};
std::string_view message;
std::optional<EntityId> entity;
std::optional<Principal> identity;
};
Table 8 - Diagnostic fields.
| Field | Description |
|---|---|
level | Severity: LogLevel::Error, Warning, Info or Debug. |
code | What the message is about, stable across every wording of it (see DiagnosticCode enum). |
message | The sentence, in English, borrowed for the duration of the call: a sink that keeps it copies it. Bytes a peer sent appear only escaped (printable ASCII as it is, every other byte as \xNN, cut after 96 bytes); text the host configured appears as written. A password, a nonce, a key or a login name a client asserted never appears. |
entity | Entity the message is about; unset for a message about the device. |
identity | Identity the message is about: set only on a line about a caller, and only for an identity the library verified. |
A logger that wants only text prints message; one with a structured code channel maps DiagnosticCode onto its own.
DiagnosticCode enum
The stable code of a diagnostic. The enumeration is append-only: a value is never reused and its meaning never changes, so a host may file and count by it. Enum declaration:
enum class DiagnosticCode : uint32_t
{
/// No code of its own. A sink may still print the message.
Unspecified = 0,
/// init refused the configuration; the reason is the message.
InitRefused = 1000,
/// The library is initialised.
Initialised = 1001,
/// Factory state: no user is configured.
FactoryState = 1002,
/// A claimed profile is not served whole by this build.
ProfileGap = 1003,
/// start refused; the reason is the message.
StartRefused = 1004,
/// The library is serving.
Started = 1005,
/// The library has stopped.
Stopped = 1006,
/// TLS is served, and on which port.
TlsServed = 1007,
/// A declaration was refused and nothing was created.
DeclarationRefused = 2000,
/// A declaration stands, and something about it had to be said anyway.
DeclarationAdapted = 2001,
/// A call named an entity that was never declared.
UndeclaredEntity = 2002,
/// An entity was marked available or unavailable.
AvailabilityChanged = 2003,
/// The hardware could not apply a configuration the library had accepted.
HardwareRefusedConfiguration = 2004,
/// An event was dropped rather than delivered.
EventDropped = 2005,
/// A confirmation older than the last one accepted was discarded whole.
StaleConfirmationDiscarded = 2006,
/// A confirmation named a generation the library never handed out.
UnexpectedGeneration = 2007,
/// The hardware did not confirm a generation within its window.
ConfirmationDeadlineMissed = 2008,
/// A reported value the declaration of the entity cannot carry was dropped.
StateNotCarried = 2009,
/// A burst of configuration changes of one entity was merged.
CommandsCoalesced = 2010,
/// A change was applied while its burst was still arriving.
CommandDelayCapped = 2011,
/// A host answered an address longer than 128 octets.
MediaAddressOversized = 2012,
/// Multicast streaming cannot outlive the run: no state directory.
MulticastStateNotRecorded = 2013,
/// A ver10 address field the schema fixes was normalised.
MediaAddressNormalised = 2014,
/// A client wrote an imaging control ImagingConfig does not carry.
ImagingSettingNotCarried = 2015,
/// A focus move turned autofocus off and the declaration has no Manual.
AutoFocusNotReflected = 2016,
/// Notification producers are announced and deliverNotification is empty.
NotificationDeliveryMissing = 2017,
/// A camera capability is declared and the camera pair is empty.
VideoSourceControlMissing = 2018,
/// An AutoStart multicast group is declared and multicastStreaming is empty.
MulticastHookMissing = 2019,
/// The host refused to start or stop multicast streaming.
MulticastCommandRefused = 2020,
/// A moving PTZ node declares no default speed or no default timeout.
PtzDefaultsMissing = 2021,
/// A host reported more addresses of one origin than IPv4 carries.
NetworkAddressNotCarried = 2022,
/// A PTZ position could not be expressed in the node's default space.
PtzStatusSpaceNotConverted = 2023,
/// A member of a ConfigReflection was dropped.
ReflectionNotCarried = 2024,
/// A reflectPreset or removeReflectedPreset was refused.
PtzPresetReflectionRefused = 2025,
/// A preset stored through ptzStorePreset could not be adopted.
PtzPresetNotAdopted = 2026,
/// A credential arrived in a scheme this device does not accept.
SchemeNotAccepted = 3000,
/// A WS-UsernameToken was refused.
UsernameTokenRefused = 3001,
/// An HTTP digest credential was refused.
DigestRefused = 3002,
/// The operation needs an identity and none was presented.
IdentityNeeded = 3003,
/// An identity was served an operation of a class it is granted.
RequestAuthorized = 3004,
/// A client changed the roster or the digest hashing algorithms.
SecurityStateChanged = 3005,
/// The HTTP listener started or stopped.
HttpListenerLifecycle = 4000,
/// The event loop itself failed.
HttpLoopFailure = 4001,
/// The connection table is full or over its warning mark.
HttpConnectionLimit = 4002,
/// A connection ran out of one of its deadlines.
HttpConnectionDeadline = 4003,
/// A connection failed or was given up.
HttpConnectionFailure = 4004,
/// A request was refused by the framing rules or the size limits.
HttpRequestRefused = 4005,
/// A handler threw and the request was answered as a fault.
HttpHandlerThrew = 4006,
/// A TLS handshake completed, was refused or ran out of its deadline.
HttpTlsHandshake = 4007,
/// The Host field of a request is not a host or host:port.
HttpAuthorityRefused = 4008,
/// A host handler kept the loop thread past the budget.
HandlerBudgetExceeded = 4009,
/// An answer was deferred, resumed or given up.
HttpRequestDeferred = 4010,
/// An answer that could have waited was given at once: too many waiting.
HttpDeferralRefused = 4011,
/// An interim "100 Continue" was written.
HttpContinueAnswered = 4012,
/// A document owed to a client could not be written.
XmlNotWritten = 5000,
/// A header block marked mustUnderstand was not understood.
XmlHeaderNotUnderstood = 5001,
/// The request body carried no SOAP message.
XmlNoSoapMessage = 5002,
/// Discovery started or stopped.
DiscoveryLifecycle = 6000,
/// Discovery could not be served; the device answers but is not found.
DiscoveryNotServed = 6001,
/// A datagram was dropped, refused or not matched.
DiscoveryDatagramDropped = 6002,
/// The device does not announce itself, or has no identifier.
DiscoveryNotAnnounced = 6003,
/// A message could not be composed or sent.
DiscoverySendFailed = 6004,
/// Discovery state could not be recorded and will not survive a restart.
DiscoveryStateNotRecorded = 6005,
/// What the device announces changed while it was serving.
DiscoveryAnnouncementChanged = 6006,
/// The random generator had nothing to give.
RandomUnavailable = 7000,
/// A client loaded or deleted TLS material, or init could not reach it.
CertificateMaterialChanged = 7001
};
Table 9 - ranges of DiagnosticCode.
| Range | Subsystem |
|---|---|
| 0 | unclassified: a message with no code of its own |
| 1000 - 1999 | lifecycle: init, start, stop and the configuration they read |
| 2000 - 2999 | the entity model: declarations, availability, state, events |
| 3000 - 3999 | authentication and authorization |
| 4000 - 4999 | the HTTP front and its event loop |
| 5000 - 5999 | reading and writing XML |
| 6000 - 6999 | WS-Discovery |
| 7000 - 7999 | cryptography and key material |
RequestAuthorized and SecurityStateChanged record a success, for an audit log. SecurityStateChanged carries the kind of the change and the resulting count, never a login name. CertificateMaterialChanged carries, on a load, the file names the material was written to, never the bytes of a key.
Enums
The enums a host meets in Config, in the answers of its hooks and in the diagnostics. Enum declarations:
enum class Profile
{
S,
T,
M
};
enum class AuthScheme
{
HttpDigest,
UsernameToken
};
enum class UnknownTokenPolicy
{
Reject,
FallbackToDefault
};
enum class TokenResolutionOutcome
{
Named,
FellBack
};
enum class LogLevel
{
Error,
Warning,
Info,
Debug
};
enum class FaultCode
{
None,
InvalidArgVal,
ConfigModify,
ConfigurationConflict,
NoConfig,
NotAuthorized,
ActionNotSupported,
OperationProhibited
};
Profile, AuthScheme and UnknownTokenPolicy are described with Config, TokenResolutionOutcome with Caller and LogLevel with Diagnostic. FaultCode is the code of a failed Result, and each code is answered on the chain below, env:Sender as HTTP 400 and env:Receiver as HTTP 500, with the text Core Table 4 gives the chain as the Reason and Fault::reason as the Detail.
Table 10 - FaultCode values.
| Value | Fault a client receives |
|---|---|
None | in a failed result: env:Receiver / ter:Action |
InvalidArgVal | env:Sender / ter:InvalidArgVal |
ConfigModify | env:Sender / ter:InvalidArgVal / ter:ConfigModify |
ConfigurationConflict | env:Receiver / ter:Action / ter:ConfigurationConflict |
NoConfig | env:Sender / ter:InvalidArgVal / ter:NoConfig |
NotAuthorized | env:Sender / ter:NotAuthorized |
ActionNotSupported | env:Receiver / ter:ActionNotSupported |
OperationProhibited | env:Sender / ter:OperationProhibited |
The other enums name the values of one ONVIF® schema type each and are documented at their declarations:
Table 11 - other enums.
| Enum | Values | Used by |
|---|---|---|
AccessLevel | Administrator, Operator, User, Extended, Anonymous | User, UserRecord, Principal |
TimeMode | Manual, Ntp | getTimeMode, SystemTime, TimeSettings |
ResetType | Hard, Soft | factoryDefault |
LogType | System, Access | getSystemLog |
NetworkChange | Interfaces, Protocols, DefaultGateway, Hostname, Dns, Ntp, AddressFilter, ZeroConfiguration | setNetwork, getNetwork |
AddressOrigin | Manual, FromDHCP, LinkLocal | NetworkAddress::origin |
AddressFilterMode | Allow, Deny | NetworkConfig |
VideoSourceChange | Configuration, SensorMode, Overlays | VideoSourceConfig::change |
MoveType | Absolute, Relative, Continuous | PtzMove |
PresetAction | Set, Goto, Remove, SetHome, GotoHome | PresetOp |
MediaServiceVersion | Ver10, Ver20 | StreamRequest, SnapshotRequest |
StreamProtocol | RtpOverUdp, RtpOverRtspTcp, RtpOverRtspHttp | StreamRequest |
PropertyStage | Current, Deleted | Event::stage |
RelayMode, RelayIdleState, RelayLogicalState | Monostable, Bistable; Open, Closed; Inactive, Active | relay declaration and hooks |
DigitalIdleState | Open, Closed | digital input declaration |
Codec, VideoEncodingProfile | Jpeg, Mpeg4, H264, H265; Simple, AdvancedSimple, Baseline, Main, Main10, Extended, High | encoder declaration and configuration |
MetadataCompression | None, Gzip, Exi | metadata declaration and configuration |
RotateMode, SceneOrientationMode | Off, On, Auto; Auto, Manual | video source declaration and configuration |
ExposureMode, ExposurePriority, AutoFocusMode, IrCutFilterMode, WhiteBalanceMode, BacklightCompensationMode, WideDynamicMode, ImageStabilizationMode | the modes of tt:ImagingSettings20 | imaging declaration and configuration |
EFlipMode, ReverseMode | Off, On, Extended; Off, On, Auto, Extended | PTZ control direction |
PtzSpaceKind | AbsolutePanTilt, AbsoluteZoom, RelativePanTilt, RelativeZoom, ContinuousPanTilt, ContinuousZoom, PanTiltSpeed, ZoomSpeed | PtzExtraSpaceDecl |
OsdType | Text, Image, Extended | OSD declaration and configuration |
ItemType | Boolean, Int, Float, String | analytics parameters |
The address a client is told
An operation addressed by a request answers with the authority of that request. GetServices and the Device block of GetCapabilities compose their XAddr from the Host field the client wrote and from whether the connection is a TLS one, the port included (Core §8.1.2.2). Behind a proxy - a device that shares port 80 between a web interface and ONVIF®, for example - the client’s Host is the address that works.
An announcement uses the announced address. A Hello, a Bye and a ProbeMatches answer no request, so they carry Config::announcedAddress, Config::announcedPort and Config::announcedTlsPort. Each left empty or zero falls back to what is bound - Config::bindAddress, Config::port and the TLS port - and with nothing configured, to the non-loopback IPv4 addresses of the host, one http URI each, plus the https form when a TLS port is configured. A device behind a proxy sets the three announced fields to the outside of that proxy. The same values answer a request that named no usable authority.
WS-Discovery is not confined by Config::bindAddress. Its socket binds every interface, joins the multicast group on each non-loopback IPv4 interface that is up and sends each Hello and Bye on each of them. A Probe is answered to the prober alone, and only when the prober is on the network of one of the host’s interfaces. The interface list is read once, when start() opens discovery, so an interface that comes up later is served only after stop() and start().
Host is validated before it is used: exactly one Host field of at most 259 bytes, with no userinfo, path, query, fragment, space or tab; a port of one to five digits with no leading zero, inside 1..65535; a host that is a bracketed IPv6 literal with no zone id or a name whose labels are non-empty, at most 63 bytes and free of leading and trailing hyphens. Any other value is ignored, the answer falls back to the configured or derived address, and DiagnosticCode::HttpAuthorityRefused is reported. A proxy that rewrites Host makes the device answer with the rewritten value.
TLS
HTTPS is served on a second listening socket of the same event loop when Config::tls enables it. Structure declaration:
struct TlsConfig
{
bool enabled{false};
uint16_t port{443};
std::string certificatePath;
std::string privateKeyPath;
bool requireTls{false};
std::string trustAnchorPath;
};
Table 12 - TlsConfig fields.
| Field | Default | Description |
|---|---|---|
enabled | false | TRUE opens the HTTPS listener. Refused by init under Mode::Fips. |
port | 443 | Port of the HTTPS listener. 0 binds a free port, which is advertised to nobody. |
certificatePath | empty | PEM certificate chain. |
privateKeyPath | empty | PEM private key: a regular file of at most 1 MiB holding an unencrypted key, read by the library itself when start() runs. A key protected by a passphrase is refused, and no passphrase is ever asked for, on a terminal or anywhere else. |
requireTls | false | Not in force. Accepted and read by nothing: a host that sets it still serves every operation over plain HTTP, and no warning says so. |
trustAnchorPath | empty | Not in force. Accepted and read by nothing: the library opens no outgoing connection. |
The policy is the library’s and is not configurable: TLS 1.2 minimum and TLS 1.3 maximum, AEAD cipher suites with forward secrecy only, OpenSSL security level 2, no compression, no renegotiation, no session resumption and no early data. The key exchange groups are X25519, P-384 and P-256, of those that the providers of the system OpenSSL configuration offer: a group no provider offers is left out of the handshake rather than refusing the start. The context is created on the default OpenSSL library context of the process, so TLS is not served under Mode::Fips: init refuses enabled together with that mode.
- TLS asked for and not servable refuses
start(): a missing certificate, a key that does not match it, an encrypted key, a file OpenSSL will not read. - The handshake is bounded at 3000 ms from the accept, and a stalled handshake holds its own connection and nothing else.
- The
httpsXAddr is advertised inHelloandProbeMatcheswhen a TLS port is configured, except forTlsConfig::port0. An operation reached over TLS reports anhttpsaddress whether or not a TLS port is configured (see The address a client is told).
Material a client uploads is presented only where TlsConfig names it. LoadCertificateWithPrivateKey, GetCertificates and DeleteCertificates keep one PEM certificate and one unencrypted PEM private key per record under Config::stateDirectory, and answer ter:ActionNotSupported without one; the rest of the certificate surface belongs to the Advanced Security service and is not served. An upload does not reconfigure the running listener: start() reads TlsConfig::certificatePath and TlsConfig::privateKeyPath, so uploaded material is presented by a device whose TlsConfig names <stateDirectory>/certificates/<id>.crt.pem and <stateDirectory>/certificates/<id>.key.pem - the id being what GetCertificates reports - from its next start(). An upload reports the file names it wrote as CertificateMaterialChanged (see DiagnosticCode enum). No upload or delete touches the files TlsConfig names.
Users, access classes and factory state
Every operation carries an access class, and the access classes follow the default access policy of ONVIF® Core §5.9.4.3 (Table 7): the identity a request authenticated as is measured against it. Config::users is the whole roster: the library invents no account, and init copies it into a store of its own, sealing the passwords when AuthScheme::UsernameToken is accepted. GetUsers, CreateUsers, DeleteUsers, SetUser and SetHashingAlgorithm are served against that store without a hook. GetUsers answers no password. SetHashingAlgorithm decides which digest algorithms a challenge offers and a credential may use; MD5 is what an absent algorithm parameter means. Structures declaration:
struct User
{
std::string name;
std::string password;
AccessLevel level{AccessLevel::User};
std::vector<std::string> roles;
};
struct UserRecord
{
std::string name;
AccessLevel level{AccessLevel::User};
std::vector<std::string> roles;
};
enum class AccessLevel
{
Administrator,
Operator,
User,
Extended,
Anonymous
};
- Levels. The order of
AccessLevelcarries no privilege ordering. AnExtendeduser is authorized by its roles (User::roles), and the rolesonvif:Administrator,onvif:Operatorandonvif:Userare equivalent to the levels of the same name (Core §5.9.4.5). A custom role grants nothing by default. - The rules of a roster, which
initenforces: a name that is not empty, not repeated and at most 256 bytes; a password of at most 1024 bytes; no user of theAnonymouslevel; anExtendeduser with at least one role and roles on no other level; at most 32 roles per user, none empty, none with a space or theonvif:prefix. WithAuthScheme::UsernameToken, the roster and the names the sealed store holds beyond it may not exceed 256. - An entry with no password takes the sealed one. An entry whose password is empty and whose name the sealed store carries takes the sealed password. Any other empty password is refused, and the refusal names the account. A sealed record whose name the roster does not carry is removed at that
init. - Keeping a roster across a restart.
Config::onUsersChangedhands the host the roster after every change a client makes, asUserRecordvalues without passwords. The host stores them in its own configuration and passes them back asConfig::userswith empty passwords: the library seals the passwords, the host persists everything else. That file holds eachAccessLevel, so protect it as the rest of the configuration. The host is told before the client, so a response that then fails to go out leaves the file ahead of the client. Without the callback, a user a client created lives until the process ends. PRE_AUTHis answered without a credential:GetSystemDateAndTime,GetHostname,GetCapabilities,GetServices,GetWsdlUrl,GetEndpointReference,GetRelayOutputOptionsand theGetServiceCapabilitiesof every service.- Two refusals, two answers. A genuine identity without the right is refused with
ter:NotAuthorized; a request with no identity for an operation that needs one gets HTTP 401 and the digest challenge of Core §5.9.1.SetSystemDateAndTime,GetSystemLog,SystemReboot,SetSystemFactoryDefaultand the five user and digest operations are for Administrators only. - An unknown operation is answered before authorization, with
env:Receiver/ter:ActionNotSupported. - Factory state. An empty
Config::usersis factory state, which isFactoryState reports; the library has no default credentials. In factory state exactly one operation,tds:CreateUsers, is reachable with no credential, so an anonymous peer can create the first accounts - plan for it. The opening covers that operation alone, closes with the first account of any level, requires the first roster to contain an Administrator, and refuses a batch of more than 256 accounts. A device that ships withConfig::usersconfigured never opens it.
Key encryption key
AuthScheme::UsernameToken - accepted by default, and mandatory for a Profile S device (Profile S §7.1.3) - is verified by recomputing Base64(SHA-1(nonce + created + password)), so the device needs the passwords themselves. They are kept in one file sealed with AES-256-GCM, and the library never holds the key that seals it: it calls Config::provideKek every time it needs the key, erases what it was given afterwards, and writes the key nowhere. Structures declaration:
struct SecretStorePath
{
std::string value;
};
struct SecretKey
{
std::vector<uint8_t> bytes;
};
Config::secretStore and Config::provideKek are both mandatory when AuthScheme::UsernameToken is in Config::authSchemes, which is the default, whatever profile the device claims; a host that does not want a store removes the scheme (config.authSchemes = {AuthScheme::HttpDigest}). init refuses the configuration without them, and refuses a store file that is not a regular file, cannot be read, created or written, is not a store, or does not open with the key supplied. A store holds at most 256 records.
- Provenance. 32 bytes from a source the host trusts, never something the device can be talked into recomputing. In order of preference: a hardware root of trust (a TPM, a secure element, an ARM TrustZone service, a fuse-backed unique key), a key sealed by the platform and released only to this application, a key derived from a device-unique secret that never leaves the hardware. A key derived from anything a client can read - the serial number, the MAC address, the model, the endpoint reference - is not a key, and neither is a key compiled into the host.
- Lifetime. The same 32 bytes across a reboot, or the store cannot be opened and every account is lost. The provider is called outside every library lock - by
init, and on the loop thread when a client changes the roster - so it may fetch the key lazily; a slow provider holds every request while a client’s change is served. A key of another length, a failedResultand an exception are each a refusal. - Rotation. There is no re-key: a store opens only under the key it was sealed with, so a provider that answers a new key makes every later roster change and the next
initfail. Rotate by re-commissioning: stop the service, delete the store, andinitwith the new key and aConfig::userscarrying every password. A lost key makes the store unrecoverable, with the same remedy. - What it does not protect against. A process that can read the memory of this one reads the passwords. Encryption at rest protects the flash chip, the backup, the returned unit and the stolen card.
- Removal. The library deletes no file at rest, not on destruction and not on
SetSystemFactoryDefault. A host that decommissions a device or resets it deletes the store named byConfig::secretStoreand, inConfig::stateDirectory,certificates/(unencrypted private keys),discovery-modeandmulticast; it keepswsdd-instance, so the WS-Discovery instance identifier never decreases.DeleteCertificatesis the one removal a client can order.
Never commit a store file to a repository or copy it into a firmware image.
Threading rules for host callbacks
The library runs one thread of its own, the event loop start() creates: it accepts connections, parses requests, calls the hooks and writes the responses, one request at a time. A host calls into the library from its own threads, at any time, concurrently with the loop.
Table 13 - who may call what, and from which thread.
| Entry point | Callable from |
|---|---|
| the ten declare methods | any host thread, before init; the connector serialises them |
redeclareVideoSource, redeclareVideoEncoder, redeclarePtz | any host thread, any time |
init | any host thread, while the service is not running |
start | any host thread, and again after stop() |
stop | any thread, a handler on the loop thread included |
isFactoryState, getVersion | any thread, any time |
setAvailable, reportState, reflect, reflectPreset, removeReflectedPreset, notify | any host thread, any time - before init, while the service runs and after stop() |
Handlers | called by the library on the loop thread, one at a time, in request order |
Config::log and the sink of the constructor | called by the library from whichever thread produced the message: the loop thread and every host thread that calls into the connector |
Config::provideKek | called by init on its calling thread, and on the loop thread when a client’s change of the roster re-seals the store |
Config::onUsersChanged, Config::onScopesChanged | called by the library on the loop thread, after the change |
IMetadataController | never called |
- No host code runs under a library lock. A hook, the log sink,
Config::provideKek,Config::onUsersChangedandConfig::onScopesChangedare called with no lock held, so they may call back into the library -setAvailablefrom a log sink,stop()from a handler. - The log sink must be thread-safe: it is called from host threads too. An exception it throws costs that one diagnostic.
- A blocking handler stalls all request serving: nothing else is read or answered, no timer fires, and a
stop()from a host thread waits for the handler. The budget is 50 ms of wall time per handler call; a call that takes longer is reported afterwards asDiagnosticCode::HandlerBudgetExceeded, naming the operation and the milliseconds, and cannot be interrupted.validate*hooks only judge,apply*hooks hand the work on and return, and anything that waits - hardware, a notification address, a file - runs on a thread of the host.getTimeMode,getTimeSettingsandgetNetworkwithNetworkChange::Hostnameare reached by anonymous clients: blocking in them hands anyone who can open a socket the loop thread. stop()from a handler does not wait: it asks the loop to end and returns at once, the request in flight is still answered, andinit()andstart()are refused until astop()from a host thread or the destructor has released what the loop holds.- Never destroy a connector on the loop thread - in a handler, in the log sink or in anything else the library calls - nor while another thread is inside one of its methods.
Metadata contract with the video service
ONVIF® states the metadata requirements of Profile M of the device, and the work is split between this library and the video service of the host: the configuration is the library’s, the metadata document is the video service’s. No detection ever enters the library.
Table 14 - who owns what.
| Area | Owner |
|---|---|
MetadataConfiguration - name, filters, compression, geolocation, analytics, multicast, session timeout - and its operations in both media services | OConnector, judged against MetadataDecl, reaching the host through Handlers::validateMetadata and Handlers::applyMetadata |
GetProfiles, GetStreamUri and the readiness of a track | OConnector: GetStreamUri waits for the gate of the profile’s metadata stream as for its video (Config::republishTimeout) |
SetSynchronizationPoint | OConnector, forwarded as Handlers::synchronizeMetadata: open a new document |
The analytics configuration (types, instances, the Analytics slot of a profile) | OConnector (see declareAnalytics) |
Collecting detections, assembling tt:MetadataStream, document boundaries, RTP transport | The video service |
What the video service has to do:
- Assemble one XML document per stream with the root element
tt:MetadataStream(the schema spelling, notMetaDataStream). - Cut documents on every
synchronizeMetadatait is handed and at least once a second (Streaming §5.1.2.1.1). - Set the RTP marker bit on the last packet of each document.
- Use a dynamic payload type (96-127), assigned per RTSP session.
- Describe the track as
m=applicationwith the encoding name of the compression in force -vnd.onvif.metadata,vnd.onvif.metadata+gziporvnd.onvif.metadata.exi.ext- and a clock rate of 90000, in the same RTSP session as the video of the profile. The library declares and validates the compressions (MetadataDecl::compressionTypes); the video service applies them. - Serve the transports Profile M mandates: RTP/UDP and RTP/RTSP/HTTP/TCP, and RTP/RTSP/HTTPS/TCP where supported.
- Confirm the generation through reportState with
StateUpdate::runningTRUE once the stream runs with the applied configuration and its track is published - not when the command arrived, since that report opens the readiness gate.
PTZStatus and Events are not carried in the metadata stream: with those optional filters off, the document carries a VideoAnalyticsStream only. The filters a client sets, and MetadataConfig::eventFilter, are stored and answered back; what the stream contains is the video service’s decision. An alert is an ONVIF® event and travels through the event service of the library.
Build and connect to your project
Dependencies
Table 15 - dependencies.
| Dependency | Requirement |
|---|---|
| CMake | 3.13 or newer. |
| Compiler | C++17, for Linux (see Portability). |
| CryptoProvider | CryptoProvider 1.4.0 or later: the library does not compile against an older one. A separate repository, used on its OpenSSL backend; see Build stand alone and Connect to your project. |
| OpenSSL | OpenSSL 3.0 or later: the configure step of the library refuses an older one. Every cryptographic operation goes through it. |
| Threads | The threads runtime of the platform: the library runs a thread of its own. |
OpenSSL location. The library resolves OpenSSL with the standard find_package(OpenSSL 3.0 REQUIRED), and the library and CryptoProvider are built against the same installation, which the standard OPENSSL_ROOT_DIR cache variable selects. An empty value means the system OpenSSL. The configure step prints the version and the include directory it picked up, so a wrong pickup is visible in the log:
cmake -S . -B build -DOPENSSL_ROOT_DIR=/opt/openssl-fips
The library runs on the OpenSSL backend of CryptoProvider only: a CryptoProvider target that is not on that backend stops the configure of the library. A project that adds CryptoProvider itself sets <PARENT>_CRYPTOPROVIDER_BACKEND=OPENSSL before it does, <PARENT> being the prefix it adds CryptoProvider with (see Connect to your project); stand alone, CryptoProvider selects the OpenSSL backend by itself on Linux.
The library also needs the CryptoProvider option <PARENT>_CRYPTOPROVIDER_MD5_SHA1 ON, its default: HTTP Digest uses MD5, WS-UsernameToken and WS-Discovery use SHA-1, and with the option OFF the library does not compile.
The cryptographic modes. The default mode, cr::crypto::Mode::Standard, needs nothing beyond the provider every OpenSSL build ships, so the library starts on any host. The optional approved mode, cr::crypto::Mode::Fips, needs an OpenSSL that ships a FIPS provider, which a stock distribution package usually does not (the OpenSSL 3.0.2 and 3.0.13 packages of the distributions the library is built on install no fips.so). FIPS is not an ONVIF® requirement, so it is an opt-in guarantee: an integrator who needs it builds OpenSSL with the FIPS provider enabled, points OPENSSL_ROOT_DIR at it, and describes the module in Config::cryptoFips. Asking for Mode::Fips on a host that cannot deliver it makes init fail rather than start in a weaker mode. Under Mode::Fips MD5 digest is not offered - some conformance clients authenticate with MD5 only and then cannot authenticate - and TlsConfig::enabled is refused.
A host whose operating system requires the FIPS mode (/proc/sys/crypto/fips_enabled is 1) refuses Mode::Standard: CryptoProvider refuses a Standard request there before it loads any module, and init fails with its reason, “the operating system requires the FIPS mode; initialise with Mode::Fips”. An init with Mode::Fips then establishes the mode, or fails with the reason of the FIPS module.
The library never reconfigures the OpenSSL of the host process: the providers and the property query of the mode go into a private OSSL_LIB_CTX owned by CryptoProvider, so neither an established mode nor a refused one changes what the application around the library can compute.
Build stand alone
The repository does not contain sub-repositories. The library OConnector depends on CryptoProvider, which must be cloned into the same folder as OConnector, at version 1.4.0 or later. The folder structure must be:
<work folder>
OConnector
CryptoProvider
Typical commands to build the library (use the URLs of the repositories provided to you, and a version of CryptoProvider of 1.4.0 or later):
git clone <OConnector repository URL>
git clone <CryptoProvider repository URL>
cd OConnector
mkdir build
cd build
cmake ..
make
If OConnector is the top-level CMake project, its main CMakeLists.txt adds the folder ../CryptoProvider before the library itself. If the folder does not exist, CMake stops with an error. CryptoProvider is then built with -Werror and the compile hardening flags the library itself carries (stack protector, _FORTIFY_SOURCE=2 outside a Debug build, stack clash protection, control flow protection); its tests and its example are not built. To set the backend of CryptoProvider explicitly, configure with -D_CRYPTOPROVIDER_BACKEND=OPENSSL -D_SUBMODULE_CACHE_OVERWRITE=OFF; without the second option the first one is overwritten.
CMake options
Table 16 - CMake options of the repository. In the options below ${PARENT} is the prefix defined by the top-level project (empty if OConnector is the top-level project).
| Option | Default | Description |
|---|---|---|
| ${PARENT}_OCONNECTOR | ON | Build the library. |
| ${PARENT}_SUBMODULE_CACHE_OVERWRITE | ON | Forced rewriting of the option above in the CMake cache. The top-level project sets it to OFF when it includes OConnector as a sub-repository. |
| CMAKE_BUILD_TYPE | Release (if not specified) | Build type. |
| OPENSSL_ROOT_DIR | empty (system OpenSSL) | OpenSSL installation prefix used by the library and by CryptoProvider, see Dependencies. |
| OCONNECTOR_LOCK_CHECK | ON in a Debug build and in a development configuration that carries the unit tests of the library, OFF otherwise | Compile the lock-order check into the library: every acquisition of a lock of the library is checked against the order of its locks, and a violation is reported as one line on the standard error stream, after which the process continues. The configure step prints “OConnector carries the lock-order check.” while it is on. -DOCONNECTOR_LOCK_CHECK=OFF turns it off. |
CryptoProvider has its own options under the same prefix; the library needs its OpenSSL backend.
Connect to your project
If you want to connect the OConnector library to your CMake project as source code, you can do the following. The library does not add its dependency when it is included in another project: your project must add CryptoProvider before OConnector. A parent that adds CryptoProvider after OConnector still builds, but the module is then compiled without -Werror and the hardening flags of the library, and its backend is not checked. For example, if your repository has the structure:
CMakeLists.txt
src
CMakeLists.txt
yourLib.h
yourLib.cpp
You can add the CryptoProvider and OConnector repositories as sub-repositories with the following commands (use the URLs of the repositories provided to you):
cd <your repository folder>
git submodule add <CryptoProvider repository URL> 3rdparty/CryptoProvider
git submodule add <OConnector repository URL> 3rdparty/OConnector
In your repository folder, the 3rdparty/CryptoProvider and 3rdparty/OConnector folders will be created. Alternatively, you can copy the repository folders to the 3rdparty folder of your repository. Create a CMakeLists.txt file in the 3rdparty folder. It adds CryptoProvider on its OpenSSL backend and then OConnector. The CMakeLists.txt file should contain:
cmake_minimum_required(VERSION 3.13)
################################################################################
## 3RD-PARTY
## dependencies for the project
################################################################################
project(3rdparty LANGUAGES CXX)
################################################################################
## SETTINGS
## basic 3rd-party settings before use
################################################################################
# To inherit the top-level architecture when the project is used as a submodule.
SET(PARENT ${PARENT}_YOUR_PROJECT_3RDPARTY)
# Disable self-overwriting of parameters inside included subdirectories.
SET(${PARENT}_SUBMODULE_CACHE_OVERWRITE OFF CACHE BOOL "" FORCE)
################################################################################
## CONFIGURATION
## 3rd-party submodules configuration
################################################################################
SET(${PARENT}_SUBMODULE_CRYPTOPROVIDER ON CACHE BOOL "" FORCE)
if (${PARENT}_SUBMODULE_CRYPTOPROVIDER)
SET(${PARENT}_CRYPTOPROVIDER ON CACHE BOOL "" FORCE)
SET(${PARENT}_CRYPTOPROVIDER_BACKEND OPENSSL CACHE STRING "" FORCE)
SET(${PARENT}_CRYPTOPROVIDER_TEST OFF CACHE BOOL "" FORCE)
SET(${PARENT}_CRYPTOPROVIDER_EXAMPLE OFF CACHE BOOL "" FORCE)
endif()
SET(${PARENT}_SUBMODULE_OCONNECTOR ON CACHE BOOL "" FORCE)
if (${PARENT}_SUBMODULE_OCONNECTOR)
SET(${PARENT}_OCONNECTOR ON CACHE BOOL "" FORCE)
endif()
################################################################################
## INCLUDING SUBDIRECTORIES
## Adding subdirectories according to the 3rd-party configuration
################################################################################
if (${PARENT}_SUBMODULE_CRYPTOPROVIDER)
add_subdirectory(CryptoProvider)
endif()
if (${PARENT}_SUBMODULE_OCONNECTOR)
add_subdirectory(OConnector)
endif()
The 3rdparty/CMakeLists.txt file adds the CryptoProvider and OConnector folders to your project, the dependency first. The structure of your repository will be:
CMakeLists.txt
src
CMakeLists.txt
yourLib.h
yourLib.cpp
3rdparty
CMakeLists.txt
CryptoProvider
OConnector
Next, you need to include the 3rdparty folder in the main CMakeLists.txt file of your repository, before the target that uses the library:
add_subdirectory(3rdparty)
Next, you need to include the OConnector library in your src/CMakeLists.txt file:
target_link_libraries(${PROJECT_NAME} OConnector)
CryptoProvider reaches the target through OConnector, so the target does not link it. If your project adds OConnector and does not add CryptoProvider, the configure succeeds and the build stops at the first source that includes the header of the module:
fatal error: CryptoProvider.h: No such file or directory
Example
A host that declares one camera with one H.264 stream, answers the address of that stream and serves for ten seconds: declare, init, start, a diagnostics sink, the key encryption key a username token needs, and one hook. The functions above main() stand for code of the host’s own.
#include <chrono>
#include <fstream>
#include <iostream>
#include <string>
#include <thread>
#include <utility>
#include "OConnector.h"
// The three functions below stand for code of the host's own.
// The key encryption key of the secret store, exactly 32 bytes, kept on another medium than the
// store: a secure element, a TPM, or a file the platform provisions at boot.
static rapidpixel::Result<rapidpixel::SecretKey> keyFromTheHardwareRootOfTrust()
{
rapidpixel::SecretKey key;
key.bytes.resize(32);
std::ifstream file("/run/mydevice/kek", std::ios::binary);
if (!file.read(reinterpret_cast<char*>(key.bytes.data()), static_cast<std::streamsize>(key.bytes.size())))
{
return rapidpixel::Result<rapidpixel::SecretKey>(rapidpixel::FaultCode::OperationProhibited,
"no key encryption key");
}
return rapidpixel::Result<rapidpixel::SecretKey>(key);
}
// The address the streaming plane of the host serves one camera at.
static rapidpixel::StreamUri myStreamAddress(rapidpixel::EntityId videoSource)
{
rapidpixel::StreamUri address;
address.uri = "rtsp://192.0.2.10:554/camera" + std::to_string(videoSource);
return address;
}
// What the host does while the library serves; this one serves for ten seconds.
static void myMainLoop()
{
std::this_thread::sleep_for(std::chrono::seconds(10));
}
int main()
{
// A sink is handed one Diagnostic: a severity, a stable code, the sentence, the entity and
// the identity. This one prints the sentence.
rapidpixel::OConnector connector([](const rapidpixel::Diagnostic& diagnostic)
{
std::cout << "[oconnector] " << diagnostic.message << std::endl;
});
// Declare the maximum the device can ever offer. Refused after init.
rapidpixel::VideoSourceDecl source;
source.nativeResolution = rapidpixel::Resolution{1920, 1080};
source.maxFrameRate = 30.0F;
source.boundsRange.widthRange = rapidpixel::Range<int>{1920, 1920};
source.boundsRange.heightRange = rapidpixel::Range<int>{1080, 1080};
const rapidpixel::EntityId sourceId = connector.declareVideoSource(source);
rapidpixel::VideoEncoderCodecDecl h264;
h264.codec = rapidpixel::Codec::H264;
h264.resolutions = {rapidpixel::Resolution{1920, 1080}};
h264.bitrateKbps = rapidpixel::Range<int>{512, 8192};
h264.qualityRange = rapidpixel::Range<float>{1.0F, 100.0F};
h264.frameRatesSupported = {30.0F, 25.0F, 15.0F};
h264.govLength = rapidpixel::Range<int>{1, 300};
h264.encodingInterval = rapidpixel::Range<int>{1, 1};
h264.profiles = {rapidpixel::VideoEncodingProfile::Main};
rapidpixel::VideoEncoderDecl encoder;
encoder.source = sourceId;
encoder.codecs = {h264};
const rapidpixel::EntityId encoderId = connector.declareVideoEncoder(encoder);
rapidpixel::ProfileDecl profile;
profile.name = "camera 1";
profile.videoSource = sourceId;
profile.videoEncoder = encoderId;
if (sourceId == OCONNECTOR_INVALID_ENTITY_ID || encoderId == OCONNECTOR_INVALID_ENTITY_ID ||
connector.declareProfile(profile) == OCONNECTOR_INVALID_ENTITY_ID)
{
return 1; // The reason has already reached the sink above.
}
rapidpixel::Config config;
config.port = 8080; // 80 is the ONVIF default and needs root.
// Config::authSchemes accepts WS-UsernameToken by default, and that is what makes the two
// fields below mandatory: the scheme needs the password back in the clear.
config.secretStore.value = "/var/lib/mydevice/onvif.secrets";
config.provideKek = keyFromTheHardwareRootOfTrust; // Called by init, and on every re-seal.
config.stateDirectory = "/var/lib/mydevice";
config.discovery.manufacturer = "My Company";
config.discovery.model = "My Camera";
config.users = {rapidpixel::User{"admin", "the-password-the-operator-chose",
rapidpixel::AccessLevel::Administrator, {}}};
rapidpixel::Handlers handlers;
// Every hook takes a Caller first. The request says which profile, which camera, which
// service asked, which transport and the address the client reached this device on.
handlers.getStreamUri = [](const rapidpixel::Caller&, const rapidpixel::StreamRequest& request)
{ return rapidpixel::Result<rapidpixel::StreamUri>(myStreamAddress(request.videoSource.value_or(0))); };
if (!connector.init(config, std::move(handlers)) || !connector.start())
{
return 1; // Both report their reason through the sink.
}
myMainLoop(); // The library runs its own thread; this one stays yours.
connector.stop();
return 0;
}
The program needs /var/lib/mydevice to be writable and /run/mydevice/kek to hold 32 bytes; without them init refuses the configuration, with the reason on the sink, and main() returns 1. It claims no profile (Config::profiles); a device that claims one serves what the profile mandates - Profile S, for one, push notifications (Config::maxNotificationProducers, Handlers::deliverNotification). A host that applies configuration changes installs the validate*/apply* pairs of Handlers and confirms each application with reportState; a PTZ head is shown under declarePtz; a camera that comes and goes is masked with setAvailable.
Portability
The library is standard C++17, without compiler extensions, for Linux: it reaches the network and the file system through POSIX headers (sys/socket.h, poll.h, arpa/inet.h, ifaddrs.h, unistd.h, dirent.h and others) and uses socket extensions that not every POSIX system has (accept4, SOCK_CLOEXEC, SOCK_NONBLOCK, MSG_NOSIGNAL, getifaddrs). Its sources have no Windows branch, so it does not build on Windows, neither with MSVC nor with MinGW. With the default options the configure step already stops on Windows: CryptoProvider selects its CNG backend there, and the library refuses any backend other than OpenSSL (see Dependencies). It needs OpenSSL 3.0 or newer and the threads runtime. It has no SIMD code and no option that selects an instruction set.
The library has been built with no warning in its own code by GCC 12 and 13 and by Clang 15 and 18 for x86-64, and by an aarch64 GCC cross compiler and by Clang 18 for aarch64, each in Release and in Debug, against OpenSSL 3.0.2 or 3.0.13, and its tests have passed on each of those builds - the aarch64 ones under user-mode emulation. Nothing else - another architecture, a 32-bit or big-endian target, another operating system - has been built or run.