ff_decode/video/builder/scale.rs
1//! Output scale options for [`VideoDecoderBuilder`].
2
3use super::{OutputScale, VideoDecoderBuilder};
4
5impl VideoDecoderBuilder {
6 /// Scales decoded frames to the given exact dimensions.
7 ///
8 /// The frame is scaled in the same `libswscale` pass as pixel-format
9 /// conversion, so there is no extra copy. If `output_format` is not set,
10 /// the source pixel format is preserved while scaling.
11 ///
12 /// Width and height must be greater than zero. They are rounded up to the
13 /// nearest even number if necessary (required by most pixel formats).
14 ///
15 /// Calling this method overwrites any previous `output_width` or
16 /// `output_height` call. The last setter wins.
17 ///
18 /// # Errors
19 ///
20 /// [`build()`](Self::build) returns `DecodeError::InvalidOutputDimensions`
21 /// if either dimension is zero after rounding.
22 ///
23 /// # Examples
24 ///
25 /// ```ignore
26 /// use ff_decode::VideoDecoder;
27 ///
28 /// // Decode every frame at 320×240
29 /// let decoder = VideoDecoder::open("video.mp4")?
30 /// .output_size(320, 240)
31 /// .build()?;
32 /// ```
33 #[must_use]
34 pub fn output_size(mut self, width: u32, height: u32) -> Self {
35 self.output_scale = Some(OutputScale::Exact { width, height });
36 self
37 }
38
39 /// Scales decoded frames to the given width, preserving the aspect ratio.
40 ///
41 /// The height is computed from the source aspect ratio and rounded to the
42 /// nearest even number. Calling this method overwrites any previous
43 /// `output_size` or `output_height` call. The last setter wins.
44 ///
45 /// # Errors
46 ///
47 /// [`build()`](Self::build) returns `DecodeError::InvalidOutputDimensions`
48 /// if `width` is zero.
49 ///
50 /// # Examples
51 ///
52 /// ```ignore
53 /// use ff_decode::VideoDecoder;
54 ///
55 /// // Decode at 1280 px wide, preserving aspect ratio
56 /// let decoder = VideoDecoder::open("video.mp4")?
57 /// .output_width(1280)
58 /// .build()?;
59 /// ```
60 #[must_use]
61 pub fn output_width(mut self, width: u32) -> Self {
62 self.output_scale = Some(OutputScale::FitWidth(width));
63 self
64 }
65
66 /// Scales decoded frames to the given height, preserving the aspect ratio.
67 ///
68 /// The width is computed from the source aspect ratio and rounded to the
69 /// nearest even number. Calling this method overwrites any previous
70 /// `output_size` or `output_width` call. The last setter wins.
71 ///
72 /// # Errors
73 ///
74 /// [`build()`](Self::build) returns `DecodeError::InvalidOutputDimensions`
75 /// if `height` is zero.
76 ///
77 /// # Examples
78 ///
79 /// ```ignore
80 /// use ff_decode::VideoDecoder;
81 ///
82 /// // Decode at 720 px tall, preserving aspect ratio
83 /// let decoder = VideoDecoder::open("video.mp4")?
84 /// .output_height(720)
85 /// .build()?;
86 /// ```
87 #[must_use]
88 pub fn output_height(mut self, height: u32) -> Self {
89 self.output_scale = Some(OutputScale::FitHeight(height));
90 self
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crate::error::DecodeError;
98 use crate::video::builder::VideoDecoder;
99 use std::path::PathBuf;
100
101 #[test]
102 fn builder_output_size_should_set_exact_scale() {
103 let builder = VideoDecoderBuilder::new(PathBuf::from("test.mp4")).output_size(1280, 720);
104
105 assert_eq!(
106 builder.output_scale,
107 Some(OutputScale::Exact {
108 width: 1280,
109 height: 720
110 })
111 );
112 }
113
114 #[test]
115 fn builder_output_width_should_set_fit_width() {
116 let builder = VideoDecoderBuilder::new(PathBuf::from("test.mp4")).output_width(1920);
117
118 assert_eq!(builder.output_scale, Some(OutputScale::FitWidth(1920)));
119 }
120
121 #[test]
122 fn builder_output_height_should_set_fit_height() {
123 let builder = VideoDecoderBuilder::new(PathBuf::from("test.mp4")).output_height(1080);
124
125 assert_eq!(builder.output_scale, Some(OutputScale::FitHeight(1080)));
126 }
127
128 #[test]
129 fn builder_output_size_last_setter_wins() {
130 let builder = VideoDecoderBuilder::new(PathBuf::from("test.mp4"))
131 .output_width(1280)
132 .output_size(640, 480);
133
134 assert_eq!(
135 builder.output_scale,
136 Some(OutputScale::Exact {
137 width: 640,
138 height: 480
139 })
140 );
141 }
142
143 #[test]
144 fn build_with_zero_width_should_return_invalid_dimensions() {
145 let result = VideoDecoder::open("nonexistent.mp4")
146 .output_size(0, 480)
147 .build();
148 assert!(matches!(
149 result,
150 Err(DecodeError::InvalidOutputDimensions { .. })
151 ));
152 }
153
154 #[test]
155 fn build_with_zero_height_should_return_invalid_dimensions() {
156 let result = VideoDecoder::open("nonexistent.mp4")
157 .output_size(640, 0)
158 .build();
159 assert!(matches!(
160 result,
161 Err(DecodeError::InvalidOutputDimensions { .. })
162 ));
163 }
164
165 #[test]
166 fn build_with_zero_output_width_should_return_invalid_dimensions() {
167 let result = VideoDecoder::open("nonexistent.mp4")
168 .output_width(0)
169 .build();
170 assert!(matches!(
171 result,
172 Err(DecodeError::InvalidOutputDimensions { .. })
173 ));
174 }
175
176 #[test]
177 fn build_with_zero_output_height_should_return_invalid_dimensions() {
178 let result = VideoDecoder::open("nonexistent.mp4")
179 .output_height(0)
180 .build();
181 assert!(matches!(
182 result,
183 Err(DecodeError::InvalidOutputDimensions { .. })
184 ));
185 }
186}