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    /// Payload bytes, captured only when
36    /// [`set_capture_data`](PacketScanner::set_capture_data) enabled it.
37    /// `Arc` keeps the struct cheap to clone either way.
38    data: Option<std::sync::Arc<[u8]>>,
39}
40
41impl PacketInfo {
42    /// The index of the stream this packet belongs to.
43    pub fn stream_index(&self) -> usize {
44        self.stream_index
45    }
46
47    /// Presentation timestamp in stream time-base units, if available.
48    pub fn pts(&self) -> Option<i64> {
49        self.pts
50    }
51
52    /// Decompression timestamp in stream time-base units, if available.
53    pub fn dts(&self) -> Option<i64> {
54        self.dts
55    }
56
57    /// Duration of this packet in stream time-base units.
58    pub fn duration(&self) -> i64 {
59        self.duration
60    }
61
62    /// Size of the packet data in bytes.
63    pub fn size(&self) -> usize {
64        self.size
65    }
66
67    /// Byte position of this packet in the input file, or -1 if unknown.
68    pub fn pos(&self) -> i64 {
69        self.pos
70    }
71
72    /// Whether this packet contains a keyframe.
73    pub fn is_keyframe(&self) -> bool {
74        self.is_keyframe
75    }
76
77    /// Whether this packet is flagged as corrupt.
78    pub fn is_corrupt(&self) -> bool {
79        self.is_corrupt
80    }
81
82    /// Whether this packet belongs to a video stream.
83    pub fn is_video(&self) -> bool {
84        self.is_video
85    }
86
87    /// Whether this packet belongs to an audio stream.
88    pub fn is_audio(&self) -> bool {
89        self.is_audio
90    }
91
92    /// The packet payload, when capture was enabled via
93    /// [`PacketScanner::set_capture_data`] (`None` otherwise). Useful for
94    /// content-level comparisons, e.g. verifying NAL layout of demuxed
95    /// samples.
96    pub fn data(&self) -> Option<&[u8]> {
97        self.data.as_deref()
98    }
99}
100
101/// A stateful packet-level scanner for media files.
102///
103/// `PacketScanner` opens a media file (or URL) and iterates over demuxed packets
104/// without decoding. This is useful for inspecting packet metadata such as
105/// timestamps, keyframe flags, sizes, and stream indices.
106///
107/// # Example
108///
109/// ```rust,ignore
110/// use ez_ffmpeg::packet_scanner::PacketScanner;
111///
112/// let mut scanner = PacketScanner::open("test.mp4")?;
113/// for packet in scanner.packets() {
114///     let packet = packet?;
115///     println!(
116///         "stream={} pts={:?} size={} keyframe={}",
117///         packet.stream_index(),
118///         packet.pts(),
119///         packet.size(),
120///         packet.is_keyframe(),
121///     );
122/// }
123/// ```
124pub struct PacketScanner {
125    fmt_ctx_box: FormatContext,
126    pkt: *mut AVPacket,
127    streams: Vec<StreamInfo>,
128    capture_data: bool,
129}
130
131// SAFETY: PacketScanner owns its AVFormatContext and AVPacket exclusively.
132// It is moved between threads, never shared. No thread-affine callbacks are registered.
133// This is safe only because `open()` does not expose custom AVIO or interrupt callbacks.
134// If custom callbacks are added in the future, this impl must be re-evaluated.
135// This matches the safety reasoning of FormatContext's own `unsafe impl Send`.
136unsafe impl Send for PacketScanner {}
137
138impl PacketScanner {
139    /// Open a media file or URL for packet scanning.
140    ///
141    /// Stream information is extracted and cached at open time so that
142    /// [`streams`](Self::streams), [`video_stream`](Self::video_stream),
143    /// [`audio_stream`](Self::audio_stream), and
144    /// [`stream_for_packet`](Self::stream_for_packet) are available
145    /// immediately without additional I/O.
146    pub fn open(url: impl Into<String>) -> Result<Self> {
147        let fmt_ctx_box = crate::core::stream_info::init_format_context(url)?;
148        // SAFETY: fmt_ctx_box is fully initialized by init_format_context.
149        let streams = unsafe { crate::core::stream_info::extract_stream_infos(&fmt_ctx_box)? };
150
151        // SAFETY: av_packet_alloc returns a valid packet or null.
152        // Null is checked immediately; the packet is freed in Drop.
153        unsafe {
154            let pkt = av_packet_alloc();
155            if pkt.is_null() {
156                return Err(OpenInputError::OutOfMemory.into());
157            }
158
159            Ok(Self {
160                fmt_ctx_box,
161                pkt,
162                streams,
163                capture_data: false,
164            })
165        }
166    }
167
168    /// Enables (or disables) payload capture: when on, each returned
169    /// [`PacketInfo`] carries a copy of the packet bytes in
170    /// [`PacketInfo::data`]. Off by default — scanning stays copy-free.
171    pub fn set_capture_data(&mut self, capture: bool) {
172        self.capture_data = capture;
173    }
174
175    /// Seek to a timestamp in microseconds.
176    ///
177    /// Seeks to the nearest keyframe before the given timestamp.
178    /// Can be called repeatedly for jump-reading patterns.
179    ///
180    /// On failure you may continue reading or attempt another seek, though
181    /// the exact read position is not guaranteed to be unchanged.
182    pub fn seek(&mut self, timestamp_us: i64) -> Result<()> {
183        // SAFETY: fmt_ctx is valid for the lifetime of self. avformat_seek_file
184        // accepts any timestamp and returns a negative value on failure.
185        unsafe {
186            let ret = avformat_seek_file(
187                self.fmt_ctx_box.as_ptr(),
188                -1,
189                i64::MIN,
190                timestamp_us,
191                timestamp_us,
192                0,
193            );
194            if ret < 0 {
195                return Err(PacketScannerError::SeekError(DemuxingError::from(ret)).into());
196            }
197        }
198        Ok(())
199    }
200
201    /// Read the next packet's info. Returns `None` at EOF.
202    ///
203    /// If the underlying demuxer returns `EAGAIN` (common with network streams),
204    /// this method retries with a 10 ms sleep up to 500 times (~5 seconds).
205    /// After exhausting retries it returns an error.
206    pub fn next_packet(&mut self) -> Result<Option<PacketInfo>> {
207        const MAX_EAGAIN_RETRIES: u32 = 500;
208
209        // SAFETY: self.pkt is a valid, non-null AVPacket allocated in open().
210        // av_packet_unref resets the packet for reuse; av_read_frame fills it.
211        // We read only scalar fields from the filled packet.
212        unsafe {
213            av_packet_unref(self.pkt);
214
215            let mut eagain_retries: u32 = 0;
216            loop {
217                let ret = av_read_frame(self.fmt_ctx_box.as_ptr(), self.pkt);
218                if ret == AVERROR(EAGAIN) {
219                    eagain_retries += 1;
220                    if eagain_retries > MAX_EAGAIN_RETRIES {
221                        return Err(PacketScannerError::ReadError(DemuxingError::from(ret)).into());
222                    }
223                    std::thread::sleep(std::time::Duration::from_millis(10));
224                    continue;
225                }
226                if ret < 0 {
227                    if ret == ffmpeg_sys_next::AVERROR_EOF {
228                        return Ok(None);
229                    }
230                    return Err(PacketScannerError::ReadError(DemuxingError::from(ret)).into());
231                }
232                break;
233            }
234
235            let pkt = &*self.pkt;
236            let pts = if pkt.pts == ffmpeg_sys_next::AV_NOPTS_VALUE {
237                None
238            } else {
239                Some(pkt.pts)
240            };
241            let dts = if pkt.dts == ffmpeg_sys_next::AV_NOPTS_VALUE {
242                None
243            } else {
244                Some(pkt.dts)
245            };
246
247            // FFmpeg guarantees via av_assert0 in handle_new_packet() (demux.c:571)
248            // that stream_index is in [0, nb_streams). The .max(0) and .unwrap_or()
249            // are purely defensive and not expected to trigger in practice.
250            let stream_index = pkt.stream_index.max(0) as usize;
251            let (is_video, is_audio) = self
252                .streams
253                .get(stream_index)
254                .map(|s| (s.is_video(), s.is_audio()))
255                .unwrap_or((false, false));
256
257            Ok(Some(PacketInfo {
258                stream_index,
259                pts,
260                dts,
261                duration: pkt.duration,
262                // FFmpeg does not document negative size; clamp to 0 defensively.
263                size: {
264                    debug_assert!(pkt.size >= 0, "negative pkt.size: {}", pkt.size);
265                    pkt.size.max(0) as usize
266                },
267                pos: pkt.pos,
268                is_keyframe: (pkt.flags & AV_PKT_FLAG_KEY) != 0,
269                is_corrupt: (pkt.flags & AV_PKT_FLAG_CORRUPT) != 0,
270                is_video,
271                is_audio,
272                data: (self.capture_data && !pkt.data.is_null() && pkt.size > 0).then(|| {
273                    std::sync::Arc::from(std::slice::from_raw_parts(
274                        pkt.data,
275                        pkt.size as usize,
276                    ))
277                }),
278            }))
279        }
280    }
281
282    /// Returns all stream information cached at open time.
283    pub fn streams(&self) -> &[StreamInfo] {
284        &self.streams
285    }
286
287    /// Returns the first video stream, if any.
288    pub fn video_stream(&self) -> Option<&StreamInfo> {
289        self.streams.iter().find(|s| s.is_video())
290    }
291
292    /// Returns the first audio stream, if any.
293    pub fn audio_stream(&self) -> Option<&StreamInfo> {
294        self.streams.iter().find(|s| s.is_audio())
295    }
296
297    /// Returns the stream information for the given packet, if the stream
298    /// index is within bounds.
299    pub fn stream_for_packet(&self, packet: &PacketInfo) -> Option<&StreamInfo> {
300        self.streams.get(packet.stream_index())
301    }
302
303    /// Returns an iterator for convenient `for packet in scanner.packets()` usage.
304    ///
305    /// Each call creates a fresh iterator, so you can `seek()` and then call
306    /// `packets()` again to iterate from the new position.
307    ///
308    /// The iterator is fused: once it yields `None` (EOF) or an `Err`, all
309    /// subsequent calls to `next()` return `None`.
310    pub fn packets(&mut self) -> PacketIter<'_> {
311        PacketIter {
312            scanner: self,
313            done: false,
314        }
315    }
316}
317
318impl Drop for PacketScanner {
319    fn drop(&mut self) {
320        // SAFETY: pkt was allocated by av_packet_alloc in open().
321        // av_packet_free handles null gracefully, but we check anyway.
322        unsafe {
323            if !self.pkt.is_null() {
324                av_packet_free(&mut self.pkt);
325            }
326        }
327        // FormatContext handles closing the format context
328    }
329}
330
331/// Iterator wrapper for [`PacketScanner`].
332///
333/// Yields `Result<PacketInfo>` for each packet until EOF or an error occurs.
334/// The iterator is fused: after returning `None` or `Err`, it always returns `None`.
335pub struct PacketIter<'a> {
336    scanner: &'a mut PacketScanner,
337    done: bool,
338}
339
340impl<'a> Iterator for PacketIter<'a> {
341    type Item = Result<PacketInfo>;
342
343    fn next(&mut self) -> Option<Self::Item> {
344        if self.done {
345            return None;
346        }
347        match self.scanner.next_packet() {
348            Ok(Some(info)) => Some(Ok(info)),
349            Ok(None) => {
350                self.done = true;
351                None
352            }
353            Err(e) => {
354                self.done = true;
355                Some(Err(e))
356            }
357        }
358    }
359}
360
361impl<'a> FusedIterator for PacketIter<'a> {}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366
367    #[test]
368    fn test_open_not_found() {
369        let result = PacketScanner::open("not_found.mp4");
370        assert!(result.is_err());
371    }
372
373    #[test]
374    fn test_scan_packets() {
375        let mut scanner = PacketScanner::open("test.mp4").unwrap();
376        let mut count = 0;
377        let mut keyframes = 0;
378        for packet in scanner.packets() {
379            let info = packet.unwrap();
380            count += 1;
381            if info.is_keyframe() {
382                keyframes += 1;
383            }
384        }
385        assert!(count > 0, "expected at least one packet");
386        assert!(keyframes > 0, "expected at least one keyframe");
387        println!("total packets: {}, keyframes: {}", count, keyframes);
388    }
389
390    #[test]
391    fn capture_data_defaults_off_and_copies_payloads_when_enabled() {
392        // Default: scanning stays copy-free — no packet carries a payload.
393        // Record the sizes as the capture-independent reference.
394        let mut scanner = PacketScanner::open("test.mp4").unwrap();
395        let mut sizes = Vec::new();
396        for packet in scanner.packets() {
397            let info = packet.unwrap();
398            assert!(
399                info.data().is_none(),
400                "a capture-off scan must not copy payloads"
401            );
402            sizes.push(info.size());
403        }
404        assert!(!sizes.is_empty(), "expected packets in the fixture");
405
406        // Re-scan with capture enabled, twice: every packet carries exactly
407        // `size` bytes, and two independent demux passes over the same file
408        // yield identical bytes — the capture is the packet's payload, not a
409        // transient buffer snapshot.
410        fn capture_scan() -> Vec<PacketInfo> {
411            let mut scanner = PacketScanner::open("test.mp4").unwrap();
412            scanner.set_capture_data(true);
413            scanner.packets().map(|p| p.unwrap()).collect()
414        }
415        let first = capture_scan();
416        let second = capture_scan();
417        assert_eq!(first.len(), sizes.len(), "both scans see the same packets");
418        assert_eq!(
419            second.len(),
420            first.len(),
421            "repeated capture scans see the same packets"
422        );
423        for (i, (a, b)) in first.iter().zip(&second).enumerate() {
424            let data_a = a
425                .data()
426                .expect("a capture-on scan must carry the payload");
427            assert!(!data_a.is_empty(), "packet {i}: captured payload is empty");
428            assert_eq!(
429                data_a.len(),
430                a.size(),
431                "packet {i}: captured length must equal the packet size"
432            );
433            assert_eq!(
434                a.size(),
435                sizes[i],
436                "packet {i}: size differs from the capture-off scan"
437            );
438            let data_b = b
439                .data()
440                .expect("a capture-on rescan must carry the payload");
441            assert_eq!(
442                data_b.len(),
443                b.size(),
444                "packet {i}: rescan captured length must equal the packet size"
445            );
446            assert!(
447                data_a == data_b,
448                "packet {i}: payload bytes differ across two scans"
449            );
450        }
451    }
452
453    #[test]
454    fn test_seek_and_read() {
455        let mut scanner = PacketScanner::open("test.mp4").unwrap();
456        // Seek to 1 second (1_000_000 microseconds)
457        scanner.seek(1_000_000).unwrap();
458        let packet = scanner.next_packet().unwrap();
459        assert!(packet.is_some(), "expected a packet after seeking");
460    }
461
462    #[test]
463    fn test_streams() {
464        let scanner = PacketScanner::open("test.mp4").unwrap();
465        let streams = scanner.streams();
466        assert!(!streams.is_empty(), "expected at least one stream");
467        assert_eq!(
468            streams.len(),
469            2,
470            "test.mp4 should have 2 streams (video + audio)"
471        );
472    }
473
474    #[test]
475    fn test_video_stream() {
476        let scanner = PacketScanner::open("test.mp4").unwrap();
477        let video = scanner.video_stream();
478        assert!(video.is_some(), "expected a video stream");
479        assert!(video.unwrap().is_video());
480    }
481
482    #[test]
483    fn test_audio_stream() {
484        let scanner = PacketScanner::open("test.mp4").unwrap();
485        let audio = scanner.audio_stream();
486        assert!(audio.is_some(), "expected an audio stream");
487        assert!(audio.unwrap().is_audio());
488    }
489
490    #[test]
491    fn test_stream_for_packet() {
492        let mut scanner = PacketScanner::open("test.mp4").unwrap();
493        let packet = scanner.next_packet().unwrap();
494        assert!(packet.is_some(), "expected at least one packet");
495        let info = packet.unwrap();
496        let stream = scanner.stream_for_packet(&info);
497        assert!(
498            stream.is_some(),
499            "stream_for_packet should return Some for valid packet"
500        );
501    }
502}