mirror of
https://github.com/Dangoware/dmp-core.git
synced 2025-04-19 13:22:54 -05:00
Implemented GStreamer playback
This commit is contained in:
parent
c0d7f01b38
commit
8071a90dfe
4 changed files with 197 additions and 7 deletions
|
@ -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"
|
||||||
|
|
|
@ -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
177
src/music_player.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() {
|
||||||
|
|
Loading…
Reference in a new issue