ff_decode/image/async_decoder.rs
1//! Async image decoder backed by `tokio::task::spawn_blocking`.
2
3use std::path::{Path, PathBuf};
4
5use ff_format::VideoFrame;
6
7use crate::error::DecodeError;
8use crate::image::builder::ImageDecoder;
9
10/// Async wrapper around [`ImageDecoder`].
11///
12/// Both `open` (file I/O + codec init) and `decode` (pixel conversion) are
13/// performed on `spawn_blocking` threads so the async executor is not blocked.
14///
15/// There is no `into_stream` method because an image is a single frame, not a
16/// sequence.
17///
18/// # Examples
19///
20/// ```ignore
21/// use ff_decode::AsyncImageDecoder;
22///
23/// let frame = AsyncImageDecoder::open("photo.png").await?.decode().await?;
24/// println!("{}x{}", frame.width(), frame.height());
25/// ```
26pub struct AsyncImageDecoder {
27 inner: ImageDecoder,
28}
29
30impl AsyncImageDecoder {
31 /// Opens the image file asynchronously.
32 ///
33 /// File I/O and codec initialisation are performed on a `spawn_blocking`
34 /// thread so the async executor is not blocked.
35 ///
36 /// # Errors
37 ///
38 /// Returns [`DecodeError`] if the file is missing, contains no video
39 /// stream, or uses an unsupported codec.
40 pub async fn open(path: impl AsRef<Path> + Send + 'static) -> Result<Self, DecodeError> {
41 let path: PathBuf = path.as_ref().to_path_buf();
42 let decoder = tokio::task::spawn_blocking(move || ImageDecoder::open(&path).build())
43 .await
44 .map_err(|e| DecodeError::Ffmpeg {
45 code: 0,
46 message: format!("spawn_blocking panicked: {e}"),
47 })??;
48 Ok(Self { inner: decoder })
49 }
50
51 /// Decodes the image into a [`VideoFrame`].
52 ///
53 /// This consuming method runs on a `spawn_blocking` thread.
54 ///
55 /// # Errors
56 ///
57 /// Returns [`DecodeError`] on codec or I/O errors.
58 pub async fn decode(self) -> Result<VideoFrame, DecodeError> {
59 tokio::task::spawn_blocking(move || self.inner.decode())
60 .await
61 .map_err(|e| DecodeError::Ffmpeg {
62 code: 0,
63 message: format!("spawn_blocking panicked: {e}"),
64 })?
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 #[tokio::test]
73 async fn async_image_decoder_should_fail_on_missing_file() {
74 let result = AsyncImageDecoder::open("/nonexistent/path/photo.png").await;
75 assert!(
76 matches!(result, Err(DecodeError::FileNotFound { .. })),
77 "expected FileNotFound"
78 );
79 }
80}