onvif-server 0.1.1

An ONVIF Profile S streaming-core device server library for Rust — build a camera/encoder that standard ONVIF clients can discover and stream from
Documentation
use async_trait::async_trait;
use bytes::Bytes;
use quick_xml::events::Event;
use quick_xml::NsReader;
use soap_server::{escape_attr, escape_text, SoapFault, SoapHandler};
use std::sync::Arc;

use crate::constants::{
    PTZ_CONFIG_TOKEN, PTZ_NODE_TOKEN, TRANSLATION_SPACE_FOV, VIDEO_ENCODER_TOKEN,
    VIDEO_SOURCE_TOKEN,
};
use crate::error::OnvifError;
use crate::service::xml_util::extract_text_ns;
use crate::traits::MediaService;

/// ONVIF namespaces accepted for media/device request elements.
const ONVIF_MEDIA_NS: &[u8] = b"http://www.onvif.org/ver10/media/wsdl";
const ONVIF_SCHEMA_NS: &[u8] = b"http://www.onvif.org/ver10/schema";

#[allow(dead_code)]
pub struct MediaServiceHandler {
    pub(crate) svc: Arc<dyn MediaService>,
    pub(crate) xaddr: String,
}

impl MediaServiceHandler {
    pub fn new(svc: Arc<dyn MediaService>, xaddr: impl Into<String>) -> Self {
        Self {
            svc,
            xaddr: xaddr.into(),
        }
    }
}

#[async_trait]
impl SoapHandler for MediaServiceHandler {
    async fn handle(&self, body: Bytes) -> Result<Bytes, SoapFault> {
        let op = extract_local_name(&body)?;
        match op.as_str() {
            "GetProfiles" => self.handle_get_profiles().await,
            "GetStreamUri" => self.handle_get_stream_uri(&body).await,
            "GetSnapshotUri" => self.handle_get_snapshot_uri(&body).await,
            "GetVideoSources" => self.handle_get_video_sources().await,
            "GetVideoSourceConfigurations" => self.handle_get_video_source_configurations().await,
            "GetVideoEncoderConfigurations" => self.handle_get_video_encoder_configurations().await,
            _ => Err(OnvifError::ActionNotSupported.into_soap_fault()),
        }
    }
}

fn extract_local_name(body: &Bytes) -> Result<String, SoapFault> {
    let mut reader = NsReader::from_reader(body.as_ref());
    reader.config_mut().trim_text(true);
    loop {
        match reader
            .read_resolved_event()
            .map_err(|e| SoapFault::sender(format!("{e}")))?
        {
            (_, Event::Start(e)) | (_, Event::Empty(e)) => {
                let local = std::str::from_utf8(e.local_name().as_ref())
                    .map_err(|e| SoapFault::sender(format!("{e}")))?
                    .to_string();
                return Ok(local);
            }
            (_, Event::Eof) => return Err(SoapFault::sender("Empty body".to_string())),
            _ => {}
        }
    }
}

fn extract_text_element(body: &Bytes, element_name: &str) -> Result<String, SoapFault> {
    extract_text_ns(body, element_name, &[ONVIF_MEDIA_NS, ONVIF_SCHEMA_NS])
}

impl MediaServiceHandler {
    async fn handle_get_profiles(&self) -> Result<Bytes, SoapFault> {
        let profiles = self.svc.profiles();
        let mut profile_blocks = String::new();
        for p in &profiles {
            profile_blocks.push_str(&format!(
                r#"  <trt:Profiles token="{profile_token}" fixed="true">
    <tt:Name>{name}</tt:Name>
    <tt:VideoSourceConfiguration token="{vs_cfg_token}">
      <tt:Name>VideoSourceConfig</tt:Name>
      <tt:UseCount>1</tt:UseCount>
      <tt:SourceToken>{vs_token}</tt:SourceToken>
      <tt:Bounds x="0" y="0" width="{width}" height="{height}"/>
    </tt:VideoSourceConfiguration>
    <tt:VideoEncoderConfiguration token="{ve_cfg_token}">
      <tt:Name>VideoEncoderConfig</tt:Name>
      <tt:UseCount>1</tt:UseCount>
      <tt:Encoding>{encoding}</tt:Encoding>
      <tt:Resolution><tt:Width>{width}</tt:Width><tt:Height>{height}</tt:Height></tt:Resolution>
      <tt:Quality>5</tt:Quality>
      <tt:RateControl>
        <tt:FrameRateLimit>{framerate}</tt:FrameRateLimit>
        <tt:EncodingInterval>1</tt:EncodingInterval>
        <tt:BitrateLimit>{bitrate}</tt:BitrateLimit>
      </tt:RateControl>
      <tt:Multicast>
        <tt:Address><tt:Type>IPv4</tt:Type><tt:IPv4Address>0.0.0.0</tt:IPv4Address></tt:Address>
        <tt:Port>0</tt:Port>
        <tt:TTL>0</tt:TTL>
        <tt:AutoStart>false</tt:AutoStart>
      </tt:Multicast>
      <tt:SessionTimeout>PT10S</tt:SessionTimeout>
    </tt:VideoEncoderConfiguration>
    <tt:PTZConfiguration token="{ptz_cfg_token}">
      <tt:Name>PTZConfig</tt:Name>
      <tt:UseCount>1</tt:UseCount>
      <tt:NodeToken>{ptz_node_token}</tt:NodeToken>
      <tt:DefaultContinuousPanTiltVelocitySpace>{translation_space_fov}</tt:DefaultContinuousPanTiltVelocitySpace>
    </tt:PTZConfiguration>
  </trt:Profiles>
"#,
                profile_token = escape_attr(&p.token),
                name = escape_text(&p.name),
                vs_cfg_token = escape_attr(&p.video_source_token),
                vs_token = escape_text(&p.video_source_token),
                ve_cfg_token = VIDEO_ENCODER_TOKEN,
                width = p.width,
                height = p.height,
                encoding = escape_text(&p.encoding),
                framerate = p.framerate,
                bitrate = p.bitrate,
                ptz_cfg_token = PTZ_CONFIG_TOKEN,
                ptz_node_token = PTZ_NODE_TOKEN,
                translation_space_fov = TRANSLATION_SPACE_FOV,
            ));
        }
        let xml = format!(
            r#"<trt:GetProfilesResponse xmlns:trt="http://www.onvif.org/ver10/media/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema">
{profile_blocks}</trt:GetProfilesResponse>"#,
        );
        Ok(Bytes::from(xml))
    }

    async fn handle_get_stream_uri(&self, body: &Bytes) -> Result<Bytes, SoapFault> {
        let profile_token = extract_text_element(body, "ProfileToken")?;
        let uri = self
            .svc
            .get_stream_uri(&profile_token)
            .await
            .map_err(|e| e.into_soap_fault())?;
        let xml = format!(
            r#"<trt:GetStreamUriResponse xmlns:trt="http://www.onvif.org/ver10/media/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema">
  <trt:MediaUri>
    <tt:Uri>{uri}</tt:Uri>
    <tt:InvalidAfterConnect>false</tt:InvalidAfterConnect>
    <tt:InvalidAfterReboot>false</tt:InvalidAfterReboot>
    <tt:Timeout>PT0S</tt:Timeout>
  </trt:MediaUri>
</trt:GetStreamUriResponse>"#,
            uri = escape_text(&uri)
        );
        Ok(Bytes::from(xml))
    }

    async fn handle_get_snapshot_uri(&self, body: &Bytes) -> Result<Bytes, SoapFault> {
        let profile_token = extract_text_element(body, "ProfileToken")?;
        let uri = self
            .svc
            .get_snapshot_uri(&profile_token)
            .await
            .map_err(|e| e.into_soap_fault())?;
        let xml = format!(
            r#"<trt:GetSnapshotUriResponse xmlns:trt="http://www.onvif.org/ver10/media/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema">
  <trt:MediaUri>
    <tt:Uri>{uri}</tt:Uri>
    <tt:InvalidAfterConnect>false</tt:InvalidAfterConnect>
    <tt:InvalidAfterReboot>false</tt:InvalidAfterReboot>
    <tt:Timeout>PT0S</tt:Timeout>
  </trt:MediaUri>
</trt:GetSnapshotUriResponse>"#,
            uri = escape_text(&uri)
        );
        Ok(Bytes::from(xml))
    }

    async fn handle_get_video_sources(&self) -> Result<Bytes, SoapFault> {
        let xml = format!(
            r#"<trt:GetVideoSourcesResponse xmlns:trt="http://www.onvif.org/ver10/media/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema">
  <trt:VideoSources token="{vs_token}">
    <tt:Framerate>30</tt:Framerate>
    <tt:Resolution><tt:Width>1920</tt:Width><tt:Height>1080</tt:Height></tt:Resolution>
  </trt:VideoSources>
</trt:GetVideoSourcesResponse>"#,
            vs_token = VIDEO_SOURCE_TOKEN,
        );
        Ok(Bytes::from(xml))
    }

    async fn handle_get_video_source_configurations(&self) -> Result<Bytes, SoapFault> {
        let xml = format!(
            r#"<trt:GetVideoSourceConfigurationsResponse xmlns:trt="http://www.onvif.org/ver10/media/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema">
  <trt:Configurations token="{vs_token}">
    <tt:Name>VideoSourceConfig</tt:Name>
    <tt:UseCount>1</tt:UseCount>
    <tt:SourceToken>{vs_token}</tt:SourceToken>
    <tt:Bounds x="0" y="0" width="1920" height="1080"/>
  </trt:Configurations>
</trt:GetVideoSourceConfigurationsResponse>"#,
            vs_token = VIDEO_SOURCE_TOKEN,
        );
        Ok(Bytes::from(xml))
    }

    async fn handle_get_video_encoder_configurations(&self) -> Result<Bytes, SoapFault> {
        let xml = format!(
            r#"<trt:GetVideoEncoderConfigurationsResponse xmlns:trt="http://www.onvif.org/ver10/media/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema">
  <trt:Configurations token="{ve_token}">
    <tt:Name>VideoEncoderConfig</tt:Name>
    <tt:UseCount>1</tt:UseCount>
    <tt:Encoding>H264</tt:Encoding>
    <tt:Resolution><tt:Width>1920</tt:Width><tt:Height>1080</tt:Height></tt:Resolution>
    <tt:Quality>5</tt:Quality>
    <tt:RateControl>
      <tt:FrameRateLimit>30</tt:FrameRateLimit>
      <tt:EncodingInterval>1</tt:EncodingInterval>
      <tt:BitrateLimit>4096</tt:BitrateLimit>
    </tt:RateControl>
    <tt:Multicast>
      <tt:Address><tt:Type>IPv4</tt:Type><tt:IPv4Address>0.0.0.0</tt:IPv4Address></tt:Address>
      <tt:Port>0</tt:Port>
      <tt:TTL>0</tt:TTL>
      <tt:AutoStart>false</tt:AutoStart>
    </tt:Multicast>
    <tt:SessionTimeout>PT10S</tt:SessionTimeout>
  </trt:Configurations>
</trt:GetVideoEncoderConfigurationsResponse>"#,
            ve_token = VIDEO_ENCODER_TOKEN,
        );
        Ok(Bytes::from(xml))
    }
}