Skip to main content

ff_decode/analysis/
keyframe_enumerator.rs

1//! Keyframe timestamp enumeration.
2
3#![allow(unsafe_code)]
4
5use std::path::{Path, PathBuf};
6use std::time::Duration;
7
8use crate::DecodeError;
9
10/// Enumerates the timestamps of all keyframes in a video stream.
11///
12/// Reads only packet headers — **no decoding is performed** — making this
13/// significantly faster than frame-by-frame decoding.  By default the first
14/// video stream is selected; call [`stream_index`](Self::stream_index) to
15/// target a specific stream.
16///
17/// # Examples
18///
19/// ```ignore
20/// use ff_decode::KeyframeEnumerator;
21///
22/// let keyframes = KeyframeEnumerator::new("video.mp4").run()?;
23/// for ts in &keyframes {
24///     println!("Keyframe at {:?}", ts);
25/// }
26/// ```
27pub struct KeyframeEnumerator {
28    input: PathBuf,
29    stream_index: Option<usize>,
30}
31
32impl KeyframeEnumerator {
33    /// Creates a new enumerator for the given video file.
34    ///
35    /// The first video stream is used by default.  Call
36    /// [`stream_index`](Self::stream_index) to select a different stream.
37    pub fn new(input: impl AsRef<Path>) -> Self {
38        Self {
39            input: input.as_ref().to_path_buf(),
40            stream_index: None,
41        }
42    }
43
44    /// Selects a specific stream by zero-based index.
45    ///
46    /// When not set (the default), the first video stream in the file is used.
47    #[must_use]
48    pub fn stream_index(self, idx: usize) -> Self {
49        Self {
50            stream_index: Some(idx),
51            ..self
52        }
53    }
54
55    /// Enumerates keyframe timestamps and returns them in presentation order.
56    ///
57    /// # Errors
58    ///
59    /// - [`DecodeError::AnalysisFailed`] — input file not found, no video
60    ///   stream exists, the requested stream index is out of range, or an
61    ///   internal `FFmpeg` error occurs.
62    pub fn run(self) -> Result<Vec<Duration>, DecodeError> {
63        if !self.input.exists() {
64            return Err(DecodeError::AnalysisFailed {
65                reason: format!("file not found: {}", self.input.display()),
66            });
67        }
68        // SAFETY: enumerate_keyframes_unsafe manages all raw pointer lifetimes:
69        // avformat_open_input / avformat_close_input own the format context;
70        // av_packet_alloc / av_packet_free own the packet; av_packet_unref is
71        // called after every av_read_frame success.
72        unsafe { super::analysis_inner::enumerate_keyframes_unsafe(&self.input, self.stream_index) }
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn keyframe_enumerator_missing_file_should_return_analysis_failed() {
82        let result = KeyframeEnumerator::new("does_not_exist_99999.mp4").run();
83        assert!(
84            matches!(result, Err(DecodeError::AnalysisFailed { .. })),
85            "expected AnalysisFailed for missing file, got {result:?}"
86        );
87    }
88}