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}