Ajout du rendering control
This commit is contained in:
229
pmocontrol/examples/renderer_demo.rs
Normal file
229
pmocontrol/examples/renderer_demo.rs
Normal file
@@ -0,0 +1,229 @@
|
||||
// pmocontrol/examples/renderer_demo.rs
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use pmocontrol::{ControlPoint, Renderer};
|
||||
use std::env;
|
||||
use std::io::{self, Write};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
// -------------------------------------------------------------------------
|
||||
// CLI arguments
|
||||
//
|
||||
// Usage:
|
||||
// renderer_demo
|
||||
// -> URI par défaut, renderer index 0
|
||||
//
|
||||
// renderer_demo 1
|
||||
// -> URI par défaut, renderer index 1
|
||||
//
|
||||
// renderer_demo https://.../track.flac
|
||||
// -> cette URI, renderer index 0
|
||||
//
|
||||
// renderer_demo https://.../track.flac 1
|
||||
// -> cette URI, renderer index 1
|
||||
// -------------------------------------------------------------------------
|
||||
let default_uri =
|
||||
"https://audio-fb.radioparadise.com/chan/1/x/1117/4/g/1117-3.flac".to_string();
|
||||
let mut uri = default_uri.clone();
|
||||
let mut renderer_index: usize = 0;
|
||||
|
||||
let args: Vec<String> = env::args().collect();
|
||||
if args.len() >= 2 {
|
||||
let first = &args[1];
|
||||
if let Ok(idx) = first.parse::<usize>() {
|
||||
// cas: renderer_demo 1 [URI]
|
||||
renderer_index = idx;
|
||||
if args.len() >= 3 {
|
||||
uri = args[2].clone();
|
||||
}
|
||||
} else {
|
||||
// cas: renderer_demo URI [INDEX]
|
||||
uri = first.clone();
|
||||
if args.len() >= 3 {
|
||||
if let Ok(idx) = args[2].parse::<usize>() {
|
||||
renderer_index = idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("Using URI : {}", uri);
|
||||
println!("Requested renderer index : {}", renderer_index);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 1. Démarrer le control point et laisser la découverte tourner un peu
|
||||
// -------------------------------------------------------------------------
|
||||
let cp = ControlPoint::spawn(5)?;
|
||||
thread::sleep(Duration::from_secs(5));
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 2. Récupérer la liste des renderers (handles haut niveau)
|
||||
// -------------------------------------------------------------------------
|
||||
let mut renderers: Vec<Renderer> = cp.list_renderer_handles();
|
||||
|
||||
// Filtre : on ignore le renderer PMOMusic interne en développement
|
||||
renderers.retain(|r| {
|
||||
!r.friendly_name()
|
||||
.to_ascii_lowercase()
|
||||
.contains("pmomusic audio renderer")
|
||||
});
|
||||
|
||||
if renderers.is_empty() {
|
||||
println!("No valid UPnP MediaRenderer discovered.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("\nDiscovered MediaRenderers:");
|
||||
for (idx, r) in renderers.iter().enumerate() {
|
||||
println!(
|
||||
" [{}] {} | model={} | manufacturer={} | has_avt={} | has_rc={}",
|
||||
idx,
|
||||
r.friendly_name(),
|
||||
r.info.model_name,
|
||||
r.info.manufacturer,
|
||||
r.has_avtransport(),
|
||||
r.has_rendering_control(),
|
||||
);
|
||||
}
|
||||
|
||||
if renderer_index >= renderers.len() {
|
||||
return Err(anyhow!(
|
||||
"Renderer index {} out of range (0..={})",
|
||||
renderer_index,
|
||||
renderers.len().saturating_sub(1)
|
||||
));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 3. Sélection du renderer
|
||||
// -------------------------------------------------------------------------
|
||||
let renderer = &renderers[renderer_index];
|
||||
|
||||
println!("\nSelected renderer (index {}):", renderer_index);
|
||||
println!(" Name : {}", renderer.friendly_name());
|
||||
println!(" Model : {}", renderer.info.model_name);
|
||||
println!(" Manufacturer: {}", renderer.info.manufacturer);
|
||||
println!(" UDN : {}", renderer.info.udn);
|
||||
println!(" Location : {}", renderer.info.location);
|
||||
println!(" has_avt : {}", renderer.has_avtransport());
|
||||
println!(" has_rc : {}", renderer.has_rendering_control());
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 4. Si RenderingControl dispo : tester get/set volume + mute
|
||||
// (set_* remet juste la même valeur pour ne rien changer en pratique)
|
||||
// -------------------------------------------------------------------------
|
||||
if renderer.has_rendering_control() {
|
||||
println!("\n[RenderingControl]");
|
||||
match renderer.get_master_volume() {
|
||||
Ok(vol) => {
|
||||
println!(" Current master volume: {}", vol);
|
||||
if let Err(e) = renderer.set_master_volume(vol) {
|
||||
println!(" set_master_volume({}) failed: {}", vol, e);
|
||||
} else {
|
||||
println!(" set_master_volume({}) OK (no-op)", vol);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" get_master_volume() failed: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
match renderer.get_master_mute() {
|
||||
Ok(muted) => {
|
||||
println!(" Current master mute : {}", muted);
|
||||
if let Err(e) = renderer.set_master_mute(muted) {
|
||||
println!(" set_master_mute({}) failed: {}", muted, e);
|
||||
} else {
|
||||
println!(" set_master_mute({}) OK (no-op)", muted);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" get_master_mute() failed: {}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("\n[RenderingControl]");
|
||||
println!(" Renderer has no RenderingControl service.");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 5. Si AVTransport dispo : tester play_uri / seek / pause / stop
|
||||
// -------------------------------------------------------------------------
|
||||
if !renderer.has_avtransport() {
|
||||
println!("\n[AVTransport]");
|
||||
println!(" Renderer has no AVTransport service, skipping playback tests.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("\n[AVTransport]");
|
||||
|
||||
// Helper pour afficher l'état courant
|
||||
let dump_info = |label: &str| -> Result<()> {
|
||||
let info = renderer
|
||||
.avtransport()
|
||||
.and_then(|_| renderer.avtransport().unwrap().get_transport_info(0))?;
|
||||
println!("\n [{}]", label);
|
||||
println!(" State : {}", info.current_transport_state);
|
||||
println!(" Status : {}", info.current_transport_status);
|
||||
println!(" Speed : {}", info.current_speed);
|
||||
Ok(())
|
||||
};
|
||||
|
||||
// Set + Play
|
||||
println!("\n Calling play_uri(...)");
|
||||
renderer.play_uri(&uri, "")?;
|
||||
println!(" play_uri: OK");
|
||||
thread::sleep(Duration::from_secs(8));
|
||||
let _ = dump_info("After play_uri");
|
||||
|
||||
// Seek (si supporté)
|
||||
println!("\n Calling seek_rel_time(\"00:01:00\")...");
|
||||
match renderer.seek_rel_time("00:01:00") {
|
||||
Ok(()) => {
|
||||
println!(" seek_rel_time: OK");
|
||||
thread::sleep(Duration::from_secs(3));
|
||||
let _ = dump_info("After seek_rel_time");
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" seek_rel_time failed: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Pause (ENTER pour laisser jouer)
|
||||
print!("\nPress ENTER to Pause...");
|
||||
io::stdout().flush().ok();
|
||||
let _ = io::stdin().read_line(&mut String::new());
|
||||
|
||||
println!("\n Calling pause()...");
|
||||
match renderer.pause() {
|
||||
Ok(()) => {
|
||||
println!(" pause: OK");
|
||||
thread::sleep(Duration::from_secs(2));
|
||||
let _ = dump_info("After pause");
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" pause failed: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Stop
|
||||
print!("\nPress ENTER to Stop...");
|
||||
io::stdout().flush().ok();
|
||||
let _ = io::stdin().read_line(&mut String::new());
|
||||
|
||||
println!("\n Calling stop()...");
|
||||
match renderer.stop() {
|
||||
Ok(()) => {
|
||||
println!(" stop: OK");
|
||||
let _ = dump_info("After stop");
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" stop failed: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
println!("\nDone.");
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,101 +1,158 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use pmocontrol::{ControlPoint, DeviceRegistryRead, RendererInfo};
|
||||
use std::env;
|
||||
use std::io::{self, Write};
|
||||
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();
|
||||
// Default values
|
||||
let default_uri =
|
||||
"https://audio-fb.radioparadise.com/chan/1/x/1117/4/g/1117-3.flac".to_string();
|
||||
let mut uri = default_uri.clone();
|
||||
let mut renderer_index: usize = 0;
|
||||
|
||||
// 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")?;
|
||||
// Args:
|
||||
// - si args[1] est un entier : index, args[2] éventuel = uri
|
||||
// - sinon : args[1] = uri, args[2] éventuel = index
|
||||
let args: Vec<String> = env::args().collect();
|
||||
if args.len() >= 2 {
|
||||
let first = &args[1];
|
||||
if let Ok(idx) = first.parse::<usize>() {
|
||||
// cas: avtransport_demo 1 [URI]
|
||||
renderer_index = idx;
|
||||
if args.len() >= 3 {
|
||||
uri = args[2].clone();
|
||||
}
|
||||
} else {
|
||||
// cas: avtransport_demo URI [INDEX]
|
||||
uri = first.clone();
|
||||
if args.len() >= 3 {
|
||||
if let Ok(idx) = args[2].parse::<usize>() {
|
||||
renderer_index = idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let registry = cp.registry();
|
||||
println!("Using URI: {}", uri);
|
||||
println!("Requested renderer index: {}", renderer_index);
|
||||
|
||||
// 2. Laisser un peu de temps à la découverte
|
||||
println!("Waiting 5 seconds for UPnP discovery...");
|
||||
// 1. Start control point and let discovery run a bit
|
||||
let cp = ControlPoint::spawn(5)?;
|
||||
thread::sleep(Duration::from_secs(5));
|
||||
|
||||
// 3. Lister les renderers connus
|
||||
let renderers = {
|
||||
let reg = registry.read().expect("DeviceRegistry RwLock poisoned");
|
||||
reg.list_renderers()
|
||||
};
|
||||
// 2. Get registry snapshot
|
||||
let registry = cp.registry();
|
||||
let reg = registry.read().unwrap();
|
||||
let all_renderers = reg.list_renderers();
|
||||
|
||||
// Filter out the in-dev PMOMusic renderer
|
||||
let renderers: Vec<RendererInfo> = all_renderers
|
||||
.into_iter()
|
||||
.filter(|r| {
|
||||
!r.friendly_name
|
||||
.to_ascii_lowercase()
|
||||
.contains("pmomusic audio renderer")
|
||||
})
|
||||
.collect();
|
||||
|
||||
if renderers.is_empty() {
|
||||
println!("No UPnP renderers discovered.");
|
||||
println!("No valid UPnP MediaRenderer discovered.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("Discovered renderers:");
|
||||
println!("Discovered MediaRenderers:");
|
||||
for (idx, r) in renderers.iter().enumerate() {
|
||||
println!(
|
||||
" [{}] {} (model: {}, UDN: {})",
|
||||
idx, r.friendly_name, r.model_name, r.udn
|
||||
" [{}] {} | model={} | udn={} | location={}",
|
||||
idx,
|
||||
r.friendly_name,
|
||||
r.model_name,
|
||||
r.udn,
|
||||
r.location,
|
||||
);
|
||||
}
|
||||
|
||||
// 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);
|
||||
if renderer_index >= renderers.len() {
|
||||
return Err(anyhow!(
|
||||
"Renderer index {} out of range (0..={})",
|
||||
renderer_index,
|
||||
renderers.len().saturating_sub(1)
|
||||
));
|
||||
}
|
||||
|
||||
// 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,
|
||||
);
|
||||
// 3. Selection by index
|
||||
let renderer: &RendererInfo = &renderers[renderer_index];
|
||||
|
||||
// 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")?;
|
||||
println!("\nSelected renderer (index {}):", renderer_index);
|
||||
println!(" Name : {}", renderer.friendly_name);
|
||||
println!(" Model : {}", renderer.model_name);
|
||||
println!(" Manufacturer: {}", renderer.manufacturer);
|
||||
println!(" UDN : {}", renderer.udn);
|
||||
println!(" Location : {}", renderer.location);
|
||||
|
||||
let client = match client_opt {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
println!(
|
||||
"Renderer {} has no AVTransport service (or no controlURL).",
|
||||
renderer.friendly_name
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
// 4. Get AVTransport client
|
||||
let avtransport = reg
|
||||
.avtransport_client_for_renderer(&renderer.id)
|
||||
.expect("Selected renderer has no AVTransport service");
|
||||
|
||||
println!(" AVTransport control URL : {}", avtransport.control_url);
|
||||
println!(" AVTransport service type: {}", avtransport.service_type);
|
||||
|
||||
// We are done with the registry lock
|
||||
drop(reg);
|
||||
|
||||
// 5. Configure the URI
|
||||
let meta = ""; // or a full DIDL-Lite string
|
||||
|
||||
println!("\nCalling SetAVTransportURI...");
|
||||
avtransport.set_av_transport_uri(&uri, meta)?;
|
||||
println!(" SetAVTransportURI: OK");
|
||||
|
||||
// Helper closure to dump current TransportInfo
|
||||
let dump_info = |label: &str| -> Result<()> {
|
||||
let info = avtransport.get_transport_info(0)?;
|
||||
println!("\n[{}]", label);
|
||||
println!(" State : {}", info.current_transport_state);
|
||||
println!(" Status : {}", info.current_transport_status);
|
||||
println!(" Speed : {}", info.current_speed);
|
||||
Ok(())
|
||||
};
|
||||
|
||||
println!(
|
||||
"AVTransport endpoint:\n service_type = {}\n control_url = {}",
|
||||
client.service_type, client.control_url
|
||||
);
|
||||
dump_info("After SetAVTransportURI")?;
|
||||
|
||||
// 7. Appeler GetTransportInfo(InstanceID=0)
|
||||
println!("\nCalling GetTransportInfo (InstanceID = 0)...");
|
||||
let info = client
|
||||
.get_transport_info(0)
|
||||
.context("GetTransportInfo SOAP call failed")?;
|
||||
// 6. Play
|
||||
println!("\nCalling Play (Speed=\"1\")...");
|
||||
avtransport.play(0, "1")?;
|
||||
println!(" Play: OK");
|
||||
thread::sleep(Duration::from_secs(20));
|
||||
dump_info("After Play")?;
|
||||
|
||||
println!("GetTransportInfo result:");
|
||||
println!(" CurrentTransportState = {}", info.current_transport_state);
|
||||
println!(" CurrentTransportStatus = {}", info.current_transport_status);
|
||||
println!(" CurrentSpeed = {}", info.current_speed);
|
||||
// 7. Optional: wait before Pause/Stop
|
||||
print!("\nPress ENTER to Pause...");
|
||||
io::stdout().flush().ok();
|
||||
let _ = io::stdin().read_line(&mut String::new());
|
||||
|
||||
// 8. Pause
|
||||
println!("\nCalling Pause...");
|
||||
if let Err(e) = avtransport.pause(0) {
|
||||
println!(" Pause failed: {e}");
|
||||
} else {
|
||||
println!(" Pause: OK");
|
||||
thread::sleep(Duration::from_secs(2));
|
||||
dump_info("After Pause")?;
|
||||
}
|
||||
|
||||
// 9. Stop
|
||||
println!("\nCalling Stop...");
|
||||
if let Err(e) = avtransport.stop(0) {
|
||||
println!(" Stop failed: {e}");
|
||||
} else {
|
||||
println!(" Stop: OK");
|
||||
dump_info("After Stop")?;
|
||||
}
|
||||
|
||||
println!("\nDone.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// pmocontrol/src/avtransport_client.rs
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use crate::soap_client::invoke_upnp_action;
|
||||
use crate::soap_client::{invoke_upnp_action, SoapCallResult};
|
||||
use pmoupnp::soap::SoapEnvelope;
|
||||
use xmltree::{Element, XMLNode};
|
||||
|
||||
@@ -24,6 +26,7 @@ impl AvTransportClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// AVTransport:1 — GetTransportInfo
|
||||
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())];
|
||||
@@ -49,6 +52,131 @@ impl AvTransportClient {
|
||||
|
||||
parse_transport_info(envelope)
|
||||
}
|
||||
|
||||
/// AVTransport:1 — SetAVTransportURI
|
||||
///
|
||||
/// Pour l’instant on force `InstanceID = 0`, ce qui couvre la majorité
|
||||
/// des MediaRenderers UPnP AV (un seul instance de transport).
|
||||
///
|
||||
/// - `uri` : CurrentURI
|
||||
/// - `meta` : CurrentURIMetaData (DIDL-Lite ou chaîne vide)
|
||||
pub fn set_av_transport_uri(&self, uri: &str, meta: &str) -> Result<()> {
|
||||
let args = [
|
||||
("InstanceID", "0"),
|
||||
("CurrentURI", uri),
|
||||
("CurrentURIMetaData", meta),
|
||||
];
|
||||
|
||||
let call_result = invoke_upnp_action(
|
||||
&self.control_url,
|
||||
&self.service_type,
|
||||
"SetAVTransportURI",
|
||||
&args,
|
||||
)?;
|
||||
|
||||
handle_action_response("SetAVTransportURI", &call_result)
|
||||
}
|
||||
|
||||
/// AVTransport:1 — Play
|
||||
pub fn play(&self, instance_id: u32, speed: &str) -> Result<()> {
|
||||
let instance_id_str = instance_id.to_string();
|
||||
let args = [
|
||||
("InstanceID", instance_id_str.as_str()),
|
||||
("Speed", speed),
|
||||
];
|
||||
|
||||
let call_result = invoke_upnp_action(
|
||||
&self.control_url,
|
||||
&self.service_type,
|
||||
"Play",
|
||||
&args,
|
||||
)?;
|
||||
|
||||
handle_action_response("Play", &call_result)
|
||||
}
|
||||
|
||||
/// AVTransport:1 — Pause
|
||||
pub fn pause(&self, instance_id: u32) -> Result<()> {
|
||||
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,
|
||||
"Pause",
|
||||
&args,
|
||||
)?;
|
||||
|
||||
handle_action_response("Pause", &call_result)
|
||||
}
|
||||
|
||||
/// AVTransport:1 — Stop
|
||||
pub fn stop(&self, instance_id: u32) -> Result<()> {
|
||||
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,
|
||||
"Stop",
|
||||
&args,
|
||||
)?;
|
||||
|
||||
handle_action_response("Stop", &call_result)
|
||||
}
|
||||
|
||||
/// AVTransport:1 — Seek
|
||||
pub fn seek(&self, instance_id: u32, unit: &str, target: &str) -> Result<()> {
|
||||
let instance_id_str = instance_id.to_string();
|
||||
let args = [
|
||||
("InstanceID", instance_id_str.as_str()),
|
||||
("Unit", unit),
|
||||
("Target", target),
|
||||
];
|
||||
|
||||
let call_result = invoke_upnp_action(
|
||||
&self.control_url,
|
||||
&self.service_type,
|
||||
"Seek",
|
||||
&args,
|
||||
)?;
|
||||
|
||||
handle_action_response("Seek", &call_result)
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_action_response(action: &str, call_result: &SoapCallResult) -> Result<()> {
|
||||
if !call_result.status.is_success() {
|
||||
if let Some(env) = &call_result.envelope {
|
||||
if let Some(upnp_error) = parse_upnp_error(env) {
|
||||
return Err(anyhow!(
|
||||
"{action} failed with UPnP error {}: {} (HTTP status {})",
|
||||
upnp_error.error_code,
|
||||
upnp_error.error_description,
|
||||
call_result.status
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return Err(anyhow!(
|
||||
"{action} failed with HTTP status {} and body: {}",
|
||||
call_result.status,
|
||||
call_result.raw_body
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(env) = &call_result.envelope {
|
||||
if let Some(upnp_error) = parse_upnp_error(env) {
|
||||
return Err(anyhow!(
|
||||
"{action} returned UPnP error {}: {} (HTTP status {})",
|
||||
upnp_error.error_code,
|
||||
upnp_error.error_description,
|
||||
call_result.status
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_transport_info(envelope: &SoapEnvelope) -> Result<TransportInfo> {
|
||||
@@ -68,6 +196,64 @@ fn parse_transport_info(envelope: &SoapEnvelope) -> Result<TransportInfo> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Représente une erreur UPnP extraite d’un SOAP Fault.
|
||||
#[derive(Debug, Clone)]
|
||||
struct UpnpError {
|
||||
pub error_code: u32,
|
||||
pub error_description: String,
|
||||
}
|
||||
|
||||
/// Parse un éventuel SOAP Fault contenant un UPnPError.
|
||||
///
|
||||
/// Schéma typique (SOAP 1.1) :
|
||||
///
|
||||
/// ```xml
|
||||
/// <s:Body>
|
||||
/// <s:Fault>
|
||||
/// <faultcode>...</faultcode>
|
||||
/// <faultstring>...</faultstring>
|
||||
/// <detail>
|
||||
/// <UPnPError xmlns="urn:schemas-upnp-org:control-1-0">
|
||||
/// <errorCode>401</errorCode>
|
||||
/// <errorDescription>Invalid Action</errorDescription>
|
||||
/// </UPnPError>
|
||||
/// </detail>
|
||||
/// </s:Fault>
|
||||
/// </s:Body>
|
||||
/// ```
|
||||
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")?;
|
||||
|
||||
// errorCode (obligatoire dans la spec)
|
||||
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()?;
|
||||
|
||||
// errorDescription (optionnel, mais utile)
|
||||
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),
|
||||
@@ -110,7 +296,9 @@ mod tests {
|
||||
"CurrentTransportStatus",
|
||||
"OK",
|
||||
)));
|
||||
response.children.push(XMLNode::Element(text_element("CurrentSpeed", "1")));
|
||||
response
|
||||
.children
|
||||
.push(XMLNode::Element(text_element("CurrentSpeed", "1")));
|
||||
|
||||
let mut body = Element::new("s:Body");
|
||||
body.children.push(XMLNode::Element(response));
|
||||
@@ -126,3 +314,45 @@ mod tests {
|
||||
assert_eq!(info.current_speed, "1");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod upnp_error_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_upnp_error_extracts_error_code_and_description() {
|
||||
let error_code = text_element("errorCode", "401");
|
||||
let error_description = text_element("errorDescription", "Invalid Action");
|
||||
|
||||
let mut upnp_error = Element::new("UPnPError");
|
||||
upnp_error.children.push(XMLNode::Element(error_code));
|
||||
upnp_error
|
||||
.children
|
||||
.push(XMLNode::Element(error_description));
|
||||
|
||||
let mut detail = Element::new("detail");
|
||||
detail.children.push(XMLNode::Element(upnp_error));
|
||||
|
||||
let mut fault = Element::new("s:Fault");
|
||||
fault.children.push(XMLNode::Element(detail));
|
||||
|
||||
let mut body = Element::new("s:Body");
|
||||
body.children.push(XMLNode::Element(fault));
|
||||
|
||||
let envelope = SoapEnvelope {
|
||||
header: None,
|
||||
body: SoapBody { content: body },
|
||||
};
|
||||
|
||||
let err = parse_upnp_error(&envelope).expect("Expected UPnPError");
|
||||
assert_eq!(err.error_code, 401);
|
||||
assert_eq!(err.error_description, "Invalid Action");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ use std::time::Duration;
|
||||
use pmoupnp::ssdp::SsdpClient;
|
||||
|
||||
use crate::discovery::DiscoveryManager;
|
||||
use crate::model::RendererId;
|
||||
use crate::provider::HttpXmlDescriptionProvider;
|
||||
use crate::registry::{DeviceRegistry, DeviceUpdate};
|
||||
use crate::registry::{DeviceRegistry, DeviceRegistryRead, DeviceUpdate};
|
||||
use crate::renderer::Renderer;
|
||||
|
||||
/// Control point minimal :
|
||||
/// - lance un SsdpClient dans un thread,
|
||||
@@ -76,4 +78,29 @@ impl ControlPoint {
|
||||
pub fn registry(&self) -> Arc<RwLock<DeviceRegistry>> {
|
||||
Arc::clone(&self.registry)
|
||||
}
|
||||
|
||||
/// Snapshot list of renderers currently known by the registry.
|
||||
pub fn list_renderer_handles(&self) -> Vec<Renderer> {
|
||||
let reg = self.registry.read().unwrap();
|
||||
reg.list_renderers()
|
||||
.into_iter()
|
||||
.map(|info| Renderer::from_registry(info, ®))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Return the first renderer in the registry, if any.
|
||||
pub fn default_renderer(&self) -> Option<Renderer> {
|
||||
let reg = self.registry.read().unwrap();
|
||||
reg.list_renderers()
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|info| Renderer::from_registry(info, ®))
|
||||
}
|
||||
|
||||
/// Lookup a renderer by id.
|
||||
pub fn renderer_by_id(&self, id: &RendererId) -> Option<Renderer> {
|
||||
let reg = self.registry.read().unwrap();
|
||||
reg.get_renderer(id)
|
||||
.map(|info| Renderer::from_registry(info, ®))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,15 @@ pub mod control_point;
|
||||
pub mod discovery;
|
||||
pub mod model;
|
||||
pub mod provider;
|
||||
pub mod renderer;
|
||||
pub mod rendering_control_client;
|
||||
pub mod registry;
|
||||
pub mod soap_client;
|
||||
|
||||
pub use avtransport_client::{AvTransportClient, TransportInfo};
|
||||
pub use control_point::ControlPoint;
|
||||
pub use rendering_control_client::RenderingControlClient;
|
||||
pub use renderer::Renderer;
|
||||
|
||||
pub use discovery::{DeviceDescriptionProvider, DiscoveredEndpoint, DiscoveryManager};
|
||||
pub use model::{
|
||||
|
||||
@@ -43,6 +43,8 @@ pub struct RendererInfo {
|
||||
|
||||
pub avtransport_service_type: Option<String>,
|
||||
pub avtransport_control_url: Option<String>,
|
||||
pub rendering_control_service_type: Option<String>,
|
||||
pub rendering_control_control_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
|
||||
@@ -42,6 +42,10 @@ struct ParsedDeviceDescription {
|
||||
// New: AVTransport endpoint (if present in serviceList)
|
||||
avtransport_service_type: Option<String>,
|
||||
avtransport_control_url: Option<String>,
|
||||
|
||||
// RenderingControl endpoint (if present in serviceList)
|
||||
rendering_control_service_type: Option<String>,
|
||||
rendering_control_control_url: Option<String>,
|
||||
}
|
||||
|
||||
impl ParsedDeviceDescription {
|
||||
@@ -161,6 +165,19 @@ impl HttpXmlDescriptionProvider {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if lower
|
||||
.contains("urn:schemas-upnp-org:service:renderingcontrol:")
|
||||
{
|
||||
if parsed.rendering_control_service_type.is_none() {
|
||||
parsed.rendering_control_service_type = Some(st.clone());
|
||||
parsed.rendering_control_control_url = Some(ctrl.clone());
|
||||
debug!(
|
||||
"Found RenderingControl service for {}: type={} controlURL={}",
|
||||
endpoint.udn, st, ctrl
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
in_service = false;
|
||||
@@ -260,6 +277,11 @@ impl HttpXmlDescriptionProvider {
|
||||
.avtransport_control_url
|
||||
.as_ref()
|
||||
.map(|ctrl| resolve_control_url(&endpoint.location, ctrl)),
|
||||
rendering_control_service_type: parsed.rendering_control_service_type.clone(),
|
||||
rendering_control_control_url: parsed
|
||||
.rendering_control_control_url
|
||||
.as_ref()
|
||||
.map(|ctrl| resolve_control_url(&endpoint.location, ctrl)),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -463,4 +485,4 @@ impl DeviceDescriptionProvider for HttpXmlDescriptionProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::collections::HashMap;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use crate::avtransport_client::AvTransportClient;
|
||||
use crate::rendering_control_client::RenderingControlClient;
|
||||
use crate::model::{MediaServerId, MediaServerInfo, RendererId, RendererInfo};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -158,4 +159,20 @@ impl DeviceRegistry {
|
||||
|
||||
Some(AvTransportClient::new(control_url, service_type))
|
||||
}
|
||||
|
||||
/// Construct a RenderingControlClient for a given renderer id, if possible.
|
||||
pub fn rendering_control_client_for_renderer(
|
||||
&self,
|
||||
id: &RendererId,
|
||||
) -> Option<RenderingControlClient> {
|
||||
let info = self.renderers.get(id)?;
|
||||
|
||||
let service_type = info.rendering_control_service_type.as_ref()?;
|
||||
let control_url = info.rendering_control_control_url.as_ref()?;
|
||||
|
||||
Some(RenderingControlClient::new(
|
||||
control_url.clone(),
|
||||
service_type.clone(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
152
pmocontrol/src/renderer.rs
Normal file
152
pmocontrol/src/renderer.rs
Normal file
@@ -0,0 +1,152 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
|
||||
use crate::rendering_control_client::RenderingControlClient;
|
||||
use crate::{AvTransportClient, DeviceRegistry, RendererId, RendererInfo};
|
||||
|
||||
/// High-level handle representing a renderer and its optional AVTransport client.
|
||||
pub struct Renderer {
|
||||
pub info: RendererInfo,
|
||||
avtransport: Option<AvTransportClient>,
|
||||
rendering_control: Option<RenderingControlClient>,
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
pub fn id(&self) -> &RendererId {
|
||||
&self.info.id
|
||||
}
|
||||
|
||||
pub fn friendly_name(&self) -> &str {
|
||||
&self.info.friendly_name
|
||||
}
|
||||
|
||||
pub fn has_avtransport(&self) -> bool {
|
||||
self.avtransport.is_some()
|
||||
}
|
||||
|
||||
pub fn has_rendering_control(&self) -> bool {
|
||||
self.rendering_control.is_some()
|
||||
}
|
||||
|
||||
pub fn avtransport(&self) -> Result<&AvTransportClient> {
|
||||
self.avtransport
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("Renderer has no AVTransport service"))
|
||||
}
|
||||
|
||||
pub fn rendering_control(&self) -> Result<&RenderingControlClient> {
|
||||
self.rendering_control
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("Renderer has no RenderingControl service"))
|
||||
}
|
||||
|
||||
pub fn play_uri(&self, uri: &str, meta: &str) -> Result<()> {
|
||||
let avt = self.avtransport()?;
|
||||
avt.set_av_transport_uri(uri, meta)?;
|
||||
avt.play(0, "1")
|
||||
}
|
||||
|
||||
pub fn pause(&self) -> Result<()> {
|
||||
let avt = self.avtransport()?;
|
||||
avt.pause(0)
|
||||
}
|
||||
|
||||
pub fn stop(&self) -> Result<()> {
|
||||
let avt = self.avtransport()?;
|
||||
avt.stop(0)
|
||||
}
|
||||
|
||||
pub fn seek_rel_time(&self, hhmmss: &str) -> Result<()> {
|
||||
let avt = self.avtransport()?;
|
||||
avt.seek(0, "REL_TIME", hhmmss)
|
||||
}
|
||||
|
||||
pub fn get_master_volume(&self) -> Result<u16> {
|
||||
let rc = self.rendering_control()?;
|
||||
rc.get_volume(0, "Master")
|
||||
}
|
||||
|
||||
pub fn set_master_volume(&self, volume: u16) -> Result<()> {
|
||||
let rc = self.rendering_control()?;
|
||||
rc.set_volume(0, "Master", volume)
|
||||
}
|
||||
|
||||
pub fn get_master_mute(&self) -> Result<bool> {
|
||||
let rc = self.rendering_control()?;
|
||||
rc.get_mute(0, "Master")
|
||||
}
|
||||
|
||||
pub fn set_master_mute(&self, mute: bool) -> Result<()> {
|
||||
let rc = self.rendering_control()?;
|
||||
rc.set_mute(0, "Master", mute)
|
||||
}
|
||||
|
||||
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);
|
||||
Self {
|
||||
info,
|
||||
avtransport,
|
||||
rendering_control,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::model::{RendererCapabilities, RendererProtocol};
|
||||
use crate::registry::{DeviceRegistry, DeviceUpdate};
|
||||
use std::time::SystemTime;
|
||||
|
||||
fn renderer_info(id_suffix: &str, with_avtransport: bool) -> RendererInfo {
|
||||
RendererInfo {
|
||||
id: RendererId(format!("renderer-{id_suffix}")),
|
||||
udn: format!("uuid:renderer-{id_suffix}"),
|
||||
friendly_name: format!("Renderer {id_suffix}"),
|
||||
model_name: "Model".into(),
|
||||
manufacturer: "Manufacturer".into(),
|
||||
protocol: RendererProtocol::UpnpAvOnly,
|
||||
capabilities: RendererCapabilities {
|
||||
has_avtransport: with_avtransport,
|
||||
..RendererCapabilities::default()
|
||||
},
|
||||
location: "http://127.0.0.1/device.xml".into(),
|
||||
server_header: "TestServer/1.0".into(),
|
||||
online: true,
|
||||
last_seen: SystemTime::now(),
|
||||
max_age: 1800,
|
||||
avtransport_service_type: with_avtransport
|
||||
.then(|| "urn:schemas-upnp-org:service:AVTransport:1".into()),
|
||||
avtransport_control_url: with_avtransport
|
||||
.then(|| "http://127.0.0.1/avtransport".into()),
|
||||
rendering_control_service_type: None,
|
||||
rendering_control_control_url: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn registry_with_renderer(info: RendererInfo) -> DeviceRegistry {
|
||||
let mut registry = DeviceRegistry::new();
|
||||
registry.apply_update(DeviceUpdate::RendererOnline(info));
|
||||
registry
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renderer_without_avtransport() {
|
||||
let info = renderer_info("no-avt", false);
|
||||
let registry = registry_with_renderer(info.clone());
|
||||
let renderer = Renderer::from_registry(info, ®istry);
|
||||
|
||||
assert_eq!(renderer.has_avtransport(), false);
|
||||
assert_eq!(renderer.id().0, "renderer-no-avt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renderer_with_avtransport() {
|
||||
let info = renderer_info("with-avt", true);
|
||||
let registry = registry_with_renderer(info.clone());
|
||||
let renderer = Renderer::from_registry(info, ®istry);
|
||||
|
||||
assert!(renderer.has_avtransport());
|
||||
assert_eq!(renderer.friendly_name(), "Renderer with-avt");
|
||||
}
|
||||
}
|
||||
248
pmocontrol/src/rendering_control_client.rs
Normal file
248
pmocontrol/src/rendering_control_client.rs
Normal file
@@ -0,0 +1,248 @@
|
||||
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 RenderingControlClient {
|
||||
pub control_url: String,
|
||||
pub service_type: String,
|
||||
}
|
||||
|
||||
impl RenderingControlClient {
|
||||
pub fn new(control_url: String, service_type: String) -> Self {
|
||||
Self {
|
||||
control_url,
|
||||
service_type,
|
||||
}
|
||||
}
|
||||
|
||||
/// RenderingControl:1 — GetVolume
|
||||
pub fn get_volume(&self, instance_id: u32, channel: &str) -> Result<u16> {
|
||||
let instance_id_str = instance_id.to_string();
|
||||
let args = [
|
||||
("InstanceID", instance_id_str.as_str()),
|
||||
("Channel", channel),
|
||||
];
|
||||
|
||||
let call_result = invoke_upnp_action(
|
||||
&self.control_url,
|
||||
&self.service_type,
|
||||
"GetVolume",
|
||||
&args,
|
||||
)?;
|
||||
|
||||
ensure_success("GetVolume", &call_result)?;
|
||||
|
||||
let envelope = call_result
|
||||
.envelope
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("Missing SOAP envelope in GetVolume response"))?;
|
||||
|
||||
if let Some(err) = parse_upnp_error(envelope) {
|
||||
return Err(anyhow!(
|
||||
"GetVolume returned UPnP error {}: {} (HTTP status {})",
|
||||
err.error_code,
|
||||
err.error_description,
|
||||
call_result.status
|
||||
));
|
||||
}
|
||||
|
||||
let response =
|
||||
find_child_with_suffix(&envelope.body.content, "GetVolumeResponse")
|
||||
.ok_or_else(|| anyhow!("Missing GetVolumeResponse element in SOAP body"))?;
|
||||
|
||||
let text = extract_child_text(response, "CurrentVolume")?;
|
||||
let volume = text
|
||||
.parse::<u16>()
|
||||
.map_err(|_| anyhow!("Invalid CurrentVolume value: {}", text))?;
|
||||
|
||||
Ok(volume)
|
||||
}
|
||||
|
||||
/// RenderingControl:1 — SetVolume
|
||||
pub fn set_volume(&self, instance_id: u32, channel: &str, volume: u16) -> Result<()> {
|
||||
let instance_id_str = instance_id.to_string();
|
||||
let volume_str = volume.to_string();
|
||||
let args = [
|
||||
("InstanceID", instance_id_str.as_str()),
|
||||
("Channel", channel),
|
||||
("DesiredVolume", volume_str.as_str()),
|
||||
];
|
||||
|
||||
let call_result = invoke_upnp_action(
|
||||
&self.control_url,
|
||||
&self.service_type,
|
||||
"SetVolume",
|
||||
&args,
|
||||
)?;
|
||||
|
||||
handle_action_response("SetVolume", &call_result)
|
||||
}
|
||||
|
||||
/// RenderingControl:1 — GetMute
|
||||
pub fn get_mute(&self, instance_id: u32, channel: &str) -> Result<bool> {
|
||||
let instance_id_str = instance_id.to_string();
|
||||
let args = [
|
||||
("InstanceID", instance_id_str.as_str()),
|
||||
("Channel", channel),
|
||||
];
|
||||
|
||||
let call_result = invoke_upnp_action(
|
||||
&self.control_url,
|
||||
&self.service_type,
|
||||
"GetMute",
|
||||
&args,
|
||||
)?;
|
||||
|
||||
ensure_success("GetMute", &call_result)?;
|
||||
|
||||
let envelope = call_result
|
||||
.envelope
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("Missing SOAP envelope in GetMute response"))?;
|
||||
|
||||
if let Some(err) = parse_upnp_error(envelope) {
|
||||
return Err(anyhow!(
|
||||
"GetMute returned UPnP error {}: {} (HTTP status {})",
|
||||
err.error_code,
|
||||
err.error_description,
|
||||
call_result.status
|
||||
));
|
||||
}
|
||||
|
||||
let response = find_child_with_suffix(&envelope.body.content, "GetMuteResponse")
|
||||
.ok_or_else(|| anyhow!("Missing GetMuteResponse element in SOAP body"))?;
|
||||
|
||||
let text = extract_child_text(response, "CurrentMute")?;
|
||||
let mute = match text.as_str() {
|
||||
"0" => false,
|
||||
"1" => true,
|
||||
_ => {
|
||||
return Err(anyhow!(
|
||||
"Invalid CurrentMute value: {} (expected 0 or 1)",
|
||||
text
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(mute)
|
||||
}
|
||||
|
||||
/// RenderingControl:1 — SetMute
|
||||
pub fn set_mute(&self, instance_id: u32, channel: &str, mute: bool) -> Result<()> {
|
||||
let instance_id_str = instance_id.to_string();
|
||||
let mute_str = if mute { "1" } else { "0" };
|
||||
let args = [
|
||||
("InstanceID", instance_id_str.as_str()),
|
||||
("Channel", channel),
|
||||
("DesiredMute", mute_str),
|
||||
];
|
||||
|
||||
let call_result = invoke_upnp_action(
|
||||
&self.control_url,
|
||||
&self.service_type,
|
||||
"SetMute",
|
||||
&args,
|
||||
)?;
|
||||
|
||||
handle_action_response("SetMute", &call_result)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
))
|
||||
}
|
||||
|
||||
fn handle_action_response(action: &str, call_result: &SoapCallResult) -> Result<()> {
|
||||
ensure_success(action, call_result)?;
|
||||
|
||||
if let Some(env) = &call_result.envelope {
|
||||
if let Some(err) = parse_upnp_error(env) {
|
||||
return Err(anyhow!(
|
||||
"{action} returned UPnP error {}: {} (HTTP status {})",
|
||||
err.error_code,
|
||||
err.error_description,
|
||||
call_result.status
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[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 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())
|
||||
.filter(|t| !t.is_empty())
|
||||
.ok_or_else(|| anyhow!("{suffix} element missing text in response"))?;
|
||||
|
||||
Ok(text)
|
||||
}
|
||||
Reference in New Issue
Block a user