use crate::LOGGER; use crate::backend::audio::ControllerMsg::Request; use crate::backend::{BackendThread, Registrator}; use crossbeam_channel::{Receiver, Sender}; use eventfd::{EfdFlags, EventFD}; use jmb_logger_rs::*; use mainloop::standard::Mainloop; use pulse::callbacks::ListResult; use pulse::context::subscribe::{Facility, InterestMaskSet}; use pulse::context::{Context, FlagSet, State}; use pulse::mainloop::api; use pulse::mainloop::events::io; use pulse::proplist::Proplist; use pulse::volume::{ChannelVolumes, Volume}; use pulse::{mainloop, operation}; use std::boxed::Box; use std::cell::Cell; use std::collections::HashMap; use std::os::fd::{AsRawFd, RawFd}; use std::rc::Rc; use std::sync::{Arc, Mutex}; use std::thread; use zbus::export::ordered_stream::OrderedStreamExt; use zbus::fdo::{NameOwnerChanged, PropertiesProxy}; use zbus::message::Type; use zbus::names::InterfaceName; use zbus::zvariant::OwnedValue; use zbus::{MatchRule, MessageStream}; pub struct PulseController { pub mainloop: Mainloop, context: Context, device_index: Option, device_bt: Option, current_vol: u8, current_mute: bool, } #[derive(Debug)] pub enum ControllerMsg { NotifyServer, NotifySink(u32), Request, Set(u8, bool), } impl PulseController { pub fn new(tx: Sender) -> Self { let mut mainloop = Mainloop::new().unwrap(); let mut prop_list = Proplist::new().unwrap(); prop_list .set_str(pulse::proplist::properties::APPLICATION_NAME, "jmb-bar") .unwrap(); let mut context = Context::new_with_proplist(&mainloop, "jmb-bar", &prop_list).unwrap(); context.connect(None, FlagSet::NOFLAGS, None).unwrap(); loop { match context.get_state() { State::Ready => break, State::Failed | State::Terminated => panic!("PulseAudio connection failed"), _ => mainloop.iterate(false), }; } let device_list = Arc::new(Mutex::new(Vec::new())); let clone = Arc::clone(&device_list); let op = context .introspect() .get_sink_info_list(move |info| match info { ListResult::Item(device) => { if let Some(description) = device.description.as_ref() && let Some(name) = device.name.as_ref() { if let Ok(mut lock) = clone.lock() { lock.push((name.to_string(), description.to_string())); } else { error!(&LOGGER, "Failed to unlock device list on device {}", name); } } else { error!(&LOGGER, "Failed to access device description"); } } ListResult::End => {} ListResult::Error => {} }); while op.get_state() == operation::State::Running { mainloop.iterate(false); } context.set_subscribe_callback(Some(Box::new(move |facility, _, index| { if facility.is_some_and(|f| f == Facility::Sink) { let _ = tx.send(ControllerMsg::NotifySink(index)); } else if facility.is_some_and(|f| f == Facility::Server) { let _ = tx.send(ControllerMsg::NotifyServer); } }))); let _op = context.subscribe(InterestMaskSet::ALL, |success| { trace!(&LOGGER, "Subscribed to pulseaudio events: {success}"); }); let mut controller = PulseController { context, mainloop, device_index: None, device_bt: None, current_vol: 0, current_mute: true, }; controller.query_default_device_blocking(); controller } pub fn query_default_device_blocking(&mut self) { let default_device = Arc::new(Mutex::new(None)); let dev_clone = default_device.clone(); let op = self.context.introspect().get_server_info(move |info| { if let Some(name) = info.default_sink_name.as_ref() { let _ = dev_clone .lock() .expect("Couldn't get lock on default dev") .insert(name.to_string()); } }); while op.get_state() == operation::State::Running { self.mainloop.iterate(false); } let name = default_device .lock() .expect("Couldn't get lock on device name") .take() .unwrap_or(String::new()); let index = Arc::new(Mutex::new(None)); let index_clone = index.clone(); let op = self .context .introspect() .get_sink_info_by_name(&name, move |res| match res { ListResult::Item(item) => { let _ = index_clone .lock() .expect("Couldn't get lock on device index") .insert(item.index); } ListResult::End => {} ListResult::Error => {} }); while op.get_state() == operation::State::Running { self.mainloop.iterate(false); } let val = index .lock() .expect("Couldn't get lock on device index") .take(); self.device_index = val; self.device_bt = Some(name.starts_with("bluez")); debug!( &LOGGER, "Found default Audio Sink Idx: {:?} Bluetooth: {}", self.device_index, self.device_bt.unwrap() ); } pub fn query_state_blocking(&mut self) -> bool { if let Some(idx) = self.device_index { let volume = Rc::new(Cell::new(self.current_vol)); let vol_clone = volume.clone(); let mute = Rc::new(Cell::new(self.current_mute)); let mute_clone = mute.clone(); let op = self.context .introspect() .get_sink_info_by_index(idx, move |info| match info { ListResult::Item(info) => { let percent = info.volume.avg().0 as f64 * 100.0 / Volume::NORMAL.0 as f64; trace!( &LOGGER, "State of default Sink changed: {percent} {}", info.mute ); vol_clone.set(percent as u8); mute_clone.set(info.mute); } ListResult::End => {} ListResult::Error => {} }); while op.get_state() == operation::State::Running { self.mainloop.iterate(false); } let mut dirty = false; let volume = volume.take(); if self.current_vol != volume { self.current_vol = volume; dirty = true; } let mute = mute.take(); if self.current_mute != mute { self.current_mute = mute; dirty = true; } return dirty; } false } pub fn set_state(&mut self, volume: u8, mute: bool) { if let Some(idx) = self.device_index { if self.current_vol != volume { self.current_vol = volume; let volume = Volume( ((volume.min(150) as f64 / 100.0) * Volume::NORMAL.0 as f64).round() as u32, ); let mut channel_volumes = ChannelVolumes::default(); channel_volumes.set(2, volume); self.context.introspect().set_sink_volume_by_index( idx, &channel_volumes, Some(Box::new(|success| { if !success { warn!(&LOGGER, "Failed setting sink volume"); } })), ); } if self.current_mute != mute { self.current_mute = mute; let a = self.context.introspect().set_sink_mute_by_index( idx, mute, Some(Box::new(|success| { if !success { warn!(&LOGGER, "Failed setting sink mute"); } })), ); while a.get_state() == operation::State::Running { self.mainloop.iterate(false); } } } } } #[derive(Clone, Debug)] pub enum AudioMessage { Init(Sender), Request, Cmd(u8, bool, bool), } pub struct AudioBackend { subscribers: Vec>, command_tx: Option>, } impl AudioBackend { pub fn new() -> Self { Self { subscribers: vec![], command_tx: None, } } } impl BackendThread> for AudioBackend { fn subscribe( registrator: Registrator, ) -> (Receiver, Sender) { let (result_tx, result_rx) = crossbeam_channel::unbounded::(); let _ = registrator.send(result_tx); while let Ok(state) = result_rx.recv() { return match state { AudioMessage::Init(sender) => (result_rx, sender), _ => continue, }; } panic!("Channel closed unexpectedly, without returning anything") } fn subscribers(&self) -> &Vec> { &self.subscribers } fn init(mut self, registration_rx: Receiver>) { let (command_tx, command_rx) = crossbeam_channel::unbounded::(); self.command_tx = Some(command_tx); let instance = Arc::new(Mutex::new(self)); let (controller_tx, controller_rx) = crossbeam_channel::unbounded::(); let notify = Arc::new(EventFD::new(0, EfdFlags::EFD_CLOEXEC).unwrap()); thread::spawn({ let instance = instance.clone(); let controller_tx = controller_tx.clone(); let notify = notify.clone(); move || { let mut controller = PulseController::new(controller_tx); let _event = api::Mainloop::new_io_event( &mut controller.mainloop, notify.as_raw_fd(), io::FlagSet::INPUT, Box::new(move |_, fd, _flags| match drain_eventfd(fd) { Ok(_) => trace!(&LOGGER, "Woke pulseaudio loop using EventFD"), Err(e) => warn!(&LOGGER, "Error draining EventFD {}", e.to_string()), }), ) .unwrap(); loop { while let Ok(msg) = controller_rx.try_recv() { match msg { Request => { instance.lock().expect("Couldn't get lock").publish( AudioMessage::Cmd( controller.current_vol, controller.current_mute, controller.device_bt.unwrap_or(false), ), ); } ControllerMsg::NotifyServer => { controller.query_default_device_blocking(); let _ = controller.query_state_blocking(); instance.lock().expect("Couldn't get lock").publish( AudioMessage::Cmd( controller.current_vol, controller.current_mute, controller.device_bt.unwrap_or(false), ), ); } ControllerMsg::NotifySink(idx) => { if controller.device_index.is_some_and(|i| i == idx) { let changed = controller.query_state_blocking(); if changed { instance.lock().expect("Couldn't get lock").publish( AudioMessage::Cmd( controller.current_vol, controller.current_mute, controller.device_bt.unwrap_or(false), ), ); trace!( &LOGGER, "Change: {} muted: {}", controller.current_vol, controller.current_mute ); } } } ControllerMsg::Set(volume, mute) => { controller.set_state(volume, mute); instance.lock().expect("Couldn't get lock").publish( AudioMessage::Cmd( controller.current_vol, controller.current_mute, controller.device_bt.unwrap_or(false), ), ); } } } controller.mainloop.iterate(true); } } }); thread::spawn({ let instance = instance.clone(); move || { while let Ok(sender) = registration_rx.recv() { if sender .send(AudioMessage::Init( instance .lock() .expect("Couldn't get lock") .command_tx .clone() .unwrap(), )) .is_ok() { let _ = sender.send(AudioMessage::Request); instance .lock() .expect("Couldn't get subscribers") .subscribers .push(sender); } } } }); thread::spawn(move || { while let Ok(command) = command_rx.recv() { match command { AudioMessage::Cmd(vol, mute, _) => { let _ = controller_tx.send(ControllerMsg::Set(vol, mute)); notify.write(1).expect("Error writing notify"); } AudioMessage::Request => { let _ = controller_tx.send(Request); notify.write(1).expect("Error writing notify"); } _ => { warn!(&LOGGER, "Frontend sent invalid Command") } } } }); } } const MPRIS_PREFIX: &str = "org.mpris.MediaPlayer2"; const MPRIS_PATH: &str = "/org/mpris/MediaPlayer2"; const MPRIS_PLAYER_INTERFACE: &str = "org.mpris.MediaPlayer2.Player"; #[derive(Clone, Debug)] pub enum PlayerStatus { Playing(String), Paused(String), Stopped, } #[derive(Clone, Debug)] pub enum MediaMessage { Init(Sender), PlayerStatus(PlayerStatus), PlayerPlay, PlayerPause, PlayerStop, } pub struct MediaBackend { subscribers: Vec>, players: HashMap, active_player: Option, } impl BackendThread> for MediaBackend { fn subscribe( registrator: Registrator, ) -> (Receiver, Sender) { let (result_tx, result_rx) = crossbeam_channel::unbounded::(); let _ = registrator.send(result_tx); while let Ok(state) = result_rx.recv() { return match state { MediaMessage::Init(sender) => (result_rx, sender), _ => continue, }; } panic!("Channel closed unexpectedly, without returning anything") } fn subscribers(&self) -> &Vec> { &self.subscribers } fn init(self, registration_rx: Receiver>) { let (tx, rx) = crossbeam_channel::unbounded::(); let instance = Arc::new(Mutex::new(self)); thread::spawn({ let instance = instance.clone(); move || { while let Ok(sender) = registration_rx.recv() { if let Ok(_) = sender.send(MediaMessage::Init(tx.clone())) { let mut instance_guard = instance.lock().unwrap(); if let Some(name) = &instance_guard.active_player && let Some(status) = instance_guard.players.get(name) { let _ = sender.send(MediaMessage::PlayerStatus(status.clone())); } instance_guard.subscribers.push(sender); } } } }); thread::spawn({ let instance = instance.clone(); move || { let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .expect("Failed to create Tokio runtime"); runtime.block_on(async move { if let Err(e) = Self::wait_for_players(instance).await { error!(&LOGGER, "Error waiting for players: {e}"); } }); } }); thread::spawn(move || { let runtime = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .expect("Failed to create Tokio runtime"); runtime.block_on({ let instance = instance.clone(); async move { let Ok(connection) = zbus::Connection::session().await else { error!(&LOGGER, "Couldn't create D-Bus connection"); return; }; while let Ok(msg) = rx.recv() { let mut instance_guard = instance.lock().unwrap(); if let Some(name) = &instance_guard.active_player { if let Some(status) = instance_guard.players.get(name) { match msg { MediaMessage::PlayerPlay => { if let PlayerStatus::Paused(_) = status { instance_guard.active_player_play(&connection).await; } } MediaMessage::PlayerPause => { if let PlayerStatus::Playing(_) = status { instance_guard.active_player_pause(&connection).await; } } MediaMessage::PlayerStop => { if let PlayerStatus::Playing(_) | PlayerStatus::Paused(_) = status { instance_guard.active_player_stop(&connection).await; } } _ => {} } } } } } }); }); } } impl MediaBackend { pub fn new() -> Self { Self { subscribers: vec![], players: HashMap::new(), active_player: None, } } async fn find_existing_players( connection: &zbus::Connection, instance: Arc>, ) -> zbus::fdo::Result<()> { let dbus = zbus::Proxy::new( connection, "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", ) .await?; let names: Vec = dbus.call("ListNames", &()).await?; let players: Vec = names .into_iter() .filter(|name| name.starts_with(&format!("{MPRIS_PREFIX}."))) .collect(); for player in players { Self::spawn_new_player_watcher(&connection, &instance, &player); } Ok(()) } async fn wait_for_players(instance: Arc>) -> zbus::fdo::Result<()> { let connection = zbus::Connection::session().await?; if let Err(e) = Self::find_existing_players(&connection, instance.clone()).await { warn!(&LOGGER, "Error finding existing Media Players: {e}"); } let rule = MatchRule::builder() .msg_type(Type::Signal) .sender("org.freedesktop.DBus")? .interface("org.freedesktop.DBus")? .member("NameOwnerChanged")? .build(); let mut stream = MessageStream::for_match_rule(rule, &connection, Some(32)).await?; while let Some(Ok(message)) = stream.next().await { let Some(signal) = NameOwnerChanged::from_message(message) else { continue; }; let Ok(args) = signal.args() else { continue; }; let name = args.name.as_str(); if !name.starts_with(&format!("{MPRIS_PREFIX}.")) { continue; } let old_owner = args.old_owner(); let new_owner = args.new_owner(); let map = &mut instance.lock().unwrap().players; if old_owner.is_none() && new_owner.is_some() { Self::spawn_new_player_watcher(&connection, &instance, name); } else if old_owner.is_some() && new_owner.is_none() { map.remove(name); debug!(&LOGGER, "(1) Player removed: {name}"); } else { debug!(&LOGGER, "Player owner changed: {name}"); } } warn!(&LOGGER, "Player Finder ended prematurely"); Ok(()) } fn spawn_new_player_watcher( connection: &zbus::Connection, instance: &Arc>, name: &str, ) { tokio::spawn({ let instance = instance.clone(); let connection = connection.clone(); let name = name.to_string(); async move { let _ = Self::watch_player_status(&connection, instance, &name).await; } }); } async fn watch_player_status( connection: &zbus::Connection, instance: Arc>, name: &str, ) -> zbus::fdo::Result<()> { let props = PropertiesProxy::builder(connection) .destination(name)? .path(MPRIS_PATH)? .build() .await?; let mut changes = props.receive_properties_changed().await?; let try_string: Result = props .get( InterfaceName::from_static_str_unchecked(MPRIS_PLAYER_INTERFACE), "PlaybackStatus", ) .await? .try_into(); let initial: String = if let Ok(s) = try_string { s } else { String::from("Stopped") }; debug!(&LOGGER, "Player added: {name} {initial}"); let _ = Self::insert_or_update_player(&instance, &props, name.into(), initial).await; while let Some(change) = changes.next().await { let args = change.args()?; if !args.interface_name().eq(MPRIS_PLAYER_INTERFACE) { continue; } if let Some(value) = args.changed_properties().get("PlaybackStatus") { if let Ok(status) = ::try_from(value.clone()) { debug!(&LOGGER, "Player ({name}) status has changed to: '{status}'"); Self::insert_or_update_player(&instance, &props, name.into(), status).await; } } } debug!(&LOGGER, "(2) Removing player: {name}"); instance.lock().unwrap().players.remove(name); Ok(()) } async fn insert_or_update_player( instance: &Arc>, props: &PropertiesProxy<'_>, player_name: String, player_status: String, ) { match Self::get_player_status_from_string(&props, player_status).await { Ok(status) => { let mut instance_guard = instance.lock().unwrap(); instance_guard.players.insert(player_name.clone(), status); instance_guard.select_active_player(Some(player_name)); } Err(e) => { warn!(&LOGGER, "Error parsing player status: {e}"); } } } async fn get_title(props: &PropertiesProxy<'_>) -> zbus::fdo::Result { let metadata = props .get( InterfaceName::from_static_str_unchecked(MPRIS_PLAYER_INTERFACE), "Metadata", ) .await?; let metadata = >::try_from(metadata); match metadata { Ok(metadata) => { if let Some(title) = metadata.get("xesam:title") { ::try_from(title.clone()) .map_err(|e| zbus::fdo::Error::Failed(e.to_string())) } else { Err(zbus::fdo::Error::Failed("Couldn't parse title".into())) } } Err(err) => { warn!(&LOGGER, "Error parsing metadata {}", err); Err(zbus::fdo::Error::Failed("Couldn't parse metadata".into())) } } } async fn get_player_status_from_string( props: &PropertiesProxy<'_>, status: String, ) -> zbus::fdo::Result { match status.as_str() { "Playing" => match Self::get_title(props).await { Ok(title) => Ok(PlayerStatus::Playing(title)), Err(e) => Err(e), }, "Paused" => match Self::get_title(props).await { Ok(title) => Ok(PlayerStatus::Paused(title)), Err(e) => Err(e), }, "Stopped" => Ok(PlayerStatus::Stopped), other => Err(zbus::fdo::Error::Failed(format!( "Invalid status string: '{other}' {:?}", other.as_bytes() ))), } } async fn active_player_play(&self, connection: &zbus::Connection) { if let Some(player_name) = &self.active_player { if let Ok(proxy) = zbus::Proxy::new( &connection, player_name.as_str(), MPRIS_PATH, MPRIS_PLAYER_INTERFACE, ) .await { if let Err(e) = proxy.call_method("Play", &()).await { warn!( &LOGGER, "Error starting play for player: {player_name}, {e}" ); } } } } async fn active_player_pause(&mut self, connection: &zbus::Connection) { if let Some(player_name) = &self.active_player { if let Ok(proxy) = zbus::Proxy::new( &connection, player_name.as_str(), MPRIS_PATH, MPRIS_PLAYER_INTERFACE, ) .await { if let Err(e) = proxy.call_method("Pause", &()).await { warn!(&LOGGER, "Error pausing player: {player_name}, {e}"); } } self.select_active_player(self.active_player.clone()); } } async fn active_player_stop(&mut self, connection: &zbus::Connection) { if let Some(player_name) = &self.active_player { if let Ok(proxy) = zbus::Proxy::new( &connection, player_name.as_str(), MPRIS_PATH, MPRIS_PLAYER_INTERFACE, ) .await { if let Err(e) = proxy.call_method("Stop", &()).await { warn!(&LOGGER, "Error stopping player: {player_name}, {e}"); } } } self.select_active_player(None); } fn select_active_player(&mut self, changed_player: Option) { let mut changed_also_playing = false; let mut new_active: Option = None; let mut send_status = PlayerStatus::Stopped; if let Some(player) = changed_player && let Some(status) = self.players.get(&player) { match status { PlayerStatus::Playing(_) => { new_active = Some(player); send_status = status.clone(); changed_also_playing = true; } PlayerStatus::Paused(_) => { new_active = Some(player); send_status = status.clone(); } PlayerStatus::Stopped => {} // Stopped players shouldn't be active } } if !changed_also_playing { for (name, status) in &self.players { match status { PlayerStatus::Playing(_) => { // Found a player that is playing media -> prioritize this one new_active = Some(name.clone()); send_status = status.clone(); break; } PlayerStatus::Paused(_) => { if new_active.is_none() { new_active = Some(name.clone()); send_status = status.clone(); } } PlayerStatus::Stopped => {} // Stopped players shouldn't be active } } } self.active_player = new_active; self.publish(MediaMessage::PlayerStatus(send_status)); } } fn drain_eventfd(fd: RawFd) -> std::io::Result { let mut value: u64 = 0; let result = unsafe { libc::read(fd, (&mut value as *mut u64).cast(), size_of::()) }; if result == size_of::() as isize { Ok(value) } else if result == -1 { Err(std::io::Error::last_os_error()) } else { Err(std::io::Error::new( std::io::ErrorKind::UnexpectedEof, "short eventfd read", )) } }