diff --git a/Cargo.lock b/Cargo.lock index 3e9886b..f6f7402 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,6 +49,12 @@ version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +[[package]] +name = "atomic_refcell" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c" + [[package]] name = "autocfg" version = "1.1.0" @@ -140,6 +146,12 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + [[package]] name = "fallible-iterator" version = "0.2.0" @@ -162,6 +174,12 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "fragile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" + [[package]] name = "futures-channel" version = "0.3.28" @@ -458,6 +476,125 @@ dependencies = [ "system-deps", ] +[[package]] +name = "gstreamer" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b369a1eb2f7db49920d3d590bd988c5fb56dbf2347e1efb60307fe953546ee5d" +dependencies = [ + "cfg-if", + "futures-channel", + "futures-core", + "futures-util", + "glib", + "gstreamer-sys", + "itertools", + "libc", + "muldiv", + "num-integer", + "num-rational", + "option-operations", + "paste", + "pretty-hex", + "smallvec", + "thiserror", +] + +[[package]] +name = "gstreamer-base" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe38a6d5c1e516ce3fd6069e972a540d315448ed69fdadad739e6c6c6eb2a01" +dependencies = [ + "atomic_refcell", + "cfg-if", + "glib", + "gstreamer", + "gstreamer-base-sys", + "libc", +] + +[[package]] +name = "gstreamer-base-sys" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ca701f9078fe115b29b24c80910b577f9cb5b039182f050dbadf5933594b64" +dependencies = [ + "glib-sys", + "gobject-sys", + "gstreamer-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gstreamer-player" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e68dc9772932f6133a9517742918b13ab5414db1f47e19daebc3027a1c3d20d2" +dependencies = [ + "glib", + "gstreamer", + "gstreamer-player-sys", + "gstreamer-video", + "libc", +] + +[[package]] +name = "gstreamer-player-sys" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5ef4d00b43d0aa94e9a518e6ef4a4c504b4b855304a0a5f4ed1493d5e5ca66c" +dependencies = [ + "glib-sys", + "gobject-sys", + "gstreamer-sys", + "gstreamer-video-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gstreamer-sys" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86bf9de67a6ab7af67ac11588f4939e984a936030437219f269fe969d79ad8c" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gstreamer-video" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01b4d3141362b3d44a684e697d2bc55fea73d023315449cda83f0f4324531d64" +dependencies = [ + "cfg-if", + "futures-channel", + "glib", + "gstreamer", + "gstreamer-base", + "gstreamer-video-sys", + "libc", +] + +[[package]] +name = "gstreamer-video-sys" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cdc36baab839921b05d2468524da649f373dccc5f966c75e564029dc135b1c" +dependencies = [ + "glib-sys", + "gobject-sys", + "gstreamer-base-sys", + "gstreamer-sys", + "libc", + "system-deps", +] + [[package]] name = "gtk4" version = "0.7.3" @@ -576,6 +713,15 @@ dependencies = [ "hashbrown 0.12.3", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "js-sys" version = "0.3.64" @@ -683,12 +829,20 @@ dependencies = [ "autocfg", ] +[[package]] +name = "muldiv" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0" + [[package]] name = "musicus" version = "0.1.0" dependencies = [ "chrono", + "fragile", "gettext-rs", + "gstreamer-player", "gtk4", "libadwaita", "log", @@ -710,6 +864,27 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.16" @@ -754,6 +929,15 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "option-operations" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c26d27bb1aeab65138e4bf7666045169d1717febcc9ff870166be8348b223d0" +dependencies = [ + "paste", +] + [[package]] name = "overload" version = "0.1.1" @@ -785,6 +969,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + [[package]] name = "pin-project-lite" version = "0.2.9" @@ -809,6 +999,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "pretty-hex" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5" + [[package]] name = "proc-macro-crate" version = "1.3.1" diff --git a/Cargo.toml b/Cargo.toml index eb1c906..9022a64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,9 @@ edition = "2021" [dependencies] adw = { package = "libadwaita", version = "0.5", features = ["v1_4"] } chrono = "0.4" +fragile = "2" gettext-rs = { version = "0.7", features = ["gettext-system"] } +gstreamer-player = "0.21" gtk = { package = "gtk4", version = "0.7", features = ["v4_12", "blueprint"] } log = "0.4" once_cell = "1" diff --git a/de.johrpan.musicus.json b/de.johrpan.musicus.json index e12b3ae..aa9467d 100644 --- a/de.johrpan.musicus.json +++ b/de.johrpan.musicus.json @@ -13,6 +13,7 @@ "--socket=fallback-x11", "--device=dri", "--socket=wayland", + "--socket=pulseaudio", "--filesystem=host" ], "build-options": { diff --git a/src/main.rs b/src/main.rs index 44c1411..bb3badf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,10 +18,12 @@ use self::{application::MusicusApplication, window::MusicusWindow}; use config::{GETTEXT_PACKAGE, LOCALEDIR, PKGDATADIR}; use gettextrs::{bind_textdomain_codeset, bindtextdomain, textdomain}; +use gstreamer_player::gst; use gtk::{gio, glib, prelude::*}; fn main() -> glib::ExitCode { tracing_subscriber::fmt::init(); + gst::init().expect("Failed to initialize GStreamer!"); bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR).expect("Unable to bind the text domain"); bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8") diff --git a/src/player.rs b/src/player.rs index 8cb12ab..863afaa 100644 --- a/src/player.rs +++ b/src/player.rs @@ -1,5 +1,12 @@ use crate::playlist_item::PlaylistItem; -use gtk::{gio, glib, glib::Properties, prelude::*, subclass::prelude::*}; +use fragile::Fragile; +use gstreamer_player::gst; +use gtk::{ + gio, + glib::{self, Properties}, + prelude::*, + subclass::prelude::*, +}; use std::cell::{Cell, OnceCell}; mod imp { @@ -17,34 +24,48 @@ mod imp { #[property(get, set = Self::set_current_index)] pub current_index: Cell, #[property(get, set)] - pub current_time: Cell, + pub duration_ms: Cell, #[property(get, set)] - pub remaining_time: Cell, + pub current_time_ms: Cell, #[property(get, set = Self::set_position)] pub position: Cell, + #[property(get, construct_only)] + pub player: OnceCell, } impl MusicusPlayer { pub fn set_current_index(&self, index: u32) { let playlist = self.playlist.get().unwrap(); - if let Some(item) = playlist.item(self.current_index.get()) { - item.downcast::() - .unwrap() - .set_is_playing(false); - } - - self.current_index.set(index); - if let Some(item) = playlist.item(index) { - item.downcast::() - .unwrap() - .set_is_playing(true); + if let Some(old_item) = playlist.item(self.current_index.get()) { + old_item.downcast::() + .unwrap() + .set_is_playing(false); + } + + let item = item.downcast::().unwrap(); + let uri = glib::filename_to_uri(&item.path(), None) + .expect("track path should be parsable as an URI"); + + let player = self.player.get().unwrap(); + player.set_uri(Some(&uri)); + if self.playing.get() { + player.play(); + } + + self.current_index.set(index); + item.set_is_playing(true); } } pub fn set_position(&self, position: f64) { - self.position.set(position); + self.player + .get() + .unwrap() + .seek(gst::ClockTime::from_mseconds( + (position * self.duration_ms.get() as f64) as u64, + )); } } @@ -55,7 +76,63 @@ mod imp { } #[glib::derived_properties] - impl ObjectImpl for MusicusPlayer {} + impl ObjectImpl for MusicusPlayer { + fn constructed(&self) { + self.parent_constructed(); + + let player = self.player.get().unwrap(); + + let mut config = player.config(); + config.set_position_update_interval(250); + player.set_config(config).unwrap(); + player.set_video_track_enabled(false); + + let obj = Fragile::new(self.obj().to_owned()); + player.connect_end_of_stream(move |_| { + obj.get().next(); + }); + + let obj = Fragile::new(self.obj().to_owned()); + player.connect_position_updated(move |_, current_time| { + if let Some(current_time) = current_time { + let obj = obj.get(); + let imp = obj.imp(); + + let current_time_ms = current_time.mseconds(); + let duration_ms = imp.duration_ms.get(); + let mut position = current_time_ms as f64 / duration_ms as f64; + if position > 1.0 { + position = 1.0 + } + + imp.current_time_ms.set(current_time_ms); + obj.notify_current_time_ms(); + + imp.position.set(position); + obj.notify_position(); + } + }); + + let obj = Fragile::new(self.obj().to_owned()); + player.connect_duration_changed(move |_, duration| { + if let Some(duration) = duration { + let obj = obj.get(); + let imp = obj.imp(); + + let duration_ms = duration.mseconds(); + + imp.duration_ms.set(duration_ms); + obj.notify_duration_ms(); + + imp.current_time_ms.set(0); + obj.notify_current_time_ms(); + + imp.position.set(0.0); + obj.notify_position(); + } + }); + } + } } glib::wrapper! { @@ -64,14 +141,22 @@ glib::wrapper! { impl MusicusPlayer { pub fn new() -> Self { + let player = gstreamer_player::Player::new( + None::, + Some(gstreamer_player::PlayerGMainContextSignalDispatcher::new( + None, + )), + ); + glib::Object::builder() .property("active", false) .property("playing", false) .property("playlist", gio::ListStore::new::()) .property("current-index", 0u32) - .property("current-time", 0u32) - .property("remaining-time", 10000u32) + .property("current-time-ms", 0u64) + .property("duration-ms", 60_000u64) .property("position", 0.0) + .property("player", player) .build() } @@ -82,15 +167,21 @@ impl MusicusPlayer { playlist.append(&track); } - self.set_active(true); + if !self.active() && playlist.n_items() > 0 { + self.set_active(true); + self.set_current_index(0); + self.play(); + } } pub fn play(&self) { - self.set_playing(true) + self.player().play(); + self.set_playing(true); } pub fn pause(&self) { - self.set_playing(false) + self.player().pause(); + self.set_playing(false); } pub fn current_item(&self) -> Option { diff --git a/src/player_bar.rs b/src/player_bar.rs index fa0b4bc..08ad3d1 100644 --- a/src/player_bar.rs +++ b/src/player_bar.rs @@ -111,17 +111,17 @@ mod imp { .playlist() .connect_items_changed(clone!(@weak obj => move |_, _, _, _| obj.imp().update())); - player - .bind_property("current-time", &self.current_time_label.get(), "label") - .transform_to(|_, t: u32| Some(format!("{:0>2}:{:0>2}", t / 60, t % 60))) - .sync_create() - .build(); + player.connect_current_time_ms_notify(clone!(@weak obj => move |player| { + let imp = obj.imp(); + imp.current_time_label.set_label(&format_time(player.current_time_ms())); + imp.remaining_time_label.set_label(&format_time(player.duration_ms() - player.current_time_ms())); + })); - player - .bind_property("remaining-time", &self.remaining_time_label.get(), "label") - .transform_to(|_, t: u32| Some(format!("{:0>2}:{:0>2}", t / 60, t % 60))) - .sync_create() - .build(); + player.connect_duration_ms_notify(clone!(@weak obj => move |player| { + let imp = obj.imp(); + imp.current_time_label.set_label(&format_time(player.current_time_ms())); + imp.remaining_time_label.set_label(&format_time(player.duration_ms() - player.current_time_ms())); + })); player .bind_property("position", &self.slider.adjustment(), "value") @@ -187,3 +187,15 @@ impl PlayerBar { } } } + +fn format_time(time_ms: u64) -> String { + let s = time_ms / 1000; + let (m, s) = (s / 60, s % 60); + let (h, m) = (m / 60, m % 60); + + if h > 0 { + format!("{h:0>2}:{m:0>2}:{s:0>2}") + } else { + format!("{m:0>2}:{s:0>2}") + } +}