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,
3    avformat_seek_file, AVPacket, AVERROR, EAGAIN,
4    AV_PKT_FLAG_CORRUPT, AV_PKT_FLAG_KEY,
5};
6
7use std::iter::FusedIterator;
8
9use crate::core::context::AVFormatContextBox;
10use crate::error::{DemuxingError, OpenInputError, PacketScannerError, Result};
11
12/// Read-only metadata extracted from a single demuxed packet.
13///
14/// `PacketInfo` contains scalar values copied out of an `AVPacket`, so it has no
15/// lifetime ties to the scanner. It is cheap to clone and store.
16#[derive(Debug, Clone)]
17pub struct PacketInfo {
18    stream_index: usize,
19    pts: Option<i64>,
20    dts: Option<i64>,
21    duration: i64,
22    size: usize,
23    pos: i64,
24    is_keyframe: bool,
25    is_corrupt: bool,
26}
27
28impl PacketInfo {
29    /// The index of the stream this packet belongs to.
30    pub fn stream_index(&self) -> usize {
31        self.stream_index
32    }
33
34    /// Presentation timestamp in stream time-base units, if available.
35    pub fn pts(&self) -> Option<i64> {
36        self.pts
37    }
38
39    /// Decompression timestamp in stream time-base units, if available.
40    pub fn dts(&self) -> Option<i64> {
41        self.dts
42    }
43
44    /// Duration of this packet in stream time-base units.
45    pub fn duration(&self) -> i64 {
46        self.duration
47    }
48
49    /// Size of the packet data in bytes.
50    pub fn size(&self) -> usize {
51        self.size
52    }
53
54    /// Byte position of this packet in the input file, or -1 if unknown.
55    pub fn pos(&self) -> i64 {
56        self.pos
57    }
58
59    /// Whether this packet contains a keyframe.
60    pub fn is_keyframe(&self) -> bool {
61        self.is_keyframe
62    }
63
64    /// Whether this packet is flagged as corrupt.
65    pub fn is_corrupt(&self) -> bool {
66        self.is_corrupt
67    }
68}
69
70/// A stateful packet-level scanner for media files.
71///
72/// `PacketScanner` opens a media file (or URL) and iterates over demuxed packets
73/// without decoding. This is useful for inspecting packet metadata such as
74/// timestamps, keyframe flags, sizes, and stream indices.
75///
76/// # Example
77///
78/// ```rust,ignore
79/// use ez_ffmpeg::packet_scanner::PacketScanner;
80///
81/// let mut scanner = PacketScanner::open("test.mp4")?;
82/// for packet in scanner.packets() {
83///     let packet = packet?;
84///     println!(
85///         "stream={} pts={:?} size={} keyframe={}",
86///         packet.stream_index(),
87///         packet.pts(),
88///         packet.size(),
89///         packet.is_keyframe(),
90///     );
91/// }
92/// ```
93pub struct PacketScanner {
94    fmt_ctx_box: AVFormatContextBox,
95    pkt: *mut AVPacket,
96}
97
98// SAFETY: PacketScanner owns its AVFormatContext and AVPacket exclusively.
99// It is moved between threads, never shared. No thread-affine callbacks are registered.
100// This matches the safety reasoning of AVFormatContextBox's own `unsafe impl Send`.
101unsafe impl Send for PacketScanner {}
102
103impl PacketScanner {
104    /// Open a media file or URL for packet scanning.
105    pub fn open(url: impl Into<String>) -> Result<Self> {
106        let fmt_ctx_box = crate::core::stream_info::init_format_context(url)?;
107
108        unsafe {
109            let pkt = av_packet_alloc();
110            if pkt.is_null() {
111                return Err(OpenInputError::OutOfMemory.into());
112            }
113
114            Ok(Self { fmt_ctx_box, pkt })
115        }
116    }
117
118    /// Seek to a timestamp in microseconds.
119    ///
120    /// Seeks to the nearest keyframe before the given timestamp.
121    /// Can be called repeatedly for jump-reading patterns.
122    ///
123    /// On failure you may continue reading or attempt another seek, though
124    /// the exact read position is not guaranteed to be unchanged.
125    pub fn seek(&mut self, timestamp_us: i64) -> Result<()> {
126        unsafe {
127            let ret = avformat_seek_file(
128                self.fmt_ctx_box.fmt_ctx,
129                -1,
130                i64::MIN,
131                timestamp_us,
132                timestamp_us,
133                0,
134            );
135            if ret < 0 {
136                return Err(
137                    PacketScannerError::SeekError(DemuxingError::from(ret)).into()
138                );
139            }
140        }
141        Ok(())
142    }
143
144    /// Read the next packet's info. Returns `None` at EOF.
145    ///
146    /// If the underlying demuxer returns `EAGAIN` (common with network streams),
147    /// this method retries with a 10 ms sleep up to 500 times (~5 seconds).
148    /// After exhausting retries it returns an error.
149    pub fn next_packet(&mut self) -> Result<Option<PacketInfo>> {
150        const MAX_EAGAIN_RETRIES: u32 = 500;
151
152        unsafe {
153            av_packet_unref(self.pkt);
154
155            let mut eagain_retries: u32 = 0;
156            loop {
157                let ret = av_read_frame(self.fmt_ctx_box.fmt_ctx, self.pkt);
158                if ret == AVERROR(EAGAIN) {
159                    eagain_retries += 1;
160                    if eagain_retries > MAX_EAGAIN_RETRIES {
161                        return Err(
162                            PacketScannerError::ReadError(DemuxingError::from(ret)).into()
163                        );
164                    }
165                    std::thread::sleep(std::time::Duration::from_millis(10));
166                    continue;
167                }
168                if ret < 0 {
169                    if ret == ffmpeg_sys_next::AVERROR_EOF {
170                        return Ok(None);
171                    }
172                    return Err(
173                        PacketScannerError::ReadError(DemuxingError::from(ret)).into()
174                    );
175                }
176                break;
177            }
178
179            let pkt = &*self.pkt;
180            let pts = if pkt.pts == ffmpeg_sys_next::AV_NOPTS_VALUE {
181                None
182            } else {
183                Some(pkt.pts)
184            };
185            let dts = if pkt.dts == ffmpeg_sys_next::AV_NOPTS_VALUE {
186                None
187            } else {
188                Some(pkt.dts)
189            };
190
191            Ok(Some(PacketInfo {
192                stream_index: pkt.stream_index.max(0) as usize,
193                pts,
194                dts,
195                duration: pkt.duration,
196                size: pkt.size.max(0) as usize,
197                pos: pkt.pos,
198                is_keyframe: (pkt.flags & AV_PKT_FLAG_KEY) != 0,
199                is_corrupt: (pkt.flags & AV_PKT_FLAG_CORRUPT) != 0,
200            }))
201        }
202    }
203
204    /// Returns an iterator for convenient `for packet in scanner.packets()` usage.
205    ///
206    /// Each call creates a fresh iterator, so you can `seek()` and then call
207    /// `packets()` again to iterate from the new position.
208    ///
209    /// The iterator is fused: once it yields `None` (EOF) or an `Err`, all
210    /// subsequent calls to `next()` return `None`.
211    pub fn packets(&mut self) -> PacketIter<'_> {
212        PacketIter { scanner: self, done: false }
213    }
214}
215
216impl Drop for PacketScanner {
217    fn drop(&mut self) {
218        unsafe {
219            if !self.pkt.is_null() {
220                av_packet_free(&mut self.pkt);
221            }
222        }
223        // AVFormatContextBox handles closing the format context
224    }
225}
226
227/// Iterator wrapper for [`PacketScanner`].
228///
229/// Yields `Result<PacketInfo>` for each packet until EOF or an error occurs.
230/// The iterator is fused: after returning `None` or `Err`, it always returns `None`.
231pub struct PacketIter<'a> {
232    scanner: &'a mut PacketScanner,
233    done: bool,
234}
235
236impl<'a> Iterator for PacketIter<'a> {
237    type Item = Result<PacketInfo>;
238
239    fn next(&mut self) -> Option<Self::Item> {
240        if self.done {
241            return None;
242        }
243        match self.scanner.next_packet() {
244            Ok(Some(info)) => Some(Ok(info)),
245            Ok(None) => {
246                self.done = true;
247                None
248            }
249            Err(e) => {
250                self.done = true;
251                Some(Err(e))
252            }
253        }
254    }
255}
256
257impl<'a> FusedIterator for PacketIter<'a> {}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    #[test]
264    fn test_open_not_found() {
265        let result = PacketScanner::open("not_found.mp4");
266        assert!(result.is_err());
267    }
268
269    #[test]
270    fn test_scan_packets() {
271        let mut scanner = PacketScanner::open("test.mp4").unwrap();
272        let mut count = 0;
273        let mut keyframes = 0;
274        for packet in scanner.packets() {
275            let info = packet.unwrap();
276            count += 1;
277            if info.is_keyframe() {
278                keyframes += 1;
279            }
280        }
281        assert!(count > 0, "expected at least one packet");
282        assert!(keyframes > 0, "expected at least one keyframe");
283        println!("total packets: {}, keyframes: {}", count, keyframes);
284    }
285
286    #[test]
287    fn test_seek_and_read() {
288        let mut scanner = PacketScanner::open("test.mp4").unwrap();
289        // Seek to 1 second (1_000_000 microseconds)
290        scanner.seek(1_000_000).unwrap();
291        let packet = scanner.next_packet().unwrap();
292        assert!(packet.is_some(), "expected a packet after seeking");
293    }
294}