Implemented prismriver backend, fixed a bunch of small issues, removed unused deps

This commit is contained in:
G2-Games 2024-12-26 01:45:17 -06:00
parent 4f2d5ab64a
commit 69c3cd7427
13 changed files with 74 additions and 776 deletions

View file

@ -22,16 +22,13 @@ file-format = { version = "0.23.0", features = [
"reader-xml", "reader-xml",
"serde", "serde",
] } ] }
lofty = "0.18.2" lofty = "0.21"
serde = { version = "1.0.195", features = ["derive"] } serde = { version = "1.0.195", features = ["derive"] }
walkdir = "2.4.0" walkdir = "2.4.0"
chrono = { version = "0.4.31", features = ["serde"] } chrono = { version = "0.4.31", features = ["serde"] }
rayon = "1.8.0" rayon = "1.8.0"
log = "0.4" log = "0.4"
base64 = "0.21.5"
rcue = "0.1.3" rcue = "0.1.3"
gstreamer = "0.21.3"
glib = "0.18.5"
crossbeam-channel = "0.5.8" crossbeam-channel = "0.5.8"
crossbeam = "0.8.2" crossbeam = "0.8.2"
quick-xml = "0.31.0" quick-xml = "0.31.0"
@ -44,8 +41,6 @@ serde_json = "1.0.111"
deunicode = "1.4.2" deunicode = "1.4.2"
opener = { version = "0.7.0", features = ["reveal"] } opener = { version = "0.7.0", features = ["reveal"] }
tempfile = "3.10.1" tempfile = "3.10.1"
listenbrainz = "0.7.0"
discord-rpc-client = "0.4.0"
nestify = "0.3.3" nestify = "0.3.3"
moro = "0.4.0" moro = "0.4.0"
moro-local = "0.4.0" moro-local = "0.4.0"
@ -56,3 +51,4 @@ async-channel = "2.3.1"
ciborium = "0.2.2" ciborium = "0.2.2"
itertools = "0.13.0" itertools = "0.13.0"
directories = "5.0.1" directories = "5.0.1"
prismriver = { git = "https://github.com/Dangoware/prismriver.git" }

View file

@ -1,7 +1,7 @@
use std::{ use std::{
fs::{self, File, OpenOptions}, fs::{self, File, OpenOptions},
io::{Error, Read, Write}, io::{Error, Read, Write},
path::{Path, PathBuf}, path::PathBuf,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -189,9 +189,7 @@ pub enum ConfigError {
pub mod tests { pub mod tests {
use super::{Config, ConfigLibrary}; use super::{Config, ConfigLibrary};
use crate::music_storage::library::MusicLibrary; use crate::music_storage::library::MusicLibrary;
use std::{ use std::path::PathBuf;
path::PathBuf,
};
pub fn new_config_lib() -> (Config, MusicLibrary) { pub fn new_config_lib() -> (Config, MusicLibrary) {
_ = std::fs::create_dir_all("test-config/music/"); _ = std::fs::create_dir_all("test-config/music/");

View file

@ -14,9 +14,4 @@ pub mod music_controller {
pub mod queue; pub mod queue;
} }
pub mod music_player {
pub mod gstreamer;
pub mod player;
}
pub mod config; pub mod config;

View file

@ -3,37 +3,35 @@
//! other functions //! other functions
#![allow(while_true)] #![allow(while_true)]
use itertools::Itertools;
use kushi::{Queue, QueueItemType}; use kushi::{Queue, QueueItemType};
use kushi::{QueueError, QueueItem}; use kushi::{QueueError, QueueItem};
use prismriver::{Prismriver, Volume};
use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator}; use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::to_string_pretty; use serde_json::to_string_pretty;
use std::error::Error; use std::error::Error;
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::io::Write; use std::io::Write;
use std::marker::PhantomData;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use thiserror::Error; use thiserror::Error;
use uuid::Uuid; use uuid::Uuid;
use crate::config::{self, ConfigError}; use crate::config::ConfigError;
use crate::music_player::player::{Player, PlayerError};
use crate::music_storage::library::Song; use crate::music_storage::library::Song;
use crate::music_storage::playlist::{ExternalPlaylist, Playlist, PlaylistFolderItem}; use crate::music_storage::playlist::{ExternalPlaylist, Playlist, PlaylistFolderItem};
use crate::{config::Config, music_storage::library::MusicLibrary}; use crate::{config::Config, music_storage::library::MusicLibrary};
use super::queue::{QueueAlbum, QueueSong}; use super::queue::{QueueAlbum, QueueSong};
pub struct Controller<'a, P>(&'a PhantomData<P>); pub struct Controller();
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum ControllerError { pub enum ControllerError {
#[error("{0:?}")] #[error("{0:?}")]
QueueError(#[from] QueueError), QueueError(#[from] QueueError),
#[error("{0:?}")] #[error("{0:?}")]
PlayerError(#[from] PlayerError), PlayerError(#[from] prismriver::Error),
#[error("{0:?}")] #[error("{0:?}")]
ConfigError(#[from] ConfigError), ConfigError(#[from] ConfigError),
} }
@ -213,7 +211,7 @@ impl ControllerState {
} }
#[allow(unused_variables)] #[allow(unused_variables)]
impl<'c, P: Player + Send + Sync> Controller<'c, P> { impl Controller {
pub async fn start( pub async fn start(
ControllerInput { ControllerInput {
player_mail, player_mail,
@ -222,10 +220,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
mut library, mut library,
config config
}: ControllerInput }: ControllerInput
) -> Result<(), Box<dyn Error>> ) -> Result<(), Box<dyn Error>> {
where
P: Player,
{
let queue: Queue<QueueSong, QueueAlbum> = Queue { let queue: Queue<QueueSong, QueueAlbum> = Queue {
items: Vec::new(), items: Vec::new(),
played: Vec::new(), played: Vec::new(),
@ -250,13 +245,13 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
futures::executor::block_on(async { futures::executor::block_on(async {
moro::async_scope!(|scope| { moro::async_scope!(|scope| {
println!("async scope created"); println!("async scope created");
let player = Arc::new(RwLock::new(P::new().unwrap())); let player = Arc::new(RwLock::new(Prismriver::new()));
let _player = player.clone(); let _player = player.clone();
let _lib_mail = lib_mail.0.clone(); let _lib_mail = lib_mail.0.clone();
scope scope
.spawn(async move { .spawn(async move {
Controller::<P>::player_command_loop( Controller::player_command_loop(
_player, _player,
player_mail.1, player_mail.1,
queue_mail.0, queue_mail.0,
@ -268,7 +263,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
}); });
scope scope
.spawn(async move { .spawn(async move {
Controller::<P>::player_event_loop( Controller::player_event_loop(
player, player,
player_mail.0 player_mail.0
) )
@ -277,7 +272,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
}); });
scope scope
.spawn(async { .spawn(async {
Controller::<P>::library_loop( Controller::library_loop(
lib_mail.1, lib_mail.1,
&mut library, &mut library,
config, config,
@ -292,7 +287,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
let b = scope.spawn(|| { let b = scope.spawn(|| {
futures::executor::block_on(async { futures::executor::block_on(async {
Controller::<P>::queue_loop(queue, queue_mail.1).await; Controller::queue_loop(queue, queue_mail.1).await;
}) })
}); });
a.join().unwrap(); a.join().unwrap();
@ -303,7 +298,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
} }
async fn player_command_loop( async fn player_command_loop(
player: Arc<RwLock<P>>, player: Arc<RwLock<Prismriver>>,
player_mail: MailMan<PlayerResponse, PlayerCommand>, player_mail: MailMan<PlayerResponse, PlayerCommand>,
queue_mail: MailMan<QueueCommand, QueueResponse>, queue_mail: MailMan<QueueCommand, QueueResponse>,
lib_mail: MailMan<LibraryCommand, LibraryResponse>, lib_mail: MailMan<LibraryCommand, LibraryResponse>,
@ -311,9 +306,8 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
) -> Result<(), ()> { ) -> Result<(), ()> {
let mut first = true; let mut first = true;
{ {
let volume = state.volume as f64; player.write().unwrap().set_volume(Volume::new(state.volume));
player.write().unwrap().set_volume(volume); println!("volume set to {}", state.volume);
println!("volume set to {volume}");
} }
while true { while true {
let _mail = player_mail.recv().await; let _mail = player_mail.recv().await;
@ -324,20 +318,24 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
queue_mail.send(QueueCommand::NowPlaying).await.unwrap(); queue_mail.send(QueueCommand::NowPlaying).await.unwrap();
let QueueResponse::Item(item) = queue_mail.recv().await.unwrap() else { unimplemented!() }; let QueueResponse::Item(item) = queue_mail.recv().await.unwrap() else { unimplemented!() };
let QueueItemType::Single(song) = item.item else { unimplemented!("This is temporary, handle queueItemTypes at some point") }; let QueueItemType::Single(song) = item.item else { unimplemented!("This is temporary, handle queueItemTypes at some point") };
player.write().unwrap().enqueue_next(song.song.primary_uri().unwrap().0).unwrap();
let prism_uri = prismriver::utils::path_to_uri(&song.song.primary_uri().unwrap().0.as_path().unwrap()).unwrap();
player.write().unwrap().load_new(&prism_uri).unwrap();
player.write().unwrap().play();
player_mail.send(PlayerResponse::NowPlaying(song.song)).await.unwrap(); player_mail.send(PlayerResponse::NowPlaying(song.song)).await.unwrap();
first = false first = false
} else { } else {
player.write().unwrap().play().unwrap(); player.write().unwrap().play();
player_mail.send(PlayerResponse::Empty).await.unwrap(); player_mail.send(PlayerResponse::Empty).await.unwrap();
} }
} }
PlayerCommand::Pause => { PlayerCommand::Pause => {
player.write().unwrap().pause().unwrap(); player.write().unwrap().pause();
player_mail.send(PlayerResponse::Empty).await.unwrap(); player_mail.send(PlayerResponse::Empty).await.unwrap();
} }
PlayerCommand::SetVolume(volume) => { PlayerCommand::SetVolume(volume) => {
player.write().unwrap().set_volume(volume as f64); player.write().unwrap().set_volume(Volume::new(volume));
println!("volume set to {volume}"); println!("volume set to {volume}");
player_mail.send(PlayerResponse::Empty).await.unwrap(); player_mail.send(PlayerResponse::Empty).await.unwrap();
@ -352,7 +350,11 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
QueueItemType::Single(song) => song.song.primary_uri().unwrap().0, QueueItemType::Single(song) => song.song.primary_uri().unwrap().0,
_ => unimplemented!(), _ => unimplemented!(),
}; };
player.write().unwrap().enqueue_next(uri).unwrap();
let prism_uri = prismriver::utils::path_to_uri(&uri.as_path().unwrap()).unwrap();
player.write().unwrap().load_new(&prism_uri).unwrap();
player.write().unwrap().play();
let QueueItemType::Single(np_song) = item.item else { panic!("This is temporary, handle queueItemTypes at some point")}; let QueueItemType::Single(np_song) = item.item else { panic!("This is temporary, handle queueItemTypes at some point")};
// Append next song in library // Append next song in library
@ -398,6 +400,11 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
QueueItemType::Single(song) => song.song.primary_uri().unwrap().0, QueueItemType::Single(song) => song.song.primary_uri().unwrap().0,
_ => unimplemented!(), _ => unimplemented!(),
}; };
let prism_uri = prismriver::utils::path_to_uri(&uri.as_path().unwrap()).unwrap();
player.write().unwrap().load_new(&prism_uri).unwrap();
player.write().unwrap().play();
let QueueItemType::Single(np_song) = item.item else { panic!("This is temporary, handle queueItemTypes at some point")}; let QueueItemType::Single(np_song) = item.item else { panic!("This is temporary, handle queueItemTypes at some point")};
player_mail.send(PlayerResponse::NowPlaying(np_song.song.clone())).await.unwrap(); player_mail.send(PlayerResponse::NowPlaying(np_song.song.clone())).await.unwrap();
} }
@ -410,11 +417,9 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
if let QueueResponse::Item(item) = queue_mail.recv().await.unwrap() { if let QueueResponse::Item(item) = queue_mail.recv().await.unwrap() {
match item.item { match item.item {
QueueItemType::Single(song) => { QueueItemType::Single(song) => {
player let prism_uri = prismriver::utils::path_to_uri(&song.song.primary_uri().unwrap().0.as_path().unwrap()).unwrap();
.write() player.write().unwrap().load_new(&prism_uri).unwrap();
.unwrap() player.write().unwrap().play();
.enqueue_next(song.song.primary_uri().unwrap().0)
.unwrap();
} }
_ => unimplemented!(), _ => unimplemented!(),
} }
@ -436,7 +441,10 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
unreachable!() unreachable!()
}; };
player.write().unwrap().enqueue_next(song.primary_uri().unwrap().0).unwrap(); // TODO: Handle non Local URIs here, and whenever `load_new()` or `load_gapless()` is called
let prism_uri = prismriver::utils::path_to_uri(&song.primary_uri().unwrap().0.as_path().unwrap()).unwrap();
player.write().unwrap().load_new(&prism_uri).unwrap();
player.write().unwrap().play();
// how grab all the songs in a certain subset of the library, I reckon? // how grab all the songs in a certain subset of the library, I reckon?
// ... // ...
@ -486,7 +494,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
async fn library_loop( async fn library_loop(
lib_mail: MailMan<LibraryResponse, LibraryCommand>, lib_mail: MailMan<LibraryResponse, LibraryCommand>,
library: &'c mut MusicLibrary, library: &mut MusicLibrary,
config: Arc<RwLock<Config>>, config: Arc<RwLock<Config>>,
) -> Result<(), ()> { ) -> Result<(), ()> {
while true { while true {
@ -511,7 +519,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
lib_mail.send(LibraryResponse::ImportM3UPlayList(uuid, name)).await.unwrap(); lib_mail.send(LibraryResponse::ImportM3UPlayList(uuid, name)).await.unwrap();
} }
LibraryCommand::Save => { LibraryCommand::Save => {
library.save({config.read().unwrap().libraries.get_library(&library.uuid).unwrap().path.clone()}).unwrap(); library.save(config.read().unwrap().libraries.get_library(&library.uuid).unwrap().path.clone()).unwrap();
lib_mail.send(LibraryResponse::Ok).await.unwrap(); lib_mail.send(LibraryResponse::Ok).await.unwrap();
} }
LibraryCommand::Playlists => { LibraryCommand::Playlists => {
@ -527,7 +535,7 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
} }
async fn player_event_loop( async fn player_event_loop(
player: Arc<RwLock<P>>, player: Arc<RwLock<Prismriver>>,
player_mail: MailMan<PlayerCommand, PlayerResponse>, player_mail: MailMan<PlayerCommand, PlayerResponse>,
) -> Result<(), ()> { ) -> Result<(), ()> {
// just pretend this does something // just pretend this does something
@ -586,82 +594,3 @@ impl<'c, P: Player + Send + Sync> Controller<'c, P> {
} }
} }
} }
#[cfg(test)]
mod test_super {
use std::{
path::PathBuf,
sync::{Arc, RwLock},
thread::spawn,
};
use crate::{
config::{tests::new_config_lib, Config},
music_controller::controller::{
LibraryCommand, LibraryResponse, MailMan, PlayerCommand, PlayerResponse, ControllerHandle
},
music_player::gstreamer::GStreamer,
music_storage::library::MusicLibrary,
};
use super::Controller;
#[tokio::test]
async fn construct_controller() {
// use if you don't have a config setup and add music to the music folder
new_config_lib();
let config = Config::read_file(PathBuf::from(std::env!("CONFIG-PATH"))).unwrap();
let library = {
MusicLibrary::init(
config.libraries.get_default().unwrap().path.clone(),
config.libraries.get_default().unwrap().uuid,
)
.unwrap()
};
let (handle, input) = ControllerHandle::new(library, Arc::new(RwLock::new(config)));
let b = spawn(move || {
futures::executor::block_on(async {
handle.player_mail
.send(PlayerCommand::SetVolume(0.01))
.await
.unwrap();
loop {
let buf: String = text_io::read!();
dbg!(&buf);
handle.player_mail
.send(match buf.to_lowercase().as_str() {
"next" => PlayerCommand::NextSong,
"prev" => PlayerCommand::PrevSong,
"pause" => PlayerCommand::Pause,
"play" => PlayerCommand::Play,
x if x.parse::<usize>().is_ok() => {
PlayerCommand::Enqueue(x.parse::<usize>().unwrap())
}
_ => continue,
})
.await
.unwrap();
println!("sent it");
println!("{:?}", handle.player_mail.recv().await.unwrap())
}
})
});
let a = spawn(move || {
futures::executor::block_on(async {
Controller::<GStreamer>::start(input)
.await
.unwrap();
});
});
b.join().unwrap();
a.join().unwrap();
}
}

View file

@ -1,521 +0,0 @@
// Crate things
use crate::music_storage::library::URI;
use crossbeam_channel::{unbounded, Receiver, Sender};
use std::error::Error;
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
// GStreamer things
use glib::FlagsClass;
use gst::{ClockTime, Element};
use gstreamer as gst;
use gstreamer::prelude::*;
// Extra things
use chrono::Duration;
use super::player::{Player, PlayerCommand, PlayerError, PlayerState};
impl From<gst::State> for PlayerState {
fn from(value: gst::State) -> Self {
match value {
gst::State::VoidPending => Self::VoidPending,
gst::State::Playing => Self::Playing,
gst::State::Paused => Self::Paused,
gst::State::Ready => Self::Ready,
gst::State::Null => Self::Null,
}
}
}
impl TryInto<gst::State> for PlayerState {
fn try_into(self) -> Result<gst::State, Box<dyn Error>> {
match self {
Self::VoidPending => Ok(gst::State::VoidPending),
Self::Playing => Ok(gst::State::Playing),
Self::Paused => Ok(gst::State::Paused),
Self::Ready => Ok(gst::State::Ready),
Self::Null => Ok(gst::State::Null),
state => Err(format!("Invalid gst::State: {:?}", state).into()),
}
}
type Error = Box<dyn Error>;
}
#[derive(Debug, PartialEq, Eq)]
enum PlaybackInfo {
Idle,
Switching,
Playing {
start: Duration,
end: Duration,
},
/// When this is sent, the thread will die! Use it when the [Player] is
/// done playing
Finished,
}
/// An instance of a music player with a GStreamer backend
#[derive(Debug)]
pub struct GStreamer {
source: Option<URI>,
message_rx: crossbeam::channel::Receiver<PlayerCommand>,
playback_tx: crossbeam::channel::Sender<PlaybackInfo>,
playbin: Arc<RwLock<Element>>,
volume: f64,
start: Option<Duration>,
end: Option<Duration>,
paused: Arc<RwLock<bool>>,
position: Arc<RwLock<Option<Duration>>>,
}
impl From<gst::StateChangeError> for PlayerError {
fn from(value: gst::StateChangeError) -> Self {
PlayerError::StateChange(value.to_string())
}
}
impl From<glib::BoolError> for PlayerError {
fn from(value: glib::BoolError) -> Self {
PlayerError::General(value.to_string())
}
}
impl GStreamer {
/// Set the playback URI
fn set_source(&mut self, source: &URI) -> Result<(), PlayerError> {
if !source.exists().is_ok_and(|x| x) {
// If the source doesn't exist, gstreamer will crash!
return Err(PlayerError::NotFound);
}
// Make sure the playback tracker knows the stuff is stopped
println!("Beginning switch");
self.playback_tx.send(PlaybackInfo::Switching).unwrap();
let uri = self.playbin.read().unwrap().property_value("current-uri");
self.source = Some(source.clone());
match source {
URI::Cue { start, end, .. } => {
self.playbin
.write()
.unwrap()
.set_property("uri", source.as_uri());
// Set the start and end positions of the CUE file
self.start = Some(Duration::from_std(*start).unwrap());
self.end = Some(Duration::from_std(*end).unwrap());
// Send the updated position to the tracker
self.playback_tx
.send(PlaybackInfo::Playing {
start: self.start.unwrap(),
end: self.end.unwrap(),
})
.unwrap();
// Wait for it to be ready, and then move to the proper position
self.play().unwrap();
let now = std::time::Instant::now();
while now.elapsed() < std::time::Duration::from_millis(20) {
if self.seek_to(Duration::from_std(*start).unwrap()).is_ok() {
return Ok(());
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
//panic!("Couldn't seek to beginning of cue track in reasonable time (>20ms)");
return Err(PlayerError::StateChange(
"Could not seek to beginning of CUE track".into(),
));
}
_ => {
self.playbin
.write()
.unwrap()
.set_property("uri", source.as_uri());
if self.state() != PlayerState::Playing {
self.play().unwrap();
}
while self.raw_duration().is_none() {
std::thread::sleep(std::time::Duration::from_millis(10));
}
self.start = Some(Duration::seconds(0));
self.end = self.raw_duration();
// Send the updated position to the tracker
self.playback_tx
.send(PlaybackInfo::Playing {
start: self.start.unwrap(),
end: self.end.unwrap(),
})
.unwrap();
}
}
Ok(())
}
/// Gets a mutable reference to the playbin element
fn playbin_mut(
&mut self,
) -> Result<RwLockWriteGuard<gst::Element>, std::sync::PoisonError<RwLockWriteGuard<'_, Element>>>
{
let element = match self.playbin.write() {
Ok(element) => element,
Err(err) => return Err(err),
};
Ok(element)
}
/// Gets a read-only reference to the playbin element
fn playbin(
&self,
) -> Result<RwLockReadGuard<gst::Element>, std::sync::PoisonError<RwLockReadGuard<'_, Element>>>
{
let element = match self.playbin.read() {
Ok(element) => element,
Err(err) => return Err(err),
};
Ok(element)
}
/// 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_mut().unwrap().set_property("volume", volume)
}
fn set_state(&mut self, state: gst::State) -> Result<(), gst::StateChangeError> {
self.playbin_mut().unwrap().set_state(state)?;
Ok(())
}
fn raw_duration(&self) -> Option<Duration> {
self.playbin()
.unwrap()
.query_duration::<ClockTime>()
.map(|pos| Duration::nanoseconds(pos.nseconds() as i64))
}
/// Get the current state of the playback
fn state(&mut self) -> PlayerState {
self.playbin().unwrap().current_state().into()
/*
match *self.buffer.read().unwrap() {
None => self.playbin().unwrap().current_state().into(),
Some(value) => PlayerState::Buffering(value),
}
*/
}
fn property(&self, property: &str) -> glib::Value {
self.playbin().unwrap().property_value(property)
}
fn ready(&mut self) -> Result<(), PlayerError> {
self.set_state(gst::State::Ready)?;
Ok(())
}
}
impl Player for GStreamer {
fn new() -> Result<Self, PlayerError> {
// Initialize GStreamer, maybe figure out how to nicely fail here
if let Err(err) = gst::init() {
return Err(PlayerError::Init(err.to_string()));
};
let ctx = glib::MainContext::default();
let _guard = ctx.acquire();
let mainloop = glib::MainLoop::new(Some(&ctx), false);
let playbin_arc = Arc::new(RwLock::new(
match gst::ElementFactory::make("playbin3").build() {
Ok(playbin) => playbin,
Err(error) => return Err(PlayerError::Init(error.to_string())),
},
));
let playbin = playbin_arc.clone();
let flags = playbin.read().unwrap().property_value("flags");
let flags_class = FlagsClass::with_type(flags.type_()).unwrap();
// Set up the Playbin flags to only play audio
let flags = flags_class
.builder_with_value(flags)
.ok_or(PlayerError::Build)?
.set_by_nick("audio")
.set_by_nick("download")
.unset_by_nick("video")
.unset_by_nick("text")
.build()
.ok_or(PlayerError::Build)?;
playbin
.write()
.unwrap()
.set_property_from_value("flags", &flags);
//playbin.write().unwrap().set_property("instant-uri", true);
let position = Arc::new(RwLock::new(None));
// Set up the thread to monitor the position
let (playback_tx, playback_rx) = unbounded();
let (status_tx, status_rx) = unbounded::<PlaybackInfo>();
let position_update = Arc::clone(&position);
std::thread::spawn(|| {
playback_monitor(playbin_arc, status_rx, playback_tx, position_update)
});
// Set up the thread to monitor bus messages
let playbin_bus_ctrl = Arc::clone(&playbin);
let paused = Arc::new(RwLock::new(false));
let bus_paused = Arc::clone(&paused);
let bus_watch = playbin
.read()
.unwrap()
.bus()
.expect("Failed to get GStreamer message bus")
.add_watch(move |_bus, msg| {
match msg.view() {
gst::MessageView::Eos(_) => println!("End of stream"),
gst::MessageView::StreamStart(_) => println!("Stream start"),
gst::MessageView::Error(err) => {
println!("Error recieved: {}", err);
return glib::ControlFlow::Break;
}
gst::MessageView::Buffering(buffering) => {
if *bus_paused.read().unwrap() == true {
return glib::ControlFlow::Continue;
}
// If the player is not paused, pause it
let percent = buffering.percent();
if percent < 100 {
playbin_bus_ctrl
.write()
.unwrap()
.set_state(gst::State::Paused)
.unwrap();
} else if percent >= 100 {
println!("Finished buffering");
playbin_bus_ctrl
.write()
.unwrap()
.set_state(gst::State::Playing)
.unwrap();
}
}
_ => (),
}
glib::ControlFlow::Continue
})
.expect("Failed to connect to GStreamer message bus");
// Set up a thread to watch the messages
std::thread::spawn(move || {
let _watch = bus_watch;
mainloop.run()
});
let source = None;
Ok(Self {
source,
playbin,
message_rx: playback_rx,
playback_tx: status_tx,
volume: 1.0,
start: None,
end: None,
paused,
position,
})
}
fn source(&self) -> &Option<URI> {
&self.source
}
fn enqueue_next(&mut self, next_track: &URI) -> Result<(), PlayerError> {
println!("enqueuing in fn");
self.set_source(next_track)
}
fn set_volume(&mut self, volume: f64) {
self.volume = volume.clamp(0.0, 1.0);
self.set_gstreamer_volume(self.volume);
}
fn volume(&self) -> f64 {
self.volume
}
fn play(&mut self) -> Result<(), PlayerError> {
if self.state() == PlayerState::Playing {
return Ok(());
}
*self.paused.write().unwrap() = false;
self.set_state(gst::State::Playing)?;
Ok(())
}
fn pause(&mut self) -> Result<(), PlayerError> {
if self.state() == PlayerState::Paused || *self.paused.read().unwrap() {
return Ok(());
}
*self.paused.write().unwrap() = true;
self.set_state(gst::State::Paused)?;
Ok(())
}
fn is_paused(&self) -> bool {
self.playbin().unwrap().current_state() == gst::State::Paused
}
fn position(&self) -> Option<Duration> {
*self.position.read().unwrap()
}
fn duration(&self) -> Option<Duration> {
if self.end.is_some() && self.start.is_some() {
Some(self.end.unwrap() - self.start.unwrap())
} else {
self.raw_duration()
}
}
fn seek_by(&mut self, seek_amount: Duration) -> Result<(), PlayerError> {
let time_pos = match *self.position.read().unwrap() {
Some(pos) => pos,
None => return Err(PlayerError::Seek("No position".into())),
};
let seek_pos = time_pos + seek_amount;
self.seek_to(seek_pos)?;
Ok(())
}
fn seek_to(&mut self, target_pos: Duration) -> Result<(), PlayerError> {
let start = if self.start.is_none() {
return Err(PlayerError::Seek("No START time".into()));
} else {
self.start.unwrap()
};
let end = if self.end.is_none() {
return Err(PlayerError::Seek("No END time".into()));
} else {
self.end.unwrap()
};
let adjusted_target = target_pos + start;
let clamped_target = adjusted_target.clamp(start, end);
let seek_pos_clock =
ClockTime::from_useconds(clamped_target.num_microseconds().unwrap() as u64);
self.set_gstreamer_volume(0.0);
self.playbin_mut()
.unwrap()
.seek_simple(gst::SeekFlags::FLUSH, seek_pos_clock)?;
self.set_gstreamer_volume(self.volume);
Ok(())
}
fn stop(&mut self) -> Result<(), PlayerError> {
self.pause()?;
self.ready()?;
// Send the updated position to the tracker
self.playback_tx.send(PlaybackInfo::Idle).unwrap();
// Set all positions to none
*self.position.write().unwrap() = None;
self.start = None;
self.end = None;
Ok(())
}
fn message_channel(&self) -> &crossbeam::channel::Receiver<PlayerCommand> {
&self.message_rx
}
}
impl Drop for GStreamer {
/// Cleans up the `GStreamer` pipeline and the monitoring
/// thread when [Player] is dropped.
fn drop(&mut self) {
self.playbin_mut()
.unwrap()
.set_state(gst::State::Null)
.expect("Unable to set the pipeline to the `Null` state");
let _ = self.playback_tx.send(PlaybackInfo::Finished);
}
}
fn playback_monitor(
playbin: Arc<RwLock<Element>>,
status_rx: Receiver<PlaybackInfo>,
playback_tx: Sender<PlayerCommand>,
position: Arc<RwLock<Option<Duration>>>,
) {
let mut stats = PlaybackInfo::Idle;
let mut pos_temp;
let mut sent_atf = false;
loop {
// Check for new messages to decide how to proceed
if let Ok(result) = status_rx.recv_timeout(std::time::Duration::from_millis(50)) {
stats = result
}
pos_temp = playbin
.read()
.unwrap()
.query_position::<ClockTime>()
.map(|pos| Duration::nanoseconds(pos.nseconds() as i64));
match stats {
PlaybackInfo::Playing { start, end } if pos_temp.is_some() => {
// Check if the current playback position is close to the end
let finish_point = end - Duration::milliseconds(2000);
if pos_temp.unwrap().num_microseconds() >= end.num_microseconds() {
println!("MONITOR: End of stream");
let _ = playback_tx.try_send(PlayerCommand::EndOfStream);
playbin
.write()
.unwrap()
.set_state(gst::State::Ready)
.expect("Unable to set the pipeline state");
sent_atf = false
} else if pos_temp.unwrap().num_microseconds() >= finish_point.num_microseconds()
&& !sent_atf
{
println!("MONITOR: About to finish");
let _ = playback_tx.try_send(PlayerCommand::AboutToFinish);
sent_atf = true;
}
// This has to be done AFTER the current time in the file
// is calculated, or everything else is wrong
pos_temp = Some(pos_temp.unwrap() - start)
}
PlaybackInfo::Finished => {
println!("MONITOR: Shutting down");
*position.write().unwrap() = None;
break;
}
PlaybackInfo::Idle | PlaybackInfo::Switching => sent_atf = false,
_ => (),
}
*position.write().unwrap() = pos_temp;
}
}

View file

@ -1,99 +0,0 @@
use chrono::Duration;
use thiserror::Error;
use crate::music_storage::library::URI;
#[derive(Error, Debug)]
pub enum PlayerError {
#[error("player initialization failed: {0}")]
Init(String),
#[error("could not change playback state")]
StateChange(String),
#[error("seeking failed: {0}")]
Seek(String),
#[error("the file or source is not found")]
NotFound,
#[error("failed to build gstreamer item")]
Build,
#[error("poison error")]
Poison,
#[error("general player error")]
General(String),
}
#[derive(Debug, PartialEq, Eq)]
pub enum PlayerState {
Playing,
Paused,
Ready,
Buffering(u8),
Null,
VoidPending,
}
#[derive(Debug, PartialEq, Eq)]
pub enum PlayerCommand {
Play,
Pause,
EndOfStream,
AboutToFinish,
}
pub trait Player {
/// Create a new player.
fn new() -> Result<Self, PlayerError>
where
Self: Sized;
/// Get the currently playing [URI] from the player.
fn source(&self) -> &Option<URI>;
/// Insert a new [`URI`] to be played. This method should be called at the
/// beginning to start playback of something, and once the [`PlayerCommand`]
/// indicates the track is about to finish to enqueue gaplessly.
///
/// For backends which do not support gapless playback, `AboutToFinish`
/// will not be called, and the next [`URI`] should be enqueued once `Eos`
/// occurs.
fn enqueue_next(&mut self, next_track: &URI) -> Result<(), PlayerError>;
/// Set the playback volume, accepts a float from `0` to `1`.
///
/// Values outside the range of `0` to `1` will be capped.
fn set_volume(&mut self, volume: f64);
/// Returns the current volume level, a float from `0` to `1`.
fn volume(&self) -> f64;
/// If the player is paused or stopped, starts playback.
fn play(&mut self) -> Result<(), PlayerError>;
/// If the player is playing, pause playback.
fn pause(&mut self) -> Result<(), PlayerError>;
/// Stop the playback entirely, removing the current [`URI`] from the player.
fn stop(&mut self) -> Result<(), PlayerError>;
/// Convenience function to check if playback is paused.
fn is_paused(&self) -> bool;
/// Get the current playback position of the player.
fn position(&self) -> Option<Duration>;
/// Get the duration of the currently playing track.
fn duration(&self) -> Option<Duration>;
/// Seek relative to the current position.
///
/// The position is capped at the duration of the song, and zero.
fn seek_by(&mut self, seek_amount: Duration) -> Result<(), PlayerError>;
/// Seek absolutely within the song.
///
/// The position is capped at the duration of the song, and zero.
fn seek_to(&mut self, target_pos: Duration) -> Result<(), PlayerError>;
/// Return a reference to the player message channel, which can be cloned
/// in order to monitor messages from the player.
fn message_channel(&self) -> &crossbeam::channel::Receiver<PlayerCommand>;
}

View file

@ -1,5 +1,7 @@
use file_format::FileFormat; use file_format::FileFormat;
use lofty::{AudioFile, LoftyError, ParseOptions, Probe, TagType, TaggedFileExt}; use lofty::file::{AudioFile as _, TaggedFileExt as _};
use lofty::probe::Probe;
use lofty::tag::TagType;
use quick_xml::events::Event; use quick_xml::events::Event;
use quick_xml::reader::Reader; use quick_xml::reader::Reader;
use std::collections::{BTreeMap, HashMap}; use std::collections::{BTreeMap, HashMap};
@ -221,7 +223,7 @@ fn to_tag(string: String) -> Tag {
_ => Tag::Key(string), _ => Tag::Key(string),
} }
} }
fn get_duration(file: &Path) -> Result<StdDur, lofty::LoftyError> { fn get_duration(file: &Path) -> Result<StdDur, lofty::error::LoftyError> {
let dur = match Probe::open(file)?.read() { let dur = match Probe::open(file)?.read() {
Ok(tagged_file) => tagged_file.properties().duration(), Ok(tagged_file) => tagged_file.properties().duration(),
@ -229,12 +231,12 @@ fn get_duration(file: &Path) -> Result<StdDur, lofty::LoftyError> {
}; };
Ok(dur) Ok(dur)
} }
fn get_art(file: &Path) -> Result<Vec<AlbumArt>, LoftyError> { fn get_art(file: &Path) -> Result<Vec<AlbumArt>, lofty::error::LoftyError> {
let mut album_art: Vec<AlbumArt> = Vec::new(); let mut album_art: Vec<AlbumArt> = Vec::new();
let blank_tag = &lofty::Tag::new(TagType::Id3v2); let blank_tag = &lofty::tag::Tag::new(TagType::Id3v2);
let normal_options = ParseOptions::new().parsing_mode(lofty::ParsingMode::Relaxed); let normal_options = lofty::config::ParseOptions::new().parsing_mode(lofty::config::ParsingMode::Relaxed);
let tagged_file: lofty::TaggedFile; let tagged_file: lofty::file::TaggedFile;
let tag = match Probe::open(file)?.options(normal_options).read() { let tag = match Probe::open(file)?.options(normal_options).read() {
Ok(e) => { Ok(e) => {
@ -288,7 +290,7 @@ impl ITunesSong {
Default::default() Default::default()
} }
fn from_hashmap(map: &mut HashMap<String, String>) -> Result<ITunesSong, LoftyError> { fn from_hashmap(map: &mut HashMap<String, String>) -> Result<ITunesSong, lofty::error::LoftyError> {
let mut song = ITunesSong::new(); let mut song = ITunesSong::new();
//get the path with the first bit chopped off //get the path with the first bit chopped off
let path_: String = map.get_key_value("Location").unwrap().1.clone(); let path_: String = map.get_key_value("Location").unwrap().1.clone();
@ -339,10 +341,7 @@ impl ITunesSong {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::{ use std::path::{Path, PathBuf};
path::{Path, PathBuf},
sync::{Arc, RwLock},
};
use crate::{ use crate::{
config::{Config, ConfigLibrary}, config::{Config, ConfigLibrary},

View file

@ -1,12 +1,11 @@
use super::playlist::{Playlist, PlaylistFolder}; use super::playlist::{Playlist, PlaylistFolder};
// Crate things // Crate things
use super::utils::{find_images, normalize, read_file, write_file}; use super::utils::{find_images, normalize, read_file, write_file};
use crate::config::Config;
use crate::music_storage::playlist::PlaylistFolderItem; use crate::music_storage::playlist::PlaylistFolderItem;
use std::cmp::Ordering; use std::cmp::Ordering;
// Various std things // Various std things
use std::collections::{BTreeMap, HashMap}; use std::collections::BTreeMap;
use std::error::Error; use std::error::Error;
use std::io::Read; use std::io::Read;
use std::ops::ControlFlow::{Break, Continue}; use std::ops::ControlFlow::{Break, Continue};
@ -14,9 +13,10 @@ use std::vec::IntoIter;
// Files // Files
use file_format::{FileFormat, Kind}; use file_format::{FileFormat, Kind};
use glib::filename_to_uri;
use lofty::{AudioFile, ItemKey, ItemValue, ParseOptions, Probe, TagType, TaggedFileExt}; use lofty::file::{AudioFile as _, TaggedFileExt as _};
use lofty::probe::Probe;
use lofty::tag::{ItemKey, ItemValue, TagType};
use rcue::parser::parse_from_file; use rcue::parser::parse_from_file;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -28,12 +28,11 @@ use chrono::{serde::ts_milliseconds_option, DateTime, Utc};
use std::time::Duration; use std::time::Duration;
// Serialization/Compression // Serialization/Compression
use base64::{engine::general_purpose, Engine as _};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
// Fun parallel stuff // Fun parallel stuff
use rayon::prelude::*; use rayon::prelude::*;
use std::sync::{Arc, Mutex, RwLock}; use std::sync::{Arc, Mutex};
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub enum AlbumArt { pub enum AlbumArt {
@ -225,10 +224,10 @@ impl Song {
/// Creates a `Song` from a music file /// Creates a `Song` from a music file
pub fn from_file<P: ?Sized + AsRef<Path>>(target_file: &P) -> Result<Self, Box<dyn Error>> { pub fn from_file<P: ?Sized + AsRef<Path>>(target_file: &P) -> Result<Self, Box<dyn Error>> {
let normal_options = ParseOptions::new().parsing_mode(lofty::ParsingMode::Relaxed); let normal_options = lofty::config::ParseOptions::new().parsing_mode(lofty::config::ParsingMode::Relaxed);
let blank_tag = &lofty::Tag::new(TagType::Id3v2); let blank_tag = &lofty::tag::Tag::new(TagType::Id3v2);
let tagged_file: lofty::TaggedFile; let tagged_file: lofty::file::TaggedFile;
let mut duration = Duration::from_secs(0); let mut duration = Duration::from_secs(0);
let tag = match Probe::open(target_file)?.options(normal_options).read() { let tag = match Probe::open(target_file)?.options(normal_options).read() {
Ok(file) => { Ok(file) => {
@ -273,7 +272,7 @@ impl Song {
let value = match item.value() { let value = match item.value() {
ItemValue::Text(value) => value.clone(), ItemValue::Text(value) => value.clone(),
ItemValue::Locator(value) => value.clone(), ItemValue::Locator(value) => value.clone(),
ItemValue::Binary(bin) => format!("BIN#{}", general_purpose::STANDARD.encode(bin)), ItemValue::Binary(_) => continue, // TODO: Ignoring binary values for now
}; };
tags.insert(key, value); tags.insert(key, value);
@ -548,10 +547,10 @@ impl URI {
pub fn as_uri(&self) -> String { pub fn as_uri(&self) -> String {
let path_str = match self { let path_str = match self {
URI::Local(location) => filename_to_uri(location, None) URI::Local(location) => prismriver::utils::path_to_uri(location)
.expect("couldn't convert path to URI") .expect("couldn't convert path to URI")
.to_string(), .to_string(),
URI::Cue { location, .. } => filename_to_uri(location, None) URI::Cue { location, .. } => prismriver::utils::path_to_uri(location)
.expect("couldn't convert path to URI") .expect("couldn't convert path to URI")
.to_string(), .to_string(),
URI::Remote(_, location) => location.clone(), URI::Remote(_, location) => location.clone(),

View file

@ -94,4 +94,4 @@ pub fn find_images(song_path: &Path) -> Result<Vec<AlbumArt>, Box<dyn Error>> {
} }
Ok(images) Ok(images)
} }

View file

@ -28,12 +28,12 @@ futures = "0.3.31"
crossbeam = "0.8.4" crossbeam = "0.8.4"
directories = "5.0.1" directories = "5.0.1"
uuid = { version = "1.11.0", features = ["v4", "serde"] } uuid = { version = "1.11.0", features = ["v4", "serde"] }
ciborium = "0.2.2"
mime = "0.3.17" mime = "0.3.17"
file-format = "0.26.0" file-format = "0.26.0"
chrono = { version = "0.4.38", features = ["serde"] } chrono = { version = "0.4.38", features = ["serde"] }
itertools = "0.13.0" itertools = "0.13.0"
rfd = "0.15.1" rfd = "0.15.1"
colog = "1.3.0"
[features] [features]
default = [ "custom-protocol" ] default = [ "custom-protocol" ]

View file

@ -2,8 +2,8 @@ use std::{fs, path::PathBuf, str::FromStr, thread::spawn};
use commands::{add_song_to_queue, play_now}; use commands::{add_song_to_queue, play_now};
use crossbeam::channel::{unbounded, Receiver, Sender}; use crossbeam::channel::{unbounded, Receiver, Sender};
use dmp_core::{config::{Config, ConfigLibrary}, music_controller::controller::{Controller, ControllerHandle, LibraryResponse}, music_player::gstreamer::GStreamer, music_storage::library::{AlbumArt, MusicLibrary}}; use dmp_core::{config::{Config, ConfigLibrary}, music_controller::controller::{Controller, ControllerHandle, LibraryResponse}, music_storage::library::MusicLibrary};
use tauri::{http::Response, Emitter, Manager, State, Url, WebviewWindowBuilder, Wry}; use tauri::{http::Response, Emitter, Manager, State, WebviewWindowBuilder, Wry};
use uuid::Uuid; use uuid::Uuid;
use crate::wrappers::{get_library, play, pause, prev, set_volume, get_song, next, get_queue, import_playlist, get_playlist, get_playlists}; use crate::wrappers::{get_library, play, pause, prev, set_volume, get_song, next, get_queue, import_playlist, get_playlist, get_playlists};
@ -55,7 +55,7 @@ pub fn run() {
handle_rx.send(handle).unwrap(); handle_rx.send(handle).unwrap();
let _controller = futures::executor::block_on(Controller::<GStreamer>::start(input)).unwrap(); let _controller = futures::executor::block_on(Controller::start(input)).unwrap();
}); });
let app = tauri::Builder::default() let app = tauri::Builder::default()
.plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_shell::init())

View file

@ -3,5 +3,7 @@
fn main() { fn main() {
colog::init();
dango_music_player_lib::run() dango_music_player_lib::run()
} }