Skip to main content

media_pp\elements\source\compositor/
video_layer.rs

1//! Shared, backend-agnostic layer types/math for anything that composites
2//! multiple video inputs into one output — [`crate::elements::VideoCompositor`]
3//! (CPU, `libswscale`) and [`crate::elements::D3d11VideoCompositor`] (GPU,
4//! D3D11) both use these exact same types, so a caller's layer-control code
5//! doesn't change shape when switching between them. Only the actual pixel
6//! work (scale+blend vs. shader draw) differs per backend.
7
8use thiserror::Error as ThisError;
9
10pub(crate) const MAX_DIMENSION: u32 = 16_384;
11
12/// An opaque, stable identity for one compositor input registration.
13/// Replacing an input with the same name creates a different identity, so
14/// an old sink or layer handle can never affect its replacement.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
16pub struct VideoInputId(pub(crate) u64);
17
18/// An output-space rectangle. Signed coordinates allow a layer to be
19/// moved partially outside the canvas while its size remains positive.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct VideoRect {
22    pub x: i32,
23    pub y: i32,
24    pub width: u32,
25    pub height: u32,
26}
27
28impl VideoRect {
29    pub const fn new(x: i32, y: i32, width: u32, height: u32) -> Self {
30        Self {
31            x,
32            y,
33            width,
34            height,
35        }
36    }
37}
38
39/// How an input's aspect ratio is mapped into its [`VideoRect`].
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum VideoFit {
42    /// Distort the input to exactly fill the rectangle.
43    Stretch,
44    /// Preserve aspect ratio and letterbox/pillarbox inside the rectangle.
45    Contain,
46    /// Preserve aspect ratio, fill the rectangle, and crop overflow.
47    Cover,
48}
49
50/// Runtime-adjustable spatial settings for one compositor input.
51#[derive(Debug, Clone, Copy, PartialEq)]
52pub struct VideoLayer {
53    pub rect: VideoRect,
54    pub z_index: i32,
55    pub opacity: f32,
56    pub visible: bool,
57    pub fit: VideoFit,
58}
59
60impl VideoLayer {
61    pub const fn new(rect: VideoRect) -> Self {
62        Self {
63            rect,
64            z_index: 0,
65            opacity: 1.0,
66            visible: true,
67            fit: VideoFit::Contain,
68        }
69    }
70}
71
72/// Validation/geometry failures shared by every compositor backend. Each
73/// backend's own `{Backend}CompositorError` maps these into its own
74/// variants (see e.g. [`crate::elements::VideoCompositorError`]) rather
75/// than exposing this type directly, so a caller matching on a specific
76/// backend's error type sees only that backend's own enum.
77#[derive(Debug, Clone, Copy, PartialEq, ThisError)]
78pub(crate) enum VideoLayerError {
79    #[error(
80        "invalid layer dimensions {width}x{height}; each dimension must be 1..={MAX_DIMENSION}"
81    )]
82    InvalidDimensions { width: u32, height: u32 },
83
84    #[error("layer opacity must be finite and between 0.0 and 1.0, got {0}")]
85    InvalidOpacity(f32),
86
87    #[error("input frame has invalid dimensions {width}x{height}")]
88    InvalidInputDimensions { width: u32, height: u32 },
89
90    #[error("scaled layer would exceed {MAX_DIMENSION}px: {width}x{height}")]
91    ScaledLayerTooLarge { width: u32, height: u32 },
92}
93
94pub(crate) fn validate_layer(layer: VideoLayer) -> Result<(), VideoLayerError> {
95    validate_rect(layer.rect)?;
96    validate_opacity(layer.opacity)
97}
98
99pub(crate) fn validate_rect(rect: VideoRect) -> Result<(), VideoLayerError> {
100    if rect.width == 0
101        || rect.height == 0
102        || rect.width > MAX_DIMENSION
103        || rect.height > MAX_DIMENSION
104    {
105        Err(VideoLayerError::InvalidDimensions {
106            width: rect.width,
107            height: rect.height,
108        })
109    } else {
110        Ok(())
111    }
112}
113
114pub(crate) fn validate_opacity(opacity: f32) -> Result<(), VideoLayerError> {
115    if opacity.is_finite() && (0.0..=1.0).contains(&opacity) {
116        Ok(())
117    } else {
118        Err(VideoLayerError::InvalidOpacity(opacity))
119    }
120}
121
122#[derive(Debug, Clone, Copy)]
123pub(crate) struct LayerGeometry {
124    pub(crate) image_x: i64,
125    pub(crate) image_y: i64,
126    pub(crate) image_width: u32,
127    pub(crate) image_height: u32,
128    pub(crate) clip: VideoRect,
129}
130
131/// Computes where and at what size an input actually gets drawn inside its
132/// [`VideoRect`], given [`VideoFit`] — shared, backend-agnostic pixel-space
133/// math. A CPU backend blits with this directly; a GPU backend turns it
134/// into vertex positions/UVs for the same quad.
135pub(crate) fn layer_geometry(
136    source_width: u32,
137    source_height: u32,
138    rect: VideoRect,
139    fit: VideoFit,
140) -> Result<LayerGeometry, VideoLayerError> {
141    if source_width == 0 || source_height == 0 {
142        return Err(VideoLayerError::InvalidInputDimensions {
143            width: source_width,
144            height: source_height,
145        });
146    }
147
148    let source_is_wider = u128::from(rect.width) * u128::from(source_height)
149        <= u128::from(rect.height) * u128::from(source_width);
150    let (image_width, image_height) = match fit {
151        VideoFit::Stretch => (rect.width, rect.height),
152        VideoFit::Contain if source_is_wider => (
153            rect.width,
154            scaled_dimension(source_height, rect.width, source_width),
155        ),
156        VideoFit::Contain => (
157            scaled_dimension(source_width, rect.height, source_height),
158            rect.height,
159        ),
160        VideoFit::Cover if source_is_wider => (
161            scaled_dimension(source_width, rect.height, source_height),
162            rect.height,
163        ),
164        VideoFit::Cover => (
165            rect.width,
166            scaled_dimension(source_height, rect.width, source_width),
167        ),
168    };
169    if image_width > MAX_DIMENSION || image_height > MAX_DIMENSION {
170        return Err(VideoLayerError::ScaledLayerTooLarge {
171            width: image_width,
172            height: image_height,
173        });
174    }
175
176    Ok(LayerGeometry {
177        image_x: i64::from(rect.x) + (i64::from(rect.width) - i64::from(image_width)) / 2,
178        image_y: i64::from(rect.y) + (i64::from(rect.height) - i64::from(image_height)) / 2,
179        image_width,
180        image_height,
181        clip: rect,
182    })
183}
184
185pub(crate) fn scaled_dimension(source: u32, target: u32, divisor: u32) -> u32 {
186    let scaled =
187        (u128::from(source) * u128::from(target) + u128::from(divisor) / 2) / u128::from(divisor);
188    scaled.max(1).min(u128::from(u32::MAX)) as u32
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn contain_and_cover_preserve_aspect_ratio() {
197        let rect = VideoRect::new(10, 20, 100, 100);
198        let contain = layer_geometry(160, 90, rect, VideoFit::Contain).unwrap();
199        assert_eq!((contain.image_width, contain.image_height), (100, 56));
200        assert_eq!((contain.image_x, contain.image_y), (10, 42));
201
202        let cover = layer_geometry(160, 90, rect, VideoFit::Cover).unwrap();
203        assert_eq!((cover.image_width, cover.image_height), (178, 100));
204        assert_eq!((cover.image_x, cover.image_y), (-29, 20));
205    }
206}