diff --git a/Cargo.toml b/Cargo.toml index 192d833..ba7c5fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,3 +25,5 @@ log = "0.4" base64 = "0.21.5" snap = "1.1.0" rcue = "0.1.3" +gstreamer = "0.21.2" +glib = "0.18.3" diff --git a/src/lib.rs b/src/lib.rs index d6c8bb0..d98209c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,3 +8,5 @@ pub mod music_controller { pub mod config; pub mod controller; } + +pub mod music_player; diff --git a/src/music_player.rs b/src/music_player.rs new file mode 100644 index 0000000..6ec9670 --- /dev/null +++ b/src/music_player.rs @@ -0,0 +1,177 @@ +// Crate things +use crate::music_controller::config::Config; +use crate::music_storage::music_db::URI; +use std::error::Error; +use std::sync::mpsc::{Sender, self, Receiver}; + +// GStreamer things +use gst::{ClockTime, Element}; +use gstreamer as gst; +use gstreamer::prelude::*; +use glib::FlagsClass; + +// Time things +use chrono::Duration; + +enum PlayerCmd { + Play, +} + +/// An instance of a music player with a GStreamer backend +pub struct Player { + source: Option, + events: Sender, + playbin: Element, + position: Duration, + duration: Duration, + paused: bool, + volume: f64, + gapless: bool, +} + +impl Default for Player { + fn default() -> Self { + Self::new() + } +} + + +impl Player { + pub fn new() -> Self { + gst::init().unwrap(); + + let playbin = gst::ElementFactory::make("playbin") + .build() + .unwrap(); + + let flags = playbin.property_value("flags"); + let flags_class = FlagsClass::with_type(flags.type_()).unwrap(); + + let flags = flags_class + .builder_with_value(flags) + .unwrap() + .set_by_nick("audio") + .set_by_nick("download") + .unset_by_nick("video") + .unset_by_nick("text") + .build() + .unwrap(); + playbin.set_property_from_value("flags", &flags); + + playbin + .bus() + .expect("Failed to get GStreamer message bus"); + + let source = None; + let (tx, _): (Sender, Receiver) = mpsc::channel(); + Self { + source, + events: tx, + playbin, + paused: false, + volume: 0.5, + gapless: false, + position: Duration::seconds(0), + duration: Duration::seconds(0), + } + } + + pub fn enqueue_next(&mut self, next_track: URI) { + self.set_state(gst::State::Ready); + + self.playbin.set_property("uri", next_track.as_uri()); + + self.play(); + } + + + /// Set the playback volume, accepts a float from 0 to 1 + pub fn set_volume(&mut self, volume: f64) { + self.volume = volume.clamp(0.0, 1.0); + self.set_gstreamer_volume(self.volume); + } + + /// Set volume of the internal playbin player, can be + /// used to bypass the main volume control for seeking + fn set_gstreamer_volume(&mut self, volume: f64) { + self.playbin.set_property("volume", volume) + } + + /// Returns the current volume level, a float from 0 to 1 + pub fn volume(&mut self) -> f64 { + self.volume + } + + fn set_state(&mut self, state: gst::State) { + self.playbin + .set_state(state) + .expect("Unable to set the pipeline state"); + } + + /// If the player is paused or stopped, starts playback + pub fn play(&mut self) { + self.set_state(gst::State::Playing); + } + + /// Pause, if playing + pub fn pause(&mut self) { + self.paused = true; + self.set_state(gst::State::Paused); + } + + /// Resume from being paused + pub fn resume(&mut self) { + self.paused = false; + self.set_state(gst::State::Playing); + } + + /// Check if playback is paused + pub fn is_paused(&mut self) -> bool { + self.playbin.current_state() == gst::State::Paused + } + + /// Set the playback URI + pub fn set_source(&mut self, source: URI) { + self.source = Some(source.clone()); + self.playbin.set_property("uri", source.as_uri()) + } + + /// Get the current playback position of the player + pub fn position(&mut self) -> Option { + self.playbin.query_position::().map(|pos| Duration::nanoseconds(pos.nseconds() as i64)) + } + + /// Get the duration of the currently playing track + pub fn duration(&mut self) -> Option { + self.playbin.query_duration::().map(|pos| Duration::milliseconds(pos.mseconds() as i64)) + } + + /// Seek relative to the current position + pub fn seek_by(&mut self, seek_amount: Duration) -> Result<(), Box> { + let time_pos = match self.position() { + Some(pos) => pos, + None => return Err("No position".into()) + }; + let seek_pos = time_pos + seek_amount; + + self.seek_to(seek_pos)?; + Ok(()) + } + + /// Seek absolutely + pub fn seek_to(&mut self, last_pos: Duration) -> Result<(), Box> { + let duration = match self.duration() { + Some(dur) => dur, + None => return Err("No duration".into()) + }; + let seek_pos = last_pos.clamp(Duration::seconds(0), duration); + + let seek_pos_clock = ClockTime::from_mseconds(seek_pos.num_milliseconds() as u64); + self.set_gstreamer_volume(0.0); + self + .playbin + .seek_simple(gst::SeekFlags::FLUSH, seek_pos_clock)?; + self.set_gstreamer_volume(self.volume); + Ok(()) + } +} diff --git a/src/music_storage/music_db.rs b/src/music_storage/music_db.rs index 44939df..21df4f5 100644 --- a/src/music_storage/music_db.rs +++ b/src/music_storage/music_db.rs @@ -180,7 +180,7 @@ pub enum URI { start: Duration, end: Duration, }, - Remote(Service, PathBuf), + Remote(Service, String), } impl URI { @@ -213,13 +213,22 @@ impl URI { } /// Returns the location as a PathBuf - pub fn path(&self) -> &PathBuf { + pub fn path(&self) -> PathBuf { match self { - URI::Local(location) => location, - URI::Cue { location, .. } => location, - URI::Remote(_, location) => location, + URI::Local(location) => location.clone(), + URI::Cue { location, .. } => location.clone(), + URI::Remote(_, location) => PathBuf::from(location), } } + + pub fn as_uri(&self) -> String { + let path_str = match self { + URI::Local(location) => format!("file://{}", location.as_path().to_string_lossy()), + URI::Cue { location, .. } => format!("file://{}", location.as_path().to_string_lossy()), + URI::Remote(_, location) => location.clone(), + }; + path_str.to_string() + } } impl ToString for URI { @@ -227,7 +236,7 @@ impl ToString for URI { let path_str = match self { URI::Local(location) => location.as_path().to_string_lossy(), URI::Cue { location, .. } => location.as_path().to_string_lossy(), - URI::Remote(_, location) => location.as_path().to_string_lossy(), + URI::Remote(_, location) => location.into(), }; path_str.to_string() } @@ -371,7 +380,7 @@ impl MusicLibrary { /// Queries for a [Song] by its [PathBuf], returning a `Vec` /// with matching `PathBuf`s - fn query_path(&self, path: &PathBuf) -> Option> { + fn query_path(&self, path: PathBuf) -> Option> { let result: Arc>> = Arc::new(Mutex::new(Vec::new())); self.library.par_iter().for_each(|track| { if path == track.location.path() {