diff --git a/crates/recording/src/capture_pipeline.rs b/crates/recording/src/capture_pipeline.rs index d935e0e30f2..1525fc7fbc8 100644 --- a/crates/recording/src/capture_pipeline.rs +++ b/crates/recording/src/capture_pipeline.rs @@ -325,6 +325,7 @@ impl MakeCapturePipeline for screen_capture::Direct3DCapture { } } else { let d3d_device = screen_capture.d3d_device.clone(); + let frame_rate = screen_capture.capture_frame_rate(); let bitrate_multiplier = if ultra { 0.3f32 } else { 0.15f32 }; OutputPipeline::builder(output_path.clone()) @@ -335,7 +336,7 @@ impl MakeCapturePipeline for screen_capture::Direct3DCapture { pixel_format: screen_capture::Direct3DCapture::PIXEL_FORMAT.as_dxgi(), d3d_device, bitrate_multiplier, - frame_rate: 30u32, + frame_rate, output_size: output_size.map(|(w, h)| windows::Graphics::SizeInt32 { Width: w as i32, Height: h as i32, diff --git a/crates/recording/src/output_pipeline/win.rs b/crates/recording/src/output_pipeline/win.rs index faab33d6e76..a0d741daabb 100644 --- a/crates/recording/src/output_pipeline/win.rs +++ b/crates/recording/src/output_pipeline/win.rs @@ -1334,19 +1334,6 @@ fn convert_uyvy_to_yuyv_scalar(src: &[u8], dst: &mut [u8], len: usize) { } } -fn convert_uyvy_to_yuyv( - src: &[u8], - width: u32, - height: u32, - src_stride: usize, - flip: bool, -) -> Vec { - let total_bytes = (width * height * 2) as usize; - let mut dst = vec![0u8; total_bytes]; - convert_uyvy_to_yuyv_into(src, &mut dst, width, height, src_stride, flip); - dst -} - pub(crate) fn camera_frame_to_ffmpeg( frame: &NativeCameraFrame, mjpeg_decoder: &mut MjpegDecoder, @@ -1393,37 +1380,19 @@ pub(crate) fn camera_frame_to_ffmpeg( ) })?; - let converted_data_storage; - let (final_data, final_format): (&[u8], ffmpeg::format::Pixel) = - if frame.pixel_format == cap_camera_windows::PixelFormat::UYVY422 { - converted_data_storage = convert_uyvy_to_yuyv( - data, - frame.width, - frame.height, - layout.primary_stride, - frame.is_bottom_up, - ); - ( - converted_data_storage.as_slice(), - ffmpeg::format::Pixel::YUYV422, - ) - } else { - (data, ffmpeg_format) - }; - - let mut ffmpeg_frame = ffmpeg::frame::Video::new(final_format, frame.width, frame.height); - let flip = frame.is_bottom_up && frame.pixel_format != cap_camera_windows::PixelFormat::UYVY422; + let mut ffmpeg_frame = ffmpeg::frame::Video::new(ffmpeg_format, frame.width, frame.height); + let flip = frame.is_bottom_up; match frame.pixel_format { cap_camera_windows::PixelFormat::NV12 => { let uv_height = height / 2; let y_size = layout.primary_stride * height; let uv_size = layout.secondary_stride * uv_height; - if final_data.len() >= y_size + uv_size { + if data.len() >= y_size + uv_size { let stride_y = ffmpeg_frame.stride(0); let stride_uv = ffmpeg_frame.stride(1); copy_plane( - &final_data[..y_size], + &data[..y_size], ffmpeg_frame.data_mut(0), width, height, @@ -1432,7 +1401,7 @@ pub(crate) fn camera_frame_to_ffmpeg( flip, ); copy_plane( - &final_data[y_size..y_size + uv_size], + &data[y_size..y_size + uv_size], ffmpeg_frame.data_mut(1), width, uv_height, @@ -1446,11 +1415,11 @@ pub(crate) fn camera_frame_to_ffmpeg( let uv_height = height / 2; let y_size = layout.primary_stride * height; let uv_size = layout.secondary_stride * uv_height; - if final_data.len() >= y_size + uv_size { + if data.len() >= y_size + uv_size { let stride_y = ffmpeg_frame.stride(0); let stride_uv = ffmpeg_frame.stride(1); copy_plane( - &final_data[..y_size], + &data[..y_size], ffmpeg_frame.data_mut(0), width, height, @@ -1458,7 +1427,7 @@ pub(crate) fn camera_frame_to_ffmpeg( stride_y, flip, ); - let uv_data = &final_data[y_size..y_size + uv_size]; + let uv_data = &data[y_size..y_size + uv_size]; let dest = ffmpeg_frame.data_mut(1); for row in 0..uv_height { let src_row = if flip { uv_height - 1 - row } else { row }; @@ -1475,23 +1444,15 @@ pub(crate) fn camera_frame_to_ffmpeg( } cap_camera_windows::PixelFormat::YUYV422 | cap_camera_windows::PixelFormat::UYVY422 => { let row_bytes = width * 2; - let size = if frame.pixel_format == cap_camera_windows::PixelFormat::UYVY422 { - row_bytes * height - } else { - layout.primary_stride * height - }; - if final_data.len() >= size { + let size = layout.primary_stride * height; + if data.len() >= size { let stride = ffmpeg_frame.stride(0); copy_plane( - final_data, + data, ffmpeg_frame.data_mut(0), row_bytes, height, - if frame.pixel_format == cap_camera_windows::PixelFormat::UYVY422 { - row_bytes - } else { - layout.primary_stride - }, + layout.primary_stride, stride, flip, ); @@ -1500,10 +1461,10 @@ pub(crate) fn camera_frame_to_ffmpeg( cap_camera_windows::PixelFormat::ARGB | cap_camera_windows::PixelFormat::RGB32 => { let row_bytes = width * 4; let size = layout.primary_stride * height; - if final_data.len() >= size { + if data.len() >= size { let stride = ffmpeg_frame.stride(0); copy_plane( - final_data, + data, ffmpeg_frame.data_mut(0), row_bytes, height, @@ -1516,10 +1477,10 @@ pub(crate) fn camera_frame_to_ffmpeg( cap_camera_windows::PixelFormat::RGB24 | cap_camera_windows::PixelFormat::BGR24 => { let row_bytes = width * 3; let size = layout.primary_stride * height; - if final_data.len() >= size { + if data.len() >= size { let stride = ffmpeg_frame.stride(0); copy_plane( - final_data, + data, ffmpeg_frame.data_mut(0), row_bytes, height, @@ -1533,12 +1494,12 @@ pub(crate) fn camera_frame_to_ffmpeg( let uv_height = height / 2; let y_size = layout.primary_stride * height; let uv_size = layout.secondary_stride * uv_height; - if final_data.len() >= y_size + uv_size * 2 { + if data.len() >= y_size + uv_size * 2 { let stride_y = ffmpeg_frame.stride(0); let stride_u = ffmpeg_frame.stride(1); let stride_v = ffmpeg_frame.stride(2); copy_plane( - &final_data[..y_size], + &data[..y_size], ffmpeg_frame.data_mut(0), width, height, @@ -1547,7 +1508,7 @@ pub(crate) fn camera_frame_to_ffmpeg( flip, ); copy_plane( - &final_data[y_size..y_size + uv_size], + &data[y_size..y_size + uv_size], ffmpeg_frame.data_mut(1), width / 2, uv_height, @@ -1556,7 +1517,7 @@ pub(crate) fn camera_frame_to_ffmpeg( flip, ); copy_plane( - &final_data[y_size + uv_size..y_size + uv_size * 2], + &data[y_size + uv_size..y_size + uv_size * 2], ffmpeg_frame.data_mut(2), width / 2, uv_height, @@ -1570,12 +1531,12 @@ pub(crate) fn camera_frame_to_ffmpeg( let uv_height = height / 2; let y_size = layout.primary_stride * height; let uv_size = layout.secondary_stride * uv_height; - if final_data.len() >= y_size + uv_size * 2 { + if data.len() >= y_size + uv_size * 2 { let stride_y = ffmpeg_frame.stride(0); let stride_u = ffmpeg_frame.stride(1); let stride_v = ffmpeg_frame.stride(2); copy_plane( - &final_data[..y_size], + &data[..y_size], ffmpeg_frame.data_mut(0), width, height, @@ -1584,7 +1545,7 @@ pub(crate) fn camera_frame_to_ffmpeg( flip, ); copy_plane( - &final_data[y_size + uv_size..y_size + uv_size * 2], + &data[y_size + uv_size..y_size + uv_size * 2], ffmpeg_frame.data_mut(1), width / 2, uv_height, @@ -1593,7 +1554,7 @@ pub(crate) fn camera_frame_to_ffmpeg( flip, ); copy_plane( - &final_data[y_size..y_size + uv_size], + &data[y_size..y_size + uv_size], ffmpeg_frame.data_mut(2), width / 2, uv_height, diff --git a/crates/recording/src/output_pipeline/win_fragmented_m4s.rs b/crates/recording/src/output_pipeline/win_fragmented_m4s.rs index b517b575903..e0e78d6646a 100644 --- a/crates/recording/src/output_pipeline/win_fragmented_m4s.rs +++ b/crates/recording/src/output_pipeline/win_fragmented_m4s.rs @@ -737,7 +737,7 @@ impl WindowsFragmentedM4SCameraMuxer { let mut updated = false; let desired_pixel_format = match frame.pixel_format { - cap_camera_windows::PixelFormat::UYVY422 => Pixel::YUYV422, + cap_camera_windows::PixelFormat::UYVY422 => Pixel::UYVY422, _ => self.video_config.pixel_format, }; diff --git a/crates/recording/src/sources/screen_capture/windows.rs b/crates/recording/src/sources/screen_capture/windows.rs index ef8a9dbd343..0ae3f117a91 100644 --- a/crates/recording/src/sources/screen_capture/windows.rs +++ b/crates/recording/src/sources/screen_capture/windows.rs @@ -466,6 +466,13 @@ pub struct VideoSourceConfig { settings: scap_direct3d::Settings, pub d3d_device: ID3D11Device, } + +impl VideoSourceConfig { + pub(crate) fn capture_frame_rate(&self) -> u32 { + self.video_info.fps().max(1) + } +} + pub struct VideoSource { video_info: VideoInfo, ctrl_tx: std::sync::mpsc::SyncSender, diff --git a/crates/scap-direct3d/src/lib.rs b/crates/scap-direct3d/src/lib.rs index 92e37f9d342..3f79ae851b4 100644 --- a/crates/scap-direct3d/src/lib.rs +++ b/crates/scap-direct3d/src/lib.rs @@ -582,7 +582,7 @@ impl Capturer { let frame_pool_size = settings .fps - .map(|fps| ((fps as f32 / 30.0 * 2.0).ceil() as i32).clamp(2, 4)) + .map(|fps| ((fps as f32 / 30.0 * 2.0).ceil() as i32).clamp(4, 6)) .unwrap_or(2); let item_size = item.Size().map_err(NewCapturerError::ItemSize)?; diff --git a/crates/verify/tests/codec.rs b/crates/verify/tests/codec.rs index d2efa9de025..bbff4a6afe8 100644 --- a/crates/verify/tests/codec.rs +++ b/crates/verify/tests/codec.rs @@ -215,7 +215,11 @@ fn video_code_survives_compositing_scaling_rounding_bubble_and_blur() { let dir = tempfile::tempdir().unwrap(); // 640x360 is a typical verification output; 320x180 puts the screen at // a quarter of its source size and the camera code cells at ~6 px. - for (out_w, out_h, bitrate) in [(640, 360, 150_000), (320, 180, 80_000)] { + // 320x180 is encoded at the Web export preset (0.08 bpp): at 80 kbps the + // paravirtualised VideoToolbox HEVC encoder on the CI macOS VMs + // occasionally left a blurred camera cell at margin 0.17, below the + // reader's 0.18, while this host's hardware encoder read it at 0.31. + for (out_w, out_h, bitrate) in [(640, 360, 150_000), (320, 180, 138_240)] { for blur in [false, true] { let pairs: Vec<(u32, u32)> = (0..40).map(|i| (i * 3 + 100, i / 2 + 7)).collect(); let frames: Vec = pairs diff --git a/crates/video-decode/src/media_foundation.rs b/crates/video-decode/src/media_foundation.rs index 5b4de1c8067..592480562c0 100644 --- a/crates/video-decode/src/media_foundation.rs +++ b/crates/video-decode/src/media_foundation.rs @@ -775,11 +775,11 @@ impl MediaFoundationDecoder { let uv_size = (y_stride * uv_height) as usize; let total_size = y_size + uv_size; - let mut data = vec![0u8; total_size]; - unsafe { - std::ptr::copy_nonoverlapping(mapped.pData as *const u8, data.as_mut_ptr(), total_size); + let data = unsafe { + let data = std::slice::from_raw_parts(mapped.pData as *const u8, total_size).to_vec(); self.d3d11_context.Unmap(staging, 0); - } + data + }; Ok(NV12Data { data: Arc::new(data), diff --git a/scripts/linux-desktop-harness/arch-up.sh b/scripts/linux-desktop-harness/arch-up.sh index 6a1a8028862..f3a883510d6 100755 --- a/scripts/linux-desktop-harness/arch-up.sh +++ b/scripts/linux-desktop-harness/arch-up.sh @@ -23,13 +23,16 @@ common=(LIBSEAT_BACKEND=seatd XDG_SESSION_TYPE=wayland) case "$desktop" in hyprland) - cat >"$HOME/.config/hypr/hyprland.conf" <"$HOME/.config/hypr/hyprland.lua" <"$HOME/.config/hypr/xdph.conf" < /^(Cap|so\.cap\.Cap)$/.test(w.get_wm_class() || '') && w.showing_on_its_workspace()).forEach(w => w.activate(global.display.get_current_time_roundtrip()))" \ >/dev/null 2>&1 elif [[ -n ${HYPRLAND_INSTANCE_SIGNATURE:-} ]]; then - hyprctl dispatch focuswindow class:Cap >/dev/null 2>&1 + hyprctl dispatch "hl.dsp.focus({ window = 'class:Cap' })" >/dev/null 2>&1 fi } for _ in $(seq 60); do