From 0fc9e9cd29607e4fd3ea69a7d62d3fce3478dee4 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Mon, 26 Oct 2026 17:13:59 +0000 Subject: [PATCH 1/5] ci(instant): record Linux Instant with a virtual camera --- .github/workflows/instant-reliability.yml | 99 ++++++++++++++++++- .../desktop-gpui/src/upload/real_app_tests.rs | 47 +++++++++ 2 files changed, 144 insertions(+), 2 deletions(-) diff --git a/.github/workflows/instant-reliability.yml b/.github/workflows/instant-reliability.yml index a0655c46e40..c8b195ae3d0 100644 --- a/.github/workflows/instant-reliability.yml +++ b/.github/workflows/instant-reliability.yml @@ -6,7 +6,8 @@ name: Instant reliability (real app) # followed by Recover. Each case checks what reached the mock server and what # was left on disk, so a regression that loses a recording, deletes one with # media, or leaves an empty video behind fails here. The Linux job runs the -# same cases on Xvfb, recording another app's window. +# same cases on Xvfb, recording another app's window, and a hosted job records +# Instant with a virtual camera drawn into the video. on: pull_request: @@ -23,6 +24,9 @@ on: - "crates/recording/src/upload_resume.rs" - "crates/recording/src/upload_verification.rs" - "crates/recording/src/instant_recording.rs" + - "crates/recording/src/instant_recording/**" + - "crates/camera/**" + - "crates/camera-ffmpeg/**" - "crates/recording/src/recovery.rs" - "crates/enc-ffmpeg/src/mux/segmented_stream.rs" - "crates/enc-ffmpeg/src/mux/dash_audio.rs" @@ -63,7 +67,7 @@ jobs: echo "run=true" >> "$GITHUB_OUTPUT"; exit 0 fi gh api "repos/$GITHUB_REPOSITORY/pulls/$PR/files" --paginate --jq '.[].filename' > "$RUNNER_TEMP/changed.txt" - instant='^(apps/desktop-gpui/src/(upload\.rs|upload/.*|recording\.rs|session\.rs|library\.rs|main_window\.rs|auth\.rs|store\.rs)|crates/recording/src/(upload_resume|upload_verification|instant_recording|recovery)\.rs|crates/enc-ffmpeg/src/mux/(segmented_stream|dash_audio)\.rs)$' + instant='^(apps/desktop-gpui/src/(upload\.rs|upload/.*|recording\.rs|session\.rs|library\.rs|main_window\.rs|auth\.rs|store\.rs)|crates/recording/src/(upload_resume|upload_verification|instant_recording|recovery)\.rs|crates/recording/src/instant_recording/.*|crates/camera(-ffmpeg)?/.*|crates/enc-ffmpeg/src/mux/(segmented_stream|dash_audio)\.rs)$' if grep -qE "$instant" "$RUNNER_TEMP/changed.txt"; then echo "run=true" >> "$GITHUB_OUTPUT" else @@ -236,6 +240,20 @@ jobs: print(m["executable"])' > "$RUNNER_TEMP/test-binary" test -s "$RUNNER_TEMP/test-binary" + - name: Package the build for the camera job + run: | + mkdir -p "$RUNNER_TEMP/build/libs" + cp apps/desktop-gpui/target/debug/cap-gpui "$RUNNER_TEMP/build/cap-gpui" + cp "$(cat "$RUNNER_TEMP/test-binary")" "$RUNNER_TEMP/build/real-app-tests" + strip --strip-debug "$RUNNER_TEMP/build/cap-gpui" "$RUNNER_TEMP/build/real-app-tests" + cp -a target/native-deps/lib/. "$RUNNER_TEMP/build/libs/" + + - uses: actions/upload-artifact@v4 + with: + name: instant-reliability-linux-build + path: ${{ runner.temp }}/build + retention-days: 1 + # X11 captures without a portal. Display and area recordings wait for # the Stop shortcut to start, so the cases record another app's window. # The runner has no audio device: a PulseAudio null sink plays the cue. @@ -271,3 +289,80 @@ jobs: name: instant-reliability-sandboxes-linux path: ${{ runner.temp }}/real-app retention-days: 7 + + # Linux draws the camera into the Instant screen video. The virtual camera, + # v4l2loopback, builds against the hosted runner's kernel; Blacksmith's + # kernel cannot load it, so this job runs the build from the job above. + real-app-linux-camera: + name: Instant with a camera on Linux (real app) + needs: real-app-linux + runs-on: ubuntu-24.04 + timeout-minutes: 30 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: instant-reliability-linux-build + path: ${{ runner.temp }}/build + + - name: Install Linux desktop dependencies + uses: ./.github/actions/install-desktop-deps + + - name: Instant with a camera on Xvfb + run: | + set -euo pipefail + sudo apt-get install -y -qq v4l2loopback-dkms xvfb x11-utils 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 + sudo udevadm settle --timeout=10 + sudo chmod 666 /dev/video42 + ffmpeg -hide_banner -loglevel error -nostdin -re -f lavfi -i testsrc2=size=1280x720:rate=30 \ + -pix_fmt yuyv422 -f v4l2 /dev/video42 >"$RUNNER_TEMP/feeder.log" 2>&1 & + pulse_dir=$(mktemp -d) + pulseaudio -n --daemonize=yes --exit-idle-time=-1 \ + -L "module-native-protocol-unix socket=$pulse_dir/native auth-anonymous=1" \ + -L "module-null-sink sink_name=cap_output" -L "module-always-sink" + export PULSE_SERVER="unix:$pulse_dir/native" + for _ in $(seq 40); do pactl info >/dev/null 2>&1 && break; sleep 0.25; done + pactl set-default-sink cap_output + Xvfb :78 -screen 0 1920x1080x24 >/dev/null 2>&1 & + sleep 1 + # The recorded window covers the screen, so the camera preview is + # inside the capture wherever it opens; Instant refuses one outside. + DISPLAY=:78 xmessage -geometry 1920x1080+0+0 "A window to record" >/dev/null 2>&1 & + env -u WAYLAND_DISPLAY DISPLAY=:78 XDG_SESSION_TYPE=x11 \ + LD_LIBRARY_PATH="$RUNNER_TEMP/build/libs" \ + CAP_GPUI_REAL_APP_BIN="$RUNNER_TEMP/build/cap-gpui" \ + CAP_GPUI_REAL_APP_ARTIFACTS="$RUNNER_TEMP/real-app" \ + CAP_GPUI_AUTO_OVERLAY=window CAP_GPUI_AUTO_WINDOW=xmessage \ + dbus-run-session -- "$RUNNER_TEMP/build/real-app-tests" --ignored --test-threads=1 --nocapture \ + real_app_instant_recording_with_camera_uploads_and_verifies + + # The recorded window is grey and the test pattern is saturated colour, + # so a frame without colour is a frame without the camera. + - name: Camera picture reached the video + run: | + set -euo pipefail + display=$(find "$RUNNER_TEMP/real-app" -type d -path "*content/display" | head -1) + test -n "$display" + ls -la "$display" + joined=$RUNNER_TEMP/joined.mp4 + cat "$display"/init.mp4 $(ls "$display"/*.m4s | sort -V) > "$joined" + ffprobe -v error -show_entries format=duration -of csv=p=0 "$joined" + ffmpeg -hide_banner -nostdin -i "$joined" -vf signalstats,metadata=print:key=lavfi.signalstats.SATMAX \ + -f null - 2>&1 | grep -o 'SATMAX=[0-9.]*' > "$RUNNER_TEMP/saturation.txt" + awk -F= '{ n++; if ($2 + 0 > 40) c++ } END { printf "%d of %d frames show the camera\n", c, n; exit !(n >= 60 && c >= n * 0.95) }' \ + "$RUNNER_TEMP/saturation.txt" + + - name: Upload sandboxes + if: failure() + uses: actions/upload-artifact@v4 + with: + name: instant-reliability-sandboxes-linux-camera + path: | + ${{ runner.temp }}/real-app + ${{ runner.temp }}/feeder.log + retention-days: 7 diff --git a/apps/desktop-gpui/src/upload/real_app_tests.rs b/apps/desktop-gpui/src/upload/real_app_tests.rs index 51851157af5..8889695639e 100644 --- a/apps/desktop-gpui/src/upload/real_app_tests.rs +++ b/apps/desktop-gpui/src/upload/real_app_tests.rs @@ -303,6 +303,53 @@ async fn real_app_instant_recording_uploads_and_verifies() { assert!(sandbox.journal().is_empty(), "{:?}", sandbox.journal()); } +/// An Instant recording with the camera on uploads and verifies. On Linux the +/// camera is drawn into the screen video, so a camera that stalls or sends a +/// bad frame stops the recording. Needs a camera attached; the CI job feeds a +/// virtual one and checks its picture reached the video. +#[ignore] +#[tokio::test(flavor = "multi_thread")] +async fn real_app_instant_recording_with_camera_uploads_and_verifies() { + let mock = MockCap::start(Vec::new()).await; + let sandbox = Sandbox::new(&mock.url, 0); + let app = sandbox.launch( + "camera", + &[ + ("CAP_GPUI_AUTO_CAMERA", "1"), + ("CAP_GPUI_AUTO_RECORD", "instant:8"), + ], + ); + wait_for_app( + &sandbox, + &app, + "a verified upload", + Duration::from_secs(180), + || { + sandbox + .projects() + .first() + .and_then(|project| state(project)) + .is_some_and(|state| state["phase"] == "Verified") + }, + ) + .await; + kill(app); + let log = sandbox.log("camera"); + assert!(log.contains("auto-selecting camera"), "no camera selected"); + let video = mock.created(); + assert_eq!(video.len(), 1, "{video:?}"); + assert!( + mock.confirmations() + .iter() + .any(|confirmation| confirmation["videoId"] == video[0].as_str()) + ); + let project = sandbox.projects().remove(0); + assert!(finished_segments(&project) >= 3, "{}", project.display()); + assert!(mock.deleted().is_empty(), "{:?}", mock.deleted()); + assert!(sandbox.journal().is_empty(), "{:?}", sandbox.journal()); + assert!(!log.contains("panicked at"), "the app panicked"); +} + /// Kills the app at points from the server video's creation through the /// first seconds of capture, then relaunches long after. Each recording's state /// is noted right after its kill, before anything else runs: a recording that From 1597385536b3a806efbcc5a34c4d2f4bf576aba9 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Mon, 26 Oct 2026 17:48:01 +0000 Subject: [PATCH 2/5] ci(instant): gate the camera case and load videodev --- .github/workflows/instant-reliability.yml | 8 +++++--- apps/desktop-gpui/src/upload/real_app_tests.rs | 9 +++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/instant-reliability.yml b/.github/workflows/instant-reliability.yml index c8b195ae3d0..f8635dfcb65 100644 --- a/.github/workflows/instant-reliability.yml +++ b/.github/workflows/instant-reliability.yml @@ -313,8 +313,10 @@ jobs: - name: Instant with a camera on Xvfb run: | set -euo pipefail - sudo apt-get install -y -qq v4l2loopback-dkms xvfb x11-utils dbus ffmpeg \ - pulseaudio pulseaudio-utils libasound2-plugins >/dev/null + 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 \ + 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 sudo udevadm settle --timeout=10 @@ -336,7 +338,7 @@ jobs: env -u WAYLAND_DISPLAY DISPLAY=:78 XDG_SESSION_TYPE=x11 \ LD_LIBRARY_PATH="$RUNNER_TEMP/build/libs" \ CAP_GPUI_REAL_APP_BIN="$RUNNER_TEMP/build/cap-gpui" \ - CAP_GPUI_REAL_APP_ARTIFACTS="$RUNNER_TEMP/real-app" \ + CAP_GPUI_REAL_APP_ARTIFACTS="$RUNNER_TEMP/real-app" CAP_GPUI_REAL_APP_CAMERA=1 \ CAP_GPUI_AUTO_OVERLAY=window CAP_GPUI_AUTO_WINDOW=xmessage \ dbus-run-session -- "$RUNNER_TEMP/build/real-app-tests" --ignored --test-threads=1 --nocapture \ real_app_instant_recording_with_camera_uploads_and_verifies diff --git a/apps/desktop-gpui/src/upload/real_app_tests.rs b/apps/desktop-gpui/src/upload/real_app_tests.rs index 8889695639e..cbbe7d49f6f 100644 --- a/apps/desktop-gpui/src/upload/real_app_tests.rs +++ b/apps/desktop-gpui/src/upload/real_app_tests.rs @@ -305,11 +305,16 @@ async fn real_app_instant_recording_uploads_and_verifies() { /// An Instant recording with the camera on uploads and verifies. On Linux the /// camera is drawn into the screen video, so a camera that stalls or sends a -/// bad frame stops the recording. Needs a camera attached; the CI job feeds a -/// virtual one and checks its picture reached the video. +/// bad frame stops the recording. Runs only with `CAP_GPUI_REAL_APP_CAMERA=1`, +/// since it needs a camera attached; the CI job feeds a virtual one and checks +/// its picture reached the video. #[ignore] #[tokio::test(flavor = "multi_thread")] async fn real_app_instant_recording_with_camera_uploads_and_verifies() { + if std::env::var("CAP_GPUI_REAL_APP_CAMERA").is_err() { + eprintln!("skipped: set CAP_GPUI_REAL_APP_CAMERA=1 where a camera is attached"); + return; + } let mock = MockCap::start(Vec::new()).await; let sandbox = Sandbox::new(&mock.url, 0); let app = sandbox.launch( From e9e6ec5969960f40468177c07d4079c0671f9a42 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Mon, 26 Oct 2026 18:34:22 +0000 Subject: [PATCH 3/5] ci(instant): start and stop the camera recording with the shortcut --- .github/workflows/instant-reliability.yml | 2 +- .../desktop-gpui/src/upload/real_app_tests.rs | 38 ++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/.github/workflows/instant-reliability.yml b/.github/workflows/instant-reliability.yml index f8635dfcb65..334a57b540f 100644 --- a/.github/workflows/instant-reliability.yml +++ b/.github/workflows/instant-reliability.yml @@ -315,7 +315,7 @@ jobs: 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 \ + sudo apt-get install -y -qq "linux-modules-extra-$(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 diff --git a/apps/desktop-gpui/src/upload/real_app_tests.rs b/apps/desktop-gpui/src/upload/real_app_tests.rs index cbbe7d49f6f..f75eb2b525c 100644 --- a/apps/desktop-gpui/src/upload/real_app_tests.rs +++ b/apps/desktop-gpui/src/upload/real_app_tests.rs @@ -321,9 +321,45 @@ async fn real_app_instant_recording_with_camera_uploads_and_verifies() { "camera", &[ ("CAP_GPUI_AUTO_CAMERA", "1"), - ("CAP_GPUI_AUTO_RECORD", "instant:8"), + ("CAP_GPUI_AUTO_RECORD", "instant:60"), ], ); + // On X11 a window recorded with the camera hides Cap's controls, and the + // recording waits for the Stop shortcut to start; the same shortcut stops + // it, well before the automatic stop. + #[cfg(target_os = "linux")] + { + let press = || { + let status = Command::new("xdotool") + .args(["key", "ctrl+shift+F9"]) + .status() + .expect("install xdotool"); + assert!(status.success()); + }; + let sandbox = &sandbox; + let logged = |line: &'static str| move || sandbox.log("camera").contains(line); + let start = Duration::from_secs(60); + wait_for_app( + sandbox, + &app, + "the start press", + start, + logged("overlay start pressed"), + ) + .await; + tokio::time::sleep(Duration::from_secs(2)).await; + press(); + wait_for_app( + sandbox, + &app, + "the recording to start", + start, + logged("recording started"), + ) + .await; + tokio::time::sleep(Duration::from_secs(8)).await; + press(); + } wait_for_app( &sandbox, &app, From cf7ee228d6869b8eb96a9e865c1a88b0f37de90c Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Mon, 26 Oct 2026 18:56:06 +0000 Subject: [PATCH 4/5] ci(instant): quote the camera recording's segment paths --- .github/workflows/instant-reliability.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/instant-reliability.yml b/.github/workflows/instant-reliability.yml index 334a57b540f..2650ef71d7f 100644 --- a/.github/workflows/instant-reliability.yml +++ b/.github/workflows/instant-reliability.yml @@ -352,7 +352,7 @@ jobs: test -n "$display" ls -la "$display" joined=$RUNNER_TEMP/joined.mp4 - cat "$display"/init.mp4 $(ls "$display"/*.m4s | sort -V) > "$joined" + cat "$display"/init.mp4 "$display"/segment_*.m4s > "$joined" ffprobe -v error -show_entries format=duration -of csv=p=0 "$joined" ffmpeg -hide_banner -nostdin -i "$joined" -vf signalstats,metadata=print:key=lavfi.signalstats.SATMAX \ -f null - 2>&1 | grep -o 'SATMAX=[0-9.]*' > "$RUNNER_TEMP/saturation.txt" From ee43c6ed1c59b03e7e8d7b4e9b9763cec708e7c6 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Tue, 27 Oct 2026 03:14:01 +0000 Subject: [PATCH 5/5] ci(instant): run the camera case for camera feed and preview changes --- .github/workflows/instant-reliability.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/instant-reliability.yml b/.github/workflows/instant-reliability.yml index 2650ef71d7f..af99f5174a4 100644 --- a/.github/workflows/instant-reliability.yml +++ b/.github/workflows/instant-reliability.yml @@ -21,6 +21,8 @@ on: - "apps/desktop-gpui/src/main_window.rs" - "apps/desktop-gpui/src/auth.rs" - "apps/desktop-gpui/src/store.rs" + - "apps/desktop-gpui/src/feeds.rs" + - "apps/desktop-gpui/src/camera_window.rs" - "crates/recording/src/upload_resume.rs" - "crates/recording/src/upload_verification.rs" - "crates/recording/src/instant_recording.rs" @@ -67,7 +69,7 @@ jobs: echo "run=true" >> "$GITHUB_OUTPUT"; exit 0 fi gh api "repos/$GITHUB_REPOSITORY/pulls/$PR/files" --paginate --jq '.[].filename' > "$RUNNER_TEMP/changed.txt" - instant='^(apps/desktop-gpui/src/(upload\.rs|upload/.*|recording\.rs|session\.rs|library\.rs|main_window\.rs|auth\.rs|store\.rs)|crates/recording/src/(upload_resume|upload_verification|instant_recording|recovery)\.rs|crates/recording/src/instant_recording/.*|crates/camera(-ffmpeg)?/.*|crates/enc-ffmpeg/src/mux/(segmented_stream|dash_audio)\.rs)$' + instant='^(apps/desktop-gpui/src/(upload\.rs|upload/.*|recording\.rs|session\.rs|library\.rs|main_window\.rs|auth\.rs|store\.rs|feeds\.rs|camera_window\.rs)|crates/recording/src/(upload_resume|upload_verification|instant_recording|recovery)\.rs|crates/recording/src/instant_recording/.*|crates/camera(-ffmpeg)?/.*|crates/enc-ffmpeg/src/mux/(segmented_stream|dash_audio)\.rs)$' if grep -qE "$instant" "$RUNNER_TEMP/changed.txt"; then echo "run=true" >> "$GITHUB_OUTPUT" else