use crate::music_renderer::{PlaybackPositionInfo, PlaylistBinding}; use crate::{DeviceId, DeviceIdentity}; /// Basic device information for event notifications /// Contains only the essential identification fields #[derive(Clone, Debug)] pub struct DeviceBasicInfo { pub id: DeviceId, pub friendly_name: String, pub model_name: String, pub manufacturer: String, } impl DeviceBasicInfo { /// Create from any type implementing DeviceIdentity pub fn from_device(device: &D) -> Self { Self { id: device.id(), friendly_name: device.friendly_name().to_string(), model_name: device.model_name().to_string(), manufacturer: device.manufacturer().to_string(), } } } /// High-level playback state across backends. #[derive(Clone, Debug)] pub enum PlaybackState { Stopped, Playing, Paused, Transitioning, NoMedia, /// Backend-specific or unknown state string. Unknown(String), } impl PlaybackState { /// Map a raw UPnP AVTransport CurrentTransportState string /// to a logical PlaybackState. pub fn from_upnp_state(raw: &str) -> Self { let s = raw.trim().to_ascii_uppercase(); match s.as_str() { "STOPPED" => PlaybackState::Stopped, "PLAYING" => PlaybackState::Playing, "PAUSED_PLAYBACK" => PlaybackState::Paused, // States from the AVTransport spec that we normalize: "PAUSED_RECORDING" => PlaybackState::Paused, "RECORDING" => PlaybackState::Playing, // Common vendor-specific states: "TRANSITIONING" => PlaybackState::Transitioning, "BUFFERING" | "PREPARING" => PlaybackState::Transitioning, "NO_MEDIA_PRESENT" => PlaybackState::NoMedia, _ => PlaybackState::Unknown(raw.to_string()), } } /// Returns a human-readable label for the playback state. pub fn as_str(&self) -> &str { match self { PlaybackState::Stopped => "STOPPED", PlaybackState::Playing => "PLAYING", PlaybackState::Paused => "PAUSED", PlaybackState::Transitioning => "TRANSITIONING", PlaybackState::NoMedia => "NO_MEDIA", PlaybackState::Unknown(s) => s.as_str(), } } } /// Indicates the source of the current playback. /// /// This helps the control point distinguish between playback initiated /// from the queue vs external sources (e.g., user playing from another app). #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum PlaybackSource { /// No playback active or source unknown. #[default] None, /// Playback was started from the control point's queue. FromQueue, /// Playback was started externally (e.g., from another app). External, } #[derive(Clone, Debug, PartialEq)] pub struct TrackMetadata { pub title: Option, pub artist: Option, pub album: Option, pub genre: Option, pub album_art_uri: Option, pub date: Option, pub track_number: Option, pub creator: Option, pub duration: Option, /// Indique si cette piste correspond à un flux continu (radio, stream) /// plutôt qu'à un fichier avec une durée fixe pub is_continuous_stream: bool, } #[derive(Clone, Debug, Copy)] pub enum RendererProtocol { UpnpAvOnly, OpenHomeOnly, OpenHomeHybrid, ChromecastOnly, } #[derive(Clone, Debug, Default)] pub struct RendererCapabilities { pub has_avtransport: bool, /// True if the renderer is known to support AVTransport.SetNextAVTransportURI. /// /// This is discovered lazily at runtime; default is false. pub has_avtransport_set_next: bool, pub has_rendering_control: bool, pub has_connection_manager: bool, pub has_linkplay_http: bool, pub has_arylic_tcp: bool, pub has_oh_playlist: bool, pub has_oh_volume: bool, pub has_oh_info: bool, pub has_oh_time: bool, pub has_oh_radio: bool, pub has_chromecast: bool, } impl RendererCapabilities { pub fn make( has_avtransport: bool, has_avtransport_set_next: bool, has_rendering_control: bool, has_connection_manager: bool, has_linkplay_http: bool, has_arylic_tcp: bool, has_oh_playlist: bool, has_oh_volume: bool, has_oh_info: bool, has_oh_time: bool, has_oh_radio: bool, has_chromecast: bool, ) -> Self { RendererCapabilities { has_avtransport, has_avtransport_set_next, has_rendering_control, has_connection_manager, has_linkplay_http, has_arylic_tcp, has_oh_playlist, has_oh_volume, has_oh_info, has_oh_time, has_oh_radio, has_chromecast, } } pub fn has_avtransport(&self) -> bool { self.has_avtransport } pub fn has_avtransport_set_next(&self) -> bool { self.has_avtransport_set_next } pub fn has_rendering_control(&self) -> bool { self.has_rendering_control } pub fn has_connection_manager(&self) -> bool { self.has_connection_manager } pub fn has_linkplay_http(&self) -> bool { self.has_linkplay_http } pub fn has_arylic_tcp(&self) -> bool { self.has_arylic_tcp } pub fn has_oh_playlist(&self) -> bool { self.has_oh_playlist } pub fn has_oh_volume(&self) -> bool { self.has_oh_volume } pub fn has_oh_info(&self) -> bool { self.has_oh_info } pub fn has_oh_time(&self) -> bool { self.has_oh_time } pub fn has_oh_radio(&self) -> bool { self.has_oh_radio } pub fn has_chromecast(&self) -> bool { self.has_chromecast } pub fn supports_set_next(&self) -> bool { self.has_avtransport && self.has_avtransport_set_next } } #[derive(Clone, Debug)] pub struct RendererInfo { id: DeviceId, udn: String, friendly_name: String, model_name: String, manufacturer: String, protocol: RendererProtocol, capabilities: RendererCapabilities, location: String, server_header: String, avtransport_service_type: Option, avtransport_control_url: Option, rendering_control_service_type: Option, rendering_control_control_url: Option, connection_manager_service_type: Option, connection_manager_control_url: Option, oh_playlist_service_type: Option, oh_playlist_control_url: Option, oh_playlist_event_sub_url: Option, oh_info_service_type: Option, oh_info_control_url: Option, oh_info_event_sub_url: Option, oh_time_service_type: Option, oh_time_control_url: Option, oh_time_event_sub_url: Option, oh_volume_service_type: Option, oh_volume_control_url: Option, oh_radio_service_type: Option, oh_radio_control_url: Option, oh_product_service_type: Option, oh_product_control_url: Option, } impl RendererInfo { pub fn make( id: DeviceId, udn: String, friendly_name: String, model_name: String, manufacturer: String, protocol: RendererProtocol, capabilities: RendererCapabilities, location: String, server_header: String, avtransport_service_type: Option, avtransport_control_url: Option, rendering_control_service_type: Option, rendering_control_control_url: Option, connection_manager_service_type: Option, connection_manager_control_url: Option, oh_playlist_service_type: Option, oh_playlist_control_url: Option, oh_playlist_event_sub_url: Option, oh_info_service_type: Option, oh_info_control_url: Option, oh_info_event_sub_url: Option, oh_time_service_type: Option, oh_time_control_url: Option, oh_time_event_sub_url: Option, oh_volume_service_type: Option, oh_volume_control_url: Option, oh_radio_service_type: Option, oh_radio_control_url: Option, oh_product_service_type: Option, oh_product_control_url: Option, ) -> RendererInfo { RendererInfo { id, udn, friendly_name, model_name, manufacturer, protocol, capabilities, location, server_header, avtransport_service_type, avtransport_control_url, rendering_control_service_type, rendering_control_control_url, connection_manager_service_type, connection_manager_control_url, oh_playlist_service_type, oh_playlist_control_url, oh_playlist_event_sub_url, oh_info_service_type, oh_info_control_url, oh_info_event_sub_url, oh_time_service_type, oh_time_control_url, oh_time_event_sub_url, oh_volume_service_type, oh_volume_control_url, oh_radio_service_type, oh_radio_control_url, oh_product_service_type, oh_product_control_url, } } pub fn protocol(&self) -> RendererProtocol { self.protocol } pub fn capabilities(&self) -> &RendererCapabilities { &self.capabilities } pub fn avtransport_service_type(&self) -> Option { self.avtransport_service_type.clone() } pub fn avtransport_control_url(&self) -> Option { self.avtransport_control_url.clone() } pub fn rendering_control_service_type(&self) -> Option { self.rendering_control_service_type.clone() } pub fn rendering_control_control_url(&self) -> Option { self.rendering_control_control_url.clone() } pub fn connection_manager_service_type(&self) -> Option { self.connection_manager_service_type.clone() } pub fn connection_manager_control_url(&self) -> Option { self.connection_manager_control_url.clone() } pub fn oh_playlist_service_type(&self) -> Option { self.oh_playlist_service_type.clone() } pub fn oh_playlist_control_url(&self) -> Option { self.oh_playlist_control_url.clone() } pub fn oh_playlist_event_sub_url(&self) -> Option { self.oh_playlist_event_sub_url.clone() } pub fn oh_info_service_type(&self) -> Option { self.oh_info_service_type.clone() } pub fn oh_info_control_url(&self) -> Option { self.oh_info_control_url.clone() } pub fn oh_info_event_sub_url(&self) -> Option { self.oh_info_event_sub_url.clone() } pub fn oh_time_service_type(&self) -> Option { self.oh_time_service_type.clone() } pub fn oh_time_control_url(&self) -> Option { self.oh_time_control_url.clone() } pub fn oh_time_event_sub_url(&self) -> Option { self.oh_time_event_sub_url.clone() } pub fn oh_volume_service_type(&self) -> Option { self.oh_volume_service_type.clone() } pub fn oh_volume_control_url(&self) -> Option { self.oh_volume_control_url.clone() } pub fn oh_radio_service_type(&self) -> Option { self.oh_radio_service_type.clone() } pub fn oh_radio_control_url(&self) -> Option { self.oh_radio_control_url.clone() } pub fn oh_product_service_type(&self) -> Option { self.oh_product_service_type.clone() } pub fn oh_product_control_url(&self) -> Option { self.oh_product_control_url.clone() } } impl DeviceIdentity for RendererInfo { fn id(&self) -> DeviceId { self.id.clone() } fn udn(&self) -> &str { &self.udn } fn friendly_name(&self) -> &str { &self.friendly_name } fn model_name(&self) -> &str { &self.model_name } fn manufacturer(&self) -> &str { &self.manufacturer } fn location(&self) -> &str { &self.location } fn server_header(&self) -> &str { &self.server_header } fn is_a_music_renderer(&self) -> bool { true } } #[derive(Clone, Debug)] pub enum RendererEvent { StateChanged { id: DeviceId, state: PlaybackState, }, PositionChanged { id: DeviceId, position: PlaybackPositionInfo, }, VolumeChanged { id: DeviceId, volume: u16, }, MuteChanged { id: DeviceId, mute: bool, }, MetadataChanged { id: DeviceId, metadata: TrackMetadata, }, QueueUpdated { id: DeviceId, queue_length: usize, }, QueueRefreshing { id: DeviceId, }, QueueReadyToPlay { id: DeviceId, }, QueueSyncCancelled { id: DeviceId, }, BindingChanged { id: DeviceId, binding: Option, }, StreamStateChanged { id: DeviceId, is_stream: bool, }, TimerStarted { id: DeviceId, duration_seconds: u32, remaining_seconds: u32, }, TimerUpdated { id: DeviceId, duration_seconds: u32, remaining_seconds: u32, }, TimerTick { id: DeviceId, remaining_seconds: u32, }, TimerExpired { id: DeviceId, }, TimerCancelled { id: DeviceId, }, Online { id: DeviceId, info: DeviceBasicInfo, }, Offline { id: DeviceId, }, } #[derive(Clone, Debug)] pub enum MediaServerEvent { GlobalUpdated { server_id: DeviceId, system_update_id: Option, }, ContainersUpdated { server_id: DeviceId, container_ids: Vec, }, Online { server_id: DeviceId, info: DeviceBasicInfo, }, Offline { server_id: DeviceId, }, }