scuffle_ffmpeg/io/
input.rs

1use std::ffi::CStr;
2use std::path::Path;
3
4use super::internal::{Inner, InnerOptions, read_packet, seek};
5use crate::consts::{Const, DEFAULT_BUFFER_SIZE};
6use crate::dict::Dictionary;
7use crate::error::{FfmpegError, FfmpegErrorCode};
8use crate::ffi::*;
9use crate::packet::{Packet, Packets};
10use crate::smart_object::SmartObject;
11use crate::stream::Streams;
12
13/// Represents an input stream.
14pub struct Input<T: Send + Sync> {
15    inner: SmartObject<Inner<T>>,
16}
17
18/// Safety: `Input` is safe to send between threads.
19unsafe impl<T: Send + Sync> Send for Input<T> {}
20
21/// Represents the options for an input stream.
22#[derive(Debug, Clone)]
23pub struct InputOptions<I: FnMut() -> bool> {
24    /// The buffer size for the input stream.
25    pub buffer_size: usize,
26    /// The dictionary for the input stream.
27    pub dictionary: Dictionary,
28    /// The interrupt callback for the input stream.
29    pub interrupt_callback: Option<I>,
30}
31
32/// Default implementation for `InputOptions`.
33impl Default for InputOptions<fn() -> bool> {
34    fn default() -> Self {
35        Self {
36            buffer_size: DEFAULT_BUFFER_SIZE,
37            dictionary: Dictionary::new(),
38            interrupt_callback: None,
39        }
40    }
41}
42
43impl<T: std::io::Read + Send + Sync> Input<T> {
44    /// Creates a new `Input` instance with default options.
45    pub fn new(input: T) -> Result<Self, FfmpegError> {
46        Self::with_options(input, &mut InputOptions::default())
47    }
48
49    /// Creates a new `Input` instance with custom options.
50    pub fn with_options(input: T, options: &mut InputOptions<impl FnMut() -> bool>) -> Result<Self, FfmpegError> {
51        Self::create_input(
52            Inner::new(
53                input,
54                InnerOptions {
55                    buffer_size: options.buffer_size,
56                    read_fn: Some(read_packet::<T>),
57                    ..Default::default()
58                },
59            )?,
60            None,
61            &mut options.dictionary,
62        )
63    }
64
65    /// Creates a new `Input` instance with seekable options.
66    pub fn seekable(input: T) -> Result<Self, FfmpegError>
67    where
68        T: std::io::Seek,
69    {
70        Self::seekable_with_options(input, InputOptions::default())
71    }
72
73    /// Creates a new `Input` instance with seekable options.
74    pub fn seekable_with_options(input: T, mut options: InputOptions<impl FnMut() -> bool>) -> Result<Self, FfmpegError>
75    where
76        T: std::io::Seek,
77    {
78        Self::create_input(
79            Inner::new(
80                input,
81                InnerOptions {
82                    buffer_size: options.buffer_size,
83                    read_fn: Some(read_packet::<T>),
84                    seek_fn: Some(seek::<T>),
85                    ..Default::default()
86                },
87            )?,
88            None,
89            &mut options.dictionary,
90        )
91    }
92}
93
94impl<T: Send + Sync> Input<T> {
95    /// Returns a constant pointer to the input stream.
96    pub const fn as_ptr(&self) -> *const AVFormatContext {
97        self.inner.inner_ref().context.as_ptr()
98    }
99
100    /// Returns a mutable pointer to the input stream.
101    pub const fn as_mut_ptr(&mut self) -> *mut AVFormatContext {
102        self.inner.inner_mut().context.as_mut_ptr()
103    }
104
105    /// Returns the streams of the input stream.
106    pub const fn streams(&self) -> Const<'_, Streams<'_>> {
107        // Safety: See the documentation of `Streams::new`.
108        // We upcast the pointer to be mut because the function signature requires it.
109        // However we do not mutate the pointer as its returned as a `Const<Streams>` which
110        // restricts the mutability of the streams to be const.
111        unsafe { Const::new(Streams::new(self.inner.inner_ref().context.as_ptr() as *mut _)) }
112    }
113
114    /// Returns a mutable reference to the streams of the input stream.
115    pub const fn streams_mut(&mut self) -> Streams<'_> {
116        // Safety: See the documentation of `Streams::new`.
117        unsafe { Streams::new(self.inner.inner_mut().context.as_mut_ptr()) }
118    }
119
120    /// Returns the packets of the input stream.
121    pub const fn packets(&mut self) -> Packets<'_> {
122        // Safety: See the documentation of `Packets::new`.
123        unsafe { Packets::new(self.inner.inner_mut().context.as_mut_ptr()) }
124    }
125
126    /// Receives a packet from the input stream.
127    pub fn receive_packet(&mut self) -> Result<Option<Packet>, FfmpegError> {
128        self.packets().receive()
129    }
130
131    fn create_input(mut inner: Inner<T>, path: Option<&CStr>, dictionary: &mut Dictionary) -> Result<Self, FfmpegError> {
132        // Safety: avformat_open_input is safe to call
133        FfmpegErrorCode(unsafe {
134            avformat_open_input(
135                inner.context.as_mut(),
136                path.map(|p| p.as_ptr()).unwrap_or(std::ptr::null()),
137                std::ptr::null(),
138                dictionary.as_mut_ptr_ref(),
139            )
140        })
141        .result()?;
142
143        if inner.context.as_ptr().is_null() {
144            return Err(FfmpegError::Alloc);
145        }
146
147        let mut inner = SmartObject::new(inner, |inner| {
148            // Safety: The pointer is valid. We own this resource so we need to free it
149            unsafe { avformat_close_input(inner.context.as_mut()) };
150        });
151
152        // We now own the context and this is freed when the object is dropped
153        inner.context.set_destructor(|_| {});
154
155        // Safety: avformat_find_stream_info is safe to call
156        FfmpegErrorCode(unsafe { avformat_find_stream_info(inner.context.as_mut_ptr(), std::ptr::null_mut()) }).result()?;
157
158        Ok(Self { inner })
159    }
160}
161
162impl Input<()> {
163    /// Opens an input stream from a file path.
164    pub fn open(path: impl AsRef<Path>) -> Result<Self, FfmpegError> {
165        // We immediately create an input and setup the inner, before using it.
166        // Safety: When we pass this inner to `create_input` with a valid path, the inner will be initialized by ffmpeg using the path.
167        let inner = unsafe { Inner::empty() };
168
169        let path = path.as_ref().to_string_lossy();
170
171        Self::create_input(
172            inner,
173            Some(&std::ffi::CString::new(path.as_bytes()).unwrap()),
174            &mut Dictionary::new(),
175        )
176    }
177}
178
179#[cfg(test)]
180#[cfg_attr(all(test, coverage_nightly), coverage(off))]
181mod tests {
182    use std::io::Cursor;
183
184    use insta::Settings;
185
186    use super::{DEFAULT_BUFFER_SIZE, FfmpegError, Input, InputOptions};
187    use crate::file_path;
188
189    fn configure_insta_filters(settings: &mut Settings) {
190        settings.add_filter(r"0x0000000000000000", "[NULL_POINTER]");
191        settings.add_filter(r"0x[0-9a-f]{16}", "[NON_NULL_POINTER]");
192    }
193
194    #[test]
195    fn test_input_options_default() {
196        let default_options = InputOptions::default();
197
198        assert_eq!(default_options.buffer_size, DEFAULT_BUFFER_SIZE);
199        assert!(default_options.dictionary.is_empty());
200        assert!(default_options.interrupt_callback.is_none());
201    }
202
203    #[test]
204    fn test_open_valid_file() {
205        let valid_file_path = file_path("avc_aac_large.mp4");
206        assert!(valid_file_path.exists(), "Test file does not exist");
207
208        let result = Input::open(valid_file_path);
209        assert!(result.is_ok(), "Expected success but got error");
210    }
211
212    #[test]
213    fn test_open_invalid_path() {
214        let invalid_path = "invalid_file.mp4";
215        let result = Input::open(invalid_path);
216        assert!(result.is_err(), "Expected an error for invalid path");
217        if let Err(err) = result {
218            match err {
219                FfmpegError::Code(_) => (),
220                _ => panic!("Unexpected error type: {err:?}"),
221            }
222        }
223    }
224
225    #[test]
226    fn test_new_with_default_options() {
227        let valid_media_data = std::fs::read(file_path("avc_aac_large.mp4")).unwrap();
228        let data = Cursor::new(valid_media_data);
229        let result = Input::new(data);
230
231        if let Err(e) = &result {
232            eprintln!("Error encountered: {e:?}");
233        }
234
235        assert!(result.is_ok(), "Expected success but got error");
236    }
237
238    #[test]
239    fn test_seekable_with_valid_input() {
240        let valid_media_data = std::fs::read(file_path("avc_aac_large.mp4")).unwrap();
241        let data = Cursor::new(valid_media_data);
242        let result = Input::seekable(data);
243
244        if let Err(e) = &result {
245            eprintln!("Error encountered: {e:?}");
246        }
247
248        assert!(result.is_ok(), "Expected success but got error");
249    }
250
251    #[test]
252    fn test_as_ptr() {
253        let input = Input::open(file_path("avc_aac_large.mp4")).expect("Failed to open valid file");
254
255        let ptr = input.as_ptr();
256        assert!(!ptr.is_null(), "Expected non-null pointer");
257    }
258
259    #[test]
260    fn test_as_mut_ptr() {
261        let mut input = Input::open(file_path("avc_aac_large.mp4")).expect("Failed to open valid file");
262
263        let ptr = input.as_mut_ptr();
264        assert!(!ptr.is_null(), "Expected non-null mutable pointer");
265    }
266
267    #[test]
268    fn test_streams() {
269        let input = Input::open(file_path("avc_aac_large.mp4")).expect("Failed to open valid file");
270        let streams = input.streams();
271
272        assert!(!streams.is_empty(), "Expected at least one stream");
273
274        let mut settings = Settings::new();
275        configure_insta_filters(&mut settings);
276
277        settings.bind(|| {
278            insta::assert_debug_snapshot!(streams, @r#"
279            Streams {
280                input: [NON_NULL_POINTER],
281                streams: [
282                    Stream {
283                        index: 0,
284                        id: 1,
285                        time_base: Rational {
286                            numerator: 1,
287                            denominator: 15360,
288                        },
289                        start_time: Some(
290                            0,
291                        ),
292                        duration: Some(
293                            16384,
294                        ),
295                        nb_frames: Some(
296                            64,
297                        ),
298                        disposition: 1,
299                        discard: AVDiscard::Default,
300                        sample_aspect_ratio: Rational {
301                            numerator: 1,
302                            denominator: 1,
303                        },
304                        metadata: {
305                            "language": "und",
306                            "handler_name": "GPAC ISO Video Handler",
307                            "vendor_id": "[0][0][0][0]",
308                            "encoder": "Lavc60.9.100 libx264",
309                        },
310                        avg_frame_rate: Rational {
311                            numerator: 60,
312                            denominator: 1,
313                        },
314                        r_frame_rate: Rational {
315                            numerator: 60,
316                            denominator: 1,
317                        },
318                    },
319                    Stream {
320                        index: 1,
321                        id: 2,
322                        time_base: Rational {
323                            numerator: 1,
324                            denominator: 48000,
325                        },
326                        start_time: Some(
327                            0,
328                        ),
329                        duration: Some(
330                            48096,
331                        ),
332                        nb_frames: Some(
333                            48,
334                        ),
335                        disposition: 1,
336                        discard: AVDiscard::Default,
337                        sample_aspect_ratio: Rational {
338                            numerator: 0,
339                            denominator: 1,
340                        },
341                        metadata: {
342                            "language": "und",
343                            "handler_name": "GPAC ISO Audio Handler",
344                            "vendor_id": "[0][0][0][0]",
345                        },
346                        avg_frame_rate: Rational {
347                            numerator: 0,
348                            denominator: 1,
349                        },
350                        r_frame_rate: Rational {
351                            numerator: 0,
352                            denominator: 1,
353                        },
354                    },
355                ],
356            }
357            "#);
358        });
359    }
360
361    #[test]
362    fn test_packets() {
363        let mut input = Input::open(file_path("avc_aac_large.mp4")).expect("Failed to open valid file");
364        let mut packets = input.packets();
365
366        for _ in 0..5 {
367            match packets.next() {
368                Some(Ok(_)) => (),
369                Some(Err(e)) => panic!("Error encountered while reading packets: {e:?}"),
370                None => break,
371            }
372        }
373
374        let mut settings = insta::Settings::new();
375        configure_insta_filters(&mut settings);
376
377        settings.bind(|| {
378            insta::assert_debug_snapshot!(packets, @r"
379            Packets {
380                context: [NON_NULL_POINTER],
381            }
382            ");
383        });
384    }
385
386    #[test]
387    fn test_receive_packet() {
388        let mut input = Input::open(file_path("avc_aac_large.mp4")).expect("Failed to open valid file");
389
390        let mut packets = Vec::new();
391        while let Ok(Some(packet)) = input.receive_packet() {
392            assert!(!packet.data().is_empty(), "Expected a non-empty packet");
393            assert!(packet.stream_index() >= 0, "Expected a valid stream index");
394            packets.push(packet);
395        }
396
397        if packets.is_empty() {
398            panic!("Expected at least one packet but received none");
399        }
400
401        insta::assert_debug_snapshot!(packets);
402    }
403}