Skip to content

feat(editor): hide the camera during silence #328

feat(editor): hide the camera during silence

feat(editor): hide the camera during silence #328

name: Instant reliability (real app)
# Drives the real GPUI app through Instant recordings against the in-process
# mock Cap server (`apps/desktop-gpui/src/upload/real_app_tests.rs`): a normal
# recording, a start stopped while it is still in flight, force-kills from the
# server video's creation through the first seconds of capture, and a crash
# 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, and a hosted job records
# Instant with a virtual camera drawn into the video.
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- "apps/desktop-gpui/src/upload.rs"
- "apps/desktop-gpui/src/upload/**"
- "apps/desktop-gpui/src/recording.rs"
- "apps/desktop-gpui/src/session.rs"
- "apps/desktop-gpui/src/library.rs"
- "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"
- "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"
- ".github/workflows/instant-reliability.yml"
schedule:
- cron: "40 4 * * *"
workflow_dispatch:
permissions:
contents: read
pull-requests: read
concurrency:
group: instant-reliability-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
# A pull request from a fork that changes only this workflow does not queue
# the macOS job; one that also changes Instant code does, and so does any
# change from this repository.
scope:
name: Scope
runs-on: blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 5
outputs:
run: ${{ steps.scope.outputs.run }}
steps:
- name: Scope
id: scope
env:
GH_TOKEN: ${{ github.token }}
EVENT: ${{ github.event_name }}
PR: ${{ github.event.pull_request.number }}
SAME_REPO: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
run: |
set -euo pipefail
if [[ "$EVENT" != pull_request || "$SAME_REPO" == true ]]; then
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|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
echo "run=false" >> "$GITHUB_OUTPUT"
echo "A fork pull request that changes no Instant code does not run the real-app suite." | tee -a "$GITHUB_STEP_SUMMARY"
fi
real-app:
name: Instant crash, cancel and recovery (real app)
needs: scope
if: needs.scope.outputs.run == 'true'
runs-on: blacksmith-6vcpu-macos-15
timeout-minutes: 60
env:
CARGO_PROFILE_DEV_DEBUG: "0"
TARGET: aarch64-apple-darwin
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Rust setup
uses: dtolnay/rust-toolchain@1.95.0
with:
targets: aarch64-apple-darwin
- uses: ./.github/actions/setup-rust-cache
with:
target: aarch64-apple-darwin
prefix-key: v1-rust-gpui
save-cache: "false"
workspaces: apps/desktop-gpui -> target
- name: Run setup
run: node scripts/setup.js
env:
RUST_TARGET_TRIPLE: aarch64-apple-darwin
- name: Prepare GPUI dependency
run: |
node scripts/sync-desktop-versions.mjs
./scripts/prepare-gpui-dependency.sh
- name: Build the app and its tests
run: |
set -euo pipefail
# The tests load frameworks from debug/Frameworks (beside deps/), the
# app binary from the directory above debug/.
for frameworks in \
"$GITHUB_WORKSPACE/apps/desktop-gpui/target/$TARGET/debug/Frameworks" \
"$GITHUB_WORKSPACE/apps/desktop-gpui/target/$TARGET/Frameworks"; do
mkdir -p "$frameworks"
ln -sfn "$GITHUB_WORKSPACE/target/Frameworks/Spacedrive.framework" "$frameworks/Spacedrive.framework"
done
cargo +1.95.0 build --manifest-path apps/desktop-gpui/Cargo.toml --locked --target "$TARGET" --bin cap-gpui
cargo +1.95.0 test --manifest-path apps/desktop-gpui/Cargo.toml --locked --target "$TARGET" --bin cap-gpui --no-run --message-format=json \
| python3 -c 'import json, sys
for line in sys.stdin:
m = json.loads(line)
if m.get("reason") == "compiler-artifact" and m.get("executable") and m["target"]["name"] == "cap-gpui" and m["profile"]["test"]:
print(m["executable"])' > "$RUNNER_TEMP/test-binary"
test -s "$RUNNER_TEMP/test-binary"
echo "$GITHUB_WORKSPACE/apps/desktop-gpui/target/$TARGET/debug/cap-gpui" > "$RUNNER_TEMP/app-binary"
cat "$RUNNER_TEMP/test-binary" "$RUNNER_TEMP/app-binary"
# Blacksmith runs a logged-in GUI session, but ScreenCaptureKit is denied
# until TCC grants the capturing binaries and the runner ancestry, the
# display starts asleep, and there is no audio device (the start chime
# plays before capture begins), as in verify-recording.yml.
- name: Capture permissions, display and audio device
run: |
set -euo pipefail
sudo pmset -a displaysleep 0 sleep 0 disksleep 0
caffeinate -u -t 2
nohup caffeinate -dimsu > /dev/null 2>&1 &
brew install --cask blackhole-2ch
brew install switchaudio-osx
sudo killall coreaudiod || true
for _ in $(seq 1 30); do
SwitchAudioSource -a -t output | grep -qx "BlackHole 2ch" && break
sleep 1
done
SwitchAudioSource -s "BlackHole 2ch" -t output
SwitchAudioSource -s "BlackHole 2ch" -t input
cat "$RUNNER_TEMP/test-binary" "$RUNNER_TEMP/app-binary" > "$RUNNER_TEMP/capture-binaries"
p=$$
while [ "$p" -gt 1 ]; do
ps -o comm= -p "$p"
p=$(ps -o ppid= -p "$p" | tr -d ' ')
done >> "$RUNNER_TEMP/capture-binaries"
db="/Library/Application Support/com.apple.TCC/TCC.db"
user_db="$HOME/Library/Application Support/com.apple.TCC/TCC.db"
approvals="$HOME/Library/Group Containers/group.com.apple.replayd/ScreenCaptureApprovals.plist"
mkdir -p "$(dirname "$approvals")"
now=$(date +%s)
until=$(date -u -v+1y '+%a %b %d %H:%M:%S GMT %Y')
sort -u "$RUNNER_TEMP/capture-binaries" | grep '^/' | while read -r client; do
for service in kTCCServiceScreenCapture kTCCServiceMicrophone; do
row="INSERT OR REPLACE INTO access (service,client,client_type,auth_value,auth_reason,auth_version,indirect_object_identifier,flags,last_modified) VALUES ('$service','$client',1,2,4,1,'UNUSED',0,$now);"
sudo sqlite3 "$db" "$row"
sqlite3 "$user_db" "$row"
done
/usr/libexec/PlistBuddy -c "Add :$client date $until" "$approvals" >/dev/null 2>&1 || true
echo "granted $client"
done
killall tccd || true
sudo killall tccd || true
sleep 2
- name: Real-app Instant reliability
run: |
set -euo pipefail
export CAP_GPUI_REAL_APP_BIN="$(cat "$RUNNER_TEMP/app-binary")"
export CAP_GPUI_REAL_APP_ARTIFACTS="$RUNNER_TEMP/real-app"
"$(cat "$RUNNER_TEMP/test-binary")" real_app --ignored --test-threads=1 --nocapture
- name: Upload sandboxes
if: failure()
uses: actions/upload-artifact@v4
with:
name: instant-reliability-sandboxes
path: ${{ runner.temp }}/real-app
retention-days: 7
real-app-linux:
name: Instant upload and recovery on Linux (real app)
needs: scope
if: needs.scope.outputs.run == 'true'
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 60
env:
CARGO_PROFILE_DEV_DEBUG: "0"
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Linux desktop dependencies
uses: ./.github/actions/install-desktop-deps
- name: Rust setup
uses: dtolnay/rust-toolchain@1.95.0
with:
targets: x86_64-unknown-linux-gnu
- uses: ./.github/actions/setup-rust-cache
with:
target: x86_64-unknown-linux-gnu
prefix-key: v1-rust-gpui
save-cache: "false"
workspaces: apps/desktop-gpui -> target
- name: Run setup
run: node scripts/setup.js
env:
RUST_TARGET_TRIPLE: x86_64-unknown-linux-gnu
- name: Prepare GPUI dependency
run: |
node scripts/sync-desktop-versions.mjs
./scripts/prepare-gpui-dependency.sh
- name: Build the app and its tests
run: |
set -euo pipefail
cargo +1.95.0 build --manifest-path apps/desktop-gpui/Cargo.toml --locked --bin cap-gpui
cargo +1.95.0 test --manifest-path apps/desktop-gpui/Cargo.toml --locked --bin cap-gpui --no-run --message-format=json \
| python3 -c 'import json, sys
for line in sys.stdin:
m = json.loads(line)
if m.get("reason") == "compiler-artifact" and m.get("executable") and m["target"]["name"] == "cap-gpui" and m["profile"]["test"]:
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.
- name: Real-app Instant upload and recovery on Xvfb
run: |
set -euo pipefail
sudo apt-get install -y -qq xvfb x11-utils dbus pulseaudio pulseaudio-utils libasound2-plugins >/dev/null
# An output device for the start cue, which the start-cue case waits on.
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
DISPLAY=:78 xmessage -geometry 640x360+200+200 "A window to record" >/dev/null 2>&1 &
env -u WAYLAND_DISPLAY DISPLAY=:78 XDG_SESSION_TYPE=x11 \
LD_LIBRARY_PATH="$GITHUB_WORKSPACE/target/native-deps/lib" \
CAP_GPUI_REAL_APP_BIN="$GITHUB_WORKSPACE/apps/desktop-gpui/target/debug/cap-gpui" \
CAP_GPUI_REAL_APP_ARTIFACTS="$RUNNER_TEMP/real-app" \
CAP_GPUI_AUTO_OVERLAY=window CAP_GPUI_AUTO_WINDOW=xmessage \
dbus-run-session -- "$(cat "$RUNNER_TEMP/test-binary")" --ignored --test-threads=1 --nocapture \
real_app_instant_recording_uploads_and_verifies real_app_recovered_recording_uploads \
real_app_start_cue_waits_for_a_slow_server_video real_app_cancelled_start_deletes_its_video \
real_app_crashes_never_delete_a_recording_with_media
- name: Upload sandboxes
if: failure()
uses: actions/upload-artifact@v4
with:
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 update -qq
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
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_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
# 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 "$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"
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