Replace mpris crate with own implementation

This commit is contained in:
jmb05
2026-11-02 14:36:53 +01:00
parent 39f4263db5
commit 00b2b1f64c
8 changed files with 634 additions and 226 deletions

43
Cargo.lock generated
View File

@@ -1170,17 +1170,6 @@ version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
[[package]]
name = "dbus"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e"
dependencies = [
"libc",
"libdbus-sys",
"windows-sys 0.61.2",
]
[[package]]
name = "ddc"
version = "0.2.2"
@@ -1493,17 +1482,6 @@ version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
[[package]]
name = "enum-kinds"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e40a16955681d469ab3da85aaa6b42ff656b3c67b52e1d8d3dd36afe97fd462"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "enum_dispatch"
version = "0.3.13"
@@ -2777,7 +2755,6 @@ dependencies = [
"libc",
"libpulse-binding",
"meval",
"mpris",
"nmrs",
"notify",
"nucleo-matcher",
@@ -2946,15 +2923,6 @@ version = "0.2.191"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
[[package]]
name = "libdbus-sys"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043"
dependencies = [
"pkg-config",
]
[[package]]
name = "libloading"
version = "0.8.9"
@@ -3338,17 +3306,6 @@ dependencies = [
"num-traits",
]
[[package]]
name = "mpris"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b72fbda9340cad2ca8181eabd8b96a2d8f4386e1ac9eefcf4045fc4e97d1ab7b"
dependencies = [
"dbus",
"enum-kinds",
"thiserror 2.0.21",
]
[[package]]
name = "multimap"
version = "0.10.1"

View File

@@ -18,7 +18,6 @@ gtk4-layer-shell = "0.8.1"
hyprland = { git = "https://github.com/romanstingler/hyprland-rs-next", branch = "master" }
notify = "8.2.0"
glib = "0.22.10"
mpris = "2.1.0"
time-format = "1.2.4"
nmrs = "3.5.7"
tokio = { version = "1.53.3", features = ["rt", "time", "net", "sync", "rt-multi-thread", "io-util", "macros", "signal"] }
@@ -42,13 +41,12 @@ battery = "0.7.8"
ddc-hi = { git = "https://github.com/jmb05/ddc-hi-rs" }
wayland-protocols = { version = "0.32.13", features = ["staging", "client"] }
wayland-client = "0.31.15"
zbus = "5.19.0"
zbus = "5.19.0" # Keep at this version for now because other dependencies also have it
serde = { version = "1.0.229", features = ["derive"] }
freedesktop-desktop-entry = "0.8.4"
nucleo-matcher = "0.3.1"
meval = "0.2.0"
toml = "1.1.8"
#zbus = "6.1.0"
[build-dependencies]
glib-build-tools = "0.22.8"

View File

@@ -4,7 +4,8 @@ Additionally, it acts as an idle daemon. It is written in rust and uses Gtk 4 an
gtk-layer-shell.
**This is a hobby project and for my own personal use. It might be unoptimized in
some places and naively implemented. It is also most definitely not very customizable
and highly opinionated. Use at your own risk.**
and highly opinionated. It is built with the specifics of my system in mind and
will probably not work on your machine. Use at your own risk.**
### Building and Running
To build the project you need the rust compiler and cargo installed.

View File

@@ -8,7 +8,7 @@ pub mod perf_stats;
pub mod window_manager_hypr;
use crate::LOGGER;
use crate::backend::audio::{MediaBackend, MediaCommand};
use crate::backend::audio::{AudioBackend, AudioMessage, MediaBackend, MediaMessage};
use crate::backend::brightness_control::{BrightnessManager, BrightnessMessage};
use crate::backend::commands::ControlSocket;
use crate::backend::idle::{IdleDaemon, IdleMessage, IdleState};
@@ -74,7 +74,8 @@ pub struct Backend {
net_registrator: Registrator<NetworkState>,
br_registrator: Registrator<BrightnessMessage>,
stats_registrator: Registrator<Stats>,
media_registrator: Registrator<MediaCommand>,
audio_registrator: Registrator<AudioMessage>,
media_registration: Registrator<MediaMessage>,
netbird_registrator: Registrator<State>,
idle_registrator: Registrator<IdleState>,
control_registrator: Registrator<ControlCommand>,
@@ -85,6 +86,7 @@ pub enum ChannelType {
Hypr,
Br,
Stats,
Audio,
Media,
Idle,
Netbird,
@@ -98,8 +100,9 @@ impl Backend {
let net_registrator = NetworkBackendHandle::start();
let br_registrator = BrightnessManager::start();
let stats_registrator = StatsBackend::start();
let media_registrator = MediaBackend::start();
let tailscale_registrator = JsonSocketBackend::start();
let audio_registrator = AudioBackend::start();
let media_registration = MediaBackend::start();
let netbird_registrator = JsonSocketBackend::start();
let idle_registrator = IdleDaemon::start();
let control_registrator = ControlSocket::start();
debug!(&LOGGER, "Started Backend threads");
@@ -108,8 +111,9 @@ impl Backend {
net_registrator,
br_registrator,
stats_registrator,
media_registrator,
netbird_registrator: tailscale_registrator,
audio_registrator,
media_registration,
netbird_registrator,
idle_registrator,
control_registrator,
}
@@ -124,21 +128,18 @@ impl Backend {
channels.net_tx = Some(tx);
channels.net_rx = Some(rx);
}
ChannelType::Hypr => {
channels.hypr_rx =
Some(HyprlandComms::subscribe(self.hypr_registrator.clone()).0);
}
ChannelType::Br => {
let (rx, tx) = BrightnessManager::subscribe(self.br_registrator.clone());
channels.br_rx = Some(rx);
channels.br_tx = Some(tx);
}
ChannelType::Stats => {
channels.stats_rx =
Some(StatsBackend::subscribe(self.stats_registrator.clone()).0);
ChannelType::Audio => {
let (rx, tx) = AudioBackend::subscribe(self.audio_registrator.clone());
channels.audio_rx = Some(rx);
channels.audio_tx = Some(tx);
}
ChannelType::Media => {
let (rx, tx) = MediaBackend::subscribe(self.media_registrator.clone());
let (rx, tx) = MediaBackend::subscribe(self.media_registration.clone());
channels.media_rx = Some(rx);
channels.media_tx = Some(tx);
}
@@ -155,6 +156,14 @@ impl Backend {
channels.ctl_rx =
Some(ControlSocket::subscribe(self.control_registrator.clone()).0);
}
ChannelType::Stats => {
channels.stats_rx =
Some(StatsBackend::subscribe(self.stats_registrator.clone()).0);
}
ChannelType::Hypr => {
channels.hypr_rx =
Some(HyprlandComms::subscribe(self.hypr_registrator.clone()).0);
}
}
}
channels
@@ -168,6 +177,7 @@ impl Backend {
ChannelType::Br,
ChannelType::Stats,
ChannelType::Control,
ChannelType::Audio,
ChannelType::Media,
ChannelType::Hypr,
])
@@ -182,8 +192,10 @@ pub struct BackendChannels {
br_rx: Option<Receiver<BrightnessMessage>>,
br_tx: Option<Sender<BrightnessMessage>>,
stats_rx: Option<Receiver<Stats>>,
media_rx: Option<Receiver<MediaCommand>>,
media_tx: Option<Sender<MediaCommand>>,
audio_rx: Option<Receiver<AudioMessage>>,
audio_tx: Option<Sender<AudioMessage>>,
media_rx: Option<Receiver<MediaMessage>>,
media_tx: Option<Sender<MediaMessage>>,
netbird_rx: Option<Receiver<State>>,
idle_rx: Option<Receiver<IdleState>>,
idle_tx: Option<Sender<IdleMessage>>,
@@ -206,7 +218,10 @@ impl BackendChannels {
pub fn take_netbird_rx(&mut self) -> Option<Receiver<State>> {
self.netbird_rx.take()
}
pub fn take_media_rx(&mut self) -> Option<Receiver<MediaCommand>> {
pub fn take_audio_rx(&mut self) -> Option<Receiver<AudioMessage>> {
self.audio_rx.take()
}
pub fn take_media_rx(&mut self) -> Option<Receiver<MediaMessage>> {
self.media_rx.take()
}
pub fn take_idle_rx(&mut self) -> Option<Receiver<IdleState>> {
@@ -221,7 +236,10 @@ impl BackendChannels {
pub fn br_tx(&self) -> Option<Sender<BrightnessMessage>> {
self.br_tx.clone()
}
pub fn media_tx(&self) -> Option<Sender<MediaCommand>> {
pub fn audio_tx(&self) -> Option<Sender<AudioMessage>> {
self.audio_tx.clone()
}
pub fn media_tx(&self) -> Option<Sender<MediaMessage>> {
self.media_tx.clone()
}
pub fn idle_tx(&self) -> Option<Sender<IdleMessage>> {

View File

@@ -15,12 +15,19 @@ 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 AudioController {
pub struct PulseController {
pub mainloop: Mainloop,
context: Context,
device_index: Option<u32>,
@@ -37,7 +44,7 @@ pub enum ControllerMsg {
Set(u8, bool),
}
impl AudioController {
impl PulseController {
pub fn new(tx: Sender<ControllerMsg>) -> Self {
let mut mainloop = Mainloop::new().unwrap();
let mut prop_list = Proplist::new().unwrap();
@@ -95,7 +102,7 @@ impl AudioController {
trace!(&LOGGER, "Subscribed to pulseaudio events: {success}");
});
let mut controller = AudioController {
let mut controller = PulseController {
context,
mainloop,
device_index: None,
@@ -251,147 +258,151 @@ impl AudioController {
}
#[derive(Clone, Debug)]
pub enum MediaCommand {
Init(Sender<MediaCommand>),
pub enum AudioMessage {
Init(Sender<AudioMessage>),
Request,
Cmd(u8, bool, bool),
}
pub struct MediaBackend {
subscribers: Vec<Sender<MediaCommand>>,
command_tx: Sender<MediaCommand>,
pub struct AudioBackend {
subscribers: Vec<Sender<AudioMessage>>,
command_tx: Sender<AudioMessage>,
}
impl BackendThread<MediaCommand, Sender<MediaCommand>> for MediaBackend {
impl BackendThread<AudioMessage, Sender<AudioMessage>> for AudioBackend {
fn subscribe(
registrator: Registrator<MediaCommand>,
) -> (Receiver<MediaCommand>, Sender<MediaCommand>) {
let (result_tx, result_rx) = crossbeam_channel::unbounded::<MediaCommand>();
registrator: Registrator<AudioMessage>,
) -> (Receiver<AudioMessage>, Sender<AudioMessage>) {
let (result_tx, result_rx) = crossbeam_channel::unbounded::<AudioMessage>();
let _ = registrator.send(result_tx);
while let Ok(state) = result_rx.recv() {
return match state {
MediaCommand::Init(sender) => (result_rx, sender),
AudioMessage::Init(sender) => (result_rx, sender),
_ => continue,
};
}
panic!("Channel closed unexpectedly, without returning anything")
}
fn subscribers(&self) -> &Vec<Sender<MediaCommand>> {
fn subscribers(&self) -> &Vec<Sender<AudioMessage>> {
&self.subscribers
}
fn init(registration_rx: Receiver<Sender<MediaCommand>>) {
let (command_tx, command_rx) = crossbeam_channel::unbounded::<MediaCommand>();
fn init(registration_rx: Receiver<Sender<AudioMessage>>) {
let (command_tx, command_rx) = crossbeam_channel::unbounded::<AudioMessage>();
let instance = Arc::new(Mutex::new(Self {
subscribers: vec![],
command_tx,
}));
let instance_clone = instance.clone();
let (controller_tx, controller_rx) = crossbeam_channel::unbounded::<ControllerMsg>();
let controller_tx_clone = controller_tx.clone();
let notify = Arc::new(EventFD::new(0, EfdFlags::EFD_CLOEXEC).unwrap());
let notify_clone = notify.clone();
thread::spawn(move || {
let mut controller = AudioController::new(controller_tx_clone);
let _event = api::Mainloop::new_io_event(
&mut controller.mainloop,
notify_clone.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_clone.lock().expect("Couldn't get lock").publish(
MediaCommand::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_clone.lock().expect("Couldn't get lock").publish(
MediaCommand::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_clone.lock().expect("Couldn't get lock").publish(
MediaCommand::Cmd(
controller.current_vol,
controller.current_mute,
controller.device_bt.unwrap_or(false),
),
);
trace!(
&LOGGER,
"Change: {} muted: {}",
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.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_clone.lock().expect("Couldn't get lock").publish(
MediaCommand::Cmd(
controller.current_vol,
controller.current_mute,
controller.device_bt.unwrap_or(false),
),
);
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);
}
controller.mainloop.iterate(true);
}
});
let instance_clone = instance.clone();
thread::spawn(move || {
while let Ok(sender) = registration_rx.recv() {
if sender
.send(MediaCommand::Init(
instance_clone
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(),
))
.is_ok()
{
let _ = sender.send(AudioMessage::Request);
instance
.lock()
.expect("Couldn't get lock")
.command_tx
.clone(),
))
.is_ok()
{
let _ = sender.send(MediaCommand::Request);
instance_clone
.lock()
.expect("Couldn't get subscribers")
.subscribers
.push(sender);
.expect("Couldn't get subscribers")
.subscribers
.push(sender);
}
}
}
});
thread::spawn(move || {
while let Ok(command) = command_rx.recv() {
match command {
MediaCommand::Cmd(vol, mute, _) => {
AudioMessage::Cmd(vol, mute, _) => {
let _ = controller_tx.send(ControllerMsg::Set(vol, mute));
notify.write(1).expect("Error writing notify");
}
MediaCommand::Request => {
AudioMessage::Request => {
let _ = controller_tx.send(Request);
notify.write(1).expect("Error writing notify");
}
@@ -404,6 +415,438 @@ impl BackendThread<MediaCommand, Sender<MediaCommand>> for MediaBackend {
}
}
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<MediaMessage>),
PlayerStatus(PlayerStatus),
PlayerPlay,
PlayerPause,
PlayerStop,
}
pub struct MediaBackend {
subscribers: Vec<Sender<MediaMessage>>,
players: HashMap<String, PlayerStatus>,
active_player: Option<String>,
}
impl BackendThread<MediaMessage, Sender<MediaMessage>> for MediaBackend {
fn subscribe(
registrator: Registrator<MediaMessage>,
) -> (Receiver<MediaMessage>, Sender<MediaMessage>) {
let (result_tx, result_rx) = crossbeam_channel::unbounded::<MediaMessage>();
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<Sender<MediaMessage>> {
&self.subscribers
}
fn init(registration_rx: Receiver<Sender<MediaMessage>>) {
let (tx, rx) = crossbeam_channel::unbounded::<MediaMessage>();
let instance = Arc::new(Mutex::new(Self {
subscribers: vec![],
players: HashMap::new(),
active_player: None,
}));
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 {
async fn find_existing_players(
connection: &zbus::Connection,
instance: Arc<Mutex<Self>>,
) -> zbus::fdo::Result<()> {
let dbus = zbus::Proxy::new(
connection,
"org.freedesktop.DBus",
"/org/freedesktop/DBus",
"org.freedesktop.DBus",
)
.await?;
let names: Vec<String> = dbus.call("ListNames", &()).await?;
let players: Vec<String> = 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<Mutex<Self>>) -> 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<Mutex<Self>>,
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<Mutex<Self>>,
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<String, _> = 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) = <String>::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<Mutex<Self>>,
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<String> {
let metadata = props
.get(
InterfaceName::from_static_str_unchecked(MPRIS_PLAYER_INTERFACE),
"Metadata",
)
.await?;
let metadata = <HashMap<String, OwnedValue>>::try_from(metadata);
match metadata {
Ok(metadata) => {
if let Some(title) = metadata.get("xesam:title") {
<String>::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<PlayerStatus> {
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<String>) {
let mut changed_also_playing = false;
let mut new_active: Option<String> = 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<u64> {
let mut value: u64 = 0;

View File

@@ -1,10 +1,10 @@
use crate::LOGGER;
use crate::backend::audio::MediaMessage;
use crate::backend::window_manager_hypr::{HyprlandComms, WindowManagerEvent};
use crate::backend::{Backend, BackendChannels, ChannelType};
use crate::ctl::ControlCommand;
use crate::ui::action_centre::ActionCentre;
use crate::ui::run_menu::RunMenu;
use crossbeam_channel::TryRecvError;
use glib::ControlFlow;
use glib::prelude::Cast;
use gtk::gdk::Display;
@@ -12,7 +12,6 @@ use gtk::prelude::{BoxExt, ButtonExt, DisplayExt, GtkWindowExt, ListModelExt, Wi
use gtk::{Align, Application, ApplicationWindow, Button, Orientation, gdk};
use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell};
use jmb_logger_rs::*;
use mpris::PlayerFinder;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Mutex;
@@ -98,12 +97,10 @@ fn create_and_show_bar(
let hypr_rx = channels
.take_hypr_rx()
.expect("Tried taking unavailable Hyprland Channel Receiver");
let player_opt = if let Ok(player_finder) = PlayerFinder::new() {
player_finder.find_active().ok()
} else {
None
};
let media_rx = channels
.take_media_rx()
.expect("Tried taking unavailable Media Channel Receiver");
let media_tx = channels.media_tx().unwrap();
let window = ApplicationWindow::builder()
.application(&app)
@@ -136,7 +133,7 @@ fn create_and_show_bar(
);
let title_button: Option<Button>;
let title = title_panel::build();
let title = title_panel::build(media_tx);
let utilities = utilities_panel::build(&app, action_centre, channels, monitor_idx, &display);
@@ -149,34 +146,30 @@ fn create_and_show_bar(
dock.append(&utilities);
title_button = Some(title);
title_panel::manage_title(&title_button, &player_opt);
title_panel::manage_title(&title_button, &None);
window.set_keyboard_mode(KeyboardMode::None);
window.set_child(Some(&dock));
window.present();
let window_clone = window.clone();
let mut status = None;
glib::timeout_add_local(Duration::from_millis(50), move || {
loop {
match hypr_rx.try_recv() {
Ok(event) => {
handle_wm_event(
&mut buttons,
&submap,
&title_button,
event,
window_clone.monitor(),
);
}
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Disconnected) => {
return ControlFlow::Break;
}
}
while let Ok(event) = hypr_rx.try_recv() {
handle_wm_event(
&mut buttons,
&submap,
&title_button,
event,
window_clone.monitor(),
);
}
title_panel::manage_title(&title_button, &player_opt);
while let Ok(MediaMessage::PlayerStatus(recv_status)) = media_rx.try_recv() {
status = Some(recv_status);
}
title_panel::manage_title(&title_button, &status);
ControlFlow::Continue
});

View File

@@ -1,11 +1,12 @@
use crate::backend::audio::{MediaMessage, PlayerStatus};
use crate::backend::window_manager_hypr::HyprlandComms;
use crossbeam_channel::Sender;
use glib::prelude::CastNone;
use gtk::pango::EllipsizeMode;
use gtk::prelude::{ButtonExt, GestureSingleExt, WidgetExt};
use gtk::{Align, Button, GestureClick, Label};
use mpris::{PlaybackStatus, Player, PlayerFinder};
pub fn build() -> Button {
pub fn build(media_tx: Sender<MediaMessage>) -> Button {
let title_label = Label::new(None);
title_label.set_ellipsize(EllipsizeMode::End);
title_label.set_max_width_chars(50);
@@ -16,59 +17,56 @@ pub fn build() -> Button {
title.set_child(Some(&title_label));
title.set_visible(false);
title.add_css_class("title");
title.connect_clicked(|button| {
if button.has_css_class("media")
&& let Ok(player_finder) = PlayerFinder::new()
&& let Ok(player) = player_finder.find_active()
{
let _ = player.play_pause();
title.connect_clicked({
let media_tx = media_tx.clone();
move |button| {
if button.has_css_class("media") {
if button.has_css_class("playing") {
let _ = media_tx.send(MediaMessage::PlayerPause);
} else {
let _ = media_tx.send(MediaMessage::PlayerPlay);
}
}
}
});
let right_click = GestureClick::new();
right_click.set_button(3);
right_click.connect_pressed(|_gesture, _n_press, _, _| {
if let Ok(player_finder) = PlayerFinder::new()
&& let Ok(player) = player_finder.find_active()
{
let _ = player.stop();
}
right_click.connect_pressed(move |_gesture, _n_press, _, _| {
let _ = media_tx.send(MediaMessage::PlayerStop);
});
title.add_controller(right_click);
title
}
pub fn manage_title(button_opt: &Option<Button>, player_opt: &Option<Player>) {
pub fn manage_title(button_opt: &Option<Button>, status: &Option<PlayerStatus>) {
let mut handled = false;
if let Some(button) = button_opt {
if let Some(player) = player_opt
&& let Ok(status) = player.get_playback_status()
&& let Ok(metadata) = player.get_metadata()
{
if let Some(status) = status {
match status {
PlaybackStatus::Playing => {
PlayerStatus::Playing(title) => {
button.set_visible(true);
button
.child()
.and_downcast::<Label>()
.unwrap()
.set_label(&format!("{}", metadata.title().unwrap_or("")));
.set_label(&format!("{}", title));
button.add_css_class("media");
button.add_css_class("playing");
handled = true;
}
PlaybackStatus::Paused => {
PlayerStatus::Paused(title) => {
button.set_visible(true);
button
.child()
.and_downcast::<Label>()
.unwrap()
.set_label(&format!("{}", metadata.title().unwrap_or("")));
.set_label(&format!("{}", title));
button.add_css_class("media");
button.remove_css_class("playing");
handled = true;
}
PlaybackStatus::Stopped => {
PlayerStatus::Stopped => {
button.set_visible(true);
button.remove_css_class("media");
button.remove_css_class("playing");

View File

@@ -1,5 +1,5 @@
use crate::LOGGER;
use crate::backend::audio::MediaCommand;
use crate::backend::audio::AudioMessage;
use crate::backend::brightness_control::BrightnessMessage;
use crate::backend::idle::{IdleMessage, IdleState};
use crate::backend::network::{NetworkCommand, NetworkState, NetworkType};
@@ -76,13 +76,13 @@ pub fn build(
volume.imp().second_icon.set_icon_name("bluetooth-symbolic");
volume.imp().second_icon.add_child_css_class("bt-icon");
let media_tx = channels.media_tx().unwrap();
let media_tx = channels.audio_tx().unwrap();
let changed_progr = Rc::new(Cell::new(false));
volume.connect_value_changed({
let changed_progr = changed_progr.clone();
move |slider, button, _| {
if !changed_progr.get() {
let _ = media_tx.send(MediaCommand::Cmd(
let _ = media_tx.send(AudioMessage::Cmd(
slider.value() as u8,
button.is_active(),
false,
@@ -90,12 +90,12 @@ pub fn build(
}
}
});
let media_tx = channels.media_tx().unwrap();
let media_tx = channels.audio_tx().unwrap();
volume.connect_clicked(move |slider, button, _| {
if button.is_active() {
let _ = media_tx.send(MediaCommand::Cmd(slider.value() as u8, true, false));
let _ = media_tx.send(AudioMessage::Cmd(slider.value() as u8, true, false));
} else {
let _ = media_tx.send(MediaCommand::Cmd(slider.value() as u8, false, false));
let _ = media_tx.send(AudioMessage::Cmd(slider.value() as u8, false, false));
}
});
@@ -315,7 +315,7 @@ pub fn build(
});
glib::timeout_add_local(Duration::from_millis(100), {
let media_rx = channels.take_media_rx().unwrap();
let media_rx = channels.take_audio_rx().unwrap();
let br_rx = channels.take_br_rx().unwrap();
let net_rx = channels.take_net_rx().unwrap();
let stats_rx = channels.take_stats_rx().unwrap();
@@ -325,7 +325,7 @@ pub fn build(
let br_tx = channels.br_tx().unwrap();
move || {
while let Ok(command) = media_rx.try_recv() {
if let MediaCommand::Cmd(vol, mute, bt) = command {
if let AudioMessage::Cmd(vol, mute, bt) = command {
changed_progr.set(true);
volume.set_value(vol as f64);
changed_progr.set(false);