Skip to main content

windows_capture/
graphics_capture_api.rs

1use std::sync::Arc;
2use std::sync::atomic::{self, AtomicBool, AtomicI32};
3
4use parking_lot::Mutex;
5use windows::Foundation::Metadata::ApiInformation;
6use windows::Foundation::TypedEventHandler;
7use windows::Graphics::Capture::{
8    Direct3D11CaptureFramePool, GraphicsCaptureDirtyRegionMode, GraphicsCaptureItem, GraphicsCaptureSession,
9};
10use windows::Graphics::DirectX::Direct3D11::IDirect3DDevice;
11use windows::Graphics::DirectX::DirectXPixelFormat;
12use windows::Win32::Foundation::{LPARAM, WPARAM};
13use windows::Win32::Graphics::Direct3D11::{D3D11_TEXTURE2D_DESC, ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D};
14use windows::Win32::System::WinRT::Direct3D11::IDirect3DDxgiInterfaceAccess;
15use windows::Win32::UI::WindowsAndMessaging::{PostThreadMessageW, WM_QUIT};
16use windows::core::{HSTRING, IInspectable, Interface};
17
18use crate::capture::GraphicsCaptureApiHandler;
19use crate::d3d11::{self, SendDirectX, create_direct3d_device};
20use crate::frame::Frame;
21use crate::settings::{
22    ColorFormat, CursorCaptureSettings, DirtyRegionSettings, DrawBorderSettings, GraphicsCaptureItemType,
23    MinimumUpdateIntervalSettings, SecondaryWindowSettings,
24};
25
26#[derive(thiserror::Error, Eq, PartialEq, Clone, Debug)]
27/// Errors that can occur when configuring or running a Windows Graphics Capture session.
28pub enum Error {
29    /// The Windows Graphics Capture API is not available on this OS.
30    #[error("The Graphics Capture API is not supported on this platform.")]
31    Unsupported,
32    /// Toggling cursor capture isn't supported on this platform/OS build.
33    #[error("Toggling cursor capture is not supported by the Graphics Capture API on this platform.")]
34    CursorConfigUnsupported,
35    /// Toggling the capture border isn't supported on this platform/OS build.
36    #[error("Toggling the capture border is not supported by the Graphics Capture API on this platform.")]
37    BorderConfigUnsupported,
38    /// Capturing secondary (owned) windows isn't supported on this platform/OS build.
39    #[error("Capturing secondary windows is not supported by the Graphics Capture API on this platform.")]
40    SecondaryWindowsUnsupported,
41    /// Setting a minimum frame update interval isn't supported on this platform/OS build.
42    #[error("Setting a minimum update interval is not supported by the Graphics Capture API on this platform.")]
43    MinimumUpdateIntervalUnsupported,
44    /// Dirty region tracking isn't supported on this platform/OS build.
45    #[error("Dirty region tracking is not supported by the Graphics Capture API on this platform.")]
46    DirtyRegionUnsupported,
47    /// Capture has already been started for this session.
48    #[error("The capture has already been started.")]
49    AlreadyStarted,
50    /// Underlying Direct3D (D3D11) error.
51    ///
52    /// Wraps [`crate::d3d11::Error`].
53    #[error("DirectX error: {0}")]
54    DirectXError(#[from] d3d11::Error),
55    /// Window helper error.
56    ///
57    /// Wraps [`crate::window::Error`].
58    #[error("Window error: {0}")]
59    WindowError(#[from] crate::window::Error),
60    /// A Windows Runtime/Win32 API call failed.
61    ///
62    /// Wraps [`windows::core::Error`].
63    #[error("Windows API error: {0}")]
64    WindowsError(#[from] windows::core::Error),
65}
66
67/// Provides a way to gracefully stop the capture session thread.
68pub struct InternalCaptureControl {
69    stop: Arc<AtomicBool>,
70}
71
72impl InternalCaptureControl {
73    /// Constructs a new `InternalCaptureControl` struct.
74    #[inline]
75    #[must_use]
76    pub const fn new(stop: Arc<AtomicBool>) -> Self {
77        Self { stop }
78    }
79
80    /// Signals the capture thread to stop.
81    #[inline]
82    pub fn stop(self) {
83        self.stop.store(true, atomic::Ordering::Relaxed);
84    }
85}
86
87/// Manages a graphics capture session using the Windows Graphics Capture API.
88pub struct GraphicsCaptureApi {
89    /// The [`windows::Graphics::Capture::GraphicsCaptureItem`] to be captured (e.g., a window or
90    /// monitor).
91    item_with_details: GraphicsCaptureItemType,
92    /// The Direct3D 11 device used for the capture.
93    _d3d_device: ID3D11Device,
94    /// The WinRT [`windows::Graphics::DirectX::Direct3D11::IDirect3DDevice`] wrapper.
95    _direct3d_device: IDirect3DDevice,
96    /// The Direct3D 11 device context.
97    _d3d_device_context: ID3D11DeviceContext,
98    /// The frame pool that provides frames for the capture session.
99    frame_pool: Option<Arc<Direct3D11CaptureFramePool>>,
100    /// The graphics capture session itself.
101    session: Option<GraphicsCaptureSession>,
102    /// An atomic boolean flag to signal the capture thread to stop.
103    halt: Arc<AtomicBool>,
104    /// A flag indicating whether the capture session is currently active.
105    active: bool,
106    /// The token for the `Closed` event handler.
107    capture_closed_event_token: i64,
108    /// The token for the `FrameArrived` event handler.
109    frame_arrived_event_token: i64,
110}
111
112impl GraphicsCaptureApi {
113    /// Constructs a new [`GraphicsCaptureApi`] instance.
114    ///
115    /// For parameter details, see the type names in the signature; complex behavior is documented
116    /// inline below where relevant.
117    #[allow(clippy::too_many_arguments)]
118    #[inline]
119    pub fn new<T: GraphicsCaptureApiHandler<Error = E> + Send + 'static, E: Send + Sync + 'static>(
120        d3d_device: ID3D11Device,
121        d3d_device_context: ID3D11DeviceContext,
122        item_with_details: GraphicsCaptureItemType,
123        callback: Arc<Mutex<T>>,
124        cursor_capture_settings: CursorCaptureSettings,
125        draw_border_settings: DrawBorderSettings,
126        secondary_window_settings: SecondaryWindowSettings,
127        minimum_update_interval_settings: MinimumUpdateIntervalSettings,
128        dirty_region_settings: DirtyRegionSettings,
129        color_format: ColorFormat,
130        thread_id: u32,
131        result: Arc<Mutex<Option<E>>>,
132    ) -> Result<Self, Error> {
133        // Check support
134        if !Self::is_supported()? {
135            return Err(Error::Unsupported);
136        }
137
138        if cursor_capture_settings != CursorCaptureSettings::Default && !Self::is_cursor_settings_supported()? {
139            return Err(Error::CursorConfigUnsupported);
140        }
141
142        if draw_border_settings != DrawBorderSettings::Default && !Self::is_border_settings_supported()? {
143            return Err(Error::BorderConfigUnsupported);
144        }
145
146        if secondary_window_settings != SecondaryWindowSettings::Default && !Self::is_secondary_windows_supported()? {
147            return Err(Error::SecondaryWindowsUnsupported);
148        }
149
150        if minimum_update_interval_settings != MinimumUpdateIntervalSettings::Default
151            && !Self::is_minimum_update_interval_supported()?
152        {
153            return Err(Error::MinimumUpdateIntervalUnsupported);
154        }
155
156        if dirty_region_settings != DirtyRegionSettings::Default && !Self::is_dirty_region_supported()? {
157            return Err(Error::DirtyRegionUnsupported);
158        }
159
160        // Pre-calculate the title bar height so each frame doesn't need to do it
161        let title_bar_height = match item_with_details {
162            GraphicsCaptureItemType::Window((_, window)) => Some(window.title_bar_height()?),
163            GraphicsCaptureItemType::Monitor(_) => None,
164            GraphicsCaptureItemType::Unknown(_) => None,
165        };
166
167        let item = match &item_with_details {
168            GraphicsCaptureItemType::Window((item, _)) => item,
169            GraphicsCaptureItemType::Monitor((item, _)) => item,
170            GraphicsCaptureItemType::Unknown((item, _)) => item,
171        };
172
173        // Create DirectX devices
174        let direct3d_device = create_direct3d_device(&d3d_device)?;
175
176        let pixel_format = DirectXPixelFormat(color_format as i32);
177
178        // Create frame pool
179        let frame_pool = Direct3D11CaptureFramePool::Create(&direct3d_device, pixel_format, 1, item.Size()?)?;
180        let frame_pool = Arc::new(frame_pool);
181
182        // Create capture session
183        let session = frame_pool.CreateCaptureSession(item)?;
184
185        // Indicates if the capture is closed
186        let halt = Arc::new(AtomicBool::new(false));
187
188        // Set capture session closed event
189        let capture_closed_event_token =
190            item.Closed(&TypedEventHandler::<GraphicsCaptureItem, IInspectable>::new({
191                // Init
192                let callback_closed = callback.clone();
193                let halt_closed = halt.clone();
194                let result_closed = result.clone();
195
196                move |_, _| {
197                    halt_closed.store(true, atomic::Ordering::Relaxed);
198
199                    // Notify the user that the capture session is closed.
200                    let callback_closed = callback_closed.lock().on_closed();
201                    if let Err(e) = callback_closed {
202                        *result_closed.lock() = Some(e);
203                    }
204
205                    // Stop the message loop to allow the thread to exit gracefully.
206                    unsafe {
207                        PostThreadMessageW(thread_id, WM_QUIT, WPARAM::default(), LPARAM::default())?;
208                    };
209
210                    Result::Ok(())
211                }
212            }))?;
213
214        // Set frame pool frame arrived event
215        let frame_arrived_event_token = frame_pool.FrameArrived(&TypedEventHandler::<
216            Direct3D11CaptureFramePool,
217            IInspectable,
218        >::new({
219            // Init
220            let frame_pool_recreate = frame_pool.clone();
221            let halt_frame_pool = halt.clone();
222            let d3d_device_frame_pool = d3d_device.clone();
223            let context = d3d_device_context.clone();
224            let result_frame_pool = result;
225
226            let last_size = item.Size()?;
227            let last_size = Arc::new((AtomicI32::new(last_size.Width), AtomicI32::new(last_size.Height)));
228            let callback_frame_pool = callback;
229            let direct3d_device_recreate = SendDirectX::new(direct3d_device.clone());
230
231            move |frame, _| {
232                // Return early if the capture is closed
233                if halt_frame_pool.load(atomic::Ordering::Relaxed) {
234                    return Ok(());
235                }
236
237                // Get frame
238                let frame = frame
239                    .as_ref()
240                    .expect("FrameArrived parameter was None this should never happen.")
241                    .TryGetNextFrame()?;
242
243                // Get frame content size
244                let frame_content_size = frame.ContentSize()?;
245
246                // Get frame surface
247                let frame_surface = frame.Surface()?;
248
249                // Convert surface to texture
250                let frame_dxgi_interface = frame_surface.cast::<IDirect3DDxgiInterfaceAccess>()?;
251                let frame_texture = unsafe { frame_dxgi_interface.GetInterface::<ID3D11Texture2D>()? };
252
253                // Get texture settings
254                let mut desc = D3D11_TEXTURE2D_DESC::default();
255                unsafe { frame_texture.GetDesc(&mut desc) }
256
257                // Check if the size has been changed
258                if frame_content_size.Width != last_size.0.load(atomic::Ordering::Relaxed)
259                    || frame_content_size.Height != last_size.1.load(atomic::Ordering::Relaxed)
260                {
261                    let direct3d_device_recreate = &direct3d_device_recreate;
262                    frame_pool_recreate.Recreate(&direct3d_device_recreate.0, pixel_format, 1, frame_content_size)?;
263
264                    last_size.0.store(frame_content_size.Width, atomic::Ordering::Relaxed);
265                    last_size.1.store(frame_content_size.Height, atomic::Ordering::Relaxed);
266                }
267
268                // Create a frame
269                let mut frame = Frame::new(
270                    frame,
271                    &d3d_device_frame_pool,
272                    frame_surface,
273                    frame_texture,
274                    &context,
275                    desc,
276                    color_format,
277                    title_bar_height,
278                );
279
280                // Init internal capture control
281                let stop = Arc::new(AtomicBool::new(false));
282                let internal_capture_control = InternalCaptureControl::new(stop.clone());
283
284                // Send the frame to the callback struct
285                let result = callback_frame_pool.lock().on_frame_arrived(&mut frame, internal_capture_control);
286
287                // If the user signals to stop or an error occurs, halt the capture.
288                if stop.load(atomic::Ordering::Relaxed) || result.is_err() {
289                    if let Err(e) = result {
290                        *result_frame_pool.lock() = Some(e);
291                    }
292
293                    halt_frame_pool.store(true, atomic::Ordering::Relaxed);
294
295                    // Stop the message loop to allow the thread to exit gracefully.
296                    unsafe {
297                        PostThreadMessageW(thread_id, WM_QUIT, WPARAM::default(), LPARAM::default())?;
298                    };
299                }
300
301                Result::Ok(())
302            }
303        }))?;
304
305        if cursor_capture_settings != CursorCaptureSettings::Default {
306            if Self::is_cursor_settings_supported()? {
307                match cursor_capture_settings {
308                    CursorCaptureSettings::Default => (),
309                    CursorCaptureSettings::WithCursor => session.SetIsCursorCaptureEnabled(true)?,
310                    CursorCaptureSettings::WithoutCursor => session.SetIsCursorCaptureEnabled(false)?,
311                };
312            } else {
313                return Err(Error::CursorConfigUnsupported);
314            }
315        }
316
317        if draw_border_settings != DrawBorderSettings::Default {
318            if Self::is_border_settings_supported()? {
319                match draw_border_settings {
320                    DrawBorderSettings::Default => (),
321                    DrawBorderSettings::WithBorder => {
322                        session.SetIsBorderRequired(true)?;
323                    }
324                    DrawBorderSettings::WithoutBorder => session.SetIsBorderRequired(false)?,
325                }
326            } else {
327                return Err(Error::BorderConfigUnsupported);
328            }
329        }
330
331        if secondary_window_settings != SecondaryWindowSettings::Default {
332            if Self::is_secondary_windows_supported()? {
333                match secondary_window_settings {
334                    SecondaryWindowSettings::Default => (),
335                    SecondaryWindowSettings::Include => session.SetIncludeSecondaryWindows(true)?,
336                    SecondaryWindowSettings::Exclude => session.SetIncludeSecondaryWindows(false)?,
337                }
338            } else {
339                return Err(Error::SecondaryWindowsUnsupported);
340            }
341        }
342
343        if minimum_update_interval_settings != MinimumUpdateIntervalSettings::Default {
344            if Self::is_minimum_update_interval_supported()? {
345                match minimum_update_interval_settings {
346                    MinimumUpdateIntervalSettings::Default => (),
347                    MinimumUpdateIntervalSettings::Custom(duration) => {
348                        session.SetMinUpdateInterval(duration.into())?;
349                    }
350                }
351            } else {
352                return Err(Error::MinimumUpdateIntervalUnsupported);
353            }
354        }
355
356        if dirty_region_settings != DirtyRegionSettings::Default {
357            if Self::is_dirty_region_supported()? {
358                match dirty_region_settings {
359                    DirtyRegionSettings::Default => (),
360                    DirtyRegionSettings::ReportOnly => {
361                        session.SetDirtyRegionMode(GraphicsCaptureDirtyRegionMode::ReportOnly)?
362                    }
363                    DirtyRegionSettings::ReportAndRender => {
364                        session.SetDirtyRegionMode(GraphicsCaptureDirtyRegionMode::ReportAndRender)?
365                    }
366                }
367            } else {
368                return Err(Error::DirtyRegionUnsupported);
369            }
370        }
371
372        Ok(Self {
373            item_with_details,
374            _d3d_device: d3d_device,
375            _direct3d_device: direct3d_device,
376            _d3d_device_context: d3d_device_context,
377            frame_pool: Some(frame_pool),
378            session: Some(session),
379            halt,
380            active: false,
381            frame_arrived_event_token,
382            capture_closed_event_token,
383        })
384    }
385
386    /// Starts the capture.
387    ///
388    /// # Errors
389    ///
390    /// - [`Error::AlreadyStarted`] when called more than once
391    /// - [`Error::WindowsError`] for failures starting the capture session
392    #[inline]
393    pub fn start_capture(&mut self) -> Result<(), Error> {
394        if self.active {
395            return Err(Error::AlreadyStarted);
396        }
397
398        self.session.as_ref().unwrap().StartCapture()?;
399        self.active = true;
400
401        Ok(())
402    }
403
404    /// Stops the capture session and cleans up resources.
405    #[inline]
406    pub fn stop_capture(mut self) {
407        if let Some(frame_pool) = self.frame_pool.take() {
408            frame_pool
409                .RemoveFrameArrived(self.frame_arrived_event_token)
410                .expect("Failed to remove Frame Arrived event handler");
411
412            frame_pool.Close().expect("Failed to Close Frame Pool");
413        }
414
415        if let Some(session) = self.session.take() {
416            session.Close().expect("Failed to Close Capture Session");
417        }
418
419        let item = match &self.item_with_details {
420            GraphicsCaptureItemType::Window((item, _)) => item,
421            GraphicsCaptureItemType::Monitor((item, _)) => item,
422            GraphicsCaptureItemType::Unknown((item, _)) => item,
423        };
424
425        item.RemoveClosed(self.capture_closed_event_token)
426            .expect("Failed to remove Capture Session Closed event handler");
427    }
428
429    /// Gets the halt handle.
430    ///
431    /// # Returns
432    ///
433    /// Returns an `Arc<AtomicBool>` that can be used to check if the capture is halted.
434    #[inline]
435    #[must_use]
436    pub fn halt_handle(&self) -> Arc<AtomicBool> {
437        self.halt.clone()
438    }
439
440    /// Checks if the Windows Graphics Capture API is supported.
441    #[inline]
442    pub fn is_supported() -> Result<bool, Error> {
443        Ok(ApiInformation::IsApiContractPresentByMajor(&HSTRING::from("Windows.Foundation.UniversalApiContract"), 8)?
444            && GraphicsCaptureSession::IsSupported()?)
445    }
446
447    /// Checks if the cursor capture settings can be changed.
448    #[inline]
449    pub fn is_cursor_settings_supported() -> Result<bool, Error> {
450        Ok(ApiInformation::IsPropertyPresent(
451            &HSTRING::from("Windows.Graphics.Capture.GraphicsCaptureSession"),
452            &HSTRING::from("IsCursorCaptureEnabled"),
453        )? && Self::is_supported()?)
454    }
455
456    /// Checks if the capture border settings can be changed.
457    #[inline]
458    pub fn is_border_settings_supported() -> Result<bool, Error> {
459        Ok(ApiInformation::IsPropertyPresent(
460            &HSTRING::from("Windows.Graphics.Capture.GraphicsCaptureSession"),
461            &HSTRING::from("IsBorderRequired"),
462        )? && Self::is_supported()?)
463    }
464
465    /// Checks if capturing secondary windows is supported.
466    #[inline]
467    pub fn is_secondary_windows_supported() -> Result<bool, Error> {
468        Ok(ApiInformation::IsPropertyPresent(
469            &HSTRING::from("Windows.Graphics.Capture.GraphicsCaptureSession"),
470            &HSTRING::from("IncludeSecondaryWindows"),
471        )? && Self::is_supported()?)
472    }
473
474    /// Checks if setting a minimum update interval is supported.
475    #[inline]
476    pub fn is_minimum_update_interval_supported() -> Result<bool, Error> {
477        Ok(ApiInformation::IsPropertyPresent(
478            &HSTRING::from("Windows.Graphics.Capture.GraphicsCaptureSession"),
479            &HSTRING::from("MinUpdateInterval"),
480        )? && Self::is_supported()?)
481    }
482
483    /// Checks if dirty region tracking is supported.
484    #[inline]
485    pub fn is_dirty_region_supported() -> Result<bool, Error> {
486        Ok(ApiInformation::IsPropertyPresent(
487            &HSTRING::from("Windows.Graphics.Capture.GraphicsCaptureSession"),
488            &HSTRING::from("DirtyRegionMode"),
489        )? && Self::is_supported()?)
490    }
491}
492
493impl Drop for GraphicsCaptureApi {
494    fn drop(&mut self) {
495        if let Some(frame_pool) = self.frame_pool.take() {
496            let _ = frame_pool.RemoveFrameArrived(self.frame_arrived_event_token);
497            let _ = frame_pool.Close();
498        }
499
500        if let Some(session) = self.session.take() {
501            let _ = session.Close();
502        }
503
504        let item = match &self.item_with_details {
505            GraphicsCaptureItemType::Window((item, _)) => item,
506            GraphicsCaptureItemType::Monitor((item, _)) => item,
507            GraphicsCaptureItemType::Unknown((item, _)) => item,
508        };
509
510        let _ = item.RemoveClosed(self.capture_closed_event_token);
511    }
512}