use std::{fs::File, path::Path};
use moosicbox_audio_decoder::{AudioDecodeHandler, DecodeError, decode};
use switchy_async::task::JoinError;
use symphonia::core::{
codecs::DecoderOptions,
formats::FormatOptions,
io::{MediaSourceStream, MediaSourceStreamOptions},
meta::MetadataOptions,
probe::Hint,
};
use thiserror::Error;
impl From<std::io::Error> for PlaybackError {
fn from(err: std::io::Error) -> Self {
Self::Symphonia(symphonia::core::errors::Error::IoError(err))
}
}
#[derive(Debug, Error)]
pub enum PlaybackError {
#[error(transparent)]
Decode(#[from] DecodeError),
#[error(transparent)]
Symphonia(#[from] symphonia::core::errors::Error),
#[error(transparent)]
Join(#[from] JoinError),
#[error("No audio outputs")]
NoAudioOutputs,
#[error("Invalid source")]
InvalidSource,
}
pub async fn play_file_path_str_async(
path_str: &str,
get_audio_output_handler: impl FnOnce() -> GetAudioDecodeHandlerRet + Send + 'static,
enable_gapless: bool,
verify: bool,
track_num: Option<usize>,
seek: Option<f64>,
) -> Result<i32, PlaybackError> {
let path_str = path_str.to_owned();
switchy_async::runtime::Handle::current()
.spawn_blocking_with_name("audio_decoder: Play file path", move || {
let mut handler = get_audio_output_handler()?;
play_file_path_str(
&path_str,
&mut handler,
enable_gapless,
verify,
track_num,
seek,
)
})
.await?
}
#[allow(clippy::too_many_arguments)]
fn play_file_path_str(
path_str: &str,
audio_decode_handler: &mut AudioDecodeHandler,
enable_gapless: bool,
verify: bool,
track_num: Option<usize>,
seek: Option<f64>,
) -> Result<i32, PlaybackError> {
let mut hint = Hint::new();
let path = Path::new(path_str);
if let Some(extension) = path.extension()
&& let Some(extension_str) = extension.to_str()
{
hint.with_extension(extension_str);
}
let source = Box::new(File::open(path)?);
let mss = MediaSourceStream::new(source, MediaSourceStreamOptions::default());
play_media_source(
mss,
&hint,
audio_decode_handler,
enable_gapless,
verify,
track_num,
seek,
)
}
pub type GetAudioDecodeHandlerRet = Result<AudioDecodeHandler, PlaybackError>;
pub async fn play_media_source_async(
media_source_stream: MediaSourceStream,
hint: &Hint,
get_audio_output_handler: impl FnOnce() -> GetAudioDecodeHandlerRet + Send + 'static,
enable_gapless: bool,
verify: bool,
track_num: Option<usize>,
seek: Option<f64>,
) -> Result<i32, PlaybackError> {
let hint = hint.clone();
switchy_async::runtime::Handle::current()
.spawn_blocking_with_name("audio_decoder: Play media source", move || {
let mut handler = get_audio_output_handler()?;
play_media_source(
media_source_stream,
&hint,
&mut handler,
enable_gapless,
verify,
track_num,
seek,
)
})
.await?
}
#[allow(clippy::too_many_arguments)]
pub fn play_media_source(
media_source_stream: MediaSourceStream,
hint: &Hint,
audio_decode_handler: &mut AudioDecodeHandler,
enable_gapless: bool,
verify: bool,
track_num: Option<usize>,
seek: Option<f64>,
) -> Result<i32, PlaybackError> {
let format_opts = FormatOptions {
enable_gapless,
..Default::default()
};
let metadata_opts = MetadataOptions::default();
match symphonia::default::get_probe().format(
hint,
media_source_stream,
&format_opts,
&metadata_opts,
) {
Ok(probed) => {
let seek_time = seek;
let decode_opts = DecoderOptions { verify };
Ok(decode(
probed.format,
audio_decode_handler,
track_num,
seek_time,
decode_opts,
)?)
}
Err(err) => {
log::info!("the input is not supported: {err:?}");
Err(PlaybackError::Symphonia(err))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use moosicbox_audio_decoder::AudioDecodeError;
#[test_log::test]
fn test_playback_error_display_no_audio_outputs() {
let error = PlaybackError::NoAudioOutputs;
assert!(error.to_string().contains("No audio outputs"));
}
#[test_log::test]
fn test_playback_error_display_invalid_source() {
let error = PlaybackError::InvalidSource;
assert!(error.to_string().contains("Invalid source"));
}
#[test_log::test]
fn test_playback_error_from_io_error() {
let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let playback_error: PlaybackError = io_error.into();
assert!(matches!(playback_error, PlaybackError::Symphonia(_)));
assert!(playback_error.to_string().contains("file not found"));
}
#[test_log::test]
fn test_playback_error_from_decode_error() {
let decode_error = DecodeError::AudioDecode(AudioDecodeError::OpenStream);
let playback_error: PlaybackError = decode_error.into();
assert!(matches!(playback_error, PlaybackError::Decode(_)));
assert!(!playback_error.to_string().is_empty());
}
#[test_log::test]
fn test_playback_error_decode_variants() {
let errors = [
AudioDecodeError::OpenStream,
AudioDecodeError::PlayStream,
AudioDecodeError::StreamClosed,
AudioDecodeError::StreamEnd,
AudioDecodeError::Interrupt,
];
for error in errors {
let decode_error = DecodeError::AudioDecode(error);
let playback_error: PlaybackError = decode_error.into();
assert!(matches!(playback_error, PlaybackError::Decode(_)));
assert!(!playback_error.to_string().is_empty());
}
}
}