Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/recording/src/instant_recording.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1332,6 +1332,8 @@ async fn build_instant_recording_actor(
max_capture_size,
timestamps.system_time(),
inputs.capture_system_audio,
#[cfg(windows)]
inputs.capture_target.window(),
#[cfg(target_os = "linux")]
crate::sources::screen_capture::LinuxCaptureSource::from_target(
&inputs.capture_target,
Expand Down
5 changes: 5 additions & 0 deletions crates/recording/src/sources/screen_capture/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,8 @@ pub struct Config {
crop_bounds: Option<CropBounds>,
fps: u32,
show_cursor: bool,
#[cfg(windows)]
window: Option<WindowId>,
#[cfg(target_os = "linux")]
linux_source: LinuxCaptureSource,
}
Expand Down Expand Up @@ -469,6 +471,7 @@ impl<TCaptureFormat: ScreenCaptureFormat> ScreenCaptureConfig<TCaptureFormat> {
max_capture_size: Option<(u32, u32)>,
start_time: SystemTime,
system_audio: bool,
#[cfg(windows)] window: Option<WindowId>,
#[cfg(target_os = "linux")] linux_source: LinuxCaptureSource,
#[cfg(windows)] d3d_device: ::windows::Win32::Graphics::Direct3D11::ID3D11Device,
#[cfg(target_os = "macos")] shareable_content: SendableShareableContent,
Expand Down Expand Up @@ -529,6 +532,8 @@ impl<TCaptureFormat: ScreenCaptureFormat> ScreenCaptureConfig<TCaptureFormat> {
crop_bounds,
fps,
show_cursor,
#[cfg(windows)]
window,
#[cfg(target_os = "linux")]
linux_source,
},
Expand Down
75 changes: 66 additions & 9 deletions crates/recording/src/sources/screen_capture/windows.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use crate::{
AudioFrame, SetupCtx, output_pipeline,
screen_capture::{ScreenCaptureConfig, ScreenCaptureFormat, cadence::FrameCadenceGate},
screen_capture::{
CropBounds, ScreenCaptureConfig, ScreenCaptureFormat, cadence::FrameCadenceGate,
},
};
use ::windows::Win32::Graphics::Direct3D11::{
D3D11_BIND_SHADER_RESOURCE, D3D11_BOX, D3D11_SUBRESOURCE_DATA, D3D11_TEXTURE2D_DESC,
Expand All @@ -16,7 +18,7 @@ use futures::{
channel::{mpsc, oneshot},
};
use scap_ffmpeg::*;
use scap_targets::{Display, DisplayId};
use scap_targets::{Display, DisplayId, Window, WindowId};
use std::{
sync::{
Arc, Mutex,
Expand Down Expand Up @@ -351,13 +353,29 @@ impl output_pipeline::VideoFrame for VideoFrame {
}
}

/// A window target captures the window's own GraphicsCaptureItem, so the crop
/// that carves the window out of the display is neither needed nor correct:
/// WGC already hands back just that window, and the crop would additionally
/// pin the recording to wherever the window started out.
fn crop_for_target(
window: Option<&WindowId>,
crop_bounds: Option<CropBounds>,
) -> Option<CropBounds> {
match window {
Some(_) => None,
None => crop_bounds,
}
}

impl ScreenCaptureConfig<Direct3DCapture> {
pub async fn to_sources(
&self,
) -> anyhow::Result<(VideoSourceConfig, Option<SystemAudioSourceConfig>)> {
let crop_bounds = crop_for_target(self.config.window.as_ref(), self.config.crop_bounds);

let mut settings = scap_direct3d::Settings {
pixel_format: Direct3DCapture::PIXEL_FORMAT,
crop: self.config.crop_bounds.map(|b| {
crop: crop_bounds.map(|b| {
let position = b.position();
let size = b.size().map(|v| (v / 2.0).floor() * 2.0);

Expand Down Expand Up @@ -393,12 +411,13 @@ impl ScreenCaptureConfig<Direct3DCapture> {

settings.fps = Some(self.config.fps);

// Store the display ID instead of GraphicsCaptureItem to avoid COM threading issues
// Store the target's ID instead of GraphicsCaptureItem to avoid COM threading issues
// The GraphicsCaptureItem will be created on the capture thread
Ok((
VideoSourceConfig {
video_info: self.video_info,
display_id: self.config.display.clone(),
window_id: self.config.window.clone(),
settings,
d3d_device: self.d3d_device.clone(),
},
Expand All @@ -416,6 +435,7 @@ pub enum VideoSourceError {
pub struct VideoSourceConfig {
video_info: VideoInfo,
display_id: DisplayId,
window_id: Option<WindowId>,
settings: scap_direct3d::Settings,
pub d3d_device: ID3D11Device,
}
Expand Down Expand Up @@ -487,6 +507,7 @@ struct CaptureClosureEvent {

struct CreateCapturerParams<'a> {
display_id: &'a DisplayId,
window_id: Option<&'a WindowId>,
settings: &'a scap_direct3d::Settings,
d3d_device: &'a ID3D11Device,
video_tx: &'a mpsc::Sender<VideoFrame>,
Expand All @@ -511,11 +532,18 @@ fn create_d3d_capturer(
params: &CreateCapturerParams,
error_tx: &mpsc::Sender<CaptureClosureEvent>,
) -> anyhow::Result<scap_direct3d::Capturer> {
let capture_item = Display::from_id(params.display_id)
.ok_or_else(|| anyhow!("Display not found for ID: {:?}", params.display_id))?
.raw_handle()
.try_as_capture_item()
.map_err(|e| anyhow!("Failed to create GraphicsCaptureItem: {}", e))?;
let capture_item = match params.window_id {
Some(window_id) => Window::from_id(window_id)
.ok_or_else(|| anyhow!("Window not found for ID: {:?}", window_id))?
.raw_handle()
.try_as_capture_item()
.map_err(|e| anyhow!("Failed to create window GraphicsCaptureItem: {}", e))?,
None => Display::from_id(params.display_id)
.ok_or_else(|| anyhow!("Display not found for ID: {:?}", params.display_id))?
.raw_handle()
.try_as_capture_item()
.map_err(|e| anyhow!("Failed to create GraphicsCaptureItem: {}", e))?,
};

scap_direct3d::Capturer::new(
capture_item,
Expand Down Expand Up @@ -661,6 +689,7 @@ impl output_pipeline::VideoSource for VideoSource {
VideoSourceConfig {
video_info,
display_id,
window_id,
settings,
d3d_device,
}: Self::Config,
Expand Down Expand Up @@ -723,6 +752,7 @@ impl output_pipeline::VideoSource for VideoSource {
($device:expr) => {
CreateCapturerParams {
display_id: &display_id,
window_id: window_id.as_ref(),
settings: &settings,
d3d_device: $device,
video_tx: &video_tx,
Expand Down Expand Up @@ -1598,3 +1628,30 @@ mod first_screen_frame_tests {
assert!(error.to_string().contains("closed before its first frame"));
}
}

#[cfg(test)]
mod capture_target_tests {
use super::*;
use scap_targets::bounds::{PhysicalPosition, PhysicalSize};

fn bounds() -> CropBounds {
CropBounds::new(
PhysicalPosition::new(120.0, 80.0),
PhysicalSize::new(640.0, 480.0),
)
}

#[test]
fn a_display_target_keeps_its_crop() {
let kept = crop_for_target(None, Some(bounds())).expect("crop kept");
assert_eq!(kept.position().x(), 120.0);
assert_eq!(kept.size().width(), 640.0);
assert!(crop_for_target(None, None).is_none());
}

#[test]
fn a_window_target_captures_the_window_rather_than_a_crop_of_the_display() {
let window: WindowId = "1234".parse().unwrap();
assert!(crop_for_target(Some(&window), Some(bounds())).is_none());
}
}
2 changes: 2 additions & 0 deletions crates/recording/src/studio_recording.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3270,6 +3270,8 @@ async fn create_segment_pipeline(
max_capture_size,
start_time.system_time(),
base_inputs.capture_system_audio,
#[cfg(windows)]
capture_target.window(),
#[cfg(target_os = "linux")]
sources::screen_capture::LinuxCaptureSource::from_target(&capture_target),
#[cfg(windows)]
Expand Down
48 changes: 47 additions & 1 deletion crates/scap-direct3d/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -506,14 +506,18 @@ impl Capturer {
.map(|fps| ((fps as f32 / 30.0 * 2.0).ceil() as i32).clamp(2, 4))
.unwrap_or(2);

let item_size = item.Size().map_err(NewCapturerError::ItemSize)?;

let frame_pool = Direct3D11CaptureFramePool::CreateFreeThreaded(
&direct3d_device,
settings.pixel_format.as_directx(),
frame_pool_size,
item.Size().map_err(NewCapturerError::ItemSize)?,
item_size,
)
.map_err(NewCapturerError::FramePool)?;

let pool_size = Arc::new(Mutex::new((item_size.Width, item_size.Height)));

let session = frame_pool
.CreateCaptureSession(&item)
.map_err(NewCapturerError::CaptureSession)?;
Expand Down Expand Up @@ -575,6 +579,8 @@ impl Capturer {
let stop_flag = stop_flag.clone();
let staging_pool = staging_pool.clone();
let callback_activity = callback_activity.clone();
let pixel_format = settings.pixel_format;
let pool_size = pool_size.clone();

move |frame_pool, _| {
let _activity_guard = callback_activity.enter();
Expand All @@ -591,11 +597,51 @@ impl Capturer {

let size = frame.ContentSize()?;

// The item can change size mid-capture (a recorded
// window is resized, or the display's resolution
// changes). The pool keeps handing out surfaces at its
// creation size, so the frame's own ContentSize stops
// describing its texture and every later frame reads
// back as black. Recreate the pool for the new size and
// skip this frame; the next one arrives correctly sized.
{
let mut pool_size = pool_size
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if (size.Width, size.Height) != *pool_size {
drop(frame);
// IDirect3DDevice is not Send, so it is rebuilt
// here from the capture device rather than held
// across the event handler.
let dxgi_device = d3d_device.cast::<IDXGIDevice>()?;
let direct3d_device =
unsafe { CreateDirect3D11DeviceFromDXGIDevice(&dxgi_device) }?
.cast::<IDirect3DDevice>()?;
frame_pool.Recreate(
&direct3d_device,
pixel_format.as_directx(),
frame_pool_size,
size,
)?;
*pool_size = (size.Width, size.Height);
return Ok(());
}
}

let surface = frame.Surface()?;
let dxgi_interface = surface.cast::<IDirect3DDxgiInterfaceAccess>()?;
let texture = unsafe { dxgi_interface.GetInterface::<ID3D11Texture2D>() }?;

let frame = if let Some((cropped_texture, crop)) = crop_data.clone() {
// A shrunk item leaves the crop box hanging off the
// edge of the source texture, which copies nothing
// and would vend a stale surface as if it were live.
if crop.right > size.Width.max(0) as u32
|| crop.bottom > size.Height.max(0) as u32
{
return Ok(());
}

unsafe {
d3d_context.CopySubresourceRegion(
&cropped_texture,
Expand Down
Loading