media_pp/elements/source/compositor/
video_layer.rs1use thiserror::Error as ThisError;
9
10pub(crate) const MAX_DIMENSION: u32 = 16_384;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
16pub struct VideoInputId(pub(crate) u64);
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct VideoRect {
22 pub x: i32,
24 pub y: i32,
26 pub width: u32,
28 pub height: u32,
30}
31
32impl VideoRect {
33 pub const fn new(x: i32, y: i32, width: u32, height: u32) -> Self {
35 Self {
36 x,
37 y,
38 width,
39 height,
40 }
41 }
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct VideoSourceRect {
56 pub x: u32,
58 pub y: u32,
60 pub width: u32,
62 pub height: u32,
64}
65
66impl VideoSourceRect {
67 pub const fn new(x: u32, y: u32, width: u32, height: u32) -> Self {
70 Self {
71 x,
72 y,
73 width,
74 height,
75 }
76 }
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum VideoFit {
82 Stretch,
84 Contain,
86 Cover,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq)]
92pub struct VideoLayer {
93 pub rect: VideoRect,
95 pub z_index: i32,
97 pub opacity: f32,
99 pub visible: bool,
101 pub fit: VideoFit,
103 pub source: Option<VideoSourceRect>,
111}
112
113impl VideoLayer {
114 pub const fn new(rect: VideoRect) -> Self {
116 Self {
117 rect,
118 z_index: 0,
119 opacity: 1.0,
120 visible: true,
121 fit: VideoFit::Contain,
122 source: None,
123 }
124 }
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, ThisError)]
133pub(crate) enum VideoLayerError {
134 #[error(
135 "invalid layer dimensions {width}x{height}; each dimension must be 1..={MAX_DIMENSION}"
136 )]
137 InvalidDimensions { width: u32, height: u32 },
138
139 #[error("layer opacity must be finite and between 0.0 and 1.0, got {0}")]
140 InvalidOpacity(f32),
141
142 #[error("input frame has invalid dimensions {width}x{height}")]
143 InvalidInputDimensions { width: u32, height: u32 },
144
145 #[error("scaled layer would exceed {MAX_DIMENSION}px: {width}x{height}")]
146 ScaledLayerTooLarge { width: u32, height: u32 },
147
148 #[error("layer source region has invalid dimensions {width}x{height}")]
149 InvalidSourceRegion { width: u32, height: u32 },
150}
151
152pub(crate) fn validate_layer(layer: VideoLayer) -> Result<(), VideoLayerError> {
153 validate_rect(layer.rect)?;
154 validate_source(layer.source)?;
155 validate_opacity(layer.opacity)
156}
157
158pub(crate) fn validate_source(source: Option<VideoSourceRect>) -> Result<(), VideoLayerError> {
162 match source {
163 Some(source) if source.width == 0 || source.height == 0 => {
164 Err(VideoLayerError::InvalidSourceRegion {
165 width: source.width,
166 height: source.height,
167 })
168 }
169 _ => Ok(()),
170 }
171}
172
173pub(crate) fn source_region(
181 source: Option<VideoSourceRect>,
182 frame_width: u32,
183 frame_height: u32,
184) -> Option<VideoSourceRect> {
185 let Some(source) = source else {
186 return Some(VideoSourceRect::new(0, 0, frame_width, frame_height));
187 };
188 let width = frame_width.checked_sub(source.x)?.min(source.width);
189 let height = frame_height.checked_sub(source.y)?.min(source.height);
190 (width > 0 && height > 0).then_some(VideoSourceRect::new(source.x, source.y, width, height))
191}
192
193pub(crate) fn validate_rect(rect: VideoRect) -> Result<(), VideoLayerError> {
194 if rect.width == 0
195 || rect.height == 0
196 || rect.width > MAX_DIMENSION
197 || rect.height > MAX_DIMENSION
198 {
199 Err(VideoLayerError::InvalidDimensions {
200 width: rect.width,
201 height: rect.height,
202 })
203 } else {
204 Ok(())
205 }
206}
207
208pub(crate) fn validate_opacity(opacity: f32) -> Result<(), VideoLayerError> {
209 if opacity.is_finite() && (0.0..=1.0).contains(&opacity) {
210 Ok(())
211 } else {
212 Err(VideoLayerError::InvalidOpacity(opacity))
213 }
214}
215
216#[derive(Debug, Clone, Copy)]
217pub(crate) struct LayerGeometry {
218 pub(crate) image_x: i64,
219 pub(crate) image_y: i64,
220 pub(crate) image_width: u32,
221 pub(crate) image_height: u32,
222 pub(crate) clip: VideoRect,
223}
224
225pub(crate) fn layer_geometry(
230 source_width: u32,
231 source_height: u32,
232 rect: VideoRect,
233 fit: VideoFit,
234) -> Result<LayerGeometry, VideoLayerError> {
235 if source_width == 0 || source_height == 0 {
236 return Err(VideoLayerError::InvalidInputDimensions {
237 width: source_width,
238 height: source_height,
239 });
240 }
241
242 let source_is_wider = u128::from(rect.width) * u128::from(source_height)
243 <= u128::from(rect.height) * u128::from(source_width);
244 let (image_width, image_height) = match fit {
245 VideoFit::Stretch => (rect.width, rect.height),
246 VideoFit::Contain if source_is_wider => (
247 rect.width,
248 scaled_dimension(source_height, rect.width, source_width),
249 ),
250 VideoFit::Contain => (
251 scaled_dimension(source_width, rect.height, source_height),
252 rect.height,
253 ),
254 VideoFit::Cover if source_is_wider => (
255 scaled_dimension(source_width, rect.height, source_height),
256 rect.height,
257 ),
258 VideoFit::Cover => (
259 rect.width,
260 scaled_dimension(source_height, rect.width, source_width),
261 ),
262 };
263 if image_width > MAX_DIMENSION || image_height > MAX_DIMENSION {
264 return Err(VideoLayerError::ScaledLayerTooLarge {
265 width: image_width,
266 height: image_height,
267 });
268 }
269
270 Ok(LayerGeometry {
271 image_x: i64::from(rect.x) + (i64::from(rect.width) - i64::from(image_width)) / 2,
272 image_y: i64::from(rect.y) + (i64::from(rect.height) - i64::from(image_height)) / 2,
273 image_width,
274 image_height,
275 clip: rect,
276 })
277}
278
279pub(crate) fn scaled_dimension(source: u32, target: u32, divisor: u32) -> u32 {
280 let scaled =
281 (u128::from(source) * u128::from(target) + u128::from(divisor) / 2) / u128::from(divisor);
282 scaled.max(1).min(u128::from(u32::MAX)) as u32
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn contain_and_cover_preserve_aspect_ratio() {
291 let rect = VideoRect::new(10, 20, 100, 100);
292 let contain = layer_geometry(160, 90, rect, VideoFit::Contain).unwrap();
293 assert_eq!((contain.image_width, contain.image_height), (100, 56));
294 assert_eq!((contain.image_x, contain.image_y), (10, 42));
295
296 let cover = layer_geometry(160, 90, rect, VideoFit::Cover).unwrap();
297 assert_eq!((cover.image_width, cover.image_height), (178, 100));
298 assert_eq!((cover.image_x, cover.image_y), (-29, 20));
299 }
300}