Ajout du connection manager
This commit is contained in:
294
pmocontrol/src/connection_manager_client.rs
Normal file
294
pmocontrol/src/connection_manager_client.rs
Normal file
@@ -0,0 +1,294 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
|
||||
use crate::soap_client::{invoke_upnp_action, SoapCallResult};
|
||||
use pmoupnp::soap::SoapEnvelope;
|
||||
use xmltree::{Element, XMLNode};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ConnectionManagerClient {
|
||||
pub control_url: String,
|
||||
pub service_type: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProtocolInfo {
|
||||
/// Liste brute des protocolInfo "source" (séparés par virgule dans UPnP)
|
||||
pub source: Vec<String>,
|
||||
/// Liste brute des protocolInfo "sink"
|
||||
pub sink: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ConnectionInfo {
|
||||
pub rcs_id: i32,
|
||||
pub av_transport_id: i32,
|
||||
pub protocol_info: String,
|
||||
pub peer_connection_manager: String,
|
||||
pub peer_connection_id: i32,
|
||||
pub direction: String,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
impl ConnectionManagerClient {
|
||||
pub fn new(control_url: String, service_type: String) -> Self {
|
||||
Self {
|
||||
control_url,
|
||||
service_type,
|
||||
}
|
||||
}
|
||||
|
||||
/// GetProtocolInfo
|
||||
pub fn get_protocol_info(&self) -> Result<ProtocolInfo> {
|
||||
let call_result = invoke_upnp_action(
|
||||
&self.control_url,
|
||||
&self.service_type,
|
||||
"GetProtocolInfo",
|
||||
&[],
|
||||
)?;
|
||||
|
||||
ensure_success("GetProtocolInfo", &call_result)?;
|
||||
|
||||
let envelope = call_result
|
||||
.envelope
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("Missing SOAP envelope in GetProtocolInfo response"))?;
|
||||
|
||||
if let Some(err) = parse_upnp_error(envelope) {
|
||||
return Err(anyhow!(
|
||||
"GetProtocolInfo returned UPnP error {}: {} (HTTP status {})",
|
||||
err.error_code,
|
||||
err.error_description,
|
||||
call_result.status
|
||||
));
|
||||
}
|
||||
|
||||
let response = find_child_with_suffix(&envelope.body.content, "GetProtocolInfoResponse")
|
||||
.ok_or_else(|| anyhow!("Missing GetProtocolInfoResponse element in SOAP body"))?;
|
||||
|
||||
let source_text = extract_child_text_allow_empty(response, "Source")?;
|
||||
let sink_text = extract_child_text_allow_empty(response, "Sink")?;
|
||||
|
||||
Ok(ProtocolInfo {
|
||||
source: split_list(&source_text),
|
||||
sink: split_list(&sink_text),
|
||||
})
|
||||
}
|
||||
|
||||
/// GetCurrentConnectionIDs
|
||||
pub fn get_current_connection_ids(&self) -> Result<Vec<i32>> {
|
||||
let call_result = invoke_upnp_action(
|
||||
&self.control_url,
|
||||
&self.service_type,
|
||||
"GetCurrentConnectionIDs",
|
||||
&[],
|
||||
)?;
|
||||
|
||||
ensure_success("GetCurrentConnectionIDs", &call_result)?;
|
||||
|
||||
let envelope = call_result
|
||||
.envelope
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("Missing SOAP envelope in GetCurrentConnectionIDs response"))?;
|
||||
|
||||
if let Some(err) = parse_upnp_error(envelope) {
|
||||
return Err(anyhow!(
|
||||
"GetCurrentConnectionIDs returned UPnP error {}: {} (HTTP status {})",
|
||||
err.error_code,
|
||||
err.error_description,
|
||||
call_result.status
|
||||
));
|
||||
}
|
||||
|
||||
let response =
|
||||
find_child_with_suffix(&envelope.body.content, "GetCurrentConnectionIDsResponse")
|
||||
.ok_or_else(|| {
|
||||
anyhow!("Missing GetCurrentConnectionIDsResponse element in SOAP body")
|
||||
})?;
|
||||
|
||||
let ids_text = extract_child_text_allow_empty(response, "ConnectionIDs")?;
|
||||
let trimmed = ids_text.trim();
|
||||
|
||||
if trimmed.is_empty() || trimmed == "0" {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut ids = Vec::new();
|
||||
for part in trimmed.split(',') {
|
||||
let value = part.trim();
|
||||
if value.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let parsed = value
|
||||
.parse::<i32>()
|
||||
.map_err(|_| anyhow!("Invalid ConnectionID value: {}", value))?;
|
||||
ids.push(parsed);
|
||||
}
|
||||
|
||||
Ok(ids)
|
||||
}
|
||||
|
||||
/// GetCurrentConnectionInfo
|
||||
pub fn get_current_connection_info(&self, connection_id: i32) -> Result<ConnectionInfo> {
|
||||
let connection_id_str = connection_id.to_string();
|
||||
let args = [("ConnectionID", connection_id_str.as_str())];
|
||||
|
||||
let call_result = invoke_upnp_action(
|
||||
&self.control_url,
|
||||
&self.service_type,
|
||||
"GetCurrentConnectionInfo",
|
||||
&args,
|
||||
)?;
|
||||
|
||||
ensure_success("GetCurrentConnectionInfo", &call_result)?;
|
||||
|
||||
let envelope = call_result
|
||||
.envelope
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("Missing SOAP envelope in GetCurrentConnectionInfo response"))?;
|
||||
|
||||
if let Some(err) = parse_upnp_error(envelope) {
|
||||
return Err(anyhow!(
|
||||
"GetCurrentConnectionInfo returned UPnP error {}: {} (HTTP status {})",
|
||||
err.error_code,
|
||||
err.error_description,
|
||||
call_result.status
|
||||
));
|
||||
}
|
||||
|
||||
let response =
|
||||
find_child_with_suffix(&envelope.body.content, "GetCurrentConnectionInfoResponse")
|
||||
.ok_or_else(|| {
|
||||
anyhow!("Missing GetCurrentConnectionInfoResponse element in SOAP body")
|
||||
})?;
|
||||
|
||||
let rcs_id = extract_child_text(response, "RcsID")?
|
||||
.parse::<i32>()
|
||||
.map_err(|_| anyhow!("Invalid RcsID value in response"))?;
|
||||
|
||||
let av_transport_id = extract_child_text(response, "AVTransportID")?
|
||||
.parse::<i32>()
|
||||
.map_err(|_| anyhow!("Invalid AVTransportID value in response"))?;
|
||||
|
||||
let protocol_info = extract_child_text_allow_empty(response, "ProtocolInfo")?;
|
||||
let peer_connection_manager =
|
||||
extract_child_text_allow_empty(response, "PeerConnectionManager")?;
|
||||
|
||||
let peer_connection_id = extract_child_text(response, "PeerConnectionID")?
|
||||
.parse::<i32>()
|
||||
.map_err(|_| anyhow!("Invalid PeerConnectionID value in response"))?;
|
||||
|
||||
let direction = extract_child_text(response, "Direction")?;
|
||||
let status = extract_child_text(response, "Status")?;
|
||||
|
||||
Ok(ConnectionInfo {
|
||||
rcs_id,
|
||||
av_transport_id,
|
||||
protocol_info,
|
||||
peer_connection_manager,
|
||||
peer_connection_id,
|
||||
direction,
|
||||
status,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn split_list(value: &str) -> Vec<String> {
|
||||
value
|
||||
.split(',')
|
||||
.filter_map(|part| {
|
||||
let trimmed = part.trim();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed.to_string())
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn ensure_success(action: &str, call_result: &SoapCallResult) -> Result<()> {
|
||||
if call_result.status.is_success() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(env) = &call_result.envelope {
|
||||
if let Some(err) = parse_upnp_error(env) {
|
||||
return Err(anyhow!(
|
||||
"{action} failed with UPnP error {}: {} (HTTP status {})",
|
||||
err.error_code,
|
||||
err.error_description,
|
||||
call_result.status
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow!(
|
||||
"{action} failed with HTTP status {} and body: {}",
|
||||
call_result.status,
|
||||
call_result.raw_body
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct UpnpError {
|
||||
pub error_code: u32,
|
||||
pub error_description: String,
|
||||
}
|
||||
|
||||
fn parse_upnp_error(envelope: &SoapEnvelope) -> Option<UpnpError> {
|
||||
let fault = find_child_with_suffix(&envelope.body.content, "Fault")?;
|
||||
let detail = find_child_with_suffix(fault, "detail")?;
|
||||
let upnp_error = find_child_with_suffix(detail, "UPnPError")?;
|
||||
|
||||
let error_code_elem = upnp_error.children.iter().find_map(|node| match node {
|
||||
XMLNode::Element(elem) if elem.name.ends_with("errorCode") => Some(elem),
|
||||
_ => None,
|
||||
})?;
|
||||
|
||||
let binding = error_code_elem.get_text()?;
|
||||
let error_code_text = binding.trim();
|
||||
let error_code = error_code_text.parse::<u32>().ok()?;
|
||||
|
||||
let error_description = upnp_error
|
||||
.children
|
||||
.iter()
|
||||
.find_map(|node| match node {
|
||||
XMLNode::Element(elem) if elem.name.ends_with("errorDescription") => {
|
||||
elem.get_text().map(|t| t.trim().to_string())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_else(|| String::from(""));
|
||||
|
||||
Some(UpnpError {
|
||||
error_code,
|
||||
error_description,
|
||||
})
|
||||
}
|
||||
|
||||
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 text = extract_child_text_allow_empty(parent, suffix)?;
|
||||
if text.is_empty() {
|
||||
return Err(anyhow!("{suffix} element missing text in response"));
|
||||
}
|
||||
Ok(text)
|
||||
}
|
||||
|
||||
fn extract_child_text_allow_empty(parent: &Element, suffix: &str) -> Result<String> {
|
||||
let child = find_child_with_suffix(parent, suffix)
|
||||
.ok_or_else(|| anyhow!("Missing {suffix} element in response"))?;
|
||||
|
||||
let text = child
|
||||
.get_text()
|
||||
.map(|t| t.trim().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(text)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod avtransport_client;
|
||||
pub mod connection_manager_client;
|
||||
pub mod control_point;
|
||||
pub mod discovery;
|
||||
pub mod model;
|
||||
@@ -9,6 +10,7 @@ pub mod registry;
|
||||
pub mod soap_client;
|
||||
|
||||
pub use avtransport_client::{AvTransportClient, TransportInfo};
|
||||
pub use connection_manager_client::{ConnectionInfo, ConnectionManagerClient, ProtocolInfo};
|
||||
pub use control_point::ControlPoint;
|
||||
pub use rendering_control_client::RenderingControlClient;
|
||||
pub use renderer::Renderer;
|
||||
|
||||
@@ -45,6 +45,8 @@ pub struct RendererInfo {
|
||||
pub avtransport_control_url: Option<String>,
|
||||
pub rendering_control_service_type: Option<String>,
|
||||
pub rendering_control_control_url: Option<String>,
|
||||
pub connection_manager_service_type: Option<String>,
|
||||
pub connection_manager_control_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
|
||||
@@ -46,6 +46,10 @@ struct ParsedDeviceDescription {
|
||||
// RenderingControl endpoint (if present in serviceList)
|
||||
rendering_control_service_type: Option<String>,
|
||||
rendering_control_control_url: Option<String>,
|
||||
|
||||
// ConnectionManager endpoint (if present in serviceList)
|
||||
connection_manager_service_type: Option<String>,
|
||||
connection_manager_control_url: Option<String>,
|
||||
}
|
||||
|
||||
impl ParsedDeviceDescription {
|
||||
@@ -178,6 +182,20 @@ impl HttpXmlDescriptionProvider {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if lower
|
||||
.contains("urn:schemas-upnp-org:service:connectionmanager:")
|
||||
{
|
||||
if parsed.connection_manager_service_type.is_none() {
|
||||
parsed.connection_manager_service_type = Some(st.clone());
|
||||
parsed.connection_manager_control_url =
|
||||
Some(ctrl.clone());
|
||||
debug!(
|
||||
"Found ConnectionManager service for {}: type={} controlURL={}",
|
||||
endpoint.udn, st, ctrl
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
in_service = false;
|
||||
@@ -282,6 +300,11 @@ impl HttpXmlDescriptionProvider {
|
||||
.rendering_control_control_url
|
||||
.as_ref()
|
||||
.map(|ctrl| resolve_control_url(&endpoint.location, ctrl)),
|
||||
connection_manager_service_type: parsed.connection_manager_service_type.clone(),
|
||||
connection_manager_control_url: parsed
|
||||
.connection_manager_control_url
|
||||
.as_ref()
|
||||
.map(|ctrl| resolve_control_url(&endpoint.location, ctrl)),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::collections::HashMap;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use crate::avtransport_client::AvTransportClient;
|
||||
use crate::connection_manager_client::ConnectionManagerClient;
|
||||
use crate::rendering_control_client::RenderingControlClient;
|
||||
use crate::model::{MediaServerId, MediaServerInfo, RendererId, RendererInfo};
|
||||
|
||||
@@ -175,4 +176,20 @@ impl DeviceRegistry {
|
||||
service_type.clone(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Construct a ConnectionManagerClient for a given renderer id, if possible.
|
||||
pub fn connection_manager_client_for_renderer(
|
||||
&self,
|
||||
id: &RendererId,
|
||||
) -> Option<ConnectionManagerClient> {
|
||||
let info = self.renderers.get(id)?;
|
||||
|
||||
let service_type = info.connection_manager_service_type.as_ref()?;
|
||||
let control_url = info.connection_manager_control_url.as_ref()?;
|
||||
|
||||
Some(ConnectionManagerClient::new(
|
||||
control_url.clone(),
|
||||
service_type.clone(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
|
||||
use crate::connection_manager_client::{
|
||||
ConnectionInfo, ConnectionManagerClient, ProtocolInfo,
|
||||
};
|
||||
use crate::rendering_control_client::RenderingControlClient;
|
||||
use crate::{AvTransportClient, DeviceRegistry, RendererId, RendererInfo};
|
||||
|
||||
@@ -8,6 +11,7 @@ pub struct Renderer {
|
||||
pub info: RendererInfo,
|
||||
avtransport: Option<AvTransportClient>,
|
||||
rendering_control: Option<RenderingControlClient>,
|
||||
connection_manager: Option<ConnectionManagerClient>,
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
@@ -27,6 +31,10 @@ impl Renderer {
|
||||
self.rendering_control.is_some()
|
||||
}
|
||||
|
||||
pub fn has_connection_manager(&self) -> bool {
|
||||
self.connection_manager.is_some()
|
||||
}
|
||||
|
||||
pub fn avtransport(&self) -> Result<&AvTransportClient> {
|
||||
self.avtransport
|
||||
.as_ref()
|
||||
@@ -39,6 +47,12 @@ impl Renderer {
|
||||
.ok_or_else(|| anyhow!("Renderer has no RenderingControl service"))
|
||||
}
|
||||
|
||||
pub fn connection_manager(&self) -> Result<&ConnectionManagerClient> {
|
||||
self.connection_manager
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("Renderer has no ConnectionManager service"))
|
||||
}
|
||||
|
||||
pub fn play_uri(&self, uri: &str, meta: &str) -> Result<()> {
|
||||
let avt = self.avtransport()?;
|
||||
avt.set_av_transport_uri(uri, meta)?;
|
||||
@@ -80,13 +94,30 @@ impl Renderer {
|
||||
rc.set_mute(0, "Master", mute)
|
||||
}
|
||||
|
||||
pub fn protocol_info(&self) -> Result<ProtocolInfo> {
|
||||
let cm = self.connection_manager()?;
|
||||
cm.get_protocol_info()
|
||||
}
|
||||
|
||||
pub fn connection_ids(&self) -> Result<Vec<i32>> {
|
||||
let cm = self.connection_manager()?;
|
||||
cm.get_current_connection_ids()
|
||||
}
|
||||
|
||||
pub fn connection_info(&self, connection_id: i32) -> Result<ConnectionInfo> {
|
||||
let cm = self.connection_manager()?;
|
||||
cm.get_current_connection_info(connection_id)
|
||||
}
|
||||
|
||||
pub fn from_registry(info: RendererInfo, registry: &DeviceRegistry) -> Self {
|
||||
let avtransport = registry.avtransport_client_for_renderer(&info.id);
|
||||
let rendering_control = registry.rendering_control_client_for_renderer(&info.id);
|
||||
let connection_manager = registry.connection_manager_client_for_renderer(&info.id);
|
||||
Self {
|
||||
info,
|
||||
avtransport,
|
||||
rendering_control,
|
||||
connection_manager,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,6 +152,8 @@ mod tests {
|
||||
.then(|| "http://127.0.0.1/avtransport".into()),
|
||||
rendering_control_service_type: None,
|
||||
rendering_control_control_url: None,
|
||||
connection_manager_service_type: None,
|
||||
connection_manager_control_url: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user