avtransport client
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -3169,6 +3169,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"ureq",
|
||||
"xmltree 0.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -11,3 +11,4 @@ ureq = "3.1.4"
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = "0.3"
|
||||
anyhow = "1.0"
|
||||
xmltree = "0.11.0"
|
||||
|
||||
@@ -2,8 +2,8 @@ use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use pmocontrol::{ControlPoint, DeviceRegistryRead, MediaServerInfo, RendererInfo};
|
||||
use pmocontrol::RendererProtocol;
|
||||
use pmocontrol::{ControlPoint, DeviceRegistryRead, MediaServerInfo, RendererInfo};
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
// Un tout petit logging optionnel
|
||||
|
||||
101
pmocontrol/examples/test_avtransport.rs
Normal file
101
pmocontrol/examples/test_avtransport.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use pmocontrol::{
|
||||
ControlPoint,
|
||||
DeviceRegistryRead,
|
||||
DiscoveredEndpoint,
|
||||
HttpXmlDescriptionProvider,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
// Optionnel mais pratique si tu as déjà tracing dans la stack
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// 1. Créer le control point et lancer la découverte SSDP
|
||||
//
|
||||
// timeout_secs = 5 pour les HTTP GET des description.xml
|
||||
println!("Starting control point (HTTP timeout = 5s)...");
|
||||
let cp = ControlPoint::spawn(5).context("Failed to spawn ControlPoint")?;
|
||||
|
||||
let registry = cp.registry();
|
||||
|
||||
// 2. Laisser un peu de temps à la découverte
|
||||
println!("Waiting 5 seconds for UPnP discovery...");
|
||||
thread::sleep(Duration::from_secs(5));
|
||||
|
||||
// 3. Lister les renderers connus
|
||||
let renderers = {
|
||||
let reg = registry.read().expect("DeviceRegistry RwLock poisoned");
|
||||
reg.list_renderers()
|
||||
};
|
||||
|
||||
if renderers.is_empty() {
|
||||
println!("No UPnP renderers discovered.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("Discovered renderers:");
|
||||
for (idx, r) in renderers.iter().enumerate() {
|
||||
println!(
|
||||
" [{}] {} (model: {}, UDN: {})",
|
||||
idx, r.friendly_name, r.model_name, r.udn
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Choisir un renderer (ici: le premier)
|
||||
let renderer = renderers[0].clone();
|
||||
println!(
|
||||
"\nUsing renderer [0]: {} (model: {}, UDN: {})",
|
||||
renderer.friendly_name, renderer.model_name, renderer.udn
|
||||
);
|
||||
println!("Location: {}", renderer.location);
|
||||
|
||||
// 5. Construire un DiscoveredEndpoint minimal pour ce renderer
|
||||
//
|
||||
// On reconstruit un endpoint à partir des infos du registry.
|
||||
// Ça permet de réutiliser HttpXmlDescriptionProvider::build_avtransport_client
|
||||
// qui sait parser description.xml et trouver le service AVTransport.
|
||||
let endpoint = DiscoveredEndpoint::new(
|
||||
renderer.udn.clone(),
|
||||
renderer.location.clone(),
|
||||
renderer.server_header.clone(),
|
||||
renderer.max_age,
|
||||
);
|
||||
|
||||
// 6. Construire un client AVTransport à partir de cet endpoint
|
||||
let provider = HttpXmlDescriptionProvider::new(5);
|
||||
let client_opt = provider
|
||||
.build_avtransport_client(&endpoint)
|
||||
.context("Failed to build AVTransport client from device description")?;
|
||||
|
||||
let client = match client_opt {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
println!(
|
||||
"Renderer {} has no AVTransport service (or no controlURL).",
|
||||
renderer.friendly_name
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
println!(
|
||||
"AVTransport endpoint:\n service_type = {}\n control_url = {}",
|
||||
client.service_type, client.control_url
|
||||
);
|
||||
|
||||
// 7. Appeler GetTransportInfo(InstanceID=0)
|
||||
println!("\nCalling GetTransportInfo (InstanceID = 0)...");
|
||||
let info = client
|
||||
.get_transport_info(0)
|
||||
.context("GetTransportInfo SOAP call failed")?;
|
||||
|
||||
println!("GetTransportInfo result:");
|
||||
println!(" CurrentTransportState = {}", info.current_transport_state);
|
||||
println!(" CurrentTransportStatus = {}", info.current_transport_status);
|
||||
println!(" CurrentSpeed = {}", info.current_speed);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
128
pmocontrol/src/avtransport_client.rs
Normal file
128
pmocontrol/src/avtransport_client.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use crate::soap_client::invoke_upnp_action;
|
||||
use pmoupnp::soap::SoapEnvelope;
|
||||
use xmltree::{Element, XMLNode};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AvTransportClient {
|
||||
pub control_url: String,
|
||||
pub service_type: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TransportInfo {
|
||||
pub current_transport_state: String,
|
||||
pub current_transport_status: String,
|
||||
pub current_speed: String,
|
||||
}
|
||||
|
||||
impl AvTransportClient {
|
||||
pub fn new(control_url: String, service_type: String) -> Self {
|
||||
Self {
|
||||
control_url,
|
||||
service_type,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_transport_info(&self, instance_id: u32) -> Result<TransportInfo> {
|
||||
let instance_id_str = instance_id.to_string();
|
||||
let args = [("InstanceID", instance_id_str.as_str())];
|
||||
|
||||
let call_result = invoke_upnp_action(
|
||||
&self.control_url,
|
||||
&self.service_type,
|
||||
"GetTransportInfo",
|
||||
&args,
|
||||
)?;
|
||||
|
||||
if !call_result.status.is_success() {
|
||||
return Err(anyhow!(
|
||||
"GetTransportInfo failed with HTTP status {}",
|
||||
call_result.status
|
||||
));
|
||||
}
|
||||
|
||||
let envelope = call_result
|
||||
.envelope
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("Missing SOAP envelope in GetTransportInfo response"))?;
|
||||
|
||||
parse_transport_info(envelope)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_transport_info(envelope: &SoapEnvelope) -> Result<TransportInfo> {
|
||||
let response = find_child_with_suffix(&envelope.body.content, "GetTransportInfoResponse")
|
||||
.ok_or_else(|| anyhow!("Missing GetTransportInfoResponse element in SOAP body"))?;
|
||||
|
||||
let current_transport_state =
|
||||
extract_child_text(response, "CurrentTransportState")?;
|
||||
let current_transport_status =
|
||||
extract_child_text(response, "CurrentTransportStatus")?;
|
||||
let current_speed = extract_child_text(response, "CurrentSpeed")?;
|
||||
|
||||
Ok(TransportInfo {
|
||||
current_transport_state,
|
||||
current_transport_status,
|
||||
current_speed,
|
||||
})
|
||||
}
|
||||
|
||||
fn find_child_with_suffix<'a>(parent: &'a Element, suffix: &str) -> Option<&'a Element> {
|
||||
parent.children.iter().find_map(|node| match node {
|
||||
XMLNode::Element(elem) if elem.name.ends_with(suffix) => Some(elem),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn extract_child_text(parent: &Element, suffix: &str) -> Result<String> {
|
||||
let child = find_child_with_suffix(parent, suffix)
|
||||
.ok_or_else(|| anyhow!("Missing {suffix} element in GetTransportInfoResponse"))?;
|
||||
|
||||
let text = child
|
||||
.get_text()
|
||||
.map(|t| t.trim().to_string())
|
||||
.filter(|t| !t.is_empty())
|
||||
.ok_or_else(|| anyhow!("{suffix} element missing text in GetTransportInfoResponse"))?;
|
||||
|
||||
Ok(text)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pmoupnp::soap::{SoapBody, SoapEnvelope};
|
||||
|
||||
fn text_element(name: &str, text: &str) -> Element {
|
||||
let mut elem = Element::new(name);
|
||||
elem.children.push(XMLNode::Text(text.to_string()));
|
||||
elem
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_transport_info_extracts_fields() {
|
||||
let mut response = Element::new("u:GetTransportInfoResponse");
|
||||
response.children.push(XMLNode::Element(text_element(
|
||||
"CurrentTransportState",
|
||||
"STOPPED",
|
||||
)));
|
||||
response.children.push(XMLNode::Element(text_element(
|
||||
"CurrentTransportStatus",
|
||||
"OK",
|
||||
)));
|
||||
response.children.push(XMLNode::Element(text_element("CurrentSpeed", "1")));
|
||||
|
||||
let mut body = Element::new("s:Body");
|
||||
body.children.push(XMLNode::Element(response));
|
||||
|
||||
let envelope = SoapEnvelope {
|
||||
header: None,
|
||||
body: SoapBody { content: body },
|
||||
};
|
||||
|
||||
let info = parse_transport_info(&envelope).unwrap();
|
||||
assert_eq!(info.current_transport_state, "STOPPED");
|
||||
assert_eq!(info.current_transport_status, "OK");
|
||||
assert_eq!(info.current_speed, "1");
|
||||
}
|
||||
}
|
||||
@@ -172,15 +172,12 @@ where
|
||||
max_age
|
||||
);
|
||||
|
||||
let endpoint = self
|
||||
.endpoints
|
||||
.entry(udn.clone())
|
||||
.or_insert_with({
|
||||
let udn = udn.clone();
|
||||
let location = location.clone();
|
||||
let server_header = server_header.clone();
|
||||
move || DiscoveredEndpoint::new(udn, location, server_header, max_age)
|
||||
});
|
||||
let endpoint = self.endpoints.entry(udn.clone()).or_insert_with({
|
||||
let udn = udn.clone();
|
||||
let location = location.clone();
|
||||
let server_header = server_header.clone();
|
||||
move || DiscoveredEndpoint::new(udn, location, server_header, max_age)
|
||||
});
|
||||
|
||||
endpoint.touch(location, server_header, max_age);
|
||||
endpoint.types_seen.insert(device_type);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
pub mod avtransport_client;
|
||||
pub mod control_point;
|
||||
pub mod soap_client;
|
||||
pub mod discovery;
|
||||
pub mod model;
|
||||
pub mod provider;
|
||||
pub mod registry;
|
||||
pub mod soap_client;
|
||||
|
||||
pub use avtransport_client::{AvTransportClient, TransportInfo};
|
||||
pub use control_point::ControlPoint;
|
||||
|
||||
pub use discovery::{DeviceDescriptionProvider, DiscoveredEndpoint, DiscoveryManager};
|
||||
@@ -16,7 +18,3 @@ pub use provider::HttpXmlDescriptionProvider;
|
||||
pub use registry::{DeviceRegistry, DeviceRegistryRead, DeviceUpdate};
|
||||
|
||||
pub use soap_client::invoke_upnp_action;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -40,6 +40,9 @@ pub struct RendererInfo {
|
||||
pub online: bool,
|
||||
pub last_seen: std::time::SystemTime,
|
||||
pub max_age: u32,
|
||||
|
||||
pub avtransport_service_type: Option<String>,
|
||||
pub avtransport_control_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use std::io::BufReader;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use std::time::SystemTime;
|
||||
|
||||
use quick_xml::{Error as XmlError, Reader, events::Event};
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use crate::discovery::DiscoveredEndpoint;
|
||||
use crate::avtransport_client::AvTransportClient;
|
||||
use crate::discovery::{DeviceDescriptionProvider, DiscoveredEndpoint};
|
||||
use crate::model::{
|
||||
MediaServerCapabilities, MediaServerId, MediaServerInfo, RendererCapabilities, RendererId,
|
||||
RendererInfo, RendererProtocol,
|
||||
@@ -28,6 +29,7 @@ pub enum DescriptionError {
|
||||
MissingField(&'static str),
|
||||
}
|
||||
|
||||
/// Parsed device description, plus (optionally) AVTransport endpoint.
|
||||
#[derive(Debug, Default)]
|
||||
struct ParsedDeviceDescription {
|
||||
udn: Option<String>,
|
||||
@@ -36,6 +38,10 @@ struct ParsedDeviceDescription {
|
||||
manufacturer: Option<String>,
|
||||
model_name: Option<String>,
|
||||
service_types: Vec<String>,
|
||||
|
||||
// New: AVTransport endpoint (if present in serviceList)
|
||||
avtransport_service_type: Option<String>,
|
||||
avtransport_control_url: Option<String>,
|
||||
}
|
||||
|
||||
impl ParsedDeviceDescription {
|
||||
@@ -53,6 +59,7 @@ impl ParsedDeviceDescription {
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP-based XML description provider (UPnP device description.xml)
|
||||
pub struct HttpXmlDescriptionProvider {
|
||||
timeout_secs: u64,
|
||||
}
|
||||
@@ -62,6 +69,7 @@ impl HttpXmlDescriptionProvider {
|
||||
Self { timeout_secs }
|
||||
}
|
||||
|
||||
/// Fetch and parse the device description.xml at endpoint.location.
|
||||
fn fetch_and_parse(
|
||||
&self,
|
||||
endpoint: &DiscoveredEndpoint,
|
||||
@@ -72,7 +80,7 @@ impl HttpXmlDescriptionProvider {
|
||||
);
|
||||
|
||||
let config = Agent::config_builder()
|
||||
.timeout_global(Some(Duration::from_secs(self.timeout_secs)))
|
||||
.timeout_global(Some(std::time::Duration::from_secs(self.timeout_secs)))
|
||||
.build();
|
||||
|
||||
let agent: Agent = config.into();
|
||||
@@ -99,6 +107,10 @@ impl HttpXmlDescriptionProvider {
|
||||
let mut in_service = false;
|
||||
let mut current_tag: Option<String> = None;
|
||||
|
||||
// New: track current serviceType + controlURL while inside <service>...</service>
|
||||
let mut current_service_type: Option<String> = None;
|
||||
let mut current_control_url: Option<String> = None;
|
||||
|
||||
loop {
|
||||
match reader.read_event_into(&mut buf)? {
|
||||
Event::Start(e) => {
|
||||
@@ -112,6 +124,8 @@ impl HttpXmlDescriptionProvider {
|
||||
if in_device {
|
||||
in_service = true;
|
||||
current_tag = None;
|
||||
current_service_type = None;
|
||||
current_control_url = None;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
@@ -128,7 +142,31 @@ impl HttpXmlDescriptionProvider {
|
||||
in_device = false;
|
||||
}
|
||||
"service" => {
|
||||
in_service = false;
|
||||
if in_device && in_service {
|
||||
// We just finished a <service> block: if this is AVTransport,
|
||||
// store its endpoint in parsed.*
|
||||
if let (Some(st), Some(ctrl)) =
|
||||
(¤t_service_type, ¤t_control_url)
|
||||
{
|
||||
let lower = st.to_ascii_lowercase();
|
||||
if lower.contains("urn:schemas-upnp-org:service:avtransport:") {
|
||||
// Only set once; if multiple AVTransport services exist,
|
||||
// we keep the first one.
|
||||
if parsed.avtransport_service_type.is_none() {
|
||||
parsed.avtransport_service_type = Some(st.clone());
|
||||
parsed.avtransport_control_url = Some(ctrl.clone());
|
||||
debug!(
|
||||
"Found AVTransport service for {}: type={} controlURL={}",
|
||||
endpoint.udn, st, ctrl
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
in_service = false;
|
||||
current_service_type = None;
|
||||
current_control_url = None;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -138,10 +176,7 @@ impl HttpXmlDescriptionProvider {
|
||||
if in_device {
|
||||
if let Some(tag) = ¤t_tag {
|
||||
// quick-xml ≥ 0.37 : unescape() → decode()
|
||||
let text = e
|
||||
.decode() // Result<Cow<'_, str>, EncodingError>
|
||||
.map_err(XmlError::Encoding)? // -> quick_xml::Error, donc DescriptionError::Xml via #[from]
|
||||
.into_owned(); // String
|
||||
let text = e.decode().map_err(XmlError::Encoding)?.into_owned();
|
||||
|
||||
match tag.as_str() {
|
||||
"UDN" => {
|
||||
@@ -160,7 +195,11 @@ impl HttpXmlDescriptionProvider {
|
||||
parsed.model_name = Some(text);
|
||||
}
|
||||
"serviceType" if in_service => {
|
||||
parsed.service_types.push(text);
|
||||
parsed.service_types.push(text.clone());
|
||||
current_service_type = Some(text);
|
||||
}
|
||||
"controlURL" if in_service => {
|
||||
current_control_url = Some(text);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -216,6 +255,11 @@ impl HttpXmlDescriptionProvider {
|
||||
online: true,
|
||||
last_seen: now,
|
||||
max_age: endpoint.max_age,
|
||||
avtransport_service_type: parsed.avtransport_service_type.clone(),
|
||||
avtransport_control_url: parsed
|
||||
.avtransport_control_url
|
||||
.as_ref()
|
||||
.map(|ctrl| resolve_control_url(&endpoint.location, ctrl)),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -251,8 +295,39 @@ impl HttpXmlDescriptionProvider {
|
||||
max_age: endpoint.max_age,
|
||||
})
|
||||
}
|
||||
|
||||
/// New helper: build an AvTransportClient directly from a discovered endpoint.
|
||||
///
|
||||
/// Returns Ok(Some(client)) if an AVTransport service with a controlURL is present,
|
||||
/// Ok(None) if no AVTransport service was found.
|
||||
pub fn build_avtransport_client(
|
||||
&self,
|
||||
endpoint: &DiscoveredEndpoint,
|
||||
) -> Result<Option<AvTransportClient>, DescriptionError> {
|
||||
let parsed = self.fetch_and_parse(endpoint)?;
|
||||
|
||||
let service_type = match &parsed.avtransport_service_type {
|
||||
Some(st) => st.clone(),
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let raw_control = match &parsed.avtransport_control_url {
|
||||
Some(ctrl) => ctrl.clone(),
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let control_url = resolve_control_url(&endpoint.location, &raw_control);
|
||||
debug!(
|
||||
"AVTransport client for {}: service_type={} control_url={}",
|
||||
endpoint.udn, service_type, control_url
|
||||
);
|
||||
|
||||
Ok(Some(AvTransportClient::new(control_url, service_type)))
|
||||
}
|
||||
}
|
||||
|
||||
// --- capabilities detection unchanged ---
|
||||
|
||||
fn detect_renderer_capabilities(service_types: &[String]) -> RendererCapabilities {
|
||||
let mut caps = RendererCapabilities::default();
|
||||
|
||||
@@ -321,7 +396,32 @@ fn detect_server_capabilities(service_types: &[String]) -> MediaServerCapabiliti
|
||||
caps
|
||||
}
|
||||
|
||||
use crate::discovery::DeviceDescriptionProvider;
|
||||
/// Resolve a possibly relative controlURL against the description URL.
|
||||
///
|
||||
/// - If `control_url` is already absolute (starts with http:// or https://), it is returned as-is.
|
||||
/// - Otherwise, it is resolved against the scheme://host:port of `description_url`.
|
||||
fn resolve_control_url(description_url: &str, control_url: &str) -> String {
|
||||
if control_url.starts_with("http://") || control_url.starts_with("https://") {
|
||||
return control_url.to_string();
|
||||
}
|
||||
|
||||
// Extract "scheme://host[:port]" from description_url
|
||||
if let Some((scheme, rest)) = description_url.split_once("://") {
|
||||
if let Some(pos) = rest.find('/') {
|
||||
let authority = &rest[..pos];
|
||||
let base = format!("{}://{}", scheme, authority);
|
||||
|
||||
if control_url.starts_with('/') {
|
||||
return format!("{}{}", base, control_url);
|
||||
} else {
|
||||
return format!("{}/{}", base, control_url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: just return the raw control_url if we cannot parse
|
||||
control_url.to_string()
|
||||
}
|
||||
|
||||
impl DeviceDescriptionProvider for HttpXmlDescriptionProvider {
|
||||
fn build_renderer_info(&self, endpoint: &DiscoveredEndpoint) -> Option<RendererInfo> {
|
||||
@@ -363,4 +463,4 @@ impl DeviceDescriptionProvider for HttpXmlDescriptionProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use crate::avtransport_client::AvTransportClient;
|
||||
use crate::model::{MediaServerId, MediaServerInfo, RendererId, RendererInfo};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -16,6 +17,10 @@ pub struct DeviceRegistry {
|
||||
udn_index: HashMap<String, DeviceKey>,
|
||||
}
|
||||
|
||||
/// Read-only view / trait for registry access.
|
||||
///
|
||||
/// Pour l’instant, on ne rajoute pas AVTransport ici, on se contente
|
||||
/// d’ajouter les helpers dans `impl DeviceRegistry`.
|
||||
pub trait DeviceRegistryRead {
|
||||
fn list_renderers(&self) -> Vec<RendererInfo>;
|
||||
fn list_servers(&self) -> Vec<MediaServerInfo>;
|
||||
@@ -114,4 +119,43 @@ impl DeviceRegistry {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper: get a renderer by UDN (case-insensitive, via udn_index).
|
||||
pub fn get_renderer_by_udn(&self, udn: &str) -> Option<RendererInfo> {
|
||||
let lookup = udn.to_ascii_lowercase();
|
||||
match self.udn_index.get(&lookup) {
|
||||
Some(DeviceKey::Renderer(id)) => self.renderers.get(id).cloned(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct an AvTransportClient for a given renderer id, if possible.
|
||||
///
|
||||
/// Returns:
|
||||
/// - Some(client) if the renderer exists AND has avtransport_* fields set
|
||||
/// - None if renderer not found or no AVTransport service.
|
||||
pub fn avtransport_client_for_renderer(
|
||||
&self,
|
||||
id: &RendererId,
|
||||
) -> Option<AvTransportClient> {
|
||||
let info = self.renderers.get(id)?;
|
||||
|
||||
let service_type = info.avtransport_service_type.as_ref()?;
|
||||
let control_url = info.avtransport_control_url.as_ref()?;
|
||||
|
||||
Some(AvTransportClient::new(
|
||||
control_url.clone(),
|
||||
service_type.clone(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Construct an AvTransportClient for a given UDN, if possible.
|
||||
pub fn avtransport_client_for_udn(&self, udn: &str) -> Option<AvTransportClient> {
|
||||
let info = self.get_renderer_by_udn(udn)?;
|
||||
|
||||
let service_type = info.avtransport_service_type?;
|
||||
let control_url = info.avtransport_control_url?;
|
||||
|
||||
Some(AvTransportClient::new(control_url, service_type))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,37 +15,31 @@ pub struct SoapCallResult {
|
||||
/// Invoke a UPnP SOAP action on a control URL.
|
||||
///
|
||||
/// - `control_url`: full HTTP URL of the service control endpoint
|
||||
/// - `service_type`: service URN, e.g. "urn:schemas-upnp-org:service:AVTransport:1"
|
||||
/// - `action`: action name, e.g. "GetTransportInfo"
|
||||
/// - `args`: list of (name, value) pairs, e.g. &[("InstanceID", "0")]
|
||||
/// - `service_type`: service URN
|
||||
/// - `action`: action name
|
||||
/// - `args`: list of (name, value)
|
||||
pub fn invoke_upnp_action(
|
||||
control_url: &str,
|
||||
service_type: &str,
|
||||
action: &str,
|
||||
args: &[(&str, &str)],
|
||||
) -> Result<SoapCallResult> {
|
||||
// 1. Build SOAP request body using pmoupnp::soap
|
||||
|
||||
// 1. Build SOAP request XML using pmoupnp
|
||||
let body_xml = build_soap_request(service_type, action, args)
|
||||
.context("Failed to build SOAP request body")?;
|
||||
|
||||
// 2. Build an Agent config that does NOT treat 4xx/5xx as errors.
|
||||
//
|
||||
// This is crucial: we want to be able to read the body even for
|
||||
// HTTP 500 SOAP Faults, so we must *not* get Error::StatusCode.
|
||||
// 2. Build agent that does NOT treat 4xx/5xx as errors
|
||||
let config = Agent::config_builder()
|
||||
.http_status_as_error(false)
|
||||
.build();
|
||||
|
||||
let agent: Agent = config.into();
|
||||
|
||||
// 3. Build SOAPAction header: "urn:service#Action"
|
||||
// 3. SOAPAction header
|
||||
let soap_action_header = format!(r#""{}#{}""#, service_type, action);
|
||||
|
||||
// 4. Send HTTP POST request
|
||||
//
|
||||
// - RequestBuilder::header(...) is the proper 3.x API.
|
||||
// - RequestBuilder::send(...) accepts anything implementing AsSendBody,
|
||||
// including `String`.
|
||||
// 4. HTTP POST
|
||||
let mut response = agent
|
||||
.post(control_url)
|
||||
.header("Content-Type", r#"text/xml; charset="utf-8""#)
|
||||
@@ -55,21 +49,16 @@ pub fn invoke_upnp_action(
|
||||
|
||||
let status = response.status();
|
||||
|
||||
// 5. Read full body into a String, regardless of HTTP status code.
|
||||
// 5. Read full body
|
||||
//
|
||||
// This matches the pattern you already use for description.xml:
|
||||
// response.body_mut().read_to_string(...)
|
||||
let raw_body = String::new();
|
||||
response
|
||||
// API réelle (ureq 3.1.4):
|
||||
// body_mut().read_to_string() -> Result<String, ureq::Error>
|
||||
let raw_body = response
|
||||
.body_mut()
|
||||
.read_to_string()
|
||||
.context("Failed to read SOAP response body")?;
|
||||
|
||||
// 6. Try to parse SOAP envelope.
|
||||
//
|
||||
// We *do not* fail the whole call if parsing fails:
|
||||
// - you still get status + raw_body,
|
||||
// - envelope is None if body is not valid SOAP.
|
||||
// 6. Try to parse SOAP envelope; non-fatal on failure
|
||||
let envelope = match parse_soap_envelope(raw_body.as_bytes()) {
|
||||
Ok(env) => Some(env),
|
||||
Err(_) => None,
|
||||
|
||||
Reference in New Issue
Block a user