Skip to main content

ez_ffmpeg/core/frame_export/
options.rs

1//! Public option enums for frame export: how to sample frames, what packed
2//! pixel layout to produce, and how to interpret source color.
3
4/// How to select which decoded frames are exported.
5///
6/// Index-based modes (`All`, `EveryNth`) count delivered decoder output;
7/// time-based modes (`EverySec`) float their grid on the first delivered frame
8/// and compare presentation timestamps.
9#[derive(Debug, Clone, Copy, PartialEq)]
10#[non_exhaustive]
11pub enum Sampling {
12    /// Every decoded frame (the default).
13    All,
14    /// Every `n`-th decoded frame, starting with the first (`n >= 1`).
15    EveryNth(u64),
16    /// Starting from the first delivered frame, the first frame at or after each
17    /// subsequent `k * seconds` boundary (`k = 1, 2, …`). No synthesis: sparse
18    /// sources yield fewer frames, and each source frame is selected at most once.
19    EverySec(f64),
20    /// Only frames flagged as keyframes. Pins the decoder option
21    /// `skip_frame=nokey` for a decode-time fast path, plus a key-flag check as a
22    /// belt for codecs that ignore the hint.
23    KeyframesOnly,
24    /// Exactly `n` frames (fewer only when a lower `max_frames` cap wins),
25    /// sampled uniformly by presentation time over the (trimmed) duration —
26    /// the standard VLM/CLIP primitive. Short inputs pad by repeating the
27    /// nearest displayed frame; each repeat keeps that frame's `pts_us`.
28    /// Requires a resolvable duration (see `duration_hint_us`).
29    ///
30    /// Cost model: the strategy is a single sequential decode of the (trimmed)
31    /// input — there is no seek-per-target fast path. Frames that lose the
32    /// grid race are dropped pre-filtergraph and pay no scaling, but once the
33    /// grid completes the remaining tail (~`1/(2n)` of the span) passes
34    /// through the graph — and is scaled — solely to keep the encoder-side
35    /// termination path fed.
36    UniformN(u32),
37}
38
39/// A packed, 8-bit pixel layout. Rows are tight (`width * bytes_per_pixel`, no
40/// padding) and top-down.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42#[non_exhaustive]
43pub enum PixelLayout {
44    /// 24-bit RGB, 3 bytes per pixel (`R G B`).
45    Rgb24,
46    /// 32-bit RGBA, 4 bytes per pixel (`R G B A`).
47    Rgba32,
48    /// 8-bit grayscale, 1 byte per pixel.
49    Gray8,
50}
51
52impl PixelLayout {
53    /// Bytes per pixel for this layout: 3 (`Rgb24`), 4 (`Rgba32`), or 1 (`Gray8`).
54    pub fn bytes_per_pixel(self) -> usize {
55        match self {
56            PixelLayout::Rgb24 => 3,
57            PixelLayout::Rgba32 => 4,
58            PixelLayout::Gray8 => 1,
59        }
60    }
61
62    /// The FFmpeg `format` filter pixel-format name that produces this layout.
63    pub(crate) fn ffmpeg_format_name(self) -> &'static str {
64        match self {
65            PixelLayout::Rgb24 => "rgb24",
66            PixelLayout::Rgba32 => "rgba",
67            PixelLayout::Gray8 => "gray",
68        }
69    }
70
71    /// The exact `AVPixelFormat` a packed frame of this layout must carry. Used
72    /// to reject any pixel format the graph did not produce before packing.
73    pub(crate) fn av_pixel_format(self) -> ffmpeg_sys_next::AVPixelFormat {
74        use ffmpeg_sys_next::AVPixelFormat::{AV_PIX_FMT_GRAY8, AV_PIX_FMT_RGB24, AV_PIX_FMT_RGBA};
75        match self {
76            PixelLayout::Rgb24 => AV_PIX_FMT_RGB24,
77            PixelLayout::Rgba32 => AV_PIX_FMT_RGBA,
78            PixelLayout::Gray8 => AV_PIX_FMT_GRAY8,
79        }
80    }
81}
82
83/// The swscale precision tier for the conversion stage.
84///
85/// The extractor runs any resize plus the pixel-format conversion as ONE
86/// swscale pass, and this tier sets that pass's scaler flags — so it affects
87/// resized output too, not only the YUV → RGB step. Color interpretation —
88/// which matrix and range are applied ([`ColorPolicy`]) — is identical in
89/// both tiers.
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
91#[non_exhaustive]
92pub enum ConversionPrecision {
93    /// The FFmpeg CLI's default swscale configuration (`flags=bicubic`, no
94    /// extra precision flags). This keeps swscale eligible for its fastest
95    /// converters — including the unscaled yuv420p → rgb24 special case — and
96    /// matches the bytes an `ffmpeg -vf "scale=..,format=.."` run with default
97    /// flags produces. This is the default.
98    #[default]
99    Standard,
100    /// Adds `accurate_rnd+full_chroma_int`: accurate rounding plus full
101    /// horizontal chroma interpolation. Relative to
102    /// [`Standard`](ConversionPrecision::Standard) the mean difference is a
103    /// couple of steps per 8-bit channel; individual pixels on sharp chroma
104    /// edges can differ by more, because the two tiers reconstruct subsampled
105    /// chroma differently — and `accurate_rnd` also changes rounding on
106    /// resized output, including single-plane formats like gray. The flags
107    /// disable swscale's unscaled fast-path converters, which multiplies the
108    /// conversion cost several-fold (at 1080p the conversion, not the decode,
109    /// becomes the bottleneck). Opt in when the last bit of rounding and
110    /// chroma reconstruction matters more than throughput — e.g. quality
111    /// metrics or golden-reference pipelines. v0.14.0 applied exactly these
112    /// flags unconditionally, so `High` also reproduces v0.14.0 output,
113    /// byte-identical when linked against the same libswscale build.
114    High,
115}
116
117/// A YUV → RGB conversion matrix.
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119#[non_exhaustive]
120pub enum YuvMatrix {
121    /// ITU-R BT.601 (SD).
122    Bt601,
123    /// ITU-R BT.709 (HD).
124    Bt709,
125}
126
127impl YuvMatrix {
128    /// The swscale `in_color_matrix` token.
129    pub(crate) fn in_color_matrix(self) -> &'static str {
130        match self {
131            YuvMatrix::Bt601 => "bt601",
132            YuvMatrix::Bt709 => "bt709",
133        }
134    }
135}
136
137/// The signal range of the source YUV.
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139#[non_exhaustive]
140pub enum YuvRange {
141    /// Limited / "TV" range (Y in 16..=235).
142    Limited,
143    /// Full / "PC" range (Y in 0..=255).
144    Full,
145}
146
147impl YuvRange {
148    /// The swscale `in_range` token.
149    pub(crate) fn in_range(self) -> &'static str {
150        match self {
151            YuvRange::Limited => "tv",
152            YuvRange::Full => "pc",
153        }
154    }
155}
156
157/// How the source color is interpreted during YUV → RGB conversion.
158///
159/// The single most defensible behavior of this module: [`Tagged`](ColorPolicy::Tagged)
160/// honors the frame's own colorspace tags instead of assuming BT.601 for
161/// everything (the default many decode-to-RGB paths get wrong for HD/BT.709).
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
163#[non_exhaustive]
164pub enum ColorPolicy {
165    /// Honor the frame's color tags; untagged frames fall through to swscale's
166    /// documented default (BT.601 / limited). This is the default.
167    #[default]
168    Tagged,
169    /// Like [`Tagged`](ColorPolicy::Tagged), but frames with an UNSPECIFIED
170    /// colorspace get a per-frame resolution guess: BT.709 when the frame
171    /// height is at least 720, BT.601 otherwise (an UNSPECIFIED range is
172    /// pinned to limited). Tagged frames are never overridden, and the guess
173    /// never freezes — a mid-stream change from untagged to tagged (or a
174    /// resolution change) takes effect on that very frame.
175    ///
176    /// On inputs with multiple video streams, set
177    /// [`video_stream_index`](super::FrameExtractor::video_stream_index) if the
178    /// exported stream is not the first video stream; the run is otherwise
179    /// rejected with a typed error rather than silently leaving the exported
180    /// stream unstamped.
181    TaggedOrResolutionGuess,
182    /// Force a specific interpretation for ALL frames, overriding any tags.
183    Force {
184        /// The YUV matrix to assume.
185        matrix: YuvMatrix,
186        /// The signal range to assume.
187        range: YuvRange,
188    },
189}