Skip to main content

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}