Repository navigation
feat(editor): hide the camera during silence #328
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |