ff_preview/playback/
async_player.rs1use std::path::Path;
6use std::time::Duration;
7
8use super::player::PreviewPlayer;
9use super::player_handle::PlayerHandle;
10use crate::error::PreviewError;
11use crate::event::PlayerEvent;
12
13#[derive(Clone)]
33pub struct AsyncPreviewPlayer {
34 handle: PlayerHandle,
35}
36
37impl AsyncPreviewPlayer {
38 pub async fn open(path: impl AsRef<Path> + Send + 'static) -> Result<Self, PreviewError> {
49 let path = path.as_ref().to_path_buf();
50 let task = tokio::task::spawn_blocking(move || {
51 PreviewPlayer::open(&path).map(PreviewPlayer::split)
52 });
53 let (runner, handle) = task.await.map_err(|e| PreviewError::Ffmpeg {
54 code: 0,
55 message: format!("tokio task join error: {e}"),
56 })??;
57
58 tokio::task::spawn_blocking(move || {
59 let _ = runner.run();
60 });
61
62 Ok(Self { handle })
63 }
64
65 pub fn play(&self) {
67 self.handle.play();
68 }
69
70 pub fn pause(&self) {
72 self.handle.pause();
73 }
74
75 pub fn stop(&self) {
77 self.handle.stop();
78 }
79
80 pub fn seek(&self, pts: Duration) {
82 self.handle.seek(pts);
83 }
84
85 pub fn set_rate(&self, rate: f64) {
87 self.handle.set_rate(rate);
88 }
89
90 #[must_use]
92 pub fn current_pts(&self) -> Duration {
93 self.handle.current_pts()
94 }
95
96 #[must_use]
98 pub fn duration(&self) -> Option<Duration> {
99 self.handle.duration()
100 }
101
102 #[must_use]
104 pub fn pop_audio_samples(&self, n: usize) -> Vec<f32> {
105 self.handle.pop_audio_samples(n)
106 }
107
108 #[must_use]
112 pub fn poll_event(&self) -> Option<PlayerEvent> {
113 self.handle.poll_event()
114 }
115
116 pub async fn next_event(&self) -> Option<PlayerEvent> {
121 let handle = self.handle.clone();
122 tokio::task::spawn_blocking(move || handle.recv_event())
123 .await
124 .ok()
125 .flatten()
126 }
127}
128
129impl Drop for AsyncPreviewPlayer {
130 fn drop(&mut self) {
131 self.handle.stop();
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 fn test_video_path() -> std::path::PathBuf {
140 std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../assets/video/gameplay.mp4")
141 }
142
143 #[test]
144 fn async_preview_player_is_send_and_sync() {
145 fn assert_send_sync<T: Send + Sync>() {}
146 assert_send_sync::<AsyncPreviewPlayer>();
147 }
148
149 #[test]
150 #[ignore = "requires FFmpeg and assets/video/gameplay.mp4; run with -- --include-ignored"]
151 fn async_preview_player_should_open_and_report_nonzero_duration() {
152 let path = test_video_path();
153 match tokio::runtime::Builder::new_current_thread()
154 .enable_all()
155 .build()
156 {
157 Ok(rt) => rt.block_on(async {
158 let player = match AsyncPreviewPlayer::open(path.clone()).await {
159 Ok(p) => p,
160 Err(e) => {
161 println!("skipping: open failed: {e}");
162 return;
163 }
164 };
165 assert!(
166 player.duration().is_some_and(|d| d > Duration::ZERO),
167 "duration must be positive for a valid media file"
168 );
169 }),
170 Err(e) => println!("skipping: failed to build tokio runtime: {e}"),
171 }
172 }
173}