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
7 changes: 5 additions & 2 deletions .github/workflows/instant-reliability.yml
Original file line number Diff line number Diff line change
Expand Up @@ -316,8 +316,11 @@ jobs:
run: |
set -euo pipefail
sudo apt-get update -qq
# v4l2loopback needs videodev, which is in the extra kernel modules.
sudo apt-get install -y -qq "linux-modules-extra-$(uname -r)" v4l2loopback-dkms xvfb x11-utils xdotool \
if ! sudo modprobe videodev 2>/dev/null; then
sudo apt-get install -y -qq "linux-modules-extra-$(uname -r)"
sudo modprobe videodev
fi
sudo apt-get install -y -qq "linux-headers-$(uname -r)" v4l2loopback-dkms xvfb x11-utils xdotool \
dbus ffmpeg pulseaudio pulseaudio-utils libasound2-plugins >/dev/null
chmod +x "$RUNNER_TEMP/build/cap-gpui" "$RUNNER_TEMP/build/real-app-tests"
sudo modprobe v4l2loopback video_nr=42 card_label="Cap Test Camera" exclusive_caps=1
Expand Down
139 changes: 123 additions & 16 deletions apps/desktop-gpui/src/feeds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,39 +311,52 @@ impl CameraProcessingFactory {

#[cfg(target_os = "linux")]
impl RecordingCameraWorker {
fn publish(
&self,
image: &Arc<gpui::RenderImage>,
dims: (usize, usize),
timestamp: cap_timestamp::Timestamp,
mask: Option<cap_recording::instant_recording::LinuxCameraMaskReceipt>,
) {
fn accept(&self, timestamp: cap_timestamp::Timestamp) -> bool {
if self.publisher.is_cancelled() {
return;
return false;
}
match validate_recording_camera_frame(
timestamp,
self.not_before,
self.epoch,
self.current_epoch.load(Ordering::Acquire),
) {
Ok(true) => {}
Ok(false) => return,
Ok(true) => true,
Ok(false) => false,
Err(error) => {
self.publisher.fail(error);
return;
false
}
}
}

fn publish_sample(&self, frame: cap_recording::instant_recording::LinuxProcessedCameraFrame) {
if self.accept(frame.timestamp) {
self.publisher.publish(frame);
}
}

fn publish(
&self,
image: &Arc<gpui::RenderImage>,
dims: (usize, usize),
timestamp: cap_timestamp::Timestamp,
mask: Option<cap_recording::instant_recording::LinuxCameraMaskReceipt>,
) {
if !self.accept(timestamp) {
return;
}
if image.as_bytes(0).is_none() {
self.publisher
.fail("Processed camera pixels unavailable".to_string());
return;
}
self.publisher.publish(
cap_recording::instant_recording::LinuxProcessedCameraFrame {
bgra: cap_recording::instant_recording::LinuxCameraPixels::new(RenderImagePixels(
image.clone(),
)),
pixels: cap_recording::instant_recording::LinuxCameraPixels::new(
RenderImagePixels(image.clone()),
),
format: cap_recording::instant_recording::LinuxCameraSampleFormat::Bgra,
dimensions: (dims.0 as u32, dims.1 as u32),
stride: dims.0 * 4,
timestamp,
Expand All @@ -355,6 +368,24 @@ impl RecordingCameraWorker {
}
}

#[cfg(target_os = "linux")]
fn yuyv_overlay_frame(
frame: &cap_recording::FFmpegVideoFrame,
blur: u8,
generation: u64,
processing: cap_recording::instant_recording::LinuxCameraProcessing,
) -> Option<cap_recording::instant_recording::LinuxProcessedCameraFrame> {
if blur != 0 {
return None;
}
cap_recording::instant_recording::LinuxProcessedCameraFrame::from_yuyv422(
&frame.inner,
frame.timestamp,
generation,
processing,
)
}

#[cfg(target_os = "linux")]
struct RenderImagePixels(Arc<gpui::RenderImage>);

Expand Down Expand Up @@ -562,12 +593,29 @@ fn run_camera_preview_worker(config: CameraPreviewWorkerConfig) {
.publisher
.fail("Requested camera blur is unavailable".to_string());
}
#[cfg(target_os = "linux")]
let published_yuyv = recording.as_ref().is_some_and(|recording| {
let Some(sample) = yuyv_overlay_frame(
&frame,
requested_blur,
recording.generation,
recording.processing,
) else {
return false;
};
recording.publish_sample(sample);
true
});
#[cfg(target_os = "linux")]
if published_yuyv && !active.load(Ordering::Acquire) {
continue;
}
let max_dims = blur_mode.map(|_| crate::camera_blur_portable::MAX_DIMS);
let Some((mut image, dims)) =
camera_preview_image(&frame.inner, &mut scaler, requested_mirror, max_dims)
else {
#[cfg(target_os = "linux")]
if let Some(recording) = &recording {
if !published_yuyv && let Some(recording) = &recording {
recording
.publisher
.fail("Camera frame conversion failed".to_string());
Expand Down Expand Up @@ -657,7 +705,10 @@ fn run_camera_preview_worker(config: CameraPreviewWorkerConfig) {
}
}
#[cfg(target_os = "linux")]
if effects_ready && let Some(recording) = &recording {
if !published_yuyv
&& effects_ready
&& let Some(recording) = &recording
{
recording.publish(&image, dims, frame.timestamp, applied_mask);
}
if active.load(Ordering::Acquire) {
Expand Down Expand Up @@ -2206,6 +2257,62 @@ mod tests {
assert!(scaler.is_some());
}

#[cfg(target_os = "linux")]
#[test]
fn blur_off_yuyv_recording_keeps_the_packed_plane() {
use cap_recording::instant_recording::{
LinuxCameraBlur, LinuxCameraProcessing, LinuxCameraSampleFormat,
};
use cap_timestamp::Timestamp;

let mut frame = ffmpeg::frame::Video::new(ffmpeg::format::Pixel::YUYV422, 4, 2);
let stride = frame.stride(0);
let row = 8;
frame.data_mut(0).fill(0xAB);
for y in 0..2 {
frame.data_mut(0)[y * stride..y * stride + row]
.copy_from_slice(&[10, 20, 30, 40, 50, 60, 70, 80]);
}
let timestamp = Timestamp::Instant(std::time::Instant::now());
let processing = LinuxCameraProcessing {
mirrored: true,
blur: LinuxCameraBlur::Off,
};
let packed = cap_recording::FFmpegVideoFrame {
inner: frame,
timestamp,
};
let sample = yuyv_overlay_frame(&packed, 0, 4, processing).expect("yuyv sample");
assert_eq!(sample.format, LinuxCameraSampleFormat::Yuyv422);
assert_eq!(sample.dimensions, (4, 2));
assert_eq!(sample.stride, row);
assert_eq!(sample.generation, 4);
assert_eq!(sample.processing, processing);
assert!(sample.mask.is_none());
assert_eq!(
&*sample.pixels,
&[
10, 20, 30, 40, 50, 60, 70, 80, 10, 20, 30, 40, 50, 60, 70, 80
]
);

let mut blurred = packed;
assert!(yuyv_overlay_frame(&blurred, 1, 4, processing).is_none());
blurred.inner = ffmpeg::frame::Video::new(ffmpeg::format::Pixel::BGRA, 4, 2);
assert!(yuyv_overlay_frame(&blurred, 0, 4, processing).is_none());
let heavy = LinuxCameraProcessing {
blur: LinuxCameraBlur::Heavy,
..processing
};
let mut yuyv = ffmpeg::frame::Video::new(ffmpeg::format::Pixel::YUYV422, 4, 2);
yuyv.data_mut(0).fill(1);
let source = cap_recording::FFmpegVideoFrame {
inner: yuyv,
timestamp,
};
assert!(yuyv_overlay_frame(&source, 0, 4, heavy).is_none());
}

#[cfg(target_os = "linux")]
#[test]
fn recording_camera_pixels_share_the_preview_image() {
Expand Down
4 changes: 2 additions & 2 deletions crates/recording/src/instant_recording.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ mod linux_camera;
pub use linux_camera::{
LINUX_CAMERA_MAX_MASK_AGE, LinuxCameraBlur, LinuxCameraEffect, LinuxCameraMaskReceipt,
LinuxCameraPixels, LinuxCameraPresentation, LinuxCameraPresentationError,
LinuxCameraProcessing, LinuxCameraPublisher, LinuxCameraRect, LinuxCameraShape,
LinuxProcessedCameraFrame, LinuxProcessedCameraSource,
LinuxCameraProcessing, LinuxCameraPublisher, LinuxCameraRect, LinuxCameraSampleFormat,
LinuxCameraShape, LinuxProcessedCameraFrame, LinuxProcessedCameraSource,
};

use crate::{
Expand Down
Loading
Loading