Skip to main content

screencapturekit/stream/configuration/
captured_frames.rs

1use super::internal::SCStreamConfiguration;
2use crate::cm::CMTime;
3#[cfg(feature = "macos_14_0")]
4use crate::error::{SCError, SCResult};
5
6#[cfg(feature = "macos_14_0")]
7use super::SCCaptureResolutionType;
8
9/// Smallest `queueDepth` `ScreenCaptureKit` accepts.
10pub const MIN_QUEUE_DEPTH: u32 = 3;
11/// Largest `queueDepth` `ScreenCaptureKit` accepts.
12pub const MAX_QUEUE_DEPTH: u32 = 8;
13
14impl SCStreamConfiguration {
15    /// Set the queue depth for frame buffering.
16    ///
17    /// `ScreenCaptureKit` documents a valid range of
18    /// [`MIN_QUEUE_DEPTH`] to [`MAX_QUEUE_DEPTH`] (3–8). Values outside that
19    /// range are **clamped** rather than forwarded: `SCStream` rejects an
20    /// out-of-range depth when capture starts, and it does so with a generic
21    /// `SCStreamErrorFailedToStart`, which is far harder to diagnose than a
22    /// silently clamped buffer count. Read the value back with
23    /// [`queue_depth`](Self::queue_depth) to see what was applied.
24    ///
25    /// Larger depths absorb more downstream jitter at the cost of latency and
26    /// memory (each slot holds a full frame); smaller depths minimise latency
27    /// but drop frames sooner when the consumer stalls.
28    pub fn set_queue_depth(&mut self, queue_depth: u32) -> &mut Self {
29        let clamped = queue_depth.clamp(MIN_QUEUE_DEPTH, MAX_QUEUE_DEPTH);
30        // `clamped` is 3..=8, so the isize cast can never wrap.
31        #[allow(clippy::cast_possible_wrap)]
32        unsafe {
33            crate::ffi::sc_stream_configuration_set_queue_depth(self.as_ptr(), clamped as isize);
34        }
35        self
36    }
37
38    /// Set the queue depth (builder pattern)
39    ///
40    /// See [`set_queue_depth`](Self::set_queue_depth) for the clamping rules.
41    #[must_use]
42    pub fn with_queue_depth(mut self, queue_depth: u32) -> Self {
43        self.set_queue_depth(queue_depth);
44        self
45    }
46
47    /// Get the configured queue depth.
48    pub fn queue_depth(&self) -> u32 {
49        let raw = unsafe { crate::ffi::sc_stream_configuration_get_queue_depth(self.as_ptr()) };
50        u32::try_from(raw).unwrap_or(0)
51    }
52
53    /// Set the minimum frame interval
54    pub fn set_minimum_frame_interval(&mut self, cm_time: &CMTime) -> &mut Self {
55        unsafe {
56            crate::ffi::sc_stream_configuration_set_minimum_frame_interval(
57                self.as_ptr(),
58                cm_time.value,
59                cm_time.timescale,
60                cm_time.flags,
61                cm_time.epoch,
62            );
63        }
64        self
65    }
66
67    /// Set the minimum frame interval (builder pattern)
68    #[must_use]
69    pub fn with_minimum_frame_interval(mut self, cm_time: &CMTime) -> Self {
70        self.set_minimum_frame_interval(cm_time);
71        self
72    }
73
74    pub fn minimum_frame_interval(&self) -> CMTime {
75        unsafe {
76            let mut value: i64 = 0;
77            let mut timescale: i32 = 0;
78            let mut flags: u32 = 0;
79            let mut epoch: i64 = 0;
80
81            crate::ffi::sc_stream_configuration_get_minimum_frame_interval(
82                self.as_ptr(),
83                &raw mut value,
84                &raw mut timescale,
85                &raw mut flags,
86                &raw mut epoch,
87            );
88
89            CMTime {
90                value,
91                timescale,
92                flags,
93                epoch,
94            }
95        }
96    }
97
98    /// Get the target frame rate in frames per second
99    ///
100    /// Converts the minimum frame interval (`CMTime`) to FPS.
101    /// Returns 0 if the frame interval is zero or invalid (i.e. the stream is
102    /// uncapped — see [`set_fps`](Self::set_fps)).
103    #[allow(clippy::cast_possible_truncation)]
104    pub fn fps(&self) -> u32 {
105        let cm_time = self.minimum_frame_interval();
106        if cm_time.value == 0 || cm_time.timescale == 0 {
107            return 0;
108        }
109        #[allow(clippy::cast_sign_loss)]
110        let fps = (i64::from(cm_time.timescale) / cm_time.value) as u32;
111        fps
112    }
113
114    /// Set the target frame rate in frames per second
115    ///
116    /// This is a convenience method that creates the appropriate `CMTime` for the given FPS.
117    /// For example, 60 FPS creates a frame interval of 1/60 second.
118    ///
119    /// # Arguments
120    /// * `fps` - Target frames per second (e.g., 30, 60, 120)
121    ///
122    /// Passing `0` sets the interval to `kCMTimeZero` (`0/1`), which is
123    /// `ScreenCaptureKit`'s "no minimum interval — deliver frames as fast as
124    /// the source produces them" value. The naive `1/0` this used to build was
125    /// a zero-timescale `CMTime`: still flagged valid, but degenerate, so
126    /// `CMTimeGetSeconds` divides by zero and the stream's frame pacing is
127    /// undefined.
128    ///
129    /// # Examples
130    ///
131    /// ```no_run
132    /// use screencapturekit::stream::configuration::SCStreamConfiguration;
133    ///
134    /// let config = SCStreamConfiguration::new()
135    ///     .with_fps(60);
136    /// ```
137    pub fn set_fps(&mut self, fps: u32) -> &mut Self {
138        let cm_time = if fps == 0 {
139            CMTime::new(0, 1)
140        } else {
141            #[allow(clippy::cast_possible_wrap)]
142            CMTime::new(1, fps as i32)
143        };
144        self.set_minimum_frame_interval(&cm_time)
145    }
146
147    /// Set the target frame rate (builder pattern)
148    ///
149    /// See [`set_fps`](Self::set_fps) for details.
150    #[must_use]
151    pub fn with_fps(mut self, fps: u32) -> Self {
152        self.set_fps(fps);
153        self
154    }
155
156    /// Set the capture resolution type (macOS 14.0+)
157    ///
158    /// Controls how the capture resolution is determined.
159    ///
160    /// # Arguments
161    /// * `resolution_type` - The resolution strategy to use
162    ///
163    /// # Examples
164    ///
165    /// ```no_run
166    /// use screencapturekit::stream::configuration::{SCStreamConfiguration, SCCaptureResolutionType};
167    ///
168    /// let config = SCStreamConfiguration::new()
169    ///     .with_capture_resolution_type(SCCaptureResolutionType::Best)
170    ///     .expect("macOS 14.0 or later");
171    /// ```
172    #[cfg(feature = "macos_14_0")]
173    #[allow(clippy::missing_errors_doc)]
174    pub fn set_capture_resolution_type(
175        &mut self,
176        resolution_type: SCCaptureResolutionType,
177    ) -> SCResult<&mut Self> {
178        let applied = unsafe {
179            crate::ffi::sc_stream_configuration_set_capture_resolution_type(
180                self.as_ptr(),
181                resolution_type as i32,
182            )
183        };
184        applied.then_some(self).ok_or_else(|| {
185            SCError::feature_not_available("SCStreamConfiguration.captureResolution", "14.0")
186        })
187    }
188
189    /// Set the capture resolution type (builder pattern, macOS 14.0+)
190    #[cfg(feature = "macos_14_0")]
191    #[allow(clippy::missing_errors_doc)]
192    pub fn with_capture_resolution_type(
193        mut self,
194        resolution_type: SCCaptureResolutionType,
195    ) -> SCResult<Self> {
196        self.set_capture_resolution_type(resolution_type)?;
197        Ok(self)
198    }
199
200    /// Get the capture resolution type (macOS 14.0+)
201    #[cfg(feature = "macos_14_0")]
202    #[allow(clippy::missing_errors_doc)]
203    pub fn capture_resolution_type(&self) -> SCResult<SCCaptureResolutionType> {
204        let mut raw = 0_i32;
205        let available = unsafe {
206            crate::ffi::sc_stream_configuration_get_capture_resolution_type(
207                self.as_ptr(),
208                &raw mut raw,
209            )
210        };
211        if !available {
212            return Err(SCError::feature_not_available(
213                "SCStreamConfiguration.captureResolution",
214                "14.0",
215            ));
216        }
217        SCCaptureResolutionType::from_raw(raw).ok_or_else(|| SCError::UnknownValue {
218            type_name: "SCCaptureResolutionType",
219            raw: i64::from(raw),
220        })
221    }
222}
223
224#[cfg(all(test, feature = "macos_14_0"))]
225mod tests {
226    use super::{SCCaptureResolutionType, SCStreamConfiguration};
227
228    #[test]
229    fn bridge_rejects_unknown_capture_resolution_raw_values() {
230        let mut config = SCStreamConfiguration::new();
231        config
232            .set_capture_resolution_type(SCCaptureResolutionType::Nominal)
233            .expect("macOS 14.0 or later");
234        for raw in [3, -1, i32::MAX, i32::MIN] {
235            let applied = unsafe {
236                crate::ffi::sc_stream_configuration_set_capture_resolution_type(
237                    config.as_ptr(),
238                    raw,
239                )
240            };
241            assert!(!applied, "the bridge accepted raw value {raw}");
242            assert_eq!(
243                config.capture_resolution_type(),
244                Ok(SCCaptureResolutionType::Nominal)
245            );
246        }
247    }
248}