Skip to main content

screencapturekit/stream/configuration/
stream_properties.rs

1//! Stream identification and HDR configuration
2//!
3//! This module provides methods to configure stream identification and HDR capture settings.
4
5use super::internal::SCStreamConfiguration;
6#[cfg(feature = "macos_14_0")]
7use super::InteriorNulError;
8#[cfg(feature = "macos_14_0")]
9use crate::error::{SCError, SCResult};
10#[cfg(feature = "macos_14_0")]
11use crate::utils::ffi_string::{ffi_string_from_buffer, SMALL_BUFFER_SIZE};
12
13/// Dynamic range mode for capture (macOS 15.0+)
14#[repr(i32)]
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
16pub enum SCCaptureDynamicRange {
17    /// Standard Dynamic Range (SDR) - default mode
18    #[default]
19    SDR = 0,
20    /// HDR with local display tone mapping
21    HDRLocalDisplay = 1,
22    /// HDR with canonical display tone mapping
23    HDRCanonicalDisplay = 2,
24}
25
26impl SCCaptureDynamicRange {
27    pub const fn from_raw(raw: i32) -> Option<Self> {
28        match raw {
29            0 => Some(Self::SDR),
30            1 => Some(Self::HDRLocalDisplay),
31            2 => Some(Self::HDRCanonicalDisplay),
32            _ => None,
33        }
34    }
35}
36
37impl SCStreamConfiguration {
38    /// Set the stream name for identification
39    ///
40    /// Assigns a name to the stream that can be used for debugging and identification
41    /// purposes. The name appears in system logs and debugging tools.
42    ///
43    /// Available on macOS 14.0+.
44    ///
45    /// # Errors
46    ///
47    /// Returns [`SCError::InvalidConfiguration`] — leaving the configuration
48    /// unchanged — if `name` contains an interior NUL byte, and
49    /// [`SCError::FeatureNotAvailable`] before macOS 14.0.
50    ///
51    /// # Examples
52    ///
53    /// ```rust,no_run
54    /// use screencapturekit::prelude::*;
55    ///
56    /// let config = SCStreamConfiguration::new()
57    ///     .with_stream_name(Some("MyApp-MainCapture"))
58    ///     .expect("stream name has no NUL byte");
59    /// ```
60    #[cfg(feature = "macos_14_0")]
61    pub fn set_stream_name(&mut self, name: Option<&str>) -> SCResult<&mut Self> {
62        let c_name = name
63            .map(|stream_name| std::ffi::CString::new(stream_name).map_err(|_| InteriorNulError))
64            .transpose()?;
65        let applied = unsafe {
66            crate::ffi::sc_stream_configuration_set_stream_name(
67                self.as_ptr(),
68                c_name.as_ref().map_or(std::ptr::null(), |n| n.as_ptr()),
69            )
70        };
71        applied.then_some(self).ok_or_else(|| {
72            SCError::feature_not_available("SCStreamConfiguration.streamName", "14.0")
73        })
74    }
75
76    /// Set the stream name (builder pattern)
77    #[cfg(feature = "macos_14_0")]
78    #[allow(clippy::missing_errors_doc)]
79    pub fn with_stream_name(mut self, name: Option<&str>) -> SCResult<Self> {
80        self.set_stream_name(name)?;
81        Ok(self)
82    }
83
84    /// Get the configured stream name
85    ///
86    /// Returns the name assigned to this stream, if any.
87    #[cfg(feature = "macos_14_0")]
88    pub fn stream_name(&self) -> Option<String> {
89        unsafe {
90            ffi_string_from_buffer(SMALL_BUFFER_SIZE, |buf, len| {
91                crate::ffi::sc_stream_configuration_get_stream_name(self.as_ptr(), buf, len)
92            })
93        }
94    }
95
96    /// Set the dynamic range mode for capture (macOS 15.0+)
97    ///
98    /// Controls whether to capture in SDR or HDR mode and how HDR content
99    /// should be tone-mapped for display.
100    ///
101    /// # Availability
102    /// macOS 15.0+. Requires the `macos_15_0` feature flag to be enabled.
103    ///
104    /// # Modes
105    /// - `SDR`: Standard dynamic range capture (default)
106    /// - `HDRLocalDisplay`: HDR with tone mapping optimized for the local display
107    /// - `HDRCanonicalDisplay`: HDR with canonical tone mapping for portability
108    ///
109    /// # Examples
110    ///
111    /// ```rust,no_run
112    /// use screencapturekit::prelude::*;
113    /// use screencapturekit::stream::configuration::stream_properties::SCCaptureDynamicRange;
114    ///
115    /// let config = SCStreamConfiguration::new()
116    ///     .with_width(1920)
117    ///     .with_height(1080)
118    ///     .with_capture_dynamic_range(SCCaptureDynamicRange::HDRLocalDisplay)
119    ///     .expect("macOS 15.0 or later");
120    /// ```
121    #[cfg(feature = "macos_15_0")]
122    #[allow(clippy::missing_errors_doc)]
123    pub fn set_capture_dynamic_range(
124        &mut self,
125        dynamic_range: SCCaptureDynamicRange,
126    ) -> SCResult<&mut Self> {
127        let applied = unsafe {
128            crate::ffi::sc_stream_configuration_set_capture_dynamic_range(
129                self.as_ptr(),
130                dynamic_range as i32,
131            )
132        };
133        applied.then_some(self).ok_or_else(|| {
134            SCError::feature_not_available("SCStreamConfiguration.captureDynamicRange", "15.0")
135        })
136    }
137
138    /// Set the dynamic range mode (builder pattern)
139    #[cfg(feature = "macos_15_0")]
140    #[allow(clippy::missing_errors_doc)]
141    pub fn with_capture_dynamic_range(
142        mut self,
143        dynamic_range: SCCaptureDynamicRange,
144    ) -> SCResult<Self> {
145        self.set_capture_dynamic_range(dynamic_range)?;
146        Ok(self)
147    }
148
149    /// Get the configured dynamic range mode (macOS 15.0+)
150    ///
151    /// Returns the current HDR capture mode setting.
152    ///
153    /// Requires the `macos_15_0` feature flag to be enabled.
154    #[cfg(feature = "macos_15_0")]
155    #[allow(clippy::missing_errors_doc)]
156    pub fn capture_dynamic_range(&self) -> SCResult<SCCaptureDynamicRange> {
157        let mut raw = 0_i32;
158        let available = unsafe {
159            crate::ffi::sc_stream_configuration_get_capture_dynamic_range(
160                self.as_ptr(),
161                &raw mut raw,
162            )
163        };
164        if !available {
165            return Err(SCError::feature_not_available(
166                "SCStreamConfiguration.captureDynamicRange",
167                "15.0",
168            ));
169        }
170        SCCaptureDynamicRange::from_raw(raw).ok_or_else(|| SCError::UnknownValue {
171            type_name: "SCCaptureDynamicRange",
172            raw: i64::from(raw),
173        })
174    }
175}
176
177#[cfg(all(test, feature = "macos_15_0"))]
178mod tests {
179    use super::{SCCaptureDynamicRange, SCStreamConfiguration};
180
181    #[test]
182    fn bridge_rejects_unknown_capture_dynamic_range_raw_values() {
183        let mut config = SCStreamConfiguration::new();
184        config
185            .set_capture_dynamic_range(SCCaptureDynamicRange::HDRCanonicalDisplay)
186            .expect("macOS 15.0 or later");
187        for raw in [3, -1, i32::MAX, i32::MIN] {
188            let applied = unsafe {
189                crate::ffi::sc_stream_configuration_set_capture_dynamic_range(config.as_ptr(), raw)
190            };
191            assert!(!applied, "the bridge accepted raw value {raw}");
192            assert_eq!(
193                config.capture_dynamic_range(),
194                Ok(SCCaptureDynamicRange::HDRCanonicalDisplay)
195            );
196        }
197    }
198}