parent
7c080feb86
commit
73f524c95e
4 changed files with 687 additions and 15 deletions
29
Cargo.lock
generated
29
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
||||
|
|
|
|||
221
src/main.rs
221
src/main.rs
|
|
@ -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,10 +657,12 @@ impl Application for App {
|
|||
}
|
||||
Message::AudioVolume(volume) => {
|
||||
if let Some(video) = &mut self.video_opt {
|
||||
if volume >= 0.0 && volume <= 1.0 {
|
||||
video.set_volume(volume);
|
||||
self.update_controls(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::TextCode(code) => {
|
||||
if let Ok(code) = i32::try_from(code) {
|
||||
if let Some(video) = &self.video_opt {
|
||||
|
|
@ -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
440
src/mpris.rs
Normal 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
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue