Implemented GStreamer playback

This commit is contained in:
G2-Games 2023-11-30 23:34:09 -06:00
parent c0d7f01b38
commit 8071a90dfe
4 changed files with 197 additions and 7 deletions

View file

@ -25,3 +25,5 @@ log = "0.4"
base64 = "0.21.5" base64 = "0.21.5"
snap = "1.1.0" snap = "1.1.0"
rcue = "0.1.3" rcue = "0.1.3"
gstreamer = "0.21.2"
glib = "0.18.3"

View file

@ -8,3 +8,5 @@ pub mod music_controller {
pub mod config; pub mod config;
pub mod controller; pub mod controller;
} }
pub mod music_player;

177
src/music_player.rs Normal file
View file

@ -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<URI>,
events: Sender<PlayerCmd>,
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<PlayerCmd>, Receiver<PlayerCmd>) = 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<Duration> {
self.playbin.query_position::<ClockTime>().map(|pos| Duration::nanoseconds(pos.nseconds() as i64))
}
/// Get the duration of the currently playing track
pub fn duration(&mut self) -> Option<Duration> {
self.playbin.query_duration::<ClockTime>().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<dyn Error>> {
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<dyn Error>> {
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(())
}
}

View file

@ -180,7 +180,7 @@ pub enum URI {
start: Duration, start: Duration,
end: Duration, end: Duration,
}, },
Remote(Service, PathBuf), Remote(Service, String),
} }
impl URI { impl URI {
@ -213,13 +213,22 @@ impl URI {
} }
/// Returns the location as a PathBuf /// Returns the location as a PathBuf
pub fn path(&self) -> &PathBuf { pub fn path(&self) -> PathBuf {
match self { match self {
URI::Local(location) => location, URI::Local(location) => location.clone(),
URI::Cue { location, .. } => location, URI::Cue { location, .. } => location.clone(),
URI::Remote(_, location) => location, 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 { impl ToString for URI {
@ -227,7 +236,7 @@ impl ToString for URI {
let path_str = match self { let path_str = match self {
URI::Local(location) => location.as_path().to_string_lossy(), URI::Local(location) => location.as_path().to_string_lossy(),
URI::Cue { 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() path_str.to_string()
} }
@ -371,7 +380,7 @@ impl MusicLibrary {
/// Queries for a [Song] by its [PathBuf], returning a `Vec<Song>` /// Queries for a [Song] by its [PathBuf], returning a `Vec<Song>`
/// with matching `PathBuf`s /// with matching `PathBuf`s
fn query_path(&self, path: &PathBuf) -> Option<Vec<&Song>> { fn query_path(&self, path: PathBuf) -> Option<Vec<&Song>> {
let result: Arc<Mutex<Vec<&Song>>> = Arc::new(Mutex::new(Vec::new())); let result: Arc<Mutex<Vec<&Song>>> = Arc::new(Mutex::new(Vec::new()));
self.library.par_iter().for_each(|track| { self.library.par_iter().for_each(|track| {
if path == track.location.path() { if path == track.location.path() {