use std::path::Path;
use std::time::Duration;
use super::player::PreviewPlayer;
use super::player_handle::PlayerHandle;
use crate::error::PreviewError;
use crate::event::PlayerEvent;
#[derive(Clone)]
pub struct AsyncPreviewPlayer {
handle: PlayerHandle,
}
impl AsyncPreviewPlayer {
pub async fn open(path: impl AsRef<Path> + Send + 'static) -> Result<Self, PreviewError> {
let path = path.as_ref().to_path_buf();
let task = tokio::task::spawn_blocking(move || {
PreviewPlayer::open(&path).map(PreviewPlayer::split)
});
let (runner, handle) = task.await.map_err(|e| PreviewError::Ffmpeg {
code: 0,
message: format!("tokio task join error: {e}"),
})??;
tokio::task::spawn_blocking(move || {
let _ = runner.run();
});
Ok(Self { handle })
}
pub fn play(&self) {
self.handle.play();
}
pub fn pause(&self) {
self.handle.pause();
}
pub fn stop(&self) {
self.handle.stop();
}
pub fn seek(&self, pts: Duration) {
self.handle.seek(pts);
}
pub fn set_rate(&self, rate: f64) {
self.handle.set_rate(rate);
}
#[must_use]
pub fn current_pts(&self) -> Duration {
self.handle.current_pts()
}
#[must_use]
pub fn duration(&self) -> Option<Duration> {
self.handle.duration()
}
#[must_use]
pub fn pop_audio_samples(&self, n: usize) -> Vec<f32> {
self.handle.pop_audio_samples(n)
}
#[must_use]
pub fn poll_event(&self) -> Option<PlayerEvent> {
self.handle.poll_event()
}
pub async fn next_event(&self) -> Option<PlayerEvent> {
let handle = self.handle.clone();
tokio::task::spawn_blocking(move || handle.recv_event())
.await
.ok()
.flatten()
}
}
impl Drop for AsyncPreviewPlayer {
fn drop(&mut self) {
self.handle.stop();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_video_path() -> std::path::PathBuf {
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../assets/video/gameplay.mp4")
}
#[test]
fn async_preview_player_is_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<AsyncPreviewPlayer>();
}
#[test]
#[ignore = "requires FFmpeg and assets/video/gameplay.mp4; run with -- --include-ignored"]
fn async_preview_player_should_open_and_report_nonzero_duration() {
let path = test_video_path();
match tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
{
Ok(rt) => rt.block_on(async {
let player = match AsyncPreviewPlayer::open(path.clone()).await {
Ok(p) => p,
Err(e) => {
println!("skipping: open failed: {e}");
return;
}
};
assert!(
player.duration().is_some_and(|d| d > Duration::ZERO),
"duration must be positive for a valid media file"
);
}),
Err(e) => println!("skipping: failed to build tokio runtime: {e}"),
}
}
}