-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(playback): merge anni-player into anni-playback (#47)
* feat(playback): add `CachedHttpSource` and `CachedAnnilSource` * feat(playback): add a player implementation todo: document * chore(playback): select needed features only * fix(playback): stop player before opening track * feat(common): report detailed error when parsing TrackIdentifier
- Loading branch information
Showing
9 changed files
with
578 additions
and
1 deletion.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
pub use anni_provider::providers::TypedPriorityProvider; | ||
|
||
pub use crate::sources::cached_http::provider::AudioQuality; | ||
|
||
use crossbeam::channel::Sender; | ||
use reqwest::blocking::Client; | ||
|
||
use std::{ | ||
path::PathBuf, | ||
sync::{ | ||
atomic::AtomicBool, | ||
mpsc::{self, Receiver}, | ||
Arc, RwLock, | ||
}, | ||
thread, | ||
}; | ||
|
||
use anni_common::models::TrackIdentifier; | ||
|
||
use crate::{ | ||
sources::cached_http::{cache::CacheStore, provider::ProviderProxy, CachedAnnilSource}, | ||
types::PlayerEvent, | ||
Controls, Decoder, | ||
}; | ||
|
||
pub struct AnniPlayer { | ||
pub controls: Controls, | ||
pub client: Client, | ||
pub thread_killer: Sender<bool>, | ||
provider: RwLock<TypedPriorityProvider<ProviderProxy>>, | ||
cache_store: CacheStore, // root of cache | ||
} | ||
|
||
impl AnniPlayer { | ||
pub fn new( | ||
provider: TypedPriorityProvider<ProviderProxy>, | ||
cache_path: PathBuf, | ||
) -> (Self, Receiver<PlayerEvent>) { | ||
let (controls, receiver, killer) = { | ||
let (sender, receiver) = mpsc::channel(); | ||
let controls = Controls::new(sender); | ||
let thread_killer = crate::create_unbound_channel(); | ||
|
||
thread::Builder::new() | ||
.name("anni-playback-decoder".to_owned()) | ||
.spawn({ | ||
let controls = controls.clone(); | ||
move || { | ||
let decoder = Decoder::new(controls, thread_killer.1); | ||
|
||
decoder.start(); | ||
} | ||
}) | ||
.unwrap(); | ||
|
||
(controls, receiver, thread_killer.0) | ||
}; | ||
|
||
( | ||
Self { | ||
controls, | ||
client: Client::new(), | ||
thread_killer: killer, | ||
provider: RwLock::new(provider), | ||
cache_store: CacheStore::new(cache_path), | ||
}, | ||
receiver, | ||
) | ||
} | ||
|
||
pub fn add_provider(&self, url: String, auth: String, priority: i32) { | ||
let mut provider = self.provider.write().unwrap(); | ||
|
||
provider.insert(ProviderProxy::new(url, auth, self.client.clone()), priority); | ||
} | ||
|
||
pub fn clear_provider(&self) { | ||
let mut provider = self.provider.write().unwrap(); | ||
|
||
*provider = TypedPriorityProvider::new(vec![]); | ||
} | ||
|
||
pub fn open(&self, track: TrackIdentifier, quality: AudioQuality) -> anyhow::Result<()> { | ||
log::info!("loading track: {track}"); | ||
|
||
self.controls.stop(); | ||
|
||
let provider = self.provider.read().unwrap(); | ||
|
||
let buffer_signal = Arc::new(AtomicBool::new(true)); | ||
let source = CachedAnnilSource::new( | ||
track, | ||
quality, | ||
&self.cache_store, | ||
self.client.clone(), | ||
&provider, | ||
buffer_signal.clone(), | ||
)?; | ||
|
||
self.controls.open(Box::new(source), buffer_signal, false); | ||
|
||
Ok(()) | ||
} | ||
|
||
pub fn open_and_play( | ||
&self, | ||
track: TrackIdentifier, | ||
quality: AudioQuality, | ||
) -> anyhow::Result<()> { | ||
self.open(track, quality)?; | ||
self.play(); | ||
|
||
Ok(()) | ||
} | ||
|
||
pub fn play(&self) { | ||
self.controls.play(); | ||
} | ||
|
||
pub fn pause(&self) { | ||
self.controls.pause(); | ||
} | ||
|
||
pub fn stop(&self) { | ||
self.controls.stop(); | ||
} | ||
|
||
pub fn open_file(&self, path: String) -> anyhow::Result<()> { | ||
self.controls.open_file(path, false) | ||
} | ||
|
||
pub fn set_volume(&self, volume: f32) { | ||
self.controls.set_volume(volume); | ||
} | ||
|
||
pub fn seek(&self, position: u64) { | ||
self.controls.seek(position); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
use std::{ | ||
fs::{self, File}, io::{self, ErrorKind}, path::{Path, PathBuf} | ||
}; | ||
|
||
use crate::CODEC_REGISTRY; | ||
use symphonia::{ | ||
core::{ | ||
codecs::DecoderOptions, formats::FormatOptions, io::MediaSourceStream, | ||
meta::MetadataOptions, probe::Hint, | ||
}, | ||
default::get_probe, | ||
}; | ||
|
||
use anni_common::models::RawTrackIdentifier; | ||
|
||
#[derive(Debug, Clone)] | ||
pub struct CacheStore { | ||
base: PathBuf, | ||
} | ||
|
||
impl CacheStore { | ||
pub fn new(base: PathBuf) -> Self { | ||
Self { base } | ||
} | ||
|
||
/// Returns the path to given `track` | ||
pub fn loaction_of(&self, track: RawTrackIdentifier) -> PathBuf { | ||
let mut tmp = self.base.clone(); | ||
|
||
tmp.extend([ | ||
track.album_id.as_ref(), | ||
&format!( | ||
"{}_{}", | ||
track.disc_id.to_string(), | ||
track.track_id.to_string(), | ||
), | ||
]); | ||
tmp | ||
} | ||
|
||
/// Attempts to open a cache file corresponding to `track` and validates it. | ||
/// | ||
/// On success, returns a `Result<File, File>`. | ||
/// If the cache exists and is valid, opens it in read mode and returns an `Ok(_)`. | ||
/// Otherwise, opens or creates a cache file in append mode and returns an `Err(_)`. | ||
/// | ||
/// On error, an [`Error`](std::io::Error) is returned. | ||
pub fn acquire(&self, track: RawTrackIdentifier) -> io::Result<Result<File, File>> { | ||
let path = self.loaction_of(track.copied()); | ||
|
||
if path.exists() { | ||
if validate_audio(&path).unwrap_or(false) { | ||
return File::open(path).map(|f| Ok(f)); | ||
} | ||
|
||
log::warn!("cache of {track} exists but is invalid"); | ||
} | ||
|
||
create_dir_all(path.parent().unwrap())?; // parent of `path` exists | ||
|
||
File::options() | ||
.read(true) | ||
.append(true) | ||
.create(true) | ||
.open(path) | ||
.map(|f| Err(f)) | ||
} | ||
|
||
pub fn add(&self, path: &Path, track: RawTrackIdentifier) -> io::Result<()> { | ||
let location = self.loaction_of(track); | ||
|
||
if location.exists() { | ||
Err(ErrorKind::AlreadyExists.into()) | ||
} else if validate_audio(path).unwrap_or(false) { | ||
fs::copy(path, location).map(|_| {}) | ||
} else { | ||
Err(io::Error::new(ErrorKind::Other, "invalid cache")) | ||
} | ||
} | ||
} | ||
|
||
pub fn create_dir_all(path: impl AsRef<Path>) -> io::Result<()> { | ||
match fs::create_dir_all(path.as_ref()) { | ||
Err(e) if e.kind() == ErrorKind::AlreadyExists => Ok(()), | ||
r => r, | ||
} | ||
} | ||
|
||
pub fn validate_audio(p: &Path) -> symphonia::core::errors::Result<bool> { | ||
let source = MediaSourceStream::new(Box::new(File::open(p)?), Default::default()); | ||
|
||
let format_opts = FormatOptions::default(); | ||
let metadata_opts = MetadataOptions::default(); | ||
|
||
let probed = get_probe().format(&Hint::new(), source, &format_opts, &metadata_opts)?; | ||
|
||
let mut format_reader = probed.format; | ||
let track = match format_reader.default_track() { | ||
Some(track) => track, | ||
None => return Ok(false), | ||
}; | ||
|
||
let mut decoder = CODEC_REGISTRY.make(&track.codec_params, &DecoderOptions { verify: true })?; | ||
|
||
while let Ok(packet) = format_reader.next_packet() { | ||
let _ = decoder.decode(&packet)?; | ||
} | ||
|
||
Ok(decoder.finalize().verify_ok.unwrap_or(false)) | ||
} |
Oops, something went wrong.