Skip to main content

ff_analysis/analysis/
keyframe_enumerator.rs

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