windows_capture/
frame.rs

1use std::fs::{self};
2use std::path::Path;
3use std::{io, ptr, slice};
4
5use rayon::iter::{IntoParallelIterator, ParallelIterator};
6use windows::Foundation::TimeSpan;
7use windows::Graphics::DirectX::Direct3D11::IDirect3DSurface;
8use windows::Win32::Graphics::Direct3D11::{
9    D3D11_BOX, D3D11_CPU_ACCESS_READ, D3D11_CPU_ACCESS_WRITE, D3D11_MAP_READ_WRITE,
10    D3D11_MAPPED_SUBRESOURCE, D3D11_TEXTURE2D_DESC, D3D11_USAGE_STAGING, ID3D11Device,
11    ID3D11DeviceContext, ID3D11Texture2D,
12};
13use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT, DXGI_SAMPLE_DESC};
14
15use crate::encoder::{self, ImageEncoder};
16use crate::settings::ColorFormat;
17
18#[derive(thiserror::Error, Debug)]
19pub enum Error {
20    #[error("Invalid crop size")]
21    InvalidSize,
22    #[error("Invalid title bar height")]
23    InvalidTitleBarSize,
24    #[error("This color format is not supported for saving as an image")]
25    UnsupportedFormat,
26    #[error("Failed to encode the image buffer to image bytes with the specified format: {0}")]
27    ImageEncoderError(#[from] encoder::ImageEncoderError),
28    #[error("I/O error: {0}")]
29    IoError(#[from] io::Error),
30    #[error("Windows API error: {0}")]
31    WindowsError(#[from] windows::core::Error),
32}
33
34#[derive(Eq, PartialEq, Clone, Copy, Debug)]
35pub enum ImageFormat {
36    Jpeg,
37    Png,
38    Gif,
39    Tiff,
40    Bmp,
41    JpegXr,
42}
43
44/// Represents a frame captured from a graphics capture item.
45///
46/// # Example
47/// ```ignore
48/// // Get a frame from the capture session
49/// let mut buffer = frame.buffer()?;
50/// buffer.save_as_image("screenshot.png", ImageFormat::Png)?;
51/// ```
52pub struct Frame<'a> {
53    d3d_device: &'a ID3D11Device,
54    frame_surface: IDirect3DSurface,
55    frame_texture: ID3D11Texture2D,
56    timestamp: TimeSpan,
57    context: &'a ID3D11DeviceContext,
58    buffer: &'a mut Vec<u8>,
59    width: u32,
60    height: u32,
61    color_format: ColorFormat,
62    title_bar_height: Option<u32>,
63}
64
65impl<'a> Frame<'a> {
66    /// Creates a new `Frame`.
67    ///
68    /// # Arguments
69    ///
70    /// * `d3d_device` - The `ID3D11Device` used for creating the frame.
71    /// * `frame_surface` - The `IDirect3DSurface` representing the frame's surface.
72    /// * `frame_texture` - The `ID3D11Texture2D` representing the frame's texture.
73    /// * `timestamp` - The `TimeSpan` representing the frame's timestamp, in 100-nanosecond units.
74    /// * `context` - The `ID3D11DeviceContext` used for copying the texture.
75    /// * `buffer` - A mutable reference to the buffer for the frame data.
76    /// * `width` - The width of the frame.
77    /// * `height` - The height of the frame.
78    /// * `color_format` - The `ColorFormat` of the frame.
79    /// * `title_bar_height` - The height of the title bar, if applicable.
80    ///
81    /// # Returns
82    ///
83    /// A new `Frame` instance.
84    #[allow(clippy::too_many_arguments)]
85    #[must_use]
86    #[inline]
87    pub const fn new(
88        d3d_device: &'a ID3D11Device,
89        frame_surface: IDirect3DSurface,
90        frame_texture: ID3D11Texture2D,
91        timestamp: TimeSpan,
92        context: &'a ID3D11DeviceContext,
93        buffer: &'a mut Vec<u8>,
94        width: u32,
95        height: u32,
96        color_format: ColorFormat,
97        title_bar_height: Option<u32>,
98    ) -> Self {
99        Self {
100            d3d_device,
101            frame_surface,
102            frame_texture,
103            timestamp,
104            context,
105            buffer,
106            width,
107            height,
108            color_format,
109            title_bar_height,
110        }
111    }
112
113    /// Gets the width of the frame.
114    ///
115    /// # Returns
116    ///
117    /// The width of the frame.
118    #[must_use]
119    #[inline]
120    pub const fn width(&self) -> u32 {
121        self.width
122    }
123
124    /// Gets the height of the frame.
125    ///
126    /// # Returns
127    ///
128    /// The height of the frame.
129    #[must_use]
130    #[inline]
131    pub const fn height(&self) -> u32 {
132        self.height
133    }
134
135    /// Gets the timestamp of the frame.
136    ///
137    /// # Returns
138    ///
139    /// The timestamp of the frame.
140    #[must_use]
141    #[inline]
142    pub const fn timestamp(&self) -> TimeSpan {
143        self.timestamp
144    }
145
146    /// Gets the color format of the frame.
147    ///
148    /// # Returns
149    ///
150    /// The color format of the frame.
151    #[must_use]
152    #[inline]
153    pub const fn color_format(&self) -> ColorFormat {
154        self.color_format
155    }
156
157    /// Gets the raw surface of the frame.
158    ///
159    /// # Returns
160    ///
161    /// The `IDirect3DSurface` representing the raw surface of the frame.
162    ///
163    /// # Safety
164    ///
165    /// This method is unsafe because it returns a reference to an underlying Windows API object.
166    #[allow(clippy::missing_safety_doc)]
167    #[must_use]
168    #[inline]
169    pub const unsafe fn as_raw_surface(&self) -> &IDirect3DSurface {
170        &self.frame_surface
171    }
172
173    /// Gets the raw texture of the frame.
174    ///
175    /// # Returns
176    ///
177    /// The `ID3D11Texture2D` representing the raw texture of the frame.
178    ///
179    /// # Safety
180    ///
181    /// This method is unsafe because it returns a reference to an underlying Windows API object.
182    #[allow(clippy::missing_safety_doc)]
183    #[must_use]
184    #[inline]
185    pub const unsafe fn as_raw_texture(&self) -> &ID3D11Texture2D {
186        &self.frame_texture
187    }
188
189    /// Gets the frame buffer.
190    ///
191    /// # Returns
192    ///
193    /// A `FrameBuffer` containing the frame data.
194    #[inline]
195    pub fn buffer(&mut self) -> Result<FrameBuffer, Error> {
196        // Texture Settings
197        let texture_desc = D3D11_TEXTURE2D_DESC {
198            Width: self.width,
199            Height: self.height,
200            MipLevels: 1,
201            ArraySize: 1,
202            Format: DXGI_FORMAT(self.color_format as i32),
203            SampleDesc: DXGI_SAMPLE_DESC { Count: 1, Quality: 0 },
204            Usage: D3D11_USAGE_STAGING,
205            BindFlags: 0,
206            CPUAccessFlags: D3D11_CPU_ACCESS_READ.0 as u32 | D3D11_CPU_ACCESS_WRITE.0 as u32,
207            MiscFlags: 0,
208        };
209
210        // Create a texture that the CPU can read
211        let mut texture = None;
212        unsafe {
213            self.d3d_device.CreateTexture2D(&texture_desc, None, Some(&mut texture))?;
214        };
215
216        let texture = texture.unwrap();
217
218        // Copy the real texture to the staging texture
219        unsafe {
220            self.context.CopyResource(&texture, &self.frame_texture);
221        };
222
223        // Map the texture to enable CPU access
224        let mut mapped_resource = D3D11_MAPPED_SUBRESOURCE::default();
225        unsafe {
226            self.context.Map(&texture, 0, D3D11_MAP_READ_WRITE, 0, Some(&mut mapped_resource))?;
227        };
228
229        // Get a slice of the mapped resource data
230        let mapped_frame_data = unsafe {
231            slice::from_raw_parts_mut(
232                mapped_resource.pData.cast(),
233                (self.height * mapped_resource.RowPitch) as usize,
234            )
235        };
236
237        // Create a frame buffer from the slice
238        let frame_buffer = FrameBuffer::new(
239            mapped_frame_data,
240            self.buffer,
241            self.width,
242            self.height,
243            mapped_resource.RowPitch,
244            mapped_resource.DepthPitch,
245            self.color_format,
246        );
247
248        Ok(frame_buffer)
249    }
250
251    /// Get a cropped frame buffer.
252    ///
253    /// # Arguments
254    ///
255    /// * `start_width` - The starting width of the cropped frame.
256    /// * `start_height` - The starting height of the cropped frame.
257    /// * `end_width` - The ending width of the cropped frame.
258    /// * `end_height` - The ending height of the cropped frame.
259    ///
260    /// # Returns
261    ///
262    /// A `FrameBuffer` containing the cropped frame data.
263    #[inline]
264    pub fn buffer_crop(
265        &mut self,
266        start_width: u32,
267        start_height: u32,
268        end_width: u32,
269        end_height: u32,
270    ) -> Result<FrameBuffer, Error> {
271        if start_width >= end_width || start_height >= end_height {
272            return Err(Error::InvalidSize);
273        }
274
275        let texture_width = end_width - start_width;
276        let texture_height = end_height - start_height;
277
278        // Texture Settings
279        let texture_desc = D3D11_TEXTURE2D_DESC {
280            Width: texture_width,
281            Height: texture_height,
282            MipLevels: 1,
283            ArraySize: 1,
284            Format: DXGI_FORMAT(self.color_format as i32),
285            SampleDesc: DXGI_SAMPLE_DESC { Count: 1, Quality: 0 },
286            Usage: D3D11_USAGE_STAGING,
287            BindFlags: 0,
288            CPUAccessFlags: D3D11_CPU_ACCESS_READ.0 as u32 | D3D11_CPU_ACCESS_WRITE.0 as u32,
289            MiscFlags: 0,
290        };
291
292        // Create a texture that the CPU can read
293        let mut texture = None;
294        unsafe {
295            self.d3d_device.CreateTexture2D(&texture_desc, None, Some(&mut texture))?;
296        };
297        let texture = texture.unwrap();
298
299        // Box settings
300        let resource_box = D3D11_BOX {
301            left: start_width,
302            top: start_height,
303            front: 0,
304            right: end_width,
305            bottom: end_height,
306            back: 1,
307        };
308
309        // Copy the real texture to the staging texture
310        unsafe {
311            self.context.CopySubresourceRegion(
312                &texture,
313                0,
314                0,
315                0,
316                0,
317                &self.frame_texture,
318                0,
319                Some(&resource_box),
320            );
321        };
322
323        // Map the texture to enable CPU access
324        let mut mapped_resource = D3D11_MAPPED_SUBRESOURCE::default();
325        unsafe {
326            self.context.Map(&texture, 0, D3D11_MAP_READ_WRITE, 0, Some(&mut mapped_resource))?;
327        };
328
329        // Get a slice of the mapped resource data
330        let mapped_frame_data = unsafe {
331            slice::from_raw_parts_mut(
332                mapped_resource.pData.cast(),
333                (texture_height * mapped_resource.RowPitch) as usize,
334            )
335        };
336
337        // Create a frame buffer from the slice
338        let frame_buffer = FrameBuffer::new(
339            mapped_frame_data,
340            self.buffer,
341            texture_width,
342            texture_height,
343            mapped_resource.RowPitch,
344            mapped_resource.DepthPitch,
345            self.color_format,
346        );
347
348        Ok(frame_buffer)
349    }
350
351    /// Gets the frame buffer without the title bar.
352    ///
353    /// # Returns
354    ///
355    /// A `FrameBuffer` containing the frame data without the title bar.
356    #[inline]
357    pub fn buffer_without_title_bar(&mut self) -> Result<FrameBuffer, Error> {
358        if let Some(title_bar_height) = self.title_bar_height {
359            if title_bar_height >= self.height {
360                return Err(Error::InvalidTitleBarSize);
361            }
362
363            self.buffer_crop(0, title_bar_height, self.width, self.height)
364        } else {
365            self.buffer()
366        }
367    }
368
369    /// Save the frame buffer as an image to the specified path.
370    ///
371    /// # Arguments
372    ///
373    /// * `path` - The path where the image will be saved.
374    /// * `format` - The `ImageFormat` of the saved image.
375    ///
376    /// # Returns
377    ///
378    /// An empty `Result` if successful, or an `Error` if there was an issue saving the image.
379    #[inline]
380    pub fn save_as_image<T: AsRef<Path>>(
381        &mut self,
382        path: T,
383        format: ImageFormat,
384    ) -> Result<(), Error> {
385        let mut frame_buffer = self.buffer()?;
386
387        frame_buffer.save_as_image(path, format)?;
388
389        Ok(())
390    }
391}
392
393/// Represents a frame buffer containing pixel data.
394///
395/// # Example
396/// ```ignore
397/// // Get a frame from the capture session
398/// let mut buffer = frame.buffer()?;
399/// buffer.save_as_image("screenshot.png", ImageFormat::Png)?;
400/// ```
401pub struct FrameBuffer<'a> {
402    raw_buffer: &'a mut [u8],
403    buffer: &'a mut Vec<u8>,
404    width: u32,
405    height: u32,
406    row_pitch: u32,
407    depth_pitch: u32,
408    color_format: ColorFormat,
409}
410
411impl<'a> FrameBuffer<'a> {
412    /// Creates a new `FrameBuffer`.
413    ///
414    /// # Arguments
415    ///
416    /// * `raw_buffer` - A mutable reference to the raw pixel data buffer, which may include padding.
417    /// * `buffer` - A mutable reference to a buffer used for copying pixel data without padding.
418    /// * `width` - The width of the frame buffer.
419    /// * `height` - The height of the frame buffer.
420    /// * `row_pitch` - The row pitch of the frame buffer.
421    /// * `depth_pitch` - The depth pitch of the frame buffer.
422    /// * `color_format` - The color format of the frame buffer.
423    ///
424    /// # Returns
425    ///
426    /// A new `FrameBuffer` instance.
427    #[must_use]
428    #[inline]
429    pub const fn new(
430        raw_buffer: &'a mut [u8],
431        buffer: &'a mut Vec<u8>,
432        width: u32,
433        height: u32,
434        row_pitch: u32,
435        depth_pitch: u32,
436        color_format: ColorFormat,
437    ) -> Self {
438        Self { raw_buffer, buffer, width, height, row_pitch, depth_pitch, color_format }
439    }
440
441    /// Gets the width of the frame buffer.
442    #[must_use]
443    #[inline]
444    pub const fn width(&self) -> u32 {
445        self.width
446    }
447
448    /// Gets the height of the frame buffer.
449    #[must_use]
450    #[inline]
451    pub const fn height(&self) -> u32 {
452        self.height
453    }
454
455    /// Gets the row pitch of the frame buffer.
456    #[must_use]
457    #[inline]
458    pub const fn row_pitch(&self) -> u32 {
459        self.row_pitch
460    }
461
462    /// Gets the depth pitch of the frame buffer.
463    #[must_use]
464    #[inline]
465    pub const fn depth_pitch(&self) -> u32 {
466        self.depth_pitch
467    }
468
469    /// Checks if the buffer has padding.
470    #[must_use]
471    #[inline]
472    pub const fn has_padding(&self) -> bool {
473        self.width * 4 != self.row_pitch
474    }
475
476    /// Gets the raw pixel data, which may include padding.
477    #[must_use]
478    #[inline]
479    pub const fn as_raw_buffer(&mut self) -> &mut [u8] {
480        self.raw_buffer
481    }
482
483    /// Gets the pixel data without padding.
484    ///
485    /// # Returns
486    ///
487    /// A mutable reference to the buffer containing pixel data without padding.
488    #[inline]
489    pub fn as_nopadding_buffer(&mut self) -> Result<&mut [u8], Error> {
490        if !self.has_padding() {
491            return Ok(self.raw_buffer);
492        }
493
494        let multiplier = match self.color_format {
495            ColorFormat::Rgba16F => 8,
496            ColorFormat::Rgba8 => 4,
497            ColorFormat::Bgra8 => 4,
498        };
499
500        let frame_size = (self.width * self.height * multiplier) as usize;
501        if self.buffer.capacity() < frame_size {
502            self.buffer.resize(frame_size, 0);
503        }
504
505        let width_size = (self.width * multiplier) as usize;
506        let buffer_address = self.buffer.as_mut_ptr() as isize;
507        (0..self.height).into_par_iter().for_each(|y| {
508            let index = (y * self.row_pitch) as usize;
509            let ptr = buffer_address as *mut u8;
510
511            unsafe {
512                ptr::copy_nonoverlapping(
513                    self.raw_buffer.as_ptr().add(index),
514                    ptr.add(y as usize * width_size),
515                    width_size,
516                );
517            }
518        });
519
520        Ok(&mut self.buffer[0..frame_size])
521    }
522
523    /// Save the frame buffer as an image to the specified path.
524    ///
525    /// # Arguments
526    ///
527    /// * `path` - The path where the image will be saved.
528    /// * `format` - The image format to use for saving.
529    ///
530    /// # Returns
531    ///
532    /// `Ok(())` if the image is successfully saved, or an `Error` if an error occurred.
533    #[inline]
534    pub fn save_as_image<T: AsRef<Path>>(
535        &mut self,
536        path: T,
537        format: ImageFormat,
538    ) -> Result<(), Error> {
539        let width = self.width;
540        let height = self.height;
541
542        let bytes = ImageEncoder::new(format, self.color_format).encode(
543            self.as_nopadding_buffer()?,
544            width,
545            height,
546        )?;
547
548        fs::write(path, bytes)?;
549
550        Ok(())
551    }
552}