Skip to main content

ez_ffmpeg/core/
packet_scanner.rs

1use ffmpeg_sys_next::{
2    av_packet_alloc, av_packet_free, av_packet_unref, av_read_frame, avformat_seek_file, AVPacket,
3    AVERROR, AV_PKT_FLAG_CORRUPT, AV_PKT_FLAG_KEY, EAGAIN,
4};
5
6use std::iter::FusedIterator;
7
8use crate::core::stream_info::StreamInfo;
9use crate::error::{DemuxingError, OpenInputError, PacketScannerError, Result};
10use crate::raw::FormatContext;
11
12/// Read-only metadata extracted from a single demuxed packet.
13///
14/// `PacketInfo` contains scalar values copied out of an `AVPacket` together with
15/// stream-type flags looked up at read time, so it has no lifetime ties to the
16/// scanner. It is cheap to clone and store.
17///
18/// # Defensive fields
19///
20/// `stream_index` and `size` are clamped to non-negative values before storage.
21/// FFmpeg's internal asserts guarantee valid ranges in practice, so the clamping
22/// is purely defensive and not expected to trigger.
23#[derive(Debug, Clone)]
24pub struct PacketInfo {
25    stream_index: usize,
26    pts: Option<i64>,
27    dts: Option<i64>,
28    duration: i64,
29    size: usize,
30    pos: i64,
31    is_keyframe: bool,
32    is_corrupt: bool,
33    is_video: bool,
34    is_audio: bool,
35}
36
37impl PacketInfo {
38    /// The index of the stream this packet belongs to.
39    pub fn stream_index(&self) -> usize {
40        self.stream_index
41    }
42
43    /// Presentation timestamp in stream time-base units, if available.
44    pub fn pts(&self) -> Option<i64> {
45        self.pts
46    }
47
48    /// Decompression timestamp in stream time-base units, if available.
49    pub fn dts(&self) -> Option<i64> {
50        self.dts
51    }
52
53    /// Duration of this packet in stream time-base units.
54    pub fn duration(&self) -> i64 {
55        self.duration
56    }
57
58    /// Size of the packet data in bytes.
59    pub fn size(&self) -> usize {
60        self.size
61    }
62
63    /// Byte position of this packet in the input file, or -1 if unknown.
64    pub fn pos(&self) -> i64 {
65        self.pos
66    }
67
68    /// Whether this packet contains a keyframe.
69    pub fn is_keyframe(&self) -> bool {
70        self.is_keyframe
71    }
72
73    /// Whether this packet is flagged as corrupt.
74    pub fn is_corrupt(&self) -> bool {
75        self.is_corrupt
76    }
77
78    /// Whether this packet belongs to a video stream.
79    pub fn is_video(&self) -> bool {
80        self.is_video
81    }
82
83    /// Whether this packet belongs to an audio stream.
84    pub fn is_audio(&self) -> bool {
85        self.is_audio
86    }
87}
88
89/// A stateful packet-level scanner for media files.
90///
91/// `PacketScanner` opens a media file (or URL) and iterates over demuxed packets
92/// without decoding. This is useful for inspecting packet metadata such as
93/// timestamps, keyframe flags, sizes, and stream indices.
94///
95/// # Example
96///
97/// ```rust,ignore
98/// use ez_ffmpeg::packet_scanner::PacketScanner;
99///
100/// let mut scanner = PacketScanner::open("test.mp4")?;
101/// for packet in scanner.packets() {
102///     let packet = packet?;
103///     println!(
104///         "stream={} pts={:?} size={} keyframe={}",
105///         packet.stream_index(),
106///         packet.pts(),
107///         packet.size(),
108///         packet.is_keyframe(),
109///     );
110/// }
111/// ```
112pub struct PacketScanner {
113    fmt_ctx_box: FormatContext,
114    pkt: *mut AVPacket,
115    streams: Vec<StreamInfo>,
116}
117
118// SAFETY: PacketScanner owns its AVFormatContext and AVPacket exclusively.
119// It is moved between threads, never shared. No thread-affine callbacks are registered.
120// This is safe only because `open()` does not expose custom AVIO or interrupt callbacks.
121// If custom callbacks are added in the future, this impl must be re-evaluated.
122// This matches the safety reasoning of FormatContext's own `unsafe impl Send`.
123unsafe impl Send for PacketScanner {}
124
125impl PacketScanner {
126    /// Open a media file or URL for packet scanning.
127    ///
128    /// Stream information is extracted and cached at open time so that
129    /// [`streams`](Self::streams), [`video_stream`](Self::video_stream),
130    /// [`audio_stream`](Self::audio_stream), and
131    /// [`stream_for_packet`](Self::stream_for_packet) are available
132    /// immediately without additional I/O.
133    pub fn open(url: impl Into<String>) -> Result<Self> {
134        let fmt_ctx_box = crate::core::stream_info::init_format_context(url)?;
135        // SAFETY: fmt_ctx_box is fully initialized by init_format_context.
136        let streams = unsafe { crate::core::stream_info::extract_stream_infos(&fmt_ctx_box)? };
137
138        // SAFETY: av_packet_alloc returns a valid packet or null.
139        // Null is checked immediately; the packet is freed in Drop.
140        unsafe {
141            let pkt = av_packet_alloc();
142            if pkt.is_null() {
143                return Err(OpenInputError::OutOfMemory.into());
144            }
145
146            Ok(Self {
147                fmt_ctx_box,
148                pkt,
149                streams,
150            })
151        }
152    }
153
154    /// Seek to a timestamp in microseconds.
155    ///
156    /// Seeks to the nearest keyframe before the given timestamp.
157    /// Can be called repeatedly for jump-reading patterns.
158    ///
159    /// On failure you may continue reading or attempt another seek, though
160    /// the exact read position is not guaranteed to be unchanged.
161    pub fn seek(&mut self, timestamp_us: i64) -> Result<()> {
162        // SAFETY: fmt_ctx is valid for the lifetime of self. avformat_seek_file
163        // accepts any timestamp and returns a negative value on failure.
164        unsafe {
165            let ret = avformat_seek_file(
166                self.fmt_ctx_box.as_ptr(),
167                -1,
168                i64::MIN,
169                timestamp_us,
170                timestamp_us,
171                0,
172            );
173            if ret < 0 {
174                return Err(PacketScannerError::SeekError(DemuxingError::from(ret)).into());
175            }
176        }
177        Ok(())
178    }
179
180    /// Read the next packet's info. Returns `None` at EOF.
181    ///
182    /// If the underlying demuxer returns `EAGAIN` (common with network streams),
183    /// this method retries with a 10 ms sleep up to 500 times (~5 seconds).
184    /// After exhausting retries it returns an error.
185    pub fn next_packet(&mut self) -> Result<Option<PacketInfo>> {
186        const MAX_EAGAIN_RETRIES: u32 = 500;
187
188        // SAFETY: self.pkt is a valid, non-null AVPacket allocated in open().
189        // av_packet_unref resets the packet for reuse; av_read_frame fills it.
190        // We read only scalar fields from the filled packet.
191        unsafe {
192            av_packet_unref(self.pkt);
193
194            let mut eagain_retries: u32 = 0;
195            loop {
196                let ret = av_read_frame(self.fmt_ctx_box.as_ptr(), self.pkt);
197                if ret == AVERROR(EAGAIN) {
198                    eagain_retries += 1;
199                    if eagain_retries > MAX_EAGAIN_RETRIES {
200                        return Err(PacketScannerError::ReadError(DemuxingError::from(ret)).into());
201                    }
202                    std::thread::sleep(std::time::Duration::from_millis(10));
203                    continue;
204                }
205                if ret < 0 {
206                    if ret == ffmpeg_sys_next::AVERROR_EOF {
207                        return Ok(None);
208                    }
209                    return Err(PacketScannerError::ReadError(DemuxingError::from(ret)).into());
210                }
211                break;
212            }
213
214            let pkt = &*self.pkt;
215            let pts = if pkt.pts == ffmpeg_sys_next::AV_NOPTS_VALUE {
216                None
217            } else {
218                Some(pkt.pts)
219            };
220            let dts = if pkt.dts == ffmpeg_sys_next::AV_NOPTS_VALUE {
221                None
222            } else {
223                Some(pkt.dts)
224            };
225
226            // FFmpeg guarantees via av_assert0 in handle_new_packet() (demux.c:571)
227            // that stream_index is in [0, nb_streams). The .max(0) and .unwrap_or()
228            // are purely defensive and not expected to trigger in practice.
229            let stream_index = pkt.stream_index.max(0) as usize;
230            let (is_video, is_audio) = self
231                .streams
232                .get(stream_index)
233                .map(|s| (s.is_video(), s.is_audio()))
234                .unwrap_or((false, false));
235
236            Ok(Some(PacketInfo {
237                stream_index,
238                pts,
239                dts,
240                duration: pkt.duration,
241                // FFmpeg does not document negative size; clamp to 0 defensively.
242                size: {
243                    debug_assert!(pkt.size >= 0, "negative pkt.size: {}", pkt.size);
244                    pkt.size.max(0) as usize
245                },
246                pos: pkt.pos,
247                is_keyframe: (pkt.flags & AV_PKT_FLAG_KEY) != 0,
248                is_corrupt: (pkt.flags & AV_PKT_FLAG_CORRUPT) != 0,
249                is_video,
250                is_audio,
251            }))
252        }
253    }
254
255    /// Returns all stream information cached at open time.
256    pub fn streams(&self) -> &[StreamInfo] {
257        &self.streams
258    }
259
260    /// Returns the first video stream, if any.
261    pub fn video_stream(&self) -> Option<&StreamInfo> {
262        self.streams.iter().find(|s| s.is_video())
263    }
264
265    /// Returns the first audio stream, if any.
266    pub fn audio_stream(&self) -> Option<&StreamInfo> {
267        self.streams.iter().find(|s| s.is_audio())
268    }
269
270    /// Returns the stream information for the given packet, if the stream
271    /// index is within bounds.
272    pub fn stream_for_packet(&self, packet: &PacketInfo) -> Option<&StreamInfo> {
273        self.streams.get(packet.stream_index())
274    }
275
276    /// Returns an iterator for convenient `for packet in scanner.packets()` usage.
277    ///
278    /// Each call creates a fresh iterator, so you can `seek()` and then call
279    /// `packets()` again to iterate from the new position.
280    ///
281    /// The iterator is fused: once it yields `None` (EOF) or an `Err`, all
282    /// subsequent calls to `next()` return `None`.
283    pub fn packets(&mut self) -> PacketIter<'_> {
284        PacketIter {
285            scanner: self,
286            done: false,
287        }
288    }
289}
290
291impl Drop for PacketScanner {
292    fn drop(&mut self) {
293        // SAFETY: pkt was allocated by av_packet_alloc in open().
294        // av_packet_free handles null gracefully, but we check anyway.
295        unsafe {
296            if !self.pkt.is_null() {
297                av_packet_free(&mut self.pkt);
298            }
299        }
300        // FormatContext handles closing the format context
301    }
302}
303
304/// Iterator wrapper for [`PacketScanner`].
305///
306/// Yields `Result<PacketInfo>` for each packet until EOF or an error occurs.
307/// The iterator is fused: after returning `None` or `Err`, it always returns `None`.
308pub struct PacketIter<'a> {
309    scanner: &'a mut PacketScanner,
310    done: bool,
311}
312
313impl<'a> Iterator for PacketIter<'a> {
314    type Item = Result<PacketInfo>;
315
316    fn next(&mut self) -> Option<Self::Item> {
317        if self.done {
318            return None;
319        }
320        match self.scanner.next_packet() {
321            Ok(Some(info)) => Some(Ok(info)),
322            Ok(None) => {
323                self.done = true;
324                None
325            }
326            Err(e) => {
327                self.done = true;
328                Some(Err(e))
329            }
330        }
331    }
332}
333
334impl<'a> FusedIterator for PacketIter<'a> {}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339
340    #[test]
341    fn test_open_not_found() {
342        let result = PacketScanner::open("not_found.mp4");
343        assert!(result.is_err());
344    }
345
346    #[test]
347    fn test_scan_packets() {
348        let mut scanner = PacketScanner::open("test.mp4").unwrap();
349        let mut count = 0;
350        let mut keyframes = 0;
351        for packet in scanner.packets() {
352            let info = packet.unwrap();
353            count += 1;
354            if info.is_keyframe() {
355                keyframes += 1;
356            }
357        }
358        assert!(count > 0, "expected at least one packet");
359        assert!(keyframes > 0, "expected at least one keyframe");
360        println!("total packets: {}, keyframes: {}", count, keyframes);
361    }
362
363    #[test]
364    fn test_seek_and_read() {
365        let mut scanner = PacketScanner::open("test.mp4").unwrap();
366        // Seek to 1 second (1_000_000 microseconds)
367        scanner.seek(1_000_000).unwrap();
368        let packet = scanner.next_packet().unwrap();
369        assert!(packet.is_some(), "expected a packet after seeking");
370    }
371
372    #[test]
373    fn test_streams() {
374        let scanner = PacketScanner::open("test.mp4").unwrap();
375        let streams = scanner.streams();
376        assert!(!streams.is_empty(), "expected at least one stream");
377        assert_eq!(
378            streams.len(),
379            2,
380            "test.mp4 should have 2 streams (video + audio)"
381        );
382    }
383
384    #[test]
385    fn test_video_stream() {
386        let scanner = PacketScanner::open("test.mp4").unwrap();
387        let video = scanner.video_stream();
388        assert!(video.is_some(), "expected a video stream");
389        assert!(video.unwrap().is_video());
390    }
391
392    #[test]
393    fn test_audio_stream() {
394        let scanner = PacketScanner::open("test.mp4").unwrap();
395        let audio = scanner.audio_stream();
396        assert!(audio.is_some(), "expected an audio stream");
397        assert!(audio.unwrap().is_audio());
398    }
399
400    #[test]
401    fn test_stream_for_packet() {
402        let mut scanner = PacketScanner::open("test.mp4").unwrap();
403        let packet = scanner.next_packet().unwrap();
404        assert!(packet.is_some(), "expected at least one packet");
405        let info = packet.unwrap();
406        let stream = scanner.stream_for_packet(&info);
407        assert!(
408            stream.is_some(),
409            "stream_for_packet should return Some for valid packet"
410        );
411    }
412}