Skip to main content

screencapturekit/stream/configuration/
colors.rs

1//! Color and pixel format configuration
2//!
3//! Methods for configuring color space, pixel format, and background color.
4
5use crate::utils::{
6    ffi_string::{ffi_string_from_buffer, SMALL_BUFFER_SIZE},
7    four_char_code::FourCharCode,
8};
9
10const DEFAULT_ALPHA: f32 = 1.0;
11type BackgroundColor = (f32, f32, f32, f32);
12
13use super::{internal::SCStreamConfiguration, pixel_format::PixelFormat};
14
15/// `YCbCr` matrices accepted by `SCStreamConfiguration.colorMatrix`.
16///
17/// The property takes a `CFStringRef` and `ScreenCaptureKit` only honours the
18/// `kCGDisplayStreamYCbCrMatrix_*` values reproduced here; anything else is
19/// ignored by the system with no diagnostic. Using these constants instead of
20/// a hand-written literal is the difference between "the matrix was applied"
21/// and "the matrix was silently dropped".
22///
23/// The matrix only affects `YCbCr` pixel formats (`420v` / `420f` /
24/// [`PixelFormat::YCbCr_420v`] and friends); it is inert for BGRA capture.
25///
26/// # Examples
27///
28/// ```
29/// use screencapturekit::stream::configuration::{color_matrix, SCStreamConfiguration};
30///
31/// let config = SCStreamConfiguration::new().with_color_matrix(color_matrix::ITU_R_709_2).expect("color matrix has no NUL byte");
32/// ```
33pub mod color_matrix {
34    /// `kCGDisplayStreamYCbCrMatrix_ITU_R_709_2` — HD (Rec. 709).
35    pub const ITU_R_709_2: &str = "ITU_R_709_2";
36    /// `kCGDisplayStreamYCbCrMatrix_ITU_R_601_4` — SD (Rec. 601).
37    pub const ITU_R_601_4: &str = "ITU_R_601_4";
38    /// `kCGDisplayStreamYCbCrMatrix_SMPTE_240M_1995` — SMPTE 240M.
39    pub const SMPTE_240M_1995: &str = "SMPTE_240M_1995";
40}
41
42/// Color-space names accepted by `SCStreamConfiguration.colorSpaceName`.
43///
44/// These mirror the `kCGColorSpace*` constants; `SRGB` is the system default
45/// for SDR capture and `DISPLAY_P3` / `EXTENDED_LINEAR_DISPLAY_P3` are the
46/// usual choices for wide-gamut and HDR pipelines.
47pub mod color_space {
48    /// `kCGColorSpaceSRGB`
49    pub const SRGB: &str = "kCGColorSpaceSRGB";
50    /// `kCGColorSpaceDisplayP3`
51    pub const DISPLAY_P3: &str = "kCGColorSpaceDisplayP3";
52    /// `kCGColorSpaceExtendedLinearDisplayP3`
53    pub const EXTENDED_LINEAR_DISPLAY_P3: &str = "kCGColorSpaceExtendedLinearDisplayP3";
54    /// `kCGColorSpaceExtendedLinearSRGB`
55    pub const EXTENDED_LINEAR_SRGB: &str = "kCGColorSpaceExtendedLinearSRGB";
56    /// `kCGColorSpaceITUR_2100_PQ`
57    pub const ITUR_2100_PQ: &str = "kCGColorSpaceITUR_2100_PQ";
58}
59
60/// A string that could not be forwarded to `ScreenCaptureKit`.
61///
62/// The native properties take C strings, so a value containing an interior NUL
63/// byte cannot be represented without truncating it into a different — and
64/// silently wrong — identifier.
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
66pub struct InteriorNulError;
67
68impl std::fmt::Display for InteriorNulError {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        f.write_str("value contains an interior NUL byte and cannot cross the C boundary")
71    }
72}
73
74impl std::error::Error for InteriorNulError {}
75
76impl From<InteriorNulError> for crate::error::SCError {
77    fn from(error: InteriorNulError) -> Self {
78        Self::InvalidConfiguration(error.to_string())
79    }
80}
81
82impl SCStreamConfiguration {
83    /// Set the pixel format for captured frames
84    ///
85    /// Streams created via [`Self::new`] / [`Self::default`] are pinned to
86    /// [`PixelFormat::BGRA`] at construction time, so calling this method is
87    /// only required when you want a non-BGRA format (e.g. YUV `420v` for
88    /// video encoding, or `l10r` for HDR). Apple's runtime default for
89    /// `SCStreamConfiguration()` varies by macOS release — see
90    /// [`PixelFormat::BGRA`] for context.
91    ///
92    /// # Examples
93    ///
94    /// ```
95    /// use screencapturekit::stream::configuration::{SCStreamConfiguration, PixelFormat};
96    ///
97    /// let mut config = SCStreamConfiguration::default();
98    /// config.set_pixel_format(PixelFormat::BGRA);
99    /// ```
100    pub fn set_pixel_format(&mut self, pixel_format: PixelFormat) -> &mut Self {
101        let four_char_code: FourCharCode = pixel_format.into();
102        unsafe {
103            crate::ffi::sc_stream_configuration_set_pixel_format(
104                self.as_ptr(),
105                four_char_code.as_u32(),
106            );
107        }
108        self
109    }
110
111    /// Set the pixel format (builder pattern)
112    #[must_use]
113    pub fn with_pixel_format(mut self, pixel_format: PixelFormat) -> Self {
114        self.set_pixel_format(pixel_format);
115        self
116    }
117
118    /// Get the current pixel format
119    pub fn pixel_format(&self) -> PixelFormat {
120        unsafe {
121            let value = crate::ffi::sc_stream_configuration_get_pixel_format(self.as_ptr());
122            PixelFormat::from(value)
123        }
124    }
125
126    /// Set the background color for captured content with an explicit alpha value.
127    ///
128    /// Available on macOS 13.0+
129    pub fn set_background_color_rgba(&mut self, r: f32, g: f32, b: f32, a: f32) -> &mut Self {
130        unsafe {
131            crate::ffi::sc_stream_configuration_set_background_color(self.as_ptr(), r, g, b, a);
132        }
133        self
134    }
135
136    /// Set the background color for captured content.
137    ///
138    /// This convenience overload uses an opaque alpha channel (`1.0`).
139    pub fn set_background_color(&mut self, r: f32, g: f32, b: f32) -> &mut Self {
140        self.set_background_color_rgba(r, g, b, DEFAULT_ALPHA)
141    }
142
143    /// Set the background color with an explicit alpha value (builder pattern).
144    #[must_use]
145    pub fn with_background_color_rgba(mut self, r: f32, g: f32, b: f32, a: f32) -> Self {
146        self.set_background_color_rgba(r, g, b, a);
147        self
148    }
149
150    /// Set the background color (builder pattern).
151    ///
152    /// This convenience overload uses an opaque alpha channel (`1.0`).
153    #[must_use]
154    pub fn with_background_color(mut self, r: f32, g: f32, b: f32) -> Self {
155        self.set_background_color(r, g, b);
156        self
157    }
158
159    /// Get the current background color, if it was set through this wrapper.
160    pub fn background_color(&self) -> Option<BackgroundColor> {
161        let mut r = 0.0f32;
162        let mut g = 0.0f32;
163        let mut b = 0.0f32;
164        let mut a = 0.0f32;
165        // The value is read back from the Swift bridge's per-configuration
166        // state (keyed by object identity and released with the configuration),
167        // so there is no Rust-side cache to leak or to go stale on pointer reuse.
168        let was_set = unsafe {
169            crate::ffi::sc_stream_configuration_get_background_color(
170                self.as_ptr(),
171                &raw mut r,
172                &raw mut g,
173                &raw mut b,
174                &raw mut a,
175            )
176        };
177        was_set.then_some((r, g, b, a))
178    }
179
180    /// Set the color space name for captured content.
181    ///
182    /// Available on macOS 13.0+. Use the [`color_space`] constants for the
183    /// values `ScreenCaptureKit` recognises.
184    ///
185    /// # Errors
186    ///
187    /// Returns [`InteriorNulError`] — leaving the configuration unchanged — if
188    /// `name` contains an interior NUL byte.
189    pub fn set_color_space_name(&mut self, name: &str) -> Result<&mut Self, InteriorNulError> {
190        let c_name = std::ffi::CString::new(name).map_err(|_| InteriorNulError)?;
191        unsafe {
192            crate::ffi::sc_stream_configuration_set_color_space_name(
193                self.as_ptr(),
194                c_name.as_ptr(),
195            );
196        }
197        Ok(self)
198    }
199
200    /// Set the color space name (builder pattern).
201    #[allow(clippy::missing_errors_doc)]
202    pub fn with_color_space_name(mut self, name: &str) -> Result<Self, InteriorNulError> {
203        self.set_color_space_name(name)?;
204        Ok(self)
205    }
206
207    /// Get the color space name for captured content.
208    pub fn color_space_name(&self) -> Option<String> {
209        unsafe {
210            ffi_string_from_buffer(SMALL_BUFFER_SIZE, |buf, len| {
211                crate::ffi::sc_stream_configuration_get_color_space_name(self.as_ptr(), buf, len)
212            })
213        }
214    }
215
216    /// Set the `YCbCr` color matrix for captured content.
217    ///
218    /// Available on macOS 13.0+. `matrix` must be one of the [`color_matrix`]
219    /// constants — despite the free-form `&str` signature the property is a
220    /// closed set of `kCGDisplayStreamYCbCrMatrix_*` identifiers, and any
221    /// other string is ignored by the system without an error. The setting
222    /// only affects `YCbCr` pixel formats and is inert for BGRA capture.
223    ///
224    /// # Errors
225    ///
226    /// Returns [`InteriorNulError`] — leaving the configuration unchanged — if
227    /// `matrix` contains an interior NUL byte.
228    pub fn set_color_matrix(&mut self, matrix: &str) -> Result<&mut Self, InteriorNulError> {
229        let c_matrix = std::ffi::CString::new(matrix).map_err(|_| InteriorNulError)?;
230        unsafe {
231            crate::ffi::sc_stream_configuration_set_color_matrix(self.as_ptr(), c_matrix.as_ptr());
232        }
233        Ok(self)
234    }
235
236    /// Get the color matrix for captured content.
237    ///
238    /// Returns the color matrix as a string, or `None` if not set.
239    pub fn color_matrix(&self) -> Option<String> {
240        unsafe {
241            ffi_string_from_buffer(SMALL_BUFFER_SIZE, |buf, len| {
242                crate::ffi::sc_stream_configuration_get_color_matrix(self.as_ptr(), buf, len)
243            })
244        }
245    }
246
247    /// Set the color matrix (builder pattern)
248    #[allow(clippy::missing_errors_doc)]
249    pub fn with_color_matrix(mut self, matrix: &str) -> Result<Self, InteriorNulError> {
250        self.set_color_matrix(matrix)?;
251        Ok(self)
252    }
253}