Skip to main content

windows_capture/
capture.rs

1use std::mem;
2use std::os::windows::prelude::AsRawHandle;
3use std::sync::atomic::{self, AtomicBool};
4use std::sync::{Arc, mpsc};
5use std::thread::{self, JoinHandle};
6
7use parking_lot::Mutex;
8use windows::Win32::Foundation::{ERROR_INVALID_THREAD_ID, HANDLE, LPARAM, WPARAM};
9use windows::Win32::Graphics::Direct3D11::{ID3D11Device, ID3D11DeviceContext};
10use windows::Win32::System::Threading::{GetCurrentThreadId, GetThreadId};
11use windows::Win32::System::WinRT::{
12    CreateDispatcherQueueController, DQTAT_COM_NONE, DQTYPE_THREAD_CURRENT, DispatcherQueueOptions,
13};
14use windows::Win32::UI::WindowsAndMessaging::{
15    DispatchMessageW, GetMessageW, MSG, PostQuitMessage, PostThreadMessageW, TranslateMessage, WM_QUIT,
16};
17use windows::core::Result as WindowsResult;
18use windows_future::AsyncActionCompletedHandler;
19
20use crate::d3d11::{self, create_d3d_device};
21use crate::frame::Frame;
22use crate::graphics_capture_api::{self, GraphicsCaptureApi, InternalCaptureControl};
23use crate::settings::{GraphicsCaptureItemType, Settings};
24use crate::winrt::WinRT;
25
26const fn dispatcher_queue_options() -> DispatcherQueueOptions {
27    DispatcherQueueOptions {
28        dwSize: mem::size_of::<DispatcherQueueOptions>() as u32,
29        threadType: DQTYPE_THREAD_CURRENT,
30        apartmentType: DQTAT_COM_NONE,
31    }
32}
33
34fn run_message_loop<E>() -> Result<(), GraphicsCaptureApiError<E>> {
35    let mut message = MSG::default();
36
37    loop {
38        match unsafe { GetMessageW(&mut message, None, 0, 0).0 } {
39            -1 => return Err(GraphicsCaptureApiError::FailedToRunMessageLoop),
40            0 => return Ok(()),
41            _ => unsafe {
42                let _ = TranslateMessage(&message);
43                DispatchMessageW(&message);
44            },
45        }
46    }
47}
48
49fn join_capture_thread<E>(
50    thread_handle: JoinHandle<Result<(), GraphicsCaptureApiError<E>>>,
51) -> Result<(), CaptureControlError<E>> {
52    match thread_handle.join() {
53        Ok(result) => {
54            result?;
55            Ok(())
56        }
57        Err(_) => Err(CaptureControlError::FailedToJoinThread),
58    }
59}
60
61#[derive(thiserror::Error, Debug)]
62/// Errors that can occur while controlling a running capture session via [`CaptureControl`].
63///
64/// This error wraps lower-level errors from the Windows Graphics Capture pipeline, as well as
65/// thread-control failures when starting/stopping the background capture thread.
66pub enum CaptureControlError<E> {
67    /// Joining the background capture thread failed (panic or OS-level join error).
68    ///
69    /// Returned by [`CaptureControl::wait`] and [`CaptureControl::stop`] if the internal thread
70    /// panicked or could not be joined.
71    #[error("Failed to join thread")]
72    FailedToJoinThread,
73    /// The [`std::thread::JoinHandle`] was already taken out of the struct (for example by calling
74    /// [`CaptureControl::into_thread_handle`]) so the operation cannot proceed.
75    #[error("Thread handle is taken out of the struct")]
76    ThreadHandleIsTaken,
77    /// Failed to post a WM_QUIT message to the capture thread to request shutdown.
78    ///
79    /// This can happen if the thread is no longer alive or Windows refuses the message.
80    #[error("Failed to post thread message")]
81    FailedToPostThreadMessage,
82    /// The user-provided handler returned an error after capture stopped.
83    ///
84    /// This variant carries the handler's error type.
85    #[error("Stopped handler error: {0}")]
86    StoppedHandlerError(E),
87    /// A lower-level error from the graphics capture pipeline.
88    ///
89    /// Wraps [`GraphicsCaptureApiError`].
90    #[error("Windows capture error: {0}")]
91    GraphicsCaptureApiError(#[from] GraphicsCaptureApiError<E>),
92}
93
94/// Used to control the capture session
95pub struct CaptureControl<T: GraphicsCaptureApiHandler + Send + 'static, E> {
96    thread_handle: Option<JoinHandle<Result<(), GraphicsCaptureApiError<E>>>>,
97    halt_handle: Arc<AtomicBool>,
98    callback: Arc<Mutex<T>>,
99}
100
101impl<T: GraphicsCaptureApiHandler + Send + 'static, E> CaptureControl<T, E> {
102    /// Constructs a new [`CaptureControl`].
103    #[inline]
104    #[must_use]
105    pub const fn new(
106        thread_handle: JoinHandle<Result<(), GraphicsCaptureApiError<E>>>,
107        halt_handle: Arc<AtomicBool>,
108        callback: Arc<Mutex<T>>,
109    ) -> Self {
110        Self { thread_handle: Some(thread_handle), halt_handle, callback }
111    }
112
113    /// Checks whether the capture thread has finished.
114    #[inline]
115    #[must_use]
116    pub fn is_finished(&self) -> bool {
117        self.thread_handle.as_ref().is_none_or(std::thread::JoinHandle::is_finished)
118    }
119
120    /// Gets the join handle for the capture thread.
121    #[inline]
122    #[must_use]
123    pub fn into_thread_handle(self) -> JoinHandle<Result<(), GraphicsCaptureApiError<E>>> {
124        self.thread_handle.unwrap()
125    }
126
127    /// Gets the halt handle used to pause the capture thread.
128    #[inline]
129    #[must_use]
130    pub fn halt_handle(&self) -> Arc<AtomicBool> {
131        self.halt_handle.clone()
132    }
133
134    /// Gets the callback struct used to call struct methods directly.
135    #[inline]
136    #[must_use]
137    pub fn callback(&self) -> Arc<Mutex<T>> {
138        self.callback.clone()
139    }
140
141    /// Waits for the capture thread to stop.
142    ///
143    /// # Errors
144    ///
145    /// - [`CaptureControlError::FailedToJoinThread`] when joining the internal thread fails
146    /// - [`CaptureControlError::ThreadHandleIsTaken`] when the thread handle was previously taken
147    ///   via [`CaptureControl::into_thread_handle`]
148    #[inline]
149    pub fn wait(mut self) -> Result<(), CaptureControlError<E>> {
150        if let Some(thread_handle) = self.thread_handle.take() {
151            join_capture_thread(thread_handle)?;
152        } else {
153            return Err(CaptureControlError::ThreadHandleIsTaken);
154        }
155
156        Ok(())
157    }
158
159    /// Gracefully requests the capture thread to stop and waits for it to finish.
160    ///
161    /// This posts a WM_QUIT to the capture thread and joins it.
162    ///
163    /// # Errors
164    ///
165    /// - [`CaptureControlError::FailedToPostThreadMessage`] when posting WM_QUIT to the thread
166    ///   fails and the thread is still running
167    /// - [`CaptureControlError::FailedToJoinThread`] when joining the internal thread fails
168    /// - [`CaptureControlError::ThreadHandleIsTaken`] when the thread handle was previously taken
169    ///   via [`CaptureControl::into_thread_handle`]
170    #[inline]
171    pub fn stop(mut self) -> Result<(), CaptureControlError<E>> {
172        self.halt_handle.store(true, atomic::Ordering::Relaxed);
173
174        if let Some(thread_handle) = self.thread_handle.take() {
175            let handle = thread_handle.as_raw_handle();
176            let handle = HANDLE(handle);
177            let thread_id = unsafe { GetThreadId(handle) };
178
179            if thread_id == 0 {
180                if thread_handle.is_finished() {
181                    join_capture_thread(thread_handle)?;
182                    return Ok(());
183                }
184
185                return Err(CaptureControlError::FailedToPostThreadMessage);
186            }
187
188            loop {
189                match unsafe { PostThreadMessageW(thread_id, WM_QUIT, WPARAM::default(), LPARAM::default()) } {
190                    Ok(()) => break,
191                    Err(error) => {
192                        if thread_handle.is_finished() {
193                            break;
194                        }
195
196                        if error.code() != windows::core::HRESULT::from_win32(ERROR_INVALID_THREAD_ID.0) {
197                            return Err(CaptureControlError::FailedToPostThreadMessage);
198                        }
199
200                        thread::yield_now();
201                    }
202                }
203            }
204
205            join_capture_thread(thread_handle)?;
206        } else {
207            return Err(CaptureControlError::ThreadHandleIsTaken);
208        }
209
210        Ok(())
211    }
212}
213
214#[derive(thiserror::Error, Eq, PartialEq, Clone, Debug)]
215/// Errors that can occur while initializing and running the Windows Graphics Capture pipeline.
216pub enum GraphicsCaptureApiError<E> {
217    /// Joining the worker thread failed (panic or OS-level join error).
218    #[error("Failed to join thread")]
219    FailedToJoinThread,
220    /// Failed to initialize the Windows Runtime for multithreaded apartment.
221    ///
222    /// Occurs when `RoInitialize(RO_INIT_MULTITHREADED)` returns an error other than `S_FALSE`.
223    #[error("Failed to initialize WinRT")]
224    FailedToInitWinRT,
225    /// Creating the dispatcher queue controller for the message loop failed.
226    #[error("Failed to create dispatcher queue controller")]
227    FailedToCreateDispatcherQueueController,
228    /// Shutting down the dispatcher queue failed.
229    #[error("Failed to shut down dispatcher queue")]
230    FailedToShutdownDispatcherQueue,
231    /// Registering the dispatcher queue completion handler failed.
232    #[error("Failed to set dispatcher queue completed handler")]
233    FailedToSetDispatcherQueueCompletedHandler,
234    /// The Windows message loop for the capture thread failed.
235    #[error("Failed to run the capture thread message loop")]
236    FailedToRunMessageLoop,
237    /// The free-threaded capture worker exited before publishing its control handles.
238    #[error("Failed to initialize the capture thread")]
239    FailedToStartCaptureThread,
240    /// The provided item could not be converted into a `GraphicsCaptureItem`.
241    ///
242    /// This happens when
243    /// [`crate::settings::TryIntoCaptureItemWithDetails::try_into_capture_item_with_details`]
244    /// fails for the item passed in [`crate::settings::Settings`].
245    #[error("Failed to convert item to `GraphicsCaptureItem`")]
246    ItemConvertFailed,
247    /// Underlying Direct3D (D3D11) error.
248    ///
249    /// Wraps [`crate::d3d11::Error`].
250    #[error("DirectX error: {0}")]
251    DirectXError(#[from] d3d11::Error),
252    /// Error produced by the Windows Graphics Capture API wrapper.
253    ///
254    /// Wraps [`crate::graphics_capture_api::Error`].
255    #[error("Graphics capture error: {0}")]
256    GraphicsCaptureApiError(graphics_capture_api::Error),
257    /// Error returned by the user handler when constructing it via
258    /// [`GraphicsCaptureApiHandler::new`].
259    #[error("New handler error: {0}")]
260    NewHandlerError(E),
261    /// Error returned by the user handler during frame processing via
262    /// [`GraphicsCaptureApiHandler::on_frame_arrived`] or from
263    /// [`GraphicsCaptureApiHandler::on_closed`].
264    #[error("Frame handler error: {0}")]
265    FrameHandlerError(E),
266}
267
268/// The context provided to the capture handler.
269pub struct Context<Flags> {
270    /// The flags that are retrieved from the settings.
271    pub flags: Flags,
272    /// The Direct3D device.
273    pub device: ID3D11Device,
274    /// The Direct3D device context.
275    pub device_context: ID3D11DeviceContext,
276}
277
278/// Trait implemented by types that handle graphics capture events.
279pub trait GraphicsCaptureApiHandler: Sized {
280    /// The type of flags used to get the values from the settings.
281    type Flags;
282
283    /// The type of error that can occur during capture. The error will be returned from the
284    /// [`CaptureControl`] and [`GraphicsCaptureApiHandler::start`] functions.
285    type Error: Send + Sync;
286
287    /// Starts the capture and takes control of the current thread.
288    #[inline]
289    fn start<T: TryInto<GraphicsCaptureItemType>>(
290        settings: Settings<Self::Flags, T>,
291    ) -> Result<(), GraphicsCaptureApiError<Self::Error>>
292    where
293        Self: Send + 'static,
294        <Self as GraphicsCaptureApiHandler>::Flags: Send,
295    {
296        // Initialize WinRT
297        let _winrt = WinRT::new().map_err(|_| GraphicsCaptureApiError::FailedToInitWinRT)?;
298
299        // Create a dispatcher queue for the current thread
300        let controller = unsafe {
301            CreateDispatcherQueueController(dispatcher_queue_options())
302                .map_err(|_| GraphicsCaptureApiError::FailedToCreateDispatcherQueueController)?
303        };
304
305        // Get current thread ID
306        let thread_id = unsafe { GetCurrentThreadId() };
307
308        // Create Direct3D device and context
309        let (d3d_device, d3d_device_context) = create_d3d_device()?;
310
311        // Start capture
312        let result = Arc::new(Mutex::new(None));
313
314        let ctx =
315            Context { flags: settings.flags, device: d3d_device.clone(), device_context: d3d_device_context.clone() };
316
317        let callback = Arc::new(Mutex::new(Self::new(ctx).map_err(GraphicsCaptureApiError::NewHandlerError)?));
318
319        let mut capture = GraphicsCaptureApi::new(
320            d3d_device,
321            d3d_device_context,
322            settings.item.try_into().map_err(|_| GraphicsCaptureApiError::ItemConvertFailed)?,
323            callback,
324            settings.cursor_capture_settings,
325            settings.draw_border_settings,
326            settings.secondary_window_settings,
327            settings.minimum_update_interval_settings,
328            settings.dirty_region_settings,
329            settings.color_format,
330            thread_id,
331            result.clone(),
332        )
333        .map_err(GraphicsCaptureApiError::GraphicsCaptureApiError)?;
334        capture.start_capture().map_err(GraphicsCaptureApiError::GraphicsCaptureApiError)?;
335
336        // Message loop
337        run_message_loop()?;
338
339        // Shut down dispatcher queue
340        let async_action =
341            controller.ShutdownQueueAsync().map_err(|_| GraphicsCaptureApiError::FailedToShutdownDispatcherQueue)?;
342
343        async_action
344            .SetCompleted(&AsyncActionCompletedHandler::new(move |_, _| -> WindowsResult<()> {
345                unsafe { PostQuitMessage(0) };
346                Ok(())
347            }))
348            .map_err(|_| GraphicsCaptureApiError::FailedToSetDispatcherQueueCompletedHandler)?;
349
350        // Final message loop
351        run_message_loop()?;
352
353        // Stop capture
354        capture.stop_capture();
355
356        // Check handler result
357        let result = result.lock().take();
358        if let Some(e) = result {
359            return Err(GraphicsCaptureApiError::FrameHandlerError(e));
360        }
361
362        Ok(())
363    }
364
365    /// Starts the capture without taking control of the current thread.
366    #[inline]
367    fn start_free_threaded<T: TryInto<GraphicsCaptureItemType> + Send + 'static>(
368        settings: Settings<Self::Flags, T>,
369    ) -> Result<CaptureControl<Self, Self::Error>, GraphicsCaptureApiError<Self::Error>>
370    where
371        Self: Send + 'static,
372        <Self as GraphicsCaptureApiHandler>::Flags: Send,
373    {
374        let (halt_sender, halt_receiver) = mpsc::channel::<Arc<AtomicBool>>();
375        let (callback_sender, callback_receiver) = mpsc::channel::<Arc<Mutex<Self>>>();
376
377        let thread_handle = thread::spawn(move || -> Result<(), GraphicsCaptureApiError<Self::Error>> {
378            // Initialize WinRT
379            let _winrt = WinRT::new().map_err(|_| GraphicsCaptureApiError::FailedToInitWinRT)?;
380
381            // Create a dispatcher queue for the current thread
382            let controller = unsafe {
383                CreateDispatcherQueueController(dispatcher_queue_options())
384                    .map_err(|_| GraphicsCaptureApiError::FailedToCreateDispatcherQueueController)?
385            };
386
387            // Get current thread ID
388            let thread_id = unsafe { GetCurrentThreadId() };
389
390            // Create direct3d device and context
391            let (d3d_device, d3d_device_context) = create_d3d_device()?;
392
393            // Start capture
394            let result = Arc::new(Mutex::new(None));
395
396            let ctx = Context {
397                flags: settings.flags,
398                device: d3d_device.clone(),
399                device_context: d3d_device_context.clone(),
400            };
401
402            let callback = Arc::new(Mutex::new(Self::new(ctx).map_err(GraphicsCaptureApiError::NewHandlerError)?));
403
404            let mut capture = GraphicsCaptureApi::new(
405                d3d_device,
406                d3d_device_context,
407                settings.item.try_into().map_err(|_| GraphicsCaptureApiError::ItemConvertFailed)?,
408                callback.clone(),
409                settings.cursor_capture_settings,
410                settings.draw_border_settings,
411                settings.secondary_window_settings,
412                settings.minimum_update_interval_settings,
413                settings.dirty_region_settings,
414                settings.color_format,
415                thread_id,
416                result.clone(),
417            )
418            .map_err(GraphicsCaptureApiError::GraphicsCaptureApiError)?;
419
420            capture.start_capture().map_err(GraphicsCaptureApiError::GraphicsCaptureApiError)?;
421
422            // Send halt handle
423            let halt_handle = capture.halt_handle();
424            halt_sender.send(halt_handle).map_err(|_| GraphicsCaptureApiError::FailedToStartCaptureThread)?;
425
426            // Send callback
427            callback_sender.send(callback).map_err(|_| GraphicsCaptureApiError::FailedToStartCaptureThread)?;
428
429            // Message loop
430            run_message_loop()?;
431
432            // Shutdown dispatcher queue
433            let async_action = controller
434                .ShutdownQueueAsync()
435                .map_err(|_| GraphicsCaptureApiError::FailedToShutdownDispatcherQueue)?;
436
437            async_action
438                .SetCompleted(&AsyncActionCompletedHandler::new(move |_, _| -> Result<(), windows::core::Error> {
439                    unsafe { PostQuitMessage(0) };
440                    Ok(())
441                }))
442                .map_err(|_| GraphicsCaptureApiError::FailedToSetDispatcherQueueCompletedHandler)?;
443
444            // Final message loop
445            run_message_loop()?;
446
447            // Stop capture
448            capture.stop_capture();
449
450            // Check handler result
451            let result = result.lock().take();
452            if let Some(e) = result {
453                return Err(GraphicsCaptureApiError::FrameHandlerError(e));
454            }
455
456            Ok(())
457        });
458
459        let Ok(halt_handle) = halt_receiver.recv() else {
460            match thread_handle.join() {
461                Ok(Err(error)) => return Err(error),
462                Ok(Ok(())) => return Err(GraphicsCaptureApiError::FailedToStartCaptureThread),
463                Err(_) => {
464                    return Err(GraphicsCaptureApiError::FailedToJoinThread);
465                }
466            }
467        };
468
469        let Ok(callback) = callback_receiver.recv() else {
470            match thread_handle.join() {
471                Ok(Err(error)) => return Err(error),
472                Ok(Ok(())) => return Err(GraphicsCaptureApiError::FailedToStartCaptureThread),
473                Err(_) => {
474                    return Err(GraphicsCaptureApiError::FailedToJoinThread);
475                }
476            }
477        };
478
479        Ok(CaptureControl::new(thread_handle, halt_handle, callback))
480    }
481
482    /// Function that will be called to create the struct. The flags can be
483    /// passed from settings.
484    fn new(ctx: Context<Self::Flags>) -> Result<Self, Self::Error>;
485
486    /// Called every time a new frame is available.
487    fn on_frame_arrived(
488        &mut self,
489        frame: &mut Frame,
490        capture_control: InternalCaptureControl,
491    ) -> Result<(), Self::Error>;
492
493    /// Optional handler called when the capture item (usually a window) closes.
494    #[inline]
495    fn on_closed(&mut self) -> Result<(), Self::Error> {
496        Ok(())
497    }
498}