use std::sync::Arc;
use crate::error::OnvifError;
use crate::soap::{SoapEnvelope, WsSecurityToken, find_response, parse_soap_body};
use crate::transport::{HttpTransport, Transport};
use crate::types::{
AudioEncoderConfiguration, AudioEncoderConfigurationOptions, AudioSource,
AudioSourceConfiguration, Capabilities, DeviceInfo, EventProperties, FindRecordingResults,
FocusMove, Hostname, ImagingMoveOptions, ImagingOptions, ImagingSettings, ImagingStatus,
MediaProfile, MediaProfile2, NotificationMessage, NtpInfo, OnvifService, OsdConfiguration,
OsdOptions, PtzConfiguration, PtzConfigurationOptions, PtzNode, PtzPreset, PtzStatus,
PullPointSubscription, RecordingItem, SnapshotUri, StreamUri, SystemDateTime,
VideoEncoderConfiguration, VideoEncoderConfiguration2, VideoEncoderConfigurationOptions,
VideoEncoderConfigurationOptions2, VideoEncoderInstances, VideoSource,
VideoSourceConfiguration, VideoSourceConfigurationOptions, xml_escape,
};
#[derive(Clone)]
pub struct OnvifClient {
device_url: String,
credentials: Option<(String, String)>,
utc_offset: i64,
transport: Arc<dyn Transport>,
}
impl OnvifClient {
pub fn new(device_url: impl Into<String>) -> Self {
Self {
device_url: device_url.into(),
credentials: None,
utc_offset: 0,
transport: Arc::new(HttpTransport::new()),
}
}
pub fn with_credentials(
mut self,
username: impl Into<String>,
password: impl Into<String>,
) -> Self {
self.credentials = Some((username.into(), password.into()));
self
}
pub fn with_utc_offset(mut self, offset_secs: i64) -> Self {
self.utc_offset = offset_secs;
self
}
pub fn with_transport(mut self, transport: Arc<dyn Transport>) -> Self {
self.transport = transport;
self
}
pub fn device_url(&self) -> &str {
&self.device_url
}
fn security_token(&self) -> Option<WsSecurityToken> {
self.credentials
.as_ref()
.map(|(user, pass)| WsSecurityToken::generate(user, pass, self.utc_offset))
}
async fn call(&self, url: &str, action: &str, body: &str) -> Result<String, OnvifError> {
let mut envelope = SoapEnvelope::new(body.to_string());
if let Some(token) = self.security_token() {
envelope = envelope.with_security(token);
}
Ok(self
.transport
.soap_post(url, action, envelope.build())
.await?)
}
pub async fn get_capabilities(&self) -> Result<Capabilities, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/device/wsdl/GetCapabilities";
const BODY: &str =
"<tds:GetCapabilities><tds:Category>All</tds:Category></tds:GetCapabilities>";
let xml = self.call(&self.device_url, ACTION, BODY).await?;
let body = parse_soap_body(&xml)?;
let resp = find_response(&body, "GetCapabilitiesResponse")?;
Capabilities::from_xml(resp)
}
pub async fn get_services(&self) -> Result<Vec<OnvifService>, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/device/wsdl/GetServices";
const BODY: &str = "<tds:GetServices><tds:IncludeCapability>false</tds:IncludeCapability></tds:GetServices>";
let xml = self.call(&self.device_url, ACTION, BODY).await?;
let body = parse_soap_body(&xml)?;
let resp = find_response(&body, "GetServicesResponse")?;
OnvifService::vec_from_xml(resp)
}
pub async fn get_system_date_and_time(&self) -> Result<SystemDateTime, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/device/wsdl/GetSystemDateAndTime";
const BODY: &str = "<tds:GetSystemDateAndTime/>";
let xml = self.call(&self.device_url, ACTION, BODY).await?;
let body = parse_soap_body(&xml)?;
let resp = find_response(&body, "GetSystemDateAndTimeResponse")?;
SystemDateTime::from_xml(resp)
}
pub async fn get_device_info(&self) -> Result<DeviceInfo, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/device/wsdl/GetDeviceInformation";
const BODY: &str = "<tds:GetDeviceInformation/>";
let xml = self.call(&self.device_url, ACTION, BODY).await?;
let body = parse_soap_body(&xml)?;
let resp = find_response(&body, "GetDeviceInformationResponse")?;
DeviceInfo::from_xml(resp)
}
pub async fn get_hostname(&self) -> Result<Hostname, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/device/wsdl/GetHostname";
const BODY: &str = "<tds:GetHostname/>";
let xml = self.call(&self.device_url, ACTION, BODY).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetHostnameResponse")?;
Hostname::from_xml(resp)
}
pub async fn set_hostname(&self, name: &str) -> Result<(), OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/device/wsdl/SetHostname";
let name = xml_escape(name);
let body = format!("<tds:SetHostname><tds:Name>{name}</tds:Name></tds:SetHostname>");
let xml = self.call(&self.device_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
find_response(&body_node, "SetHostnameResponse")?;
Ok(())
}
pub async fn get_ntp(&self) -> Result<NtpInfo, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/device/wsdl/GetNTP";
const BODY: &str = "<tds:GetNTP/>";
let xml = self.call(&self.device_url, ACTION, BODY).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetNTPResponse")?;
NtpInfo::from_xml(resp)
}
pub async fn set_ntp(&self, from_dhcp: bool, servers: &[&str]) -> Result<(), OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/device/wsdl/SetNTP";
let from_dhcp_str = if from_dhcp { "true" } else { "false" };
let server_els: String = servers
.iter()
.map(|s| {
format!(
"<tds:NTPManual>\
<tt:Type>DNS</tt:Type>\
<tt:DNSname>{}</tt:DNSname>\
</tds:NTPManual>",
xml_escape(s)
)
})
.collect();
let body = format!(
"<tds:SetNTP>\
<tds:FromDHCP>{from_dhcp_str}</tds:FromDHCP>\
{server_els}\
</tds:SetNTP>"
);
let xml = self.call(&self.device_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
find_response(&body_node, "SetNTPResponse")?;
Ok(())
}
pub async fn system_reboot(&self) -> Result<String, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/device/wsdl/SystemReboot";
const BODY: &str = "<tds:SystemReboot/>";
let xml = self.call(&self.device_url, ACTION, BODY).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "SystemRebootResponse")?;
Ok(resp
.child("Message")
.map(|n| n.text().to_string())
.unwrap_or_default())
}
pub async fn get_scopes(&self) -> Result<Vec<String>, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/device/wsdl/GetScopes";
const BODY: &str = "<tds:GetScopes/>";
let xml = self.call(&self.device_url, ACTION, BODY).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetScopesResponse")?;
Ok(resp
.children_named("Scopes")
.filter_map(|n| n.child("ScopeItem").map(|s| s.text().to_string()))
.collect())
}
pub async fn get_profiles(&self, media_url: &str) -> Result<Vec<MediaProfile>, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/media/wsdl/GetProfiles";
const BODY: &str = "<trt:GetProfiles/>";
let xml = self.call(media_url, ACTION, BODY).await?;
let body = parse_soap_body(&xml)?;
let resp = find_response(&body, "GetProfilesResponse")?;
MediaProfile::vec_from_xml(resp)
}
pub async fn get_stream_uri(
&self,
media_url: &str,
profile_token: &str,
) -> Result<StreamUri, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/media/wsdl/GetStreamUri";
let body = format!(
"<trt:GetStreamUri>\
<trt:StreamSetup>\
<tt:Stream>RTP-Unicast</tt:Stream>\
<tt:Transport><tt:Protocol>RTSP</tt:Protocol></tt:Transport>\
</trt:StreamSetup>\
<trt:ProfileToken>{profile_token}</trt:ProfileToken>\
</trt:GetStreamUri>"
);
let xml = self.call(media_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetStreamUriResponse")?;
StreamUri::from_xml(resp)
}
pub async fn get_snapshot_uri(
&self,
media_url: &str,
profile_token: &str,
) -> Result<SnapshotUri, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/media/wsdl/GetSnapshotUri";
let body = format!(
"<trt:GetSnapshotUri>\
<trt:ProfileToken>{profile_token}</trt:ProfileToken>\
</trt:GetSnapshotUri>"
);
let xml = self.call(media_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetSnapshotUriResponse")?;
SnapshotUri::from_xml(resp)
}
pub async fn create_profile(
&self,
media_url: &str,
name: &str,
token: Option<&str>,
) -> Result<MediaProfile, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/media/wsdl/CreateProfile";
let token_el = token
.map(|t| format!("<trt:Token>{}</trt:Token>", xml_escape(t)))
.unwrap_or_default();
let body = format!(
"<trt:CreateProfile>\
<trt:Name>{}</trt:Name>\
{token_el}\
</trt:CreateProfile>",
xml_escape(name)
);
let xml = self.call(media_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "CreateProfileResponse")?;
let p = resp
.child("Profile")
.ok_or_else(|| crate::soap::SoapError::missing("Profile"))?;
MediaProfile::from_xml(p)
}
pub async fn delete_profile(
&self,
media_url: &str,
profile_token: &str,
) -> Result<(), OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/media/wsdl/DeleteProfile";
let body = format!(
"<trt:DeleteProfile>\
<trt:ProfileToken>{profile_token}</trt:ProfileToken>\
</trt:DeleteProfile>"
);
let xml = self.call(media_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
find_response(&body_node, "DeleteProfileResponse")?;
Ok(())
}
pub async fn get_profile(
&self,
media_url: &str,
profile_token: &str,
) -> Result<MediaProfile, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/media/wsdl/GetProfile";
let body = format!(
"<trt:GetProfile>\
<trt:ProfileToken>{profile_token}</trt:ProfileToken>\
</trt:GetProfile>"
);
let xml = self.call(media_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetProfileResponse")?;
let p = resp
.child("Profile")
.ok_or_else(|| crate::soap::SoapError::missing("Profile"))?;
MediaProfile::from_xml(p)
}
pub async fn add_video_encoder_configuration(
&self,
media_url: &str,
profile_token: &str,
config_token: &str,
) -> Result<(), OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/media/wsdl/AddVideoEncoderConfiguration";
let body = format!(
"<trt:AddVideoEncoderConfiguration>\
<trt:ProfileToken>{profile_token}</trt:ProfileToken>\
<trt:ConfigurationToken>{config_token}</trt:ConfigurationToken>\
</trt:AddVideoEncoderConfiguration>"
);
let xml = self.call(media_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
find_response(&body_node, "AddVideoEncoderConfigurationResponse")?;
Ok(())
}
pub async fn remove_video_encoder_configuration(
&self,
media_url: &str,
profile_token: &str,
) -> Result<(), OnvifError> {
const ACTION: &str =
"http://www.onvif.org/ver10/media/wsdl/RemoveVideoEncoderConfiguration";
let body = format!(
"<trt:RemoveVideoEncoderConfiguration>\
<trt:ProfileToken>{profile_token}</trt:ProfileToken>\
</trt:RemoveVideoEncoderConfiguration>"
);
let xml = self.call(media_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
find_response(&body_node, "RemoveVideoEncoderConfigurationResponse")?;
Ok(())
}
pub async fn add_video_source_configuration(
&self,
media_url: &str,
profile_token: &str,
config_token: &str,
) -> Result<(), OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/media/wsdl/AddVideoSourceConfiguration";
let body = format!(
"<trt:AddVideoSourceConfiguration>\
<trt:ProfileToken>{profile_token}</trt:ProfileToken>\
<trt:ConfigurationToken>{config_token}</trt:ConfigurationToken>\
</trt:AddVideoSourceConfiguration>"
);
let xml = self.call(media_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
find_response(&body_node, "AddVideoSourceConfigurationResponse")?;
Ok(())
}
pub async fn remove_video_source_configuration(
&self,
media_url: &str,
profile_token: &str,
) -> Result<(), OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/media/wsdl/RemoveVideoSourceConfiguration";
let body = format!(
"<trt:RemoveVideoSourceConfiguration>\
<trt:ProfileToken>{profile_token}</trt:ProfileToken>\
</trt:RemoveVideoSourceConfiguration>"
);
let xml = self.call(media_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
find_response(&body_node, "RemoveVideoSourceConfigurationResponse")?;
Ok(())
}
pub async fn ptz_absolute_move(
&self,
ptz_url: &str,
profile_token: &str,
pan: f32,
tilt: f32,
zoom: f32,
) -> Result<(), OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/ptz/wsdl/AbsoluteMove";
let body = format!(
"<tptz:AbsoluteMove>\
<tptz:ProfileToken>{profile_token}</tptz:ProfileToken>\
<tptz:Position>\
<tt:PanTilt x=\"{pan}\" y=\"{tilt}\"/>\
<tt:Zoom x=\"{zoom}\"/>\
</tptz:Position>\
</tptz:AbsoluteMove>"
);
let xml = self.call(ptz_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
find_response(&body_node, "AbsoluteMoveResponse")?;
Ok(())
}
pub async fn ptz_relative_move(
&self,
ptz_url: &str,
profile_token: &str,
pan: f32,
tilt: f32,
zoom: f32,
) -> Result<(), OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/ptz/wsdl/RelativeMove";
let body = format!(
"<tptz:RelativeMove>\
<tptz:ProfileToken>{profile_token}</tptz:ProfileToken>\
<tptz:Translation>\
<tt:PanTilt x=\"{pan}\" y=\"{tilt}\"/>\
<tt:Zoom x=\"{zoom}\"/>\
</tptz:Translation>\
</tptz:RelativeMove>"
);
let xml = self.call(ptz_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
find_response(&body_node, "RelativeMoveResponse")?;
Ok(())
}
pub async fn ptz_continuous_move(
&self,
ptz_url: &str,
profile_token: &str,
pan: f32,
tilt: f32,
zoom: f32,
) -> Result<(), OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/ptz/wsdl/ContinuousMove";
let body = format!(
"<tptz:ContinuousMove>\
<tptz:ProfileToken>{profile_token}</tptz:ProfileToken>\
<tptz:Velocity>\
<tt:PanTilt x=\"{pan}\" y=\"{tilt}\"/>\
<tt:Zoom x=\"{zoom}\"/>\
</tptz:Velocity>\
</tptz:ContinuousMove>"
);
let xml = self.call(ptz_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
find_response(&body_node, "ContinuousMoveResponse")?;
Ok(())
}
pub async fn ptz_stop(&self, ptz_url: &str, profile_token: &str) -> Result<(), OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/ptz/wsdl/Stop";
let body = format!(
"<tptz:Stop>\
<tptz:ProfileToken>{profile_token}</tptz:ProfileToken>\
<tptz:PanTilt>true</tptz:PanTilt>\
<tptz:Zoom>true</tptz:Zoom>\
</tptz:Stop>"
);
let xml = self.call(ptz_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
find_response(&body_node, "StopResponse")?;
Ok(())
}
pub async fn ptz_get_presets(
&self,
ptz_url: &str,
profile_token: &str,
) -> Result<Vec<PtzPreset>, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/ptz/wsdl/GetPresets";
let body = format!(
"<tptz:GetPresets>\
<tptz:ProfileToken>{profile_token}</tptz:ProfileToken>\
</tptz:GetPresets>"
);
let xml = self.call(ptz_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetPresetsResponse")?;
PtzPreset::vec_from_xml(resp)
}
pub async fn ptz_goto_preset(
&self,
ptz_url: &str,
profile_token: &str,
preset_token: &str,
) -> Result<(), OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/ptz/wsdl/GotoPreset";
let body = format!(
"<tptz:GotoPreset>\
<tptz:ProfileToken>{profile_token}</tptz:ProfileToken>\
<tptz:PresetToken>{preset_token}</tptz:PresetToken>\
</tptz:GotoPreset>"
);
let xml = self.call(ptz_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
find_response(&body_node, "GotoPresetResponse")?;
Ok(())
}
pub async fn ptz_set_preset(
&self,
ptz_url: &str,
profile_token: &str,
preset_name: Option<&str>,
preset_token: Option<&str>,
) -> Result<String, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/ptz/wsdl/SetPreset";
let name_el = preset_name
.map(|n| format!("<tptz:PresetName>{}</tptz:PresetName>", xml_escape(n)))
.unwrap_or_default();
let token_el = preset_token
.map(|t| format!("<tptz:PresetToken>{}</tptz:PresetToken>", xml_escape(t)))
.unwrap_or_default();
let body = format!(
"<tptz:SetPreset>\
<tptz:ProfileToken>{profile_token}</tptz:ProfileToken>\
{name_el}{token_el}\
</tptz:SetPreset>"
);
let xml = self.call(ptz_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "SetPresetResponse")?;
resp.child("PresetToken")
.map(|n| n.text().to_string())
.ok_or_else(|| crate::soap::SoapError::missing("PresetToken").into())
}
pub async fn ptz_remove_preset(
&self,
ptz_url: &str,
profile_token: &str,
preset_token: &str,
) -> Result<(), OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/ptz/wsdl/RemovePreset";
let body = format!(
"<tptz:RemovePreset>\
<tptz:ProfileToken>{profile_token}</tptz:ProfileToken>\
<tptz:PresetToken>{preset_token}</tptz:PresetToken>\
</tptz:RemovePreset>"
);
let xml = self.call(ptz_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
find_response(&body_node, "RemovePresetResponse")?;
Ok(())
}
pub async fn ptz_get_status(
&self,
ptz_url: &str,
profile_token: &str,
) -> Result<PtzStatus, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/ptz/wsdl/GetStatus";
let body = format!(
"<tptz:GetStatus>\
<tptz:ProfileToken>{profile_token}</tptz:ProfileToken>\
</tptz:GetStatus>"
);
let xml = self.call(ptz_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetStatusResponse")?;
PtzStatus::from_xml(resp)
}
pub async fn ptz_goto_home_position(
&self,
ptz_url: &str,
profile_token: &str,
speed: Option<f32>,
) -> Result<(), OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/ptz/wsdl/GotoHomePosition";
let speed_el = speed
.map(|s| format!("<tptz:Speed><tt:Zoom x=\"{s}\"/></tptz:Speed>"))
.unwrap_or_default();
let body = format!(
"<tptz:GotoHomePosition>\
<tptz:ProfileToken>{profile_token}</tptz:ProfileToken>\
{speed_el}\
</tptz:GotoHomePosition>"
);
let xml = self.call(ptz_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
find_response(&body_node, "GotoHomePositionResponse")?;
Ok(())
}
pub async fn ptz_set_home_position(
&self,
ptz_url: &str,
profile_token: &str,
) -> Result<(), OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/ptz/wsdl/SetHomePosition";
let body = format!(
"<tptz:SetHomePosition>\
<tptz:ProfileToken>{profile_token}</tptz:ProfileToken>\
</tptz:SetHomePosition>"
);
let xml = self.call(ptz_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
find_response(&body_node, "SetHomePositionResponse")?;
Ok(())
}
pub async fn get_video_sources(&self, media_url: &str) -> Result<Vec<VideoSource>, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/media/wsdl/GetVideoSources";
const BODY: &str = "<trt:GetVideoSources/>";
let xml = self.call(media_url, ACTION, BODY).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetVideoSourcesResponse")?;
VideoSource::vec_from_xml(resp)
}
pub async fn get_video_source_configurations(
&self,
media_url: &str,
) -> Result<Vec<VideoSourceConfiguration>, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/media/wsdl/GetVideoSourceConfigurations";
const BODY: &str = "<trt:GetVideoSourceConfigurations/>";
let xml = self.call(media_url, ACTION, BODY).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetVideoSourceConfigurationsResponse")?;
VideoSourceConfiguration::vec_from_xml(resp)
}
pub async fn get_video_source_configuration(
&self,
media_url: &str,
token: &str,
) -> Result<VideoSourceConfiguration, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/media/wsdl/GetVideoSourceConfiguration";
let body = format!(
"<trt:GetVideoSourceConfiguration>\
<trt:ConfigurationToken>{token}</trt:ConfigurationToken>\
</trt:GetVideoSourceConfiguration>"
);
let xml = self.call(media_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetVideoSourceConfigurationResponse")?;
let node = resp
.child("Configuration")
.ok_or_else(|| crate::soap::SoapError::missing("Configuration"))?;
VideoSourceConfiguration::from_xml(node)
}
pub async fn set_video_source_configuration(
&self,
media_url: &str,
config: &VideoSourceConfiguration,
) -> Result<(), OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/media/wsdl/SetVideoSourceConfiguration";
let body = format!(
"<trt:SetVideoSourceConfiguration>\
{cfg}\
<trt:ForcePersistence>true</trt:ForcePersistence>\
</trt:SetVideoSourceConfiguration>",
cfg = config.to_xml_body()
);
let xml = self.call(media_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
find_response(&body_node, "SetVideoSourceConfigurationResponse")?;
Ok(())
}
pub async fn get_video_source_configuration_options(
&self,
media_url: &str,
config_token: Option<&str>,
) -> Result<VideoSourceConfigurationOptions, OnvifError> {
const ACTION: &str =
"http://www.onvif.org/ver10/media/wsdl/GetVideoSourceConfigurationOptions";
let inner = match config_token {
Some(tok) => format!("<trt:ConfigurationToken>{tok}</trt:ConfigurationToken>"),
None => String::new(),
};
let body = format!(
"<trt:GetVideoSourceConfigurationOptions>{inner}</trt:GetVideoSourceConfigurationOptions>"
);
let xml = self.call(media_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetVideoSourceConfigurationOptionsResponse")?;
VideoSourceConfigurationOptions::from_xml(resp)
}
pub async fn get_video_encoder_configurations(
&self,
media_url: &str,
) -> Result<Vec<VideoEncoderConfiguration>, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/media/wsdl/GetVideoEncoderConfigurations";
const BODY: &str = "<trt:GetVideoEncoderConfigurations/>";
let xml = self.call(media_url, ACTION, BODY).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetVideoEncoderConfigurationsResponse")?;
VideoEncoderConfiguration::vec_from_xml(resp)
}
pub async fn get_video_encoder_configuration(
&self,
media_url: &str,
token: &str,
) -> Result<VideoEncoderConfiguration, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/media/wsdl/GetVideoEncoderConfiguration";
let body = format!(
"<trt:GetVideoEncoderConfiguration>\
<trt:ConfigurationToken>{token}</trt:ConfigurationToken>\
</trt:GetVideoEncoderConfiguration>"
);
let xml = self.call(media_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetVideoEncoderConfigurationResponse")?;
let node = resp
.child("Configuration")
.ok_or_else(|| crate::soap::SoapError::missing("Configuration"))?;
VideoEncoderConfiguration::from_xml(node)
}
pub async fn set_video_encoder_configuration(
&self,
media_url: &str,
config: &VideoEncoderConfiguration,
) -> Result<(), OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/media/wsdl/SetVideoEncoderConfiguration";
let body = format!(
"<trt:SetVideoEncoderConfiguration>\
{cfg}\
<trt:ForcePersistence>true</trt:ForcePersistence>\
</trt:SetVideoEncoderConfiguration>",
cfg = config.to_xml_body()
);
let xml = self.call(media_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
find_response(&body_node, "SetVideoEncoderConfigurationResponse")?;
Ok(())
}
pub async fn get_video_encoder_configuration_options(
&self,
media_url: &str,
config_token: Option<&str>,
) -> Result<VideoEncoderConfigurationOptions, OnvifError> {
const ACTION: &str =
"http://www.onvif.org/ver10/media/wsdl/GetVideoEncoderConfigurationOptions";
let inner = match config_token {
Some(tok) => format!("<trt:ConfigurationToken>{tok}</trt:ConfigurationToken>"),
None => String::new(),
};
let body = format!(
"<trt:GetVideoEncoderConfigurationOptions>{inner}</trt:GetVideoEncoderConfigurationOptions>"
);
let xml = self.call(media_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetVideoEncoderConfigurationOptionsResponse")?;
VideoEncoderConfigurationOptions::from_xml(resp)
}
pub async fn get_imaging_settings(
&self,
imaging_url: &str,
video_source_token: &str,
) -> Result<ImagingSettings, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/imaging/wsdl/GetImagingSettings";
let body = format!(
"<timg:GetImagingSettings>\
<timg:VideoSourceToken>{video_source_token}</timg:VideoSourceToken>\
</timg:GetImagingSettings>"
);
let xml = self.call(imaging_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetImagingSettingsResponse")?;
ImagingSettings::from_xml(resp)
}
pub async fn set_imaging_settings(
&self,
imaging_url: &str,
video_source_token: &str,
settings: &ImagingSettings,
) -> Result<(), OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/imaging/wsdl/SetImagingSettings";
let body = format!(
"<timg:SetImagingSettings>\
<timg:VideoSourceToken>{video_source_token}</timg:VideoSourceToken>\
{settings_xml}\
<timg:ForcePersistence>true</timg:ForcePersistence>\
</timg:SetImagingSettings>",
settings_xml = settings.to_xml_body()
);
let xml = self.call(imaging_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
find_response(&body_node, "SetImagingSettingsResponse")?;
Ok(())
}
pub async fn get_imaging_options(
&self,
imaging_url: &str,
video_source_token: &str,
) -> Result<ImagingOptions, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/imaging/wsdl/GetOptions";
let body = format!(
"<timg:GetOptions>\
<timg:VideoSourceToken>{video_source_token}</timg:VideoSourceToken>\
</timg:GetOptions>"
);
let xml = self.call(imaging_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetOptionsResponse")?;
ImagingOptions::from_xml(resp)
}
pub async fn imaging_move(
&self,
imaging_url: &str,
video_source_token: &str,
focus: &FocusMove,
) -> Result<(), OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/imaging/wsdl/Move";
let body = format!(
"<timg:Move>\
<timg:VideoSourceToken>{video_source_token}</timg:VideoSourceToken>\
<timg:Focus>{}</timg:Focus>\
</timg:Move>",
focus.to_xml_body()
);
let xml = self.call(imaging_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
find_response(&body_node, "MoveResponse")?;
Ok(())
}
pub async fn imaging_stop(
&self,
imaging_url: &str,
video_source_token: &str,
) -> Result<(), OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/imaging/wsdl/Stop";
let body = format!(
"<timg:Stop>\
<timg:VideoSourceToken>{video_source_token}</timg:VideoSourceToken>\
</timg:Stop>"
);
let xml = self.call(imaging_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
find_response(&body_node, "StopResponse")?;
Ok(())
}
pub async fn imaging_get_move_options(
&self,
imaging_url: &str,
video_source_token: &str,
) -> Result<ImagingMoveOptions, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/imaging/wsdl/GetMoveOptions";
let body = format!(
"<timg:GetMoveOptions>\
<timg:VideoSourceToken>{video_source_token}</timg:VideoSourceToken>\
</timg:GetMoveOptions>"
);
let xml = self.call(imaging_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetMoveOptionsResponse")?;
ImagingMoveOptions::from_xml(resp)
}
pub async fn imaging_get_status(
&self,
imaging_url: &str,
video_source_token: &str,
) -> Result<ImagingStatus, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/imaging/wsdl/GetStatus";
let body = format!(
"<timg:GetStatus>\
<timg:VideoSourceToken>{video_source_token}</timg:VideoSourceToken>\
</timg:GetStatus>"
);
let xml = self.call(imaging_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetStatusResponse")?;
ImagingStatus::from_xml(resp)
}
pub async fn get_osds(
&self,
media_url: &str,
config_token: Option<&str>,
) -> Result<Vec<OsdConfiguration>, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/media/wsdl/GetOSDs";
let inner = config_token
.map(|t| format!("<trt:OSDToken>{t}</trt:OSDToken>"))
.unwrap_or_default();
let body = format!("<trt:GetOSDs>{inner}</trt:GetOSDs>");
let xml = self.call(media_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetOSDsResponse")?;
OsdConfiguration::vec_from_xml(resp)
}
pub async fn get_osd(
&self,
media_url: &str,
osd_token: &str,
) -> Result<OsdConfiguration, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/media/wsdl/GetOSD";
let body = format!("<trt:GetOSD><trt:OSDToken>{osd_token}</trt:OSDToken></trt:GetOSD>");
let xml = self.call(media_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetOSDResponse")?;
resp.child("OSDConfiguration")
.ok_or_else(|| crate::soap::SoapError::missing("OSDConfiguration").into())
.and_then(OsdConfiguration::from_xml)
}
pub async fn set_osd(&self, media_url: &str, osd: &OsdConfiguration) -> Result<(), OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/media/wsdl/SetOSD";
let body = format!("<trt:SetOSD>{}</trt:SetOSD>", osd.to_xml_body());
let xml = self.call(media_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
find_response(&body_node, "SetOSDResponse")?;
Ok(())
}
pub async fn create_osd(
&self,
media_url: &str,
osd: &OsdConfiguration,
) -> Result<String, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/media/wsdl/CreateOSD";
let body = format!("<trt:CreateOSD>{}</trt:CreateOSD>", osd.to_xml_body());
let xml = self.call(media_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "CreateOSDResponse")?;
resp.child("OSDToken")
.map(|n| n.text().to_string())
.ok_or_else(|| crate::soap::SoapError::missing("OSDToken").into())
}
pub async fn delete_osd(&self, media_url: &str, osd_token: &str) -> Result<(), OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/media/wsdl/DeleteOSD";
let body =
format!("<trt:DeleteOSD><trt:OSDToken>{osd_token}</trt:OSDToken></trt:DeleteOSD>");
let xml = self.call(media_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
find_response(&body_node, "DeleteOSDResponse")?;
Ok(())
}
pub async fn get_osd_options(
&self,
media_url: &str,
config_token: &str,
) -> Result<OsdOptions, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/media/wsdl/GetOSDOptions";
let body = format!(
"<trt:GetOSDOptions>\
<trt:ConfigurationToken>{config_token}</trt:ConfigurationToken>\
</trt:GetOSDOptions>"
);
let xml = self.call(media_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetOSDOptionsResponse")?;
OsdOptions::from_xml(resp)
}
pub async fn get_profiles_media2(
&self,
media2_url: &str,
) -> Result<Vec<MediaProfile2>, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/media/wsdl/GetProfiles";
const BODY: &str = "<tr2:GetProfiles/>";
let xml = self.call(media2_url, ACTION, BODY).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetProfilesResponse")?;
MediaProfile2::vec_from_xml(resp)
}
pub async fn get_stream_uri_media2(
&self,
media2_url: &str,
profile_token: &str,
) -> Result<String, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/media/wsdl/GetStreamUri";
let body = format!(
"<tr2:GetStreamUri>\
<tr2:Protocol>RTSP</tr2:Protocol>\
<tr2:ProfileToken>{profile_token}</tr2:ProfileToken>\
</tr2:GetStreamUri>"
);
let xml = self.call(media2_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetStreamUriResponse")?;
resp.child("Uri")
.map(|n| n.text().to_string())
.ok_or_else(|| crate::soap::SoapError::missing("Uri").into())
}
pub async fn get_snapshot_uri_media2(
&self,
media2_url: &str,
profile_token: &str,
) -> Result<String, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/media/wsdl/GetSnapshotUri";
let body = format!(
"<tr2:GetSnapshotUri>\
<tr2:ProfileToken>{profile_token}</tr2:ProfileToken>\
</tr2:GetSnapshotUri>"
);
let xml = self.call(media2_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetSnapshotUriResponse")?;
resp.child("Uri")
.map(|n| n.text().to_string())
.ok_or_else(|| crate::soap::SoapError::missing("Uri").into())
}
pub async fn get_video_source_configurations_media2(
&self,
media2_url: &str,
) -> Result<Vec<VideoSourceConfiguration>, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/media/wsdl/GetVideoSourceConfigurations";
const BODY: &str = "<tr2:GetVideoSourceConfigurations/>";
let xml = self.call(media2_url, ACTION, BODY).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetVideoSourceConfigurationsResponse")?;
VideoSourceConfiguration::vec_from_xml(resp)
}
pub async fn set_video_source_configuration_media2(
&self,
media2_url: &str,
config: &VideoSourceConfiguration,
) -> Result<(), OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/media/wsdl/SetVideoSourceConfiguration";
let body = format!(
"<tr2:SetVideoSourceConfiguration>{cfg}</tr2:SetVideoSourceConfiguration>",
cfg = config.to_xml_body_media2()
);
let xml = self.call(media2_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
find_response(&body_node, "SetVideoSourceConfigurationResponse")?;
Ok(())
}
pub async fn get_video_source_configuration_options_media2(
&self,
media2_url: &str,
config_token: Option<&str>,
) -> Result<VideoSourceConfigurationOptions, OnvifError> {
const ACTION: &str =
"http://www.onvif.org/ver20/media/wsdl/GetVideoSourceConfigurationOptions";
let inner = match config_token {
Some(tok) => format!("<tr2:ConfigurationToken>{tok}</tr2:ConfigurationToken>"),
None => String::new(),
};
let body = format!(
"<tr2:GetVideoSourceConfigurationOptions>{inner}</tr2:GetVideoSourceConfigurationOptions>"
);
let xml = self.call(media2_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetVideoSourceConfigurationOptionsResponse")?;
VideoSourceConfigurationOptions::from_xml(resp)
}
pub async fn get_video_encoder_configurations_media2(
&self,
media2_url: &str,
) -> Result<Vec<VideoEncoderConfiguration2>, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/media/wsdl/GetVideoEncoderConfigurations";
const BODY: &str = "<tr2:GetVideoEncoderConfigurations/>";
let xml = self.call(media2_url, ACTION, BODY).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetVideoEncoderConfigurationsResponse")?;
VideoEncoderConfiguration2::vec_from_xml(resp)
}
pub async fn get_video_encoder_configuration_media2(
&self,
media2_url: &str,
token: &str,
) -> Result<VideoEncoderConfiguration2, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/media/wsdl/GetVideoEncoderConfigurations";
let body = format!(
"<tr2:GetVideoEncoderConfigurations>\
<tr2:ConfigurationToken>{token}</tr2:ConfigurationToken>\
</tr2:GetVideoEncoderConfigurations>"
);
let xml = self.call(media2_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetVideoEncoderConfigurationsResponse")?;
let configs = VideoEncoderConfiguration2::vec_from_xml(resp)?;
configs
.into_iter()
.next()
.ok_or_else(|| crate::soap::SoapError::missing("Configurations").into())
}
pub async fn set_video_encoder_configuration_media2(
&self,
media2_url: &str,
config: &VideoEncoderConfiguration2,
) -> Result<(), OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/media/wsdl/SetVideoEncoderConfiguration";
let body = format!(
"<tr2:SetVideoEncoderConfiguration>{cfg}</tr2:SetVideoEncoderConfiguration>",
cfg = config.to_xml_body()
);
let xml = self.call(media2_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
find_response(&body_node, "SetVideoEncoderConfigurationResponse")?;
Ok(())
}
pub async fn get_video_encoder_configuration_options_media2(
&self,
media2_url: &str,
config_token: Option<&str>,
) -> Result<VideoEncoderConfigurationOptions2, OnvifError> {
const ACTION: &str =
"http://www.onvif.org/ver20/media/wsdl/GetVideoEncoderConfigurationOptions";
let inner = match config_token {
Some(tok) => format!("<tr2:ConfigurationToken>{tok}</tr2:ConfigurationToken>"),
None => String::new(),
};
let body = format!(
"<tr2:GetVideoEncoderConfigurationOptions>{inner}</tr2:GetVideoEncoderConfigurationOptions>"
);
let xml = self.call(media2_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetVideoEncoderConfigurationOptionsResponse")?;
VideoEncoderConfigurationOptions2::from_xml(resp)
}
pub async fn get_video_encoder_instances_media2(
&self,
media2_url: &str,
config_token: &str,
) -> Result<VideoEncoderInstances, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/media/wsdl/GetVideoEncoderInstances";
let body = format!(
"<tr2:GetVideoEncoderInstances>\
<tr2:ConfigurationToken>{config_token}</tr2:ConfigurationToken>\
</tr2:GetVideoEncoderInstances>"
);
let xml = self.call(media2_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetVideoEncoderInstancesResponse")?;
VideoEncoderInstances::from_xml(resp)
}
pub async fn create_profile_media2(
&self,
media2_url: &str,
name: &str,
) -> Result<String, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/media/wsdl/CreateProfile";
let body = format!(
"<tr2:CreateProfile>\
<tr2:Name>{name}</tr2:Name>\
</tr2:CreateProfile>"
);
let xml = self.call(media2_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "CreateProfileResponse")?;
resp.child("Token")
.map(|n| n.text().to_string())
.ok_or_else(|| crate::soap::SoapError::missing("Token").into())
}
pub async fn delete_profile_media2(
&self,
media2_url: &str,
token: &str,
) -> Result<(), OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/media/wsdl/DeleteProfile";
let body = format!(
"<tr2:DeleteProfile>\
<tr2:Token>{token}</tr2:Token>\
</tr2:DeleteProfile>"
);
let xml = self.call(media2_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
find_response(&body_node, "DeleteProfileResponse")?;
Ok(())
}
pub async fn get_event_properties(
&self,
events_url: &str,
) -> Result<EventProperties, OnvifError> {
const ACTION: &str =
"http://www.onvif.org/ver10/events/wsdl/EventPortType/GetEventPropertiesRequest";
const BODY: &str = "<tev:GetEventProperties/>";
let xml = self.call(events_url, ACTION, BODY).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetEventPropertiesResponse")?;
EventProperties::from_xml(resp)
}
pub async fn create_pull_point_subscription(
&self,
events_url: &str,
filter: Option<&str>,
initial_termination_time: Option<&str>,
) -> Result<PullPointSubscription, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/events/wsdl/EventPortType/CreatePullPointSubscriptionRequest";
let filter_el = filter
.map(|f| {
format!(
"<tev:Filter>\
<wsnt:TopicExpression \
Dialect=\"http://www.onvif.org/ver10/tev/topicExpression/ConcreteSet\"\
>{f}</wsnt:TopicExpression>\
</tev:Filter>"
)
})
.unwrap_or_default();
let termination_el = initial_termination_time
.map(|t| format!("<tev:InitialTerminationTime>{t}</tev:InitialTerminationTime>"))
.unwrap_or_default();
let body = format!(
"<tev:CreatePullPointSubscription>\
{filter_el}{termination_el}\
</tev:CreatePullPointSubscription>"
);
let xml = self.call(events_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "CreatePullPointSubscriptionResponse")?;
PullPointSubscription::from_xml(resp)
}
pub async fn pull_messages(
&self,
subscription_url: &str,
timeout: &str,
max_messages: u32,
) -> Result<Vec<NotificationMessage>, OnvifError> {
const ACTION: &str =
"http://www.onvif.org/ver10/events/wsdl/PullPointSubscription/PullMessagesRequest";
let body = format!(
"<tev:PullMessages>\
<tev:Timeout>{timeout}</tev:Timeout>\
<tev:MessageLimit>{max_messages}</tev:MessageLimit>\
</tev:PullMessages>"
);
let xml = self.call(subscription_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "PullMessagesResponse")?;
Ok(NotificationMessage::vec_from_xml(resp))
}
pub async fn renew_subscription(
&self,
subscription_url: &str,
termination_time: &str,
) -> Result<String, OnvifError> {
const ACTION: &str =
"http://www.onvif.org/ver10/events/wsdl/SubscriptionManager/RenewRequest";
let body = format!(
"<wsnt:Renew>\
<wsnt:TerminationTime>{termination_time}</wsnt:TerminationTime>\
</wsnt:Renew>"
);
let xml = self.call(subscription_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "RenewResponse")?;
Ok(resp
.child("TerminationTime")
.map(|n| n.text().to_string())
.unwrap_or_default())
}
pub async fn unsubscribe(&self, subscription_url: &str) -> Result<(), OnvifError> {
const ACTION: &str =
"http://www.onvif.org/ver10/events/wsdl/SubscriptionManager/UnsubscribeRequest";
const BODY: &str = "<wsnt:Unsubscribe/>";
let xml = self.call(subscription_url, ACTION, BODY).await?;
let body_node = parse_soap_body(&xml)?;
find_response(&body_node, "UnsubscribeResponse")?;
Ok(())
}
pub async fn get_audio_sources(&self, media_url: &str) -> Result<Vec<AudioSource>, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/media/wsdl/GetAudioSources";
const BODY: &str = "<trt:GetAudioSources/>";
let xml = self.call(media_url, ACTION, BODY).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetAudioSourcesResponse")?;
AudioSource::vec_from_xml(resp)
}
pub async fn get_audio_source_configurations(
&self,
media_url: &str,
) -> Result<Vec<AudioSourceConfiguration>, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/media/wsdl/GetAudioSourceConfigurations";
const BODY: &str = "<trt:GetAudioSourceConfigurations/>";
let xml = self.call(media_url, ACTION, BODY).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetAudioSourceConfigurationsResponse")?;
AudioSourceConfiguration::vec_from_xml(resp)
}
pub async fn get_audio_encoder_configurations(
&self,
media_url: &str,
) -> Result<Vec<AudioEncoderConfiguration>, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/media/wsdl/GetAudioEncoderConfigurations";
const BODY: &str = "<trt:GetAudioEncoderConfigurations/>";
let xml = self.call(media_url, ACTION, BODY).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetAudioEncoderConfigurationsResponse")?;
AudioEncoderConfiguration::vec_from_xml(resp)
}
pub async fn get_audio_encoder_configuration(
&self,
media_url: &str,
config_token: &str,
) -> Result<AudioEncoderConfiguration, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/media/wsdl/GetAudioEncoderConfiguration";
let body = format!(
"<trt:GetAudioEncoderConfiguration>\
<trt:ConfigurationToken>{}</trt:ConfigurationToken>\
</trt:GetAudioEncoderConfiguration>",
xml_escape(config_token)
);
let xml = self.call(media_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetAudioEncoderConfigurationResponse")?;
let node = resp
.child("Configuration")
.ok_or_else(|| crate::soap::SoapError::missing("Configuration"))?;
AudioEncoderConfiguration::from_xml(node)
}
pub async fn set_audio_encoder_configuration(
&self,
media_url: &str,
config: &AudioEncoderConfiguration,
) -> Result<(), OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/media/wsdl/SetAudioEncoderConfiguration";
let body = format!(
"<trt:SetAudioEncoderConfiguration>\
{}\
<trt:ForcePersistence>true</trt:ForcePersistence>\
</trt:SetAudioEncoderConfiguration>",
config.to_xml_body()
);
let xml = self.call(media_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
find_response(&body_node, "SetAudioEncoderConfigurationResponse")?;
Ok(())
}
pub async fn get_audio_encoder_configuration_options(
&self,
media_url: &str,
config_token: &str,
) -> Result<AudioEncoderConfigurationOptions, OnvifError> {
const ACTION: &str =
"http://www.onvif.org/ver10/media/wsdl/GetAudioEncoderConfigurationOptions";
let body = format!(
"<trt:GetAudioEncoderConfigurationOptions>\
<trt:ConfigurationToken>{}</trt:ConfigurationToken>\
</trt:GetAudioEncoderConfigurationOptions>",
xml_escape(config_token)
);
let xml = self.call(media_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetAudioEncoderConfigurationOptionsResponse")?;
AudioEncoderConfigurationOptions::from_xml(resp)
}
pub async fn ptz_get_configurations(
&self,
ptz_url: &str,
) -> Result<Vec<PtzConfiguration>, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/ptz/wsdl/GetConfigurations";
const BODY: &str = "<tptz:GetConfigurations/>";
let xml = self.call(ptz_url, ACTION, BODY).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetConfigurationsResponse")?;
PtzConfiguration::vec_from_xml(resp)
}
pub async fn ptz_get_configuration(
&self,
ptz_url: &str,
config_token: &str,
) -> Result<PtzConfiguration, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/ptz/wsdl/GetConfiguration";
let body = format!(
"<tptz:GetConfiguration>\
<tptz:PTZConfigurationToken>{}</tptz:PTZConfigurationToken>\
</tptz:GetConfiguration>",
xml_escape(config_token)
);
let xml = self.call(ptz_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetConfigurationResponse")?;
let node = resp
.child("PTZConfiguration")
.ok_or_else(|| crate::soap::SoapError::missing("PTZConfiguration"))?;
PtzConfiguration::from_xml(node)
}
pub async fn ptz_set_configuration(
&self,
ptz_url: &str,
config: &PtzConfiguration,
force_persist: bool,
) -> Result<(), OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/ptz/wsdl/SetConfiguration";
let persist = if force_persist { "true" } else { "false" };
let body = format!(
"<tptz:SetConfiguration>\
{}\
<tptz:ForcePersistence>{persist}</tptz:ForcePersistence>\
</tptz:SetConfiguration>",
config.to_xml_body()
);
let xml = self.call(ptz_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
find_response(&body_node, "SetConfigurationResponse")?;
Ok(())
}
pub async fn ptz_get_configuration_options(
&self,
ptz_url: &str,
config_token: &str,
) -> Result<PtzConfigurationOptions, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/ptz/wsdl/GetConfigurationOptions";
let body = format!(
"<tptz:GetConfigurationOptions>\
<tptz:ConfigurationToken>{}</tptz:ConfigurationToken>\
</tptz:GetConfigurationOptions>",
xml_escape(config_token)
);
let xml = self.call(ptz_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetConfigurationOptionsResponse")?;
PtzConfigurationOptions::from_xml(resp)
}
pub async fn ptz_get_nodes(&self, ptz_url: &str) -> Result<Vec<PtzNode>, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver20/ptz/wsdl/GetNodes";
const BODY: &str = "<tptz:GetNodes/>";
let xml = self.call(ptz_url, ACTION, BODY).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetNodesResponse")?;
PtzNode::vec_from_xml(resp)
}
pub async fn get_recordings(
&self,
recording_url: &str,
) -> Result<Vec<RecordingItem>, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/recording/wsdl/GetRecordings";
const BODY: &str = "<trc:GetRecordings/>";
let xml = self.call(recording_url, ACTION, BODY).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetRecordingsResponse")?;
RecordingItem::vec_from_xml(resp)
}
pub async fn find_recordings(
&self,
search_url: &str,
max_matches: Option<u32>,
keep_alive_timeout: &str,
) -> Result<String, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/search/wsdl/FindRecordings";
let max_el = max_matches
.map(|m| format!("<tse:MaxMatches>{m}</tse:MaxMatches>"))
.unwrap_or_default();
let body = format!(
"<tse:FindRecordings>\
{max_el}\
<tse:KeepAliveTime>{keep_alive_timeout}</tse:KeepAliveTime>\
</tse:FindRecordings>"
);
let xml = self.call(search_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "FindRecordingsResponse")?;
resp.child("SearchToken")
.map(|n| n.text().to_string())
.ok_or_else(|| crate::soap::SoapError::missing("SearchToken").into())
}
pub async fn get_recording_search_results(
&self,
search_url: &str,
search_token: &str,
max_results: u32,
wait_time: &str,
) -> Result<FindRecordingResults, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/search/wsdl/GetRecordingSearchResults";
let body = format!(
"<tse:GetRecordingSearchResults>\
<tse:SearchToken>{search_token}</tse:SearchToken>\
<tse:MaxResults>{max_results}</tse:MaxResults>\
<tse:WaitTime>{wait_time}</tse:WaitTime>\
</tse:GetRecordingSearchResults>"
);
let xml = self.call(search_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetRecordingSearchResultsResponse")?;
FindRecordingResults::from_xml(resp)
}
pub async fn end_search(&self, search_url: &str, search_token: &str) -> Result<(), OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/search/wsdl/EndSearch";
let body = format!(
"<tse:EndSearch>\
<tse:SearchToken>{search_token}</tse:SearchToken>\
</tse:EndSearch>"
);
let xml = self.call(search_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
find_response(&body_node, "EndSearchResponse")?;
Ok(())
}
pub async fn get_replay_uri(
&self,
replay_url: &str,
recording_token: &str,
stream_type: &str,
protocol: &str,
) -> Result<String, OnvifError> {
const ACTION: &str = "http://www.onvif.org/ver10/replay/wsdl/GetReplayUri";
let body = format!(
"<trp:GetReplayUri>\
<trp:StreamSetup>\
<tt:Stream>{stream_type}</tt:Stream>\
<tt:Transport><tt:Protocol>{protocol}</tt:Protocol></tt:Transport>\
</trp:StreamSetup>\
<trp:RecordingToken>{recording_token}</trp:RecordingToken>\
</trp:GetReplayUri>"
);
let xml = self.call(replay_url, ACTION, &body).await?;
let body_node = parse_soap_body(&xml)?;
let resp = find_response(&body_node, "GetReplayUriResponse")?;
resp.child("Uri")
.map(|n| n.text().to_string())
.ok_or_else(|| crate::soap::SoapError::missing("Uri").into())
}
}
#[cfg(test)]
#[path = "tests/client_tests.rs"]
mod tests;