use std::sync::{Arc, RwLock}; use anyhow::{Result, anyhow}; use crate::capabilities::{PlaybackPositionInfo, PlaybackStatus}; use crate::connection_manager_client::{ConnectionInfo, ConnectionManagerClient, ProtocolInfo}; use crate::music_renderer::op_not_supported; use crate::rendering_control_client::RenderingControlClient; use crate::{ AvTransportClient, DeviceRegistry, PlaybackPosition, PlaybackState, PositionInfo, RendererId, RendererInfo, TransportControl, VolumeControl, }; /// High-level handle representing a renderer and its optional AVTransport client. #[derive(Clone, Debug)] pub struct UpnpRenderer { pub info: RendererInfo, registry: Arc>, avtransport: Option, rendering_control: Option, connection_manager: Option, } impl UpnpRenderer { 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 has_connection_manager(&self) -> bool { self.connection_manager.is_some() } /// Returns true if this renderer is known to support SetNextAVTransportURI. pub fn supports_set_next(&self) -> bool { self.info.capabilities.supports_set_next() } 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 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)?; avt.play(0, "1") } /// Best-effort attempt to configure the next URI via AVTransport SetNextAVTransportURI. pub fn set_next_uri(&self, next_uri: &str, next_meta: &str) -> Result<()> { if !self.info.capabilities.has_avtransport { return Err(op_not_supported("SetNextAVTransportURI", "AVTransport")); } let client = self.avtransport()?; let result = client.set_next_av_transport_uri(next_uri, next_meta); if result.is_ok() { let mut reg = self.registry.write().unwrap(); reg.mark_renderer_supports_set_next(&self.info.id); } result } 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 { 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 { 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 protocol_info(&self) -> Result { let cm = self.connection_manager()?; cm.get_protocol_info() } pub fn connection_ids(&self) -> Result> { let cm = self.connection_manager()?; cm.get_current_connection_ids() } pub fn connection_info(&self, connection_id: i32) -> Result { let cm = self.connection_manager()?; cm.get_current_connection_info(connection_id) } pub fn from_registry(info: RendererInfo, registry: &Arc>) -> Self { let (avtransport, rendering_control, connection_manager) = { let reg = registry.read().unwrap(); ( reg.avtransport_client_for_renderer(&info.id), reg.rendering_control_client_for_renderer(&info.id), reg.connection_manager_client_for_renderer(&info.id), ) }; Self { info, registry: Arc::clone(registry), avtransport, rendering_control, connection_manager, } } } #[cfg(test)] mod tests { use super::*; use crate::model::{RendererCapabilities, RendererProtocol}; use crate::registry::{DeviceRegistry, DeviceUpdate}; use std::sync::{Arc, RwLock}; 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, connection_manager_service_type: None, connection_manager_control_url: None, } } fn registry_with_renderer(info: RendererInfo) -> Arc> { let mut registry = DeviceRegistry::new(); registry.apply_update(DeviceUpdate::RendererOnline(info)); Arc::new(RwLock::new(registry)) } #[test] fn renderer_without_avtransport() { let info = renderer_info("no-avt", false); let registry = registry_with_renderer(info.clone()); let renderer = UpnpRenderer::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 = UpnpRenderer::from_registry(info, ®istry); assert!(renderer.has_avtransport()); assert_eq!(renderer.friendly_name(), "Renderer with-avt"); } } /// Implémentation UPnP AV de `TransportControl` pour [`UpnpRenderer`]. /// /// Cette impl se base sur AVTransport (InstanceID = 0). impl TransportControl for UpnpRenderer { fn play_uri(&self, uri: &str, meta: &str) -> Result<()> { self.play_uri(uri, meta) } fn play(&self) -> Result<()> { let avt = self.avtransport()?; avt.play(0, "1") } fn pause(&self) -> Result<()> { self.pause() } fn stop(&self) -> Result<()> { self.stop() } fn seek_rel_time(&self, hhmmss: &str) -> Result<()> { self.seek_rel_time(hhmmss) } } /// Implémentation UPnP RenderingControl de `VolumeControl` pour [`UpnpRenderer`]. /// /// Cette impl se base sur le channel "Master" (InstanceID = 0). impl VolumeControl for UpnpRenderer { fn volume(&self) -> Result { self.get_master_volume() } fn set_volume(&self, v: u16) -> Result<()> { self.set_master_volume(v) } fn mute(&self) -> Result { self.get_master_mute() } fn set_mute(&self, m: bool) -> Result<()> { self.set_master_mute(m) } } /// Implémentation UPnP AV de `PlaybackStatus` pour [`UpnpRenderer`]. /// /// Utilise AVTransport::GetTransportInfo(InstanceID=0). impl PlaybackStatus for UpnpRenderer { fn playback_state(&self) -> Result { let avt = self.avtransport()?; let info = avt.get_transport_info(0)?; Ok(PlaybackState::from_upnp_state( &info.current_transport_state, )) } } impl PlaybackPosition for UpnpRenderer { fn playback_position(&self) -> Result { let avt = self.avtransport()?; let raw: PositionInfo = avt.get_position_info(0)?; Ok(PlaybackPositionInfo { track: Some(raw.track), rel_time: raw.rel_time, abs_time: raw.abs_time, track_duration: raw.track_duration, }) } }