windows_capture/
graphics_capture_api.rs

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