#![allow(unsafe_code)]
use super::{CaptureBorder, FrameDimensions, WindowCaptureOptions};
use crate::CaptureError;
use crate::desktop::{
CaptureOutputPreference, CaptureRegion, CursorCapture, DesktopCaptureSource,
DesktopVideoCapture, DesktopVideoCaptureConfig,
};
use mediaway_common::{
Bytes, CodecKind, GpuBufferHandle, GpuDeviceHandle, NativeHandle, PixelFormat, StreamInfo,
VideoFrame, VideoFrameStorage, VideoGeometry,
};
use windows::Graphics::Capture::{
Direct3D11CaptureFrame, Direct3D11CaptureFramePool, GraphicsCaptureAccess,
GraphicsCaptureAccessKind, GraphicsCaptureItem, GraphicsCaptureSession,
};
use windows::Graphics::DirectX::Direct3D11::IDirect3DDevice;
use windows::Graphics::DirectX::DirectXPixelFormat;
use windows::Graphics::SizeInt32;
use windows::Security::Authorization::AppCapabilityAccess::AppCapabilityAccessStatus;
use windows::Win32::Foundation::HWND;
use windows::Win32::Graphics::Direct3D11::{
D3D11_BOX, D3D11_TEXTURE2D_DESC, ID3D11Device, ID3D11DeviceContext, ID3D11Multithread,
ID3D11Texture2D,
};
use windows::Win32::Graphics::Dxgi::IDXGIDevice;
use windows::Win32::System::WinRT::Direct3D11::{
CreateDirect3D11DeviceFromDXGIDevice, IDirect3DDxgiInterfaceAccess,
};
use windows::Win32::System::WinRT::Graphics::Capture::IGraphicsCaptureItemInterop;
use windows::Win32::System::WinRT::{RO_INIT_MULTITHREADED, RoInitialize};
use windows::core::{Interface, factory};
struct HeldFrame {
_frame: Option<Direct3D11CaptureFrame>,
_texture: ID3D11Texture2D,
}
const CROP_RING_DEPTH: usize = 4;
enum Crop {
None,
Pool { region: CaptureRegion },
Copy {
region: CaptureRegion,
ring: Option<CropRing>,
},
}
struct CropRing {
context: ID3D11DeviceContext,
slots: [ID3D11Texture2D; CROP_RING_DEPTH],
next: usize,
}
struct CaptureSession {
device: ID3D11Device,
winrt_device: IDirect3DDevice,
_item: GraphicsCaptureItem,
frame_pool: Direct3D11CaptureFramePool,
_session: GraphicsCaptureSession,
stream_info: StreamInfo,
held: Option<HeldFrame>,
next_pts: i64,
border_hidden: bool,
dimensions: FrameDimensions,
crop: Crop,
}
pub struct WindowsWindowCapture {
inner: Option<CaptureSession>,
}
impl WindowsWindowCapture {
pub fn open(config: &DesktopVideoCaptureConfig) -> Result<Self, CaptureError> {
Self::open_with(config, WindowCaptureOptions::default())
}
pub fn open_with(
config: &DesktopVideoCaptureConfig,
options: WindowCaptureOptions,
) -> Result<Self, CaptureError> {
let DesktopCaptureSource::Window { window } = config.source else {
return Err(CaptureError::Unsupported);
};
if config.output != CaptureOutputPreference::ZeroCopyGpu {
return Err(CaptureError::Unsupported);
}
let Some(GpuDeviceHandle::DirectX11(handle)) = config.gpu_device else {
return Err(CaptureError::InvalidInput);
};
if !GraphicsCaptureSession::IsSupported().unwrap_or(false) {
return Err(CaptureError::Unsupported);
}
let _ = unsafe { RoInitialize(RO_INIT_MULTITHREADED) };
let raw = handle.get() as *mut std::ffi::c_void;
let device_ref =
unsafe { ID3D11Device::from_raw_borrowed(&raw) }.ok_or(CaptureError::InvalidInput)?;
let device = device_ref.clone();
let dxgi_device: IDXGIDevice = device.cast().map_err(|_| CaptureError::Backend)?;
let inspectable = unsafe { CreateDirect3D11DeviceFromDXGIDevice(&dxgi_device) }
.map_err(|_| CaptureError::Backend)?;
let winrt_device: IDirect3DDevice =
inspectable.cast().map_err(|_| CaptureError::Backend)?;
let interop = factory::<GraphicsCaptureItem, IGraphicsCaptureItemInterop>()
.map_err(|_| CaptureError::Backend)?;
let hwnd = HWND(window.get() as *mut _);
let item: GraphicsCaptureItem =
unsafe { interop.CreateForWindow(hwnd) }.map_err(|_| CaptureError::AccessDenied)?;
let item_size = item.Size().map_err(|_| CaptureError::Backend)?;
let content_w = u32::try_from(item_size.Width).map_err(|_| CaptureError::Backend)?;
let content_h = u32::try_from(item_size.Height).map_err(|_| CaptureError::Backend)?;
let CropPlan {
crop,
frame: (width, height),
pool: (pool_w, pool_h),
} = plan_crop(config.region, options.dimensions, content_w, content_h)?;
if matches!(crop, Crop::Copy { .. }) {
if let Ok(mt) = device.cast::<ID3D11Multithread>() {
let _ = unsafe { mt.SetMultithreadProtected(true) };
}
}
let frame_pool = Direct3D11CaptureFramePool::CreateFreeThreaded(
&winrt_device,
DirectXPixelFormat::B8G8R8A8UIntNormalized,
2,
size_int32(pool_w, pool_h)?,
)
.map_err(|_| CaptureError::Backend)?;
let session = frame_pool
.CreateCaptureSession(&item)
.map_err(|_| CaptureError::Backend)?;
session
.SetIsCursorCaptureEnabled(config.cursor == CursorCapture::Included)
.map_err(|_| CaptureError::Unsupported)?;
let border_hidden = options.border == CaptureBorder::Hidden && hide_border(&session);
session
.StartCapture()
.map_err(|_| CaptureError::AccessDenied)?;
let stream_info = StreamInfo::Video {
id: 0,
codec: CodecKind::RawVideo,
time_base: config.time_base,
geometry: VideoGeometry { width, height },
extra_data: Bytes::new(),
};
Ok(Self {
inner: Some(CaptureSession {
device,
winrt_device,
_item: item,
frame_pool,
_session: session,
stream_info,
held: None,
next_pts: 0,
border_hidden,
dimensions: options.dimensions,
crop,
}),
})
}
}
impl WindowsWindowCapture {
#[must_use]
pub fn border_hidden(&self) -> bool {
self.inner.as_ref().is_some_and(|s| s.border_hidden)
}
}
fn hide_border(session: &GraphicsCaptureSession) -> bool {
let granted = GraphicsCaptureAccess::RequestAccessAsync(GraphicsCaptureAccessKind::Borderless)
.and_then(|op| op.join())
.is_ok_and(|status| status == AppCapabilityAccessStatus::Allowed);
if !granted {
return false;
}
if session.SetIsBorderRequired(false).is_err() {
return false;
}
session.IsBorderRequired().is_ok_and(|required| !required)
}
impl DesktopVideoCapture for WindowsWindowCapture {
fn stream_info(&self) -> &StreamInfo {
#[allow(
clippy::option_if_let_else,
reason = "map_or_else forces 'static vs 'self lifetime clash"
)]
if let Some(s) = self.inner.as_ref() {
&s.stream_info
} else {
closed_video_info()
}
}
fn poll_frame(&mut self) -> Result<Option<VideoFrame>, CaptureError> {
let Some(session) = self.inner.as_mut() else {
return Err(CaptureError::Closed);
};
if session.held.is_some() {
return Err(CaptureError::Backend);
}
let Ok(frame) = session.frame_pool.TryGetNextFrame() else {
return Ok(None);
};
let content = frame.ContentSize().map_err(|_| CaptureError::Backend)?;
let Ok(content_w) = u32::try_from(content.Width) else {
return Ok(None);
};
let Ok(content_h) = u32::try_from(content.Height) else {
return Ok(None);
};
match &session.crop {
Crop::None => {}
Crop::Pool { region } | Crop::Copy { region, .. } => {
if !region.fits_within(content_w, content_h) {
return Err(out_of_bounds(*region, content_w, content_h));
}
return deliver_region(session, frame);
}
}
let current_geometry = session.stream_info.geometry().unwrap_or(VideoGeometry {
width: 0,
height: 0,
});
let Some((target_w, target_h)) = session.dimensions.pool_size(content_w, content_h) else {
return Ok(None);
};
if let Some(new_geometry) = resized_geometry(current_geometry, target_w, target_h) {
session
.frame_pool
.Recreate(
&session.winrt_device,
DirectXPixelFormat::B8G8R8A8UIntNormalized,
2,
size_int32(target_w, target_h)?,
)
.map_err(|_| CaptureError::Backend)?;
let time_base = session.stream_info.time_base();
session.stream_info = StreamInfo::Video {
id: 0,
codec: CodecKind::RawVideo,
time_base,
geometry: new_geometry,
extra_data: Bytes::new(),
};
}
let surface = frame.Surface().map_err(|_| CaptureError::Backend)?;
let access: IDirect3DDxgiInterfaceAccess =
surface.cast().map_err(|_| CaptureError::Backend)?;
let texture: ID3D11Texture2D =
unsafe { access.GetInterface() }.map_err(|_| CaptureError::Backend)?;
let mut desc = D3D11_TEXTURE2D_DESC::default();
unsafe { texture.GetDesc(&raw mut desc) };
let texture_handle =
NativeHandle::new(Interface::as_raw(&texture) as usize).ok_or(CaptureError::Backend)?;
let pts = session.next_pts;
session.next_pts = session.next_pts.saturating_add(1);
session.held = Some(HeldFrame {
_frame: Some(frame),
_texture: texture,
});
Ok(Some(gpu_frame(pts, &desc, texture_handle)))
}
fn release_frame(&mut self) -> Result<(), CaptureError> {
let Some(session) = self.inner.as_mut() else {
return Err(CaptureError::Closed);
};
session.held = None;
Ok(())
}
fn close(&mut self) -> Result<(), CaptureError> {
if let Some(mut session) = self.inner.take() {
session.held = None;
let _ = session.frame_pool.Close();
}
Ok(())
}
}
impl Drop for WindowsWindowCapture {
fn drop(&mut self) {
let _ = self.close();
}
}
struct CropPlan {
crop: Crop,
frame: (u32, u32),
pool: (u32, u32),
}
fn plan_crop(
region: Option<CaptureRegion>,
dimensions: FrameDimensions,
content_w: u32,
content_h: u32,
) -> Result<CropPlan, CaptureError> {
let Some(requested) = region else {
let size = dimensions
.pool_size(content_w, content_h)
.ok_or(CaptureError::Backend)?;
return Ok(CropPlan {
crop: Crop::None,
frame: size,
pool: size,
});
};
let (w, h) = dimensions
.pool_size(requested.width, requested.height)
.ok_or(CaptureError::InvalidInput)?;
let region = CaptureRegion {
width: w,
height: h,
..requested
};
if !region.fits_within(content_w, content_h) {
return Err(out_of_bounds(region, content_w, content_h));
}
let pool = (region.x + w, region.y + h);
let crop = if region.is_at_origin() {
Crop::Pool { region }
} else {
Crop::Copy { region, ring: None }
};
Ok(CropPlan {
crop,
frame: (w, h),
pool,
})
}
fn deliver_region(
session: &mut CaptureSession,
frame: Direct3D11CaptureFrame,
) -> Result<Option<VideoFrame>, CaptureError> {
let surface = frame.Surface().map_err(|_| CaptureError::Backend)?;
let access: IDirect3DDxgiInterfaceAccess = surface.cast().map_err(|_| CaptureError::Backend)?;
let source: ID3D11Texture2D =
unsafe { access.GetInterface() }.map_err(|_| CaptureError::Backend)?;
let pts = session.next_pts;
session.next_pts = session.next_pts.saturating_add(1);
let (texture, keep_frame) = match &mut session.crop {
Crop::None | Crop::Pool { .. } => (source, true),
Crop::Copy { region, ring } => {
let region = *region;
if ring.is_none() {
*ring = Some(create_crop_ring(&session.device, &source, region)?);
}
let Some(ring) = ring.as_mut() else {
return Err(CaptureError::Backend);
};
let slot = ring.slots[ring.next].clone();
ring.next = (ring.next + 1) % CROP_RING_DEPTH;
let area = D3D11_BOX {
left: region.x,
top: region.y,
front: 0,
right: region.x + region.width,
bottom: region.y + region.height,
back: 1,
};
unsafe {
ring.context.CopySubresourceRegion(
&slot,
0,
0,
0,
0,
&source,
0,
Some(&raw const area),
);
}
(slot, false)
}
};
let mut desc = D3D11_TEXTURE2D_DESC::default();
unsafe { texture.GetDesc(&raw mut desc) };
let handle =
NativeHandle::new(Interface::as_raw(&texture) as usize).ok_or(CaptureError::Backend)?;
session.held = Some(HeldFrame {
_frame: keep_frame.then_some(frame),
_texture: texture,
});
Ok(Some(gpu_frame(pts, &desc, handle)))
}
fn create_crop_ring(
device: &ID3D11Device,
source: &ID3D11Texture2D,
region: CaptureRegion,
) -> Result<CropRing, CaptureError> {
let mut desc = D3D11_TEXTURE2D_DESC::default();
unsafe { source.GetDesc(&raw mut desc) };
desc.Width = region.width;
desc.Height = region.height;
desc.MipLevels = 1;
desc.ArraySize = 1;
let context = unsafe { device.GetImmediateContext() }.map_err(|_| CaptureError::Backend)?;
let make = || -> Result<ID3D11Texture2D, CaptureError> {
let mut texture = None;
unsafe { device.CreateTexture2D(&raw const desc, None, Some(&raw mut texture)) }
.map_err(|_| CaptureError::Backend)?;
texture.ok_or(CaptureError::Backend)
};
let slots = [make()?, make()?, make()?, make()?];
Ok(CropRing {
context,
slots,
next: 0,
})
}
const fn gpu_frame(pts: i64, desc: &D3D11_TEXTURE2D_DESC, texture: NativeHandle) -> VideoFrame {
VideoFrame {
pts,
duration: 1,
width: desc.Width,
height: desc.Height,
format: PixelFormat::Bgra8,
storage: VideoFrameStorage::Gpu(GpuBufferHandle::DirectX11 {
texture,
subresource: 0,
}),
}
}
const fn out_of_bounds(region: CaptureRegion, width: u32, height: u32) -> CaptureError {
CaptureError::RegionOutOfBounds {
x: region.x,
y: region.y,
width: region.width,
height: region.height,
surface_width: width,
surface_height: height,
}
}
fn size_int32(width: u32, height: u32) -> Result<SizeInt32, CaptureError> {
Ok(SizeInt32 {
Width: i32::try_from(width).map_err(|_| CaptureError::Backend)?,
Height: i32::try_from(height).map_err(|_| CaptureError::Backend)?,
})
}
fn closed_video_info() -> &'static StreamInfo {
use std::sync::OnceLock;
static INFO: OnceLock<StreamInfo> = OnceLock::new();
INFO.get_or_init(|| StreamInfo::Video {
id: 0,
codec: CodecKind::RawVideo,
time_base: mediaway_common::Rational::new(1, 30),
geometry: VideoGeometry {
width: 0,
height: 0,
},
extra_data: Bytes::new(),
})
}
const fn resized_geometry(
current: VideoGeometry,
content_width: u32,
content_height: u32,
) -> Option<VideoGeometry> {
if content_width == current.width && content_height == current.height {
None
} else {
Some(VideoGeometry {
width: content_width,
height: content_height,
})
}
}
#[cfg(test)]
#[path = "wgc_tests.rs"]
mod tests;