Implement MPRIS and album art, fixes #57, fixes #59, part of #56

This commit is contained in:
Jeremy Soller 2025-01-18 08:47:47 -07:00
parent 7c080feb86
commit 73f524c95e
No known key found for this signature in database
GPG key ID: D02FD439211AF56F
4 changed files with 687 additions and 15 deletions

29
Cargo.lock generated
View file

@ -1075,12 +1075,15 @@ dependencies = [
"i18n-embed",
"i18n-embed-fl",
"iced_video_player",
"image",
"lazy_static",
"libcosmic",
"log",
"mpris-server",
"rust-embed",
"serde",
"smol_str",
"tempfile",
"tokio",
"url",
]
@ -2695,7 +2698,7 @@ dependencies = [
[[package]]
name = "iced_video_player"
version = "0.6.0"
source = "git+https://github.com/jackpot51/iced_video_player.git?branch=prev-cosmic#4c921bfe57f6cc91b8b5cc63373ce78cb1c1f922"
source = "git+https://github.com/jackpot51/iced_video_player.git?branch=prev-cosmic#3f9a1b690a41171d212e79fd6c8488dc9b1b8f4c"
dependencies = [
"glib",
"gstreamer",
@ -3486,6 +3489,19 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "mpris-server"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "058bc2227727af394f34aa51da3e36aeecf2c808f39315d35f754872660750ae"
dependencies = [
"async-channel",
"futures-channel",
"serde",
"trait-variant",
"zbus 4.4.0",
]
[[package]]
name = "muldiv"
version = "1.0.1"
@ -5258,6 +5274,17 @@ dependencies = [
"once_cell",
]
[[package]]
name = "trait-variant"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.96",
]
[[package]]
name = "ttf-parser"
version = "0.20.0"

View file

@ -5,8 +5,10 @@ edition = "2021"
[dependencies]
gstreamer-tag = "0.23"
image = "0.24.9"
lazy_static = "1"
serde = { version = "1", features = ["serde_derive"] }
tempfile = "3"
tokio = "1"
url = "2"
# Internationalization
@ -28,12 +30,16 @@ branch = "prev-master"
default-features = false
features = ["tokio", "winit"]
[dependencies.mpris-server]
version = "0.8.1"
optional = true
[dependencies.smol_str]
version = "0.2.1"
features = ["serde"]
[features]
default = ["xdg-portal", "wgpu"]
default = ["mpris-server", "xdg-portal", "wgpu"]
xdg-portal = ["libcosmic/xdg-portal"]
wgpu = ["iced_video_player/wgpu", "libcosmic/wgpu"]

View file

@ -27,6 +27,7 @@ use std::{
fs, process, thread,
time::{Duration, Instant},
};
use tokio::sync::mpsc;
use crate::{
config::{Config, CONFIG_VERSION},
@ -37,6 +38,8 @@ mod config;
mod key_bind;
mod localize;
mod menu;
#[cfg(feature = "mpris-server")]
mod mpris;
static CONTROLS_TIMEOUT: Duration = Duration::new(2, 0);
@ -156,6 +159,33 @@ pub enum DropdownKind {
Subtitle,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct MprisMeta {
url_opt: Option<url::Url>,
album: String,
album_art_opt: Option<url::Url>,
album_artist: String,
artists: Vec<String>,
title: String,
disc_number: i32,
track_number: i32,
duration_micros: i64,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct MprisState {
fullscreen: bool,
position_micros: i64,
paused: bool,
volume: f64,
}
#[derive(Clone, Debug)]
pub enum MprisEvent {
Meta(MprisMeta),
State(MprisState),
}
/// Messages that are used specifically by our [`App`].
#[derive(Clone, Debug)]
pub enum Message {
@ -171,12 +201,15 @@ pub enum Message {
AudioToggle,
AudioVolume(f64),
TextCode(usize),
Pause,
Play,
PlayPause,
Seek(f64),
SeekRelative(f64),
SeekRelease,
EndOfStream,
MissingPlugin(gst::Message),
MprisChannel(MprisMeta, MprisState, mpsc::UnboundedSender<MprisEvent>),
NewFrame,
Reload,
ShowControls,
@ -188,16 +221,19 @@ pub enum Message {
pub struct App {
core: Core,
flags: Flags,
album_art_opt: Option<tempfile::NamedTempFile>,
controls: bool,
controls_time: Instant,
dropdown_opt: Option<DropdownKind>,
fullscreen: bool,
key_binds: HashMap<KeyBind, Action>,
mpris_opt: Option<(MprisMeta, MprisState, mpsc::UnboundedSender<MprisEvent>)>,
video_opt: Option<Video>,
position: f64,
duration: f64,
dragging: bool,
audio_codes: Vec<String>,
audio_tags: Vec<gst::TagList>,
current_audio: i32,
text_codes: Vec<String>,
current_text: i32,
@ -205,6 +241,7 @@ pub struct App {
impl App {
fn close(&mut self) {
self.album_art_opt = None;
//TODO: drop does not work well
if let Some(mut video) = self.video_opt.take() {
log::info!("pausing video");
@ -216,10 +253,12 @@ impl App {
self.position = 0.0;
self.duration = 0.0;
self.dragging = false;
self.audio_codes = Vec::new();
self.audio_codes.clear();
self.audio_tags.clear();
self.current_audio = -1;
self.text_codes = Vec::new();
self.text_codes.clear();
self.current_text = -1;
self.update_mpris_meta();
}
fn load(&mut self) -> Command<Message> {
@ -272,11 +311,17 @@ impl App {
let pipeline = video.pipeline();
self.video_opt = Some(video);
let n_video = pipeline.property::<i32>("n-video");
for i in 0..n_video {
let tags: gst::TagList = pipeline.emit_by_name("get-video-tags", &[&i]);
log::info!("video stream {i}: {tags:#?}");
}
let n_audio = pipeline.property::<i32>("n-audio");
self.audio_codes = Vec::with_capacity(n_audio as usize);
for i in 0..n_audio {
let tags: gst::TagList = pipeline.emit_by_name("get-audio-tags", &[&i]);
log::info!("audio stream {i}: {tags:?}");
log::info!("audio stream {i}: {tags:#?}");
self.audio_codes
.push(if let Some(title) = tags.get::<gst::tags::Title>() {
title.get().to_string()
@ -286,6 +331,7 @@ impl App {
} else {
format!("Audio #{i}")
});
self.audio_tags.push(tags);
}
self.current_audio = pipeline.property::<i32>("current-audio");
@ -293,7 +339,7 @@ impl App {
self.text_codes = Vec::with_capacity(n_text as usize);
for i in 0..n_text {
let tags: gst::TagList = pipeline.emit_by_name("get-text-tags", &[&i]);
log::info!("text stream {i}: {tags:?}");
log::info!("text stream {i}: {tags:#?}");
self.text_codes
.push(if let Some(title) = tags.get::<gst::tags::Title>() {
title.get().to_string()
@ -330,22 +376,139 @@ impl App {
}
println!("updated flags {:?}", pipeline.property_value("flags"));
self.update_mpris_meta();
self.update_title()
}
fn update_controls(&mut self, in_use: bool) {
if in_use {
if in_use
|| !self
.video_opt
.as_ref()
.map_or(false, |video| video.has_video())
{
self.controls = true;
self.controls_time = Instant::now();
} else if self.controls && self.controls_time.elapsed() > CONTROLS_TIMEOUT {
self.controls = false;
}
self.update_mpris_state();
}
fn update_config(&mut self) -> Command<Message> {
cosmic::app::command::set_theme(self.flags.config.app_theme.theme())
}
fn update_mpris_meta(&mut self) {
if let Some((old, _, tx)) = &mut self.mpris_opt {
let mut new = MprisMeta {
//TODO: clear url_opt when file is closed
url_opt: self.flags.url_opt.clone(),
duration_micros: (self.duration * 1_000_000.0) as i64,
..Default::default()
};
//TODO: use any other stream tags?
if let Some(tags) = self.audio_tags.get(0) {
log::info!("{:#?}", tags);
if let Some(tag) = tags.get::<gst::tags::Album>() {
new.album = tag.get().into();
}
if let Some(tag) = tags.get::<gst::tags::AlbumArtist>() {
new.album_artist = tag.get().into();
}
if let Some(tag) = tags.get::<gst::tags::Artist>() {
//TODO: how are multiple artists handled by gstreamer?
new.artists = vec![tag.get().into()];
}
if let Some(tag) = tags.get::<gst::tags::Title>() {
new.title = tag.get().into();
}
/*TODO: no gstreamer tag
if let Some(tag) = tags.get::<gst::tags::DiscNumber>() {
new.disc_number = tag.get();
}
*/
if let Some(tag) = tags.get::<gst::tags::TrackNumber>() {
new.track_number = tag.get() as i32;
}
if self.album_art_opt.is_none() {
//TODO: run in thread or async to avoid blocking UI?
if let Some(tag) = tags.get::<gst::tags::Image>() {
let sample = tag.get();
if let Some(buffer) = sample.buffer() {
match buffer.map_readable() {
//TODO: use original format instead of converting to PNG?
Ok(buffer_map) => match image::load_from_memory(&buffer_map) {
Ok(image) => {
match tempfile::Builder::new()
.prefix(&format!("cosmic-player.pid{}.", process::id()))
.suffix(".png")
.tempfile()
{
Ok(mut album_art) => {
match image.write_with_encoder(
image::codecs::png::PngEncoder::new(
&mut album_art,
),
) {
Ok(()) => self.album_art_opt = Some(album_art),
Err(err) => {
log::warn!(
"failed to write temporary image: {}",
err
);
}
}
}
Err(err) => {
log::warn!(
"failed to create temporary image: {}",
err
);
}
}
}
Err(err) => {
log::warn!("failed to load image from memory: {}", err);
}
},
Err(err) => {
log::warn!("failed to map image buffer: {}", err);
}
}
}
}
}
if let Some(album_art) = &self.album_art_opt {
new.album_art_opt = url::Url::from_file_path(album_art.path()).ok();
}
}
if new != *old {
*old = new.clone();
let _ = tx.send(MprisEvent::Meta(new));
}
}
}
fn update_mpris_state(&mut self) {
if let Some((_, old, tx)) = &mut self.mpris_opt {
let mut new = MprisState {
fullscreen: self.fullscreen,
position_micros: (self.position * 1_000_000.0) as i64,
paused: true,
volume: 0.0,
};
if let Some(video) = &self.video_opt {
new.paused = video.paused();
new.volume = video.volume();
}
if new != *old {
*old = new.clone();
let _ = tx.send(MprisEvent::State(new));
}
}
}
fn update_title(&mut self) -> Command<Message> {
//TODO: filename?
let title = "COSMIC Media Player";
@ -382,16 +545,19 @@ impl Application for App {
let mut app = App {
core,
flags,
album_art_opt: None,
controls: true,
controls_time: Instant::now(),
dropdown_opt: None,
fullscreen: false,
key_binds: key_binds(),
mpris_opt: None,
video_opt: None,
position: 0.0,
duration: 0.0,
dragging: false,
audio_codes: Vec::new(),
audio_tags: Vec::new(),
current_audio: -1,
text_codes: Vec::new(),
current_text: -1,
@ -491,8 +657,10 @@ impl Application for App {
}
Message::AudioVolume(volume) => {
if let Some(video) = &mut self.video_opt {
video.set_volume(volume);
self.update_controls(true);
if volume >= 0.0 && volume <= 1.0 {
video.set_volume(volume);
self.update_controls(true);
}
}
}
Message::TextCode(code) => {
@ -504,12 +672,16 @@ impl Application for App {
}
}
}
Message::PlayPause => {
Message::Pause | Message::Play | Message::PlayPause => {
//TODO: cleanest way to close dropdowns
self.dropdown_opt = None;
if let Some(video) = &mut self.video_opt {
video.set_paused(!video.paused());
video.set_paused(match message {
Message::Play => false,
Message::Pause => true,
_ => !video.paused(),
});
self.update_controls(true);
}
}
@ -609,6 +781,11 @@ impl Application for App {
|x| x,
);
}
Message::MprisChannel(meta, state, tx) => {
self.mpris_opt = Some((meta, state, tx));
self.update_mpris_meta();
self.update_mpris_state();
}
Message::NewFrame => {
if let Some(video) = &self.video_opt {
if !self.dragging {
@ -666,13 +843,28 @@ impl Application for App {
let muted = video.muted();
let volume = video.volume();
let video_player = VideoPlayer::new(video)
let mut video_player: Element<_> = VideoPlayer::new(video)
.mouse_hidden(!self.controls)
.on_end_of_stream(Message::EndOfStream)
.on_missing_plugin(Message::MissingPlugin)
.on_new_frame(Message::NewFrame)
.width(Length::Fill)
.height(Length::Fill);
.height(Length::Fill)
.into();
if let Some(album_art) = &self.album_art_opt {
if !video.has_video() {
// This is a hack to have the video player running but not visible (since the controls will cover it as an overlay)
video_player = widget::column::with_children(vec![
widget::image(widget::image::Handle::from_path(album_art.path()))
.width(Length::Fill)
.height(Length::Fill)
.into(),
widget::container(video_player).height(space_m).into(),
])
.into();
}
}
let mouse_area = widget::mouse_area(video_player)
.on_press(Message::PlayPause)
@ -855,7 +1047,7 @@ impl Application for App {
struct ConfigSubscription;
struct ThemeSubscription;
Subscription::batch([
let mut subscriptions = vec![
event::listen_with(|event, _status| match event {
Event::Keyboard(KeyEvent::KeyPressed { key, modifiers, .. }) => {
Some(Message::Key(modifiers, key))
@ -885,6 +1077,13 @@ impl Application for App {
}
Message::SystemThemeModeChange(update.config)
}),
])
];
#[cfg(feature = "mpris-server")]
{
subscriptions.push(mpris::subscription());
}
Subscription::batch(subscriptions)
}
}

440
src/mpris.rs Normal file
View file

@ -0,0 +1,440 @@
use cosmic::iced::{
futures::{self, SinkExt},
subscription::{self, Subscription},
};
use mpris_server::{
zbus::{fdo, Result},
LoopStatus, Metadata, PlaybackRate, PlaybackStatus, PlayerInterface, Property, RootInterface,
Server, Signal, Time, TrackId, Volume,
};
use std::{any::TypeId, future, process};
use tokio::sync::{mpsc, Mutex};
use crate::{Message, MprisEvent, MprisMeta, MprisState};
impl MprisMeta {
fn metadata(&self) -> Metadata {
let mut meta = Metadata::builder()
//TODO: better track id
.trackid(
mpris_server::TrackId::try_from(format!(
"/com/system76/CosmicPlayer/pid{}/TrackList/0",
process::id()
))
.unwrap(),
)
.length(Time::from_micros(self.duration_micros));
if let Some(url) = &self.url_opt {
meta = meta.url(url.clone());
}
if !self.album.is_empty() {
meta = meta.album(&self.album);
}
if let Some(album_art) = &self.album_art_opt {
meta = meta.art_url(album_art.clone());
}
if !self.artists.is_empty() {
meta = meta.artist(&self.artists);
}
//TODO: content_created
if !self.title.is_empty() {
meta = meta.title(&self.title);
}
//TODO .disc_number(self.disc_number)
if self.track_number > 0 {
meta = meta.track_number(self.track_number);
}
//TODO: track count?
//TODO: more keys, see https://docs.rs/mpris-server/0.8.1/mpris_server/builder/struct.MetadataBuilder.html
meta.build()
}
}
impl MprisState {
fn playback_status(&self) -> PlaybackStatus {
if self.paused {
PlaybackStatus::Paused
} else {
PlaybackStatus::Playing
}
}
}
pub struct Player {
msg_tx: Mutex<futures::channel::mpsc::Sender<Message>>,
meta: Mutex<MprisMeta>,
state: Mutex<MprisState>,
}
impl Player {
async fn message(&self, message: Message) -> fdo::Result<()> {
self.msg_tx
.lock()
.await
.send(message)
.await
.map_err(|err| fdo::Error::Failed(err.to_string()))
}
}
impl RootInterface for Player {
async fn raise(&self) -> fdo::Result<()> {
log::info!("Raise");
Ok(())
}
async fn quit(&self) -> fdo::Result<()> {
log::info!("Quit");
Ok(())
}
async fn can_quit(&self) -> fdo::Result<bool> {
log::info!("CanQuit");
Ok(false)
}
async fn fullscreen(&self) -> fdo::Result<bool> {
log::info!("Fullscreen");
let state = self.state.lock().await;
Ok(state.fullscreen)
}
async fn set_fullscreen(&self, fullscreen: bool) -> Result<()> {
log::info!("SetFullscreen({})", fullscreen);
Ok(())
}
async fn can_set_fullscreen(&self) -> fdo::Result<bool> {
log::info!("CanSetFullscreen");
Ok(false)
}
async fn can_raise(&self) -> fdo::Result<bool> {
log::info!("CanRaise");
Ok(false)
}
async fn has_track_list(&self) -> fdo::Result<bool> {
log::info!("HasTrackList");
Ok(false)
}
async fn identity(&self) -> fdo::Result<String> {
log::info!("Identity");
Ok("COSMIC Player".to_string())
}
async fn desktop_entry(&self) -> fdo::Result<String> {
log::info!("DesktopEntry");
Ok("com.system76.CosmicPlayer".to_string())
}
async fn supported_uri_schemes(&self) -> fdo::Result<Vec<String>> {
log::info!("SupportedUriSchemes");
Ok(vec![])
}
async fn supported_mime_types(&self) -> fdo::Result<Vec<String>> {
log::info!("SupportedMimeTypes");
Ok(vec![])
}
}
impl PlayerInterface for Player {
async fn next(&self) -> fdo::Result<()> {
log::info!("Next");
Ok(())
}
async fn previous(&self) -> fdo::Result<()> {
log::info!("Previous");
Ok(())
}
async fn pause(&self) -> fdo::Result<()> {
log::info!("Pause");
self.message(Message::Pause).await
}
async fn play_pause(&self) -> fdo::Result<()> {
log::info!("PlayPause");
self.message(Message::PlayPause).await
}
async fn stop(&self) -> fdo::Result<()> {
log::info!("Stop");
Ok(())
}
async fn play(&self) -> fdo::Result<()> {
log::info!("Play");
self.message(Message::Play).await
}
async fn seek(&self, offset: Time) -> fdo::Result<()> {
log::info!("Seek({:?})", offset);
Ok(())
}
async fn set_position(&self, track_id: TrackId, position: Time) -> fdo::Result<()> {
log::info!("SetPosition({}, {:?})", track_id, position);
Ok(())
}
async fn open_uri(&self, uri: String) -> fdo::Result<()> {
log::info!("OpenUri({})", uri);
Ok(())
}
async fn playback_status(&self) -> fdo::Result<PlaybackStatus> {
log::info!("PlaybackStatus");
let state = self.state.lock().await;
Ok(state.playback_status())
}
async fn loop_status(&self) -> fdo::Result<LoopStatus> {
log::info!("LoopStatus");
Ok(LoopStatus::None)
}
async fn set_loop_status(&self, loop_status: LoopStatus) -> Result<()> {
log::info!("SetLoopStatus({})", loop_status);
Ok(())
}
async fn rate(&self) -> fdo::Result<PlaybackRate> {
log::info!("Rate");
Ok(1.0)
}
async fn set_rate(&self, rate: PlaybackRate) -> Result<()> {
log::info!("SetRate({})", rate);
Ok(())
}
async fn shuffle(&self) -> fdo::Result<bool> {
log::info!("Shuffle");
Ok(false)
}
async fn set_shuffle(&self, shuffle: bool) -> Result<()> {
log::info!("SetShuffle({})", shuffle);
Ok(())
}
async fn metadata(&self) -> fdo::Result<Metadata> {
log::info!("Metadata");
let meta = self.meta.lock().await;
Ok(meta.metadata())
}
async fn volume(&self) -> fdo::Result<Volume> {
log::info!("Volume");
let state = self.state.lock().await;
Ok(state.volume)
}
async fn set_volume(&self, volume: Volume) -> Result<()> {
log::info!("SetVolume({})", volume);
self.message(Message::AudioVolume(volume)).await?;
Ok(())
}
async fn position(&self) -> fdo::Result<Time> {
log::info!("Position");
let state = self.state.lock().await;
Ok(Time::from_micros(state.position_micros))
}
async fn minimum_rate(&self) -> fdo::Result<PlaybackRate> {
log::info!("MinimumRate");
Ok(1.0)
}
async fn maximum_rate(&self) -> fdo::Result<PlaybackRate> {
log::info!("MaximumRate");
Ok(1.0)
}
async fn can_go_next(&self) -> fdo::Result<bool> {
log::info!("CanGoNext");
Ok(false)
}
async fn can_go_previous(&self) -> fdo::Result<bool> {
log::info!("CanGoPrevious");
Ok(false)
}
async fn can_play(&self) -> fdo::Result<bool> {
log::info!("CanPlay");
Ok(true)
}
async fn can_pause(&self) -> fdo::Result<bool> {
log::info!("CanPause");
Ok(true)
}
async fn can_seek(&self) -> fdo::Result<bool> {
log::info!("CanSeek");
Ok(false)
}
async fn can_control(&self) -> fdo::Result<bool> {
log::info!("CanControl");
Ok(true)
}
}
/*TODO: implement mpris tracklist
impl TrackListInterface for Player {
async fn get_tracks_metadata(&self, track_ids: Vec<TrackId>) -> fdo::Result<Vec<Metadata>> {
log::info!("GetTracksMetadata({:?})", track_ids);
Ok(vec![])
}
async fn add_track(
&self,
uri: Uri,
after_track: TrackId,
set_as_current: bool,
) -> fdo::Result<()> {
log::info!("AddTrack({}, {}, {})", uri, after_track, set_as_current);
Ok(())
}
async fn remove_track(&self, track_id: TrackId) -> fdo::Result<()> {
log::info!("RemoveTrack({})", track_id);
Ok(())
}
async fn go_to(&self, track_id: TrackId) -> fdo::Result<()> {
log::info!("GoTo({})", track_id);
Ok(())
}
async fn tracks(&self) -> fdo::Result<Vec<TrackId>> {
log::info!("Tracks");
Ok(vec![])
}
async fn can_edit_tracks(&self) -> fdo::Result<bool> {
log::info!("CanEditTracks");
Ok(false)
}
}
*/
/*TODO: implement mpris playlists
impl PlaylistsInterface for Player {
async fn activate_playlist(&self, playlist_id: PlaylistId) -> fdo::Result<()> {
log::info!("ActivatePlaylist({})", playlist_id);
Ok(())
}
async fn get_playlists(
&self,
index: u32,
max_count: u32,
order: PlaylistOrdering,
reverse_order: bool,
) -> fdo::Result<Vec<Playlist>> {
log::info!(
"GetPlaylists({}, {}, {}, {})",
index, max_count, order, reverse_order
);
Ok(vec![])
}
async fn playlist_count(&self) -> fdo::Result<u32> {
log::info!("PlaylistCount");
Ok(0)
}
async fn orderings(&self) -> fdo::Result<Vec<PlaylistOrdering>> {
log::info!("Orderings");
Ok(vec![])
}
async fn active_playlist(&self) -> fdo::Result<Option<Playlist>> {
log::info!("ActivePlaylist");
Ok(None)
}
}
*/
pub fn subscription() -> Subscription<Message> {
struct MprisSubscription;
subscription::channel(
TypeId::of::<MprisSubscription>(),
16,
move |mut msg_tx| async move {
let (event_tx, mut event_rx) = mpsc::unbounded_channel();
let meta = MprisMeta::default();
let state = MprisState::default();
msg_tx
.send(Message::MprisChannel(meta.clone(), state.clone(), event_tx))
.await
.unwrap();
match Server::new(
&format!("org.mpris.MediaPlayer2.cosmic-player.pid{}", process::id()),
Player {
msg_tx: Mutex::new(msg_tx),
meta: Mutex::new(meta),
state: Mutex::new(state),
},
)
.await
{
Ok(server) => {
log::info!("running mpris server");
while let Some(event) = event_rx.recv().await {
let mut props = Vec::new();
let mut sigs = Vec::new();
match event {
MprisEvent::Meta(new) => {
let mut old = server.imp().meta.lock().await;
let new_metadata = new.metadata();
if new_metadata != old.metadata() {
props.push(Property::Metadata(new_metadata));
}
*old = new;
}
MprisEvent::State(new) => {
let mut old = server.imp().state.lock().await;
if new.fullscreen != old.fullscreen {
props.push(Property::Fullscreen(new.fullscreen));
}
let new_playback_status = new.playback_status();
if new_playback_status != old.playback_status() {
props.push(Property::PlaybackStatus(new_playback_status));
}
if new.volume != old.volume {
props.push(Property::Volume(new.volume));
}
if new.position_micros != old.position_micros {
sigs.push(Signal::Seeked {
position: Time::from_micros(new.position_micros),
});
}
*old = new;
}
}
if !props.is_empty() {
let _ = server.properties_changed(props).await;
}
for sig in sigs {
let _ = server.emit(sig).await;
}
}
future::pending().await
}
Err(err) => {
log::warn!("failed to start mpris server: {err}");
future::pending().await
}
}
},
)
}