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)]
138mod tests {
139 use super::*;
140
141 fn test_video_path() -> std::path::PathBuf {
142 std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../assets/video/gameplay.mp4")
143 }
144
145 #[test]
146 fn async_preview_player_is_send_and_sync() {
147 fn assert_send_sync<T: Send + Sync>() {}
148 assert_send_sync::<AsyncPreviewPlayer>();
149 }
150
151 #[test]
152 #[ignore = "requires FFmpeg and assets/video/gameplay.mp4; run with -- --include-ignored"]
153 fn async_preview_player_should_open_and_report_nonzero_duration() {
154 let path = test_video_path();
155 match tokio::runtime::Builder::new_current_thread()
156 .enable_all()
157 .build()
158 {
159 Ok(rt) => rt.block_on(async {
160 let player = match AsyncPreviewPlayer::open(path.clone()).await {
161 Ok(p) => p,
162 Err(e) => {
163 println!("skipping: open failed: {e}");
164 return;
165 }
166 };
167 assert!(
168 player.duration().is_some_and(|d| d > Duration::ZERO),
169 "duration must be positive for a valid media file"
170 );
171 }),
172 Err(e) => println!("skipping: failed to build tokio runtime: {e}"),
173 }
174 }
175}