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}