change player backend from rodio to playback-rs

because rodio struggled to load some
(possibly broken, but still playable) audio files.
this is not perfect and sometimes suffers from
small lags, and it uses song_finished_polling
instead of being event-driven.
This commit is contained in:
Mark 2024-07-14 18:01:01 +02:00
parent 3a0d28b9b4
commit aa657382fa
8 changed files with 189 additions and 8 deletions

View File

@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@ -12,7 +12,7 @@ use gui::GuiEvent;
#[cfg(feature = "playback")]
use musicdb_lib::data::cache_manager::CacheManager;
#[cfg(feature = "playback")]
use musicdb_lib::player::{rodio::PlayerBackendRodio, Player};
use musicdb_lib::player::{playback_rs::PlayerBackendPlaybackRs, Player};
use musicdb_lib::{
data::{
database::{ClientIo, Database},
@ -152,7 +152,7 @@ fn main() {
cm.set_cache_songs_count(20);
cache_manager = Some(cm);
Some(Player::new_client(
PlayerBackendRodio::new_without_command_sending().unwrap(),
PlayerBackendPlaybackRs::new_without_command_sending().unwrap(),
))
} else {
None

View File

@ -6,11 +6,14 @@ edition = "2021"
[dependencies]
base64 = "0.22.1"
colorize = "0.1.0"
playback-rs = { version = "0.4.3", optional = true }
rand = "0.8.5"
rc-u8-reader = "2.0.16"
rodio = { version = "0.18.0", optional = true }
sysinfo = "0.30.12"
[features]
# default = ["playback"]
playback = ["dep:rodio"]
default = ["playback"]
playback = ["playback-via-playback-rs"]
playback-via-playback-rs = ["dep:playback-rs"]
playback-via-rodio = ["dep:rodio"]

View File

@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@ -1,3 +1,6 @@
#[cfg(feature = "playback-via-playback-rs")]
pub mod playback_rs;
#[cfg(feature = "playback-via-rodio")]
pub mod rodio;
use std::{collections::HashMap, ffi::OsStr, sync::Arc};

View File

@ -0,0 +1,162 @@
use std::{ffi::OsStr, io::Cursor, path::Path, sync::Arc, time::Duration};
use playback_rs::Hint;
use crate::{data::SongId, server::Command};
use super::PlayerBackend;
pub struct PlayerBackendPlaybackRs<T> {
player: playback_rs::Player,
current: Option<(SongId, Option<playback_rs::Song>, T)>,
next: Option<(SongId, Option<playback_rs::Song>, T)>,
command_sender: Option<std::sync::mpsc::Sender<Command>>,
}
impl<T> PlayerBackendPlaybackRs<T> {
pub fn new(
command_sender: std::sync::mpsc::Sender<Command>,
) -> Result<Self, Box<dyn std::error::Error>> {
Self::new_with_optional_command_sending(Some(command_sender))
}
pub fn new_without_command_sending() -> Result<Self, Box<dyn std::error::Error>> {
Self::new_with_optional_command_sending(None)
}
pub fn new_with_optional_command_sending(
command_sender: Option<std::sync::mpsc::Sender<Command>>,
) -> Result<Self, Box<dyn std::error::Error>> {
Ok(Self {
player: playback_rs::Player::new(None)?,
current: None,
next: None,
command_sender,
})
}
}
impl<T> PlayerBackend<T> for PlayerBackendPlaybackRs<T> {
fn load_next_song(
&mut self,
id: SongId,
filename: &OsStr,
bytes: Arc<Vec<u8>>,
_load_duration: bool,
custom_data: T,
) {
let mut hint = Hint::new();
if let Some(ext) = Path::new(filename).extension().and_then(OsStr::to_str) {
hint.with_extension(ext);
}
let reader = Box::new(Cursor::new(ArcVec(bytes)));
let loaded_song = match playback_rs::Song::new(reader, &hint, None) {
Ok(v) => Some(v),
Err(e) => {
if let Some(s) = &self.command_sender {
s.send(Command::ErrorInfo(
format!("Couldn't decode song #{id}!"),
format!("Error: {e}"),
))
.unwrap();
}
None
}
};
if let Some(song) = &loaded_song {
if self.player.has_current_song() {
if let Err(e) = self.player.play_song_next(song, None) {
if let Some(s) = &self.command_sender {
s.send(Command::ErrorInfo(
format!("Couldn't preload song #{id}!"),
format!("Error: {e}"),
))
.unwrap();
}
}
}
}
self.next = Some((id, loaded_song, custom_data));
}
fn pause(&mut self) {
self.player.set_playing(false);
}
fn stop(&mut self) {
self.pause();
self.player.seek(Duration::ZERO);
}
fn resume(&mut self) {
self.player.set_playing(true);
}
fn next(&mut self, play: bool, _load_duration: bool) {
self.pause();
self.player.skip();
self.current = self.next.take();
if self.player.has_current_song() {
self.player.set_playing(play);
} else {
if let Some((id, song, _)) = &self.current {
if let Some(song) = song {
if let Err(e) = self.player.play_song_now(song, None) {
if let Some(s) = &self.command_sender {
s.send(Command::ErrorInfo(
format!("Couldn't play song #{id}!"),
format!("Error: {e}"),
))
.unwrap();
s.send(Command::NextSong).unwrap();
}
} else {
self.player.set_playing(play);
}
} else if let Some(s) = &self.command_sender {
s.send(Command::NextSong).unwrap();
}
}
}
}
fn clear(&mut self) {
// remove next song
let _ = self.player.force_remove_next_song();
// remove current song
let _ = self.player.force_remove_next_song();
self.current = None;
self.next = None;
}
fn playing(&self) -> bool {
self.player.is_playing()
}
fn current_song(&self) -> Option<(SongId, bool, &T)> {
self.current.as_ref().map(|v| (v.0, true, &v.2))
}
fn next_song(&self) -> Option<(SongId, bool, &T)> {
self.next.as_ref().map(|v| (v.0, true, &v.2))
}
fn gen_data_mut(&mut self) -> (Option<&mut T>, Option<&mut T>) {
(
self.current.as_mut().map(|v| &mut v.2),
self.next.as_mut().map(|v| &mut v.2),
)
}
fn song_finished_polling(&self) -> bool {
true
}
fn song_finished(&self) -> bool {
self.current.is_some() && !self.player.has_current_song()
}
fn current_song_duration(&self) -> Option<u64> {
self.player
.get_playback_position()
.map(|v| v.1.as_millis() as _)
}
fn current_song_playback_position(&self) -> Option<u64> {
self.player
.get_playback_position()
.map(|v| v.0.as_millis() as _)
}
}
pub struct ArcVec(pub Arc<Vec<u8>>);
impl AsRef<[u8]> for ArcVec {
fn as_ref(&self) -> &[u8] {
self.0.as_ref().as_ref()
}
}

View File

@ -137,18 +137,25 @@ pub fn run_server_caching_thread_opt(
use std::time::Instant;
use crate::data::cache_manager::CacheManager;
#[cfg(feature = "playback-via-playback-rs")]
use crate::player::playback_rs::PlayerBackendPlaybackRs;
#[cfg(feature = "playback-via-rodio")]
use crate::player::rodio::PlayerBackendRodio;
#[cfg(feature = "playback")]
use crate::player::{rodio::PlayerBackendRodio, PlayerBackend};
use crate::player::PlayerBackend;
// commands sent to this will be handeled later in this function in an infinite loop.
// these commands are sent to the database asap.
let (command_sender, command_receiver) = mpsc::channel();
#[cfg(feature = "playback-via-playback-rs")]
let backend = PlayerBackendPlaybackRs::new(command_sender.clone()).unwrap();
#[cfg(feature = "playback-via-rodio")]
let backend = PlayerBackendRodio::new(command_sender.clone()).unwrap();
#[cfg(feature = "playback")]
let mut player = if play_audio {
Some(Player::new(
PlayerBackendRodio::new(command_sender.clone()).unwrap(),
))
Some(Player::new(backend))
} else {
None
};

View File

@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"