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}