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}