Skip to content

Commit e00270e

Browse files
committed
ci: probe Blacksmith encoder choice and Instant mixer (scratch, not for merge)
1 parent 7a758db commit e00270e

3 files changed

Lines changed: 117 additions & 88 deletions

File tree

‎.github/workflows/bs-recording-probe.yml‎

Lines changed: 63 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,16 @@ jobs:
2222
CAP_VERIFY_SCRATCH: ${{ github.workspace }}/verify-scratch
2323
CAP_VERIFY_REPORT_DIR: ${{ github.workspace }}/verify-report
2424
CAP_VERIFY_DUMP: ${{ github.workspace }}/verify-dump
25-
# Hosted macos-26 has a virtual output device that gives
26-
# ScreenCaptureKit only silence, so system audio is not required
27-
# there; Blacksmith gets BlackHole (set up below), which carries the
28-
# stimulus's audio to ScreenCaptureKit, so system audio is required
29-
# and the system-only case runs.
25+
# Hosted runners' output devices give ScreenCaptureKit only silence, so
26+
# system audio is not checked there. Blacksmith gets BlackHole (set up
27+
# below), which does carry the stimulus's audio on macOS 15 (not on
28+
# 26), but its system-audio timing is not characterised yet (run
29+
# 38843005408: system audio +31 to +44 ms against the mic, BlackHole
30+
# latency not in the truth), so by default the stimulus stays silent
31+
# there too; dispatch system_audio=true plays and requires it.
3032
CAP_VERIFY_AUDIO_OUTPUT: ${{ startsWith(matrix.runner, 'blacksmith-') && 'BlackHole 2ch' || matrix.runner == 'macos-26' && 'Null Audio Device' || '' }}
31-
CAP_VERIFY_REQUIRE_SYSTEM_AUDIO: ${{ startsWith(matrix.runner, 'blacksmith-') && '1' || '' }}
33+
CAP_VERIFY_STIMULUS_AUDIO: ${{ startsWith(matrix.runner, 'blacksmith-') && !inputs.system_audio && '0' || '' }}
34+
CAP_VERIFY_REQUIRE_SYSTEM_AUDIO: ${{ startsWith(matrix.runner, 'blacksmith-') && inputs.system_audio && '1' || '' }}
3235
steps:
3336
- uses: actions/checkout@v4
3437

@@ -71,15 +74,25 @@ jobs:
7174
cargo test --locked -p cap-verify --test recording --no-run
7275
echo "build seconds: $(( $(date +%s) - start ))"
7376
74-
# Blacksmith runs a logged-in GUI session with SIP disabled, but
75-
# ScreenCaptureKit is denied until TCC grants the responsible process
76-
# (Runner.Worker), so grant Screen Recording (and Microphone) to every
77-
# binary that captures plus the whole runner ancestry, then restart
78-
# tccd. It has no audio device: BlackHole provides input and output.
77+
# Blacksmith runs a logged-in GUI session with SIP disabled, but:
78+
# - ScreenCaptureKit is denied until TCC grants the responsible process
79+
# (Runner.Worker), so Screen Recording and Microphone are granted to
80+
# every binary that captures plus the whole runner ancestry, in the
81+
# system and the user TCC database (Microphone is a per-user
82+
# service: without it CoreAudio blocks the stimulus forever in
83+
# AudioDeviceCreateIOProcID waiting for coreaudiod), then tccd
84+
# restarts;
85+
# - the display starts asleep (CGDisplayIsAsleep, no active display),
86+
# so ScreenCaptureKit lists windows but no display: wake it and keep
87+
# it awake for the job;
88+
# - there is no audio device: BlackHole provides input and output.
7989
- name: Blacksmith capture permissions and audio device
8090
if: startsWith(matrix.runner, 'blacksmith-')
8191
run: |
8292
set -euo pipefail
93+
sudo pmset -a displaysleep 0 sleep 0 disksleep 0
94+
caffeinate -u -t 2
95+
nohup caffeinate -dimsu > /dev/null 2>&1 &
8396
brew install --cask blackhole-2ch
8497
brew install switchaudio-osx
8598
sudo killall coreaudiod || true
@@ -107,86 +120,61 @@ jobs:
107120
for c in "$(rustup which cargo)" "$HOME/.cargo/bin/cargo"; do echo "$c"; done >> "$RUNNER_TEMP/capture-binaries"
108121
109122
db="/Library/Application Support/com.apple.TCC/TCC.db"
123+
user_db="$HOME/Library/Application Support/com.apple.TCC/TCC.db"
110124
approvals="$HOME/Library/Group Containers/group.com.apple.replayd/ScreenCaptureApprovals.plist"
111125
mkdir -p "$(dirname "$approvals")"
112126
now=$(date +%s)
113127
until=$(date -u -v+1y '+%a %b %d %H:%M:%S GMT %Y')
114128
sort -u "$RUNNER_TEMP/capture-binaries" | grep '^/' | while read -r client; do
115129
for service in kTCCServiceScreenCapture kTCCServiceMicrophone; do
116-
sudo sqlite3 "$db" "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);"
130+
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);"
131+
sudo sqlite3 "$db" "$row"
132+
sqlite3 "$user_db" "$row"
117133
done
118134
/usr/libexec/PlistBuddy -c "Add :$client date $until" "$approvals" >/dev/null 2>&1 || true
119135
echo "granted $client"
120136
done
121-
sudo killall tccd || true
122-
sleep 2
123-
124-
- name: Probe displays and ScreenCaptureKit
125-
run: |
126-
pmset -g | head -30
127-
cat > /tmp/disp.swift <<'SWIFT0'
128-
import CoreGraphics
129-
print("pre-wake main", CGMainDisplayID(), "asleep", CGDisplayIsAsleep(CGMainDisplayID()), "active", CGDisplayIsActive(CGMainDisplayID()))
130-
SWIFT0
131-
swiftc /tmp/disp.swift -o /tmp/disp0 && /tmp/disp0 || true
132-
echo "--- before wake"; ioreg -n IODisplayWrangler -r -d 1 2>/dev/null | grep -i power | head -3 || true
133-
sudo pmset -a displaysleep 0 sleep 0 disksleep 0 || true
134-
caffeinate -u -t 2 || true
135-
nohup caffeinate -dimsu > /dev/null 2>&1 &
136-
sleep 2
137-
# Microphone lives in the per-user TCC database
138-
udb="$HOME/Library/Application Support/com.apple.TCC/TCC.db"
139-
ls -la "$udb" || true
140-
now=$(date +%s)
141-
sort -u "$RUNNER_TEMP/capture-binaries" | grep '^/' | while read -r client; do
142-
for service in kTCCServiceMicrophone kTCCServiceScreenCapture; do
143-
sqlite3 "$udb" "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);" || echo "user db insert failed for $client"
144-
done
145-
done
146137
killall tccd || true
147138
sudo killall tccd || true
148139
sleep 2
149-
cat > /tmp/disp.swift <<'SWIFT'
140+
cat > "$RUNNER_TEMP/display.swift" <<'SWIFT'
150141
import CoreGraphics
151-
import ScreenCaptureKit
152-
import Foundation
153-
print("CGMainDisplayID", CGMainDisplayID(), "preflight", CGPreflightScreenCaptureAccess(), "asleep", CGDisplayIsAsleep(CGMainDisplayID()), "active", CGDisplayIsActive(CGMainDisplayID()), "online", CGDisplayIsOnline(CGMainDisplayID()), "bounds", CGDisplayBounds(CGMainDisplayID()))
154-
var on = [CGDirectDisplayID](repeating: 0, count: 8); var m: UInt32 = 0
155-
CGGetOnlineDisplayList(8, &on, &m)
156-
for i in 0..<Int(m) { print("online", on[i], CGDisplayBounds(on[i]), "asleep", CGDisplayIsAsleep(on[i])) }
157-
var ids = [CGDirectDisplayID](repeating: 0, count: 8); var n: UInt32 = 0
158-
CGGetActiveDisplayList(8, &ids, &n)
159-
for i in 0..<Int(n) { print("active", ids[i], CGDisplayBounds(ids[i])) }
160-
let sem = DispatchSemaphore(value: 0)
161-
SCShareableContent.getWithCompletionHandler { c, e in
162-
if let e = e { print("sck error", e) }
163-
for d in c?.displays ?? [] { print("sck display", d.displayID, d.width, d.height, d.frame) }
164-
print("sck windows", c?.windows.count ?? -1)
165-
sem.signal()
166-
}
167-
sem.wait()
142+
let main = CGMainDisplayID()
143+
print("display \(main): asleep \(CGDisplayIsAsleep(main)), active \(CGDisplayIsActive(main)), \(CGDisplayBounds(main)), screen capture \(CGPreflightScreenCaptureAccess())")
168144
SWIFT
169-
swiftc -O /tmp/disp.swift -o /tmp/disp
170-
/tmp/disp
171-
sudo sqlite3 "/Library/Application Support/com.apple.TCC/TCC.db" "select client from access where service='kTCCServiceScreenCapture';" | head -3
172-
/tmp/disp_bin_dummy 2>/dev/null || true
145+
swiftc "$RUNNER_TEMP/display.swift" -o "$RUNNER_TEMP/display"
146+
"$RUNNER_TEMP/display"
173147
174-
- name: Probe stimulus audio start
148+
- name: Probe VideoToolbox
175149
run: |
176-
stim=$(grep cap-verify-stimulus "$RUNNER_TEMP/capture-binaries" | head -1)
177-
spec='{"fps":15.0,"seconds":4.0,"holds":[],"busy":true,"warmup":1.0,"audio":true,"audio_period":0.5,"audio_origin":0,"window":null}'
178-
for variant in blackhole default; do
179-
echo "=== variant $variant"
180-
if [ "$variant" = default ]; then unset CAP_VERIFY_AUDIO_OUTPUT; fi
181-
( sleep 100000 | "$stim" "$spec" /tmp/stim-$variant.jsonl > /tmp/stim-$variant.out 2>&1 ) &
182-
sleep 12
183-
pid=$(pgrep -f "cap-verify-stimulus" | head -1)
184-
echo "pid $pid"; cat /tmp/stim-$variant.out; echo "log:"; cat /tmp/stim-$variant.jsonl | head -5
185-
if [ -n "$pid" ]; then log show --last 20s --style compact --predicate 'process == "tccd" OR process == "coreaudiod"' 2>/dev/null | grep -iE "deny|prompt|auth|microphone|tcc|error" | tail -15 || true; sample "$pid" 2 -file /tmp/sample-$variant.txt >/dev/null 2>&1; grep -A45 "Thread_.*DispatchQueue_1: com.apple.main-thread" /tmp/sample-$variant.txt | cut -c1-160 | head -60; pkill -f cap-verify-stimulus || true; fi
186-
sleep 2
187-
done
150+
cat > /tmp/vt.swift <<'SWIFT'
151+
import VideoToolbox
152+
for (w, h) in [(1920, 1200), (1920, 1080), (1024, 768)] {
153+
for hw in [true, false] {
154+
var s: VTCompressionSession?
155+
let spec: [CFString: Any] = hw ? [kVTVideoEncoderSpecification_RequireHardwareAcceleratedVideoEncoder: true] : [:]
156+
let st = VTCompressionSessionCreate(allocator: nil, width: Int32(w), height: Int32(h), codecType: kCMVideoCodecType_H264, encoderSpecification: spec as CFDictionary, imageBufferAttributes: nil, compressedDataAllocator: nil, outputCallback: nil, refcon: nil, compressionSessionOut: &s)
157+
var usingHW: CFTypeRef?
158+
if let s = s { VTSessionCopyProperty(s, key: kVTCompressionPropertyKey_UsingHardwareAcceleratedVideoEncoder, allocator: nil, valueOut: &usingHW) }
159+
print("vt h264 \(w)x\(h) requireHW=\(hw): status \(st) usingHW \(String(describing: usingHW))")
160+
}
161+
}
162+
SWIFT
163+
swiftc /tmp/vt.swift -o /tmp/vt && /tmp/vt
188164
189-
- name: Probe one capture case
165+
- name: Probe crash and instant cases
166+
env:
167+
RUST_LOG: cap_enc_ffmpeg=debug,cap_recording::sources::audio_mixer=debug,cap_recording::output_pipeline=info
168+
CAP_VERIFY_CASE: recording/studio-recovery/crash,recording/instant/screen-mic-system
169+
CAP_VERIFY_TIER: full
170+
CAP_VERIFY_REPEAT: "2"
190171
run: |
191-
RUST_LOG=cap_recording=debug,scap_targets=debug CAP_VERIFY_CASE=recording/instant/late-mic,recording/studio-screen/gated,recording/instant/system-only CAP_VERIFY_TIER=full CAP_VERIFY_REPEAT=1 \
192-
cargo test --locked -p cap-verify --test recording real_capture -- --nocapture 2>&1 | grep -iE "display|content filter|AsContentFilter|FAIL|pass|xfail|shareable" | head -40 || true
172+
mkdir -p "$CAP_VERIFY_DUMP"
173+
cargo test --locked -p cap-verify --test recording real_capture -- --nocapture > "$CAP_VERIFY_DUMP/probe.log" 2>&1 || true
174+
sed 's/\x1b\[[0-9;]*m//g' "$CAP_VERIFY_DUMP/probe.log" | grep -iE "videotoolbox|libx264|encoder|keyint|software|\[cap-verify\] (pass|xfail|FAIL)|recovery kept|markers inside|mixer|gap|silence|overlap" | grep -v "Opening" | cut -c1-260 | head -150
175+
176+
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
177+
if: always()
178+
with:
179+
name: probe-${{ matrix.runner }}
180+
path: ${{ github.workspace }}/verify-dump

‎.github/workflows/verify-recording.yml‎

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ on:
2828
description: "Preserve recordings and raw crash snapshots for passing cases"
2929
type: boolean
3030
default: false
31+
system_audio:
32+
description: "Blacksmith only: play and require system audio through BlackHole (not yet characterised)"
33+
type: boolean
34+
default: false
3135
runners:
3236
description: "blacksmith (default), hosted (GitHub macOS, fallback) or both"
3337
type: choice
@@ -66,13 +70,16 @@ jobs:
6670
CAP_VERIFY_SCRATCH: ${{ github.workspace }}/verify-scratch
6771
CAP_VERIFY_REPORT_DIR: ${{ github.workspace }}/verify-report
6872
CAP_VERIFY_DUMP: ${{ github.workspace }}/verify-dump
69-
# Hosted macos-26 has a virtual output device that gives
70-
# ScreenCaptureKit only silence, so system audio is not required
71-
# there; Blacksmith gets BlackHole (set up below), which carries the
72-
# stimulus's audio to ScreenCaptureKit, so system audio is required
73-
# and the system-only case runs.
73+
# Hosted runners' output devices give ScreenCaptureKit only silence, so
74+
# system audio is not checked there. Blacksmith gets BlackHole (set up
75+
# below), which does carry the stimulus's audio on macOS 15 (not on
76+
# 26), but its system-audio timing is not characterised yet (run
77+
# 38843005408: system audio +31 to +44 ms against the mic, BlackHole
78+
# latency not in the truth), so by default the stimulus stays silent
79+
# there too; dispatch system_audio=true plays and requires it.
7480
CAP_VERIFY_AUDIO_OUTPUT: ${{ startsWith(matrix.runner, 'blacksmith-') && 'BlackHole 2ch' || matrix.runner == 'macos-26' && 'Null Audio Device' || '' }}
75-
CAP_VERIFY_REQUIRE_SYSTEM_AUDIO: ${{ startsWith(matrix.runner, 'blacksmith-') && '1' || '' }}
81+
CAP_VERIFY_STIMULUS_AUDIO: ${{ startsWith(matrix.runner, 'blacksmith-') && !inputs.system_audio && '0' || '' }}
82+
CAP_VERIFY_REQUIRE_SYSTEM_AUDIO: ${{ startsWith(matrix.runner, 'blacksmith-') && inputs.system_audio && '1' || '' }}
7683
steps:
7784
- uses: actions/checkout@v4
7885

@@ -115,15 +122,25 @@ jobs:
115122
cargo test --locked -p cap-verify --test recording --no-run
116123
echo "build seconds: $(( $(date +%s) - start ))"
117124
118-
# Blacksmith runs a logged-in GUI session with SIP disabled, but
119-
# ScreenCaptureKit is denied until TCC grants the responsible process
120-
# (Runner.Worker), so grant Screen Recording (and Microphone) to every
121-
# binary that captures plus the whole runner ancestry, then restart
122-
# tccd. It has no audio device: BlackHole provides input and output.
125+
# Blacksmith runs a logged-in GUI session with SIP disabled, but:
126+
# - ScreenCaptureKit is denied until TCC grants the responsible process
127+
# (Runner.Worker), so Screen Recording and Microphone are granted to
128+
# every binary that captures plus the whole runner ancestry, in the
129+
# system and the user TCC database (Microphone is a per-user
130+
# service: without it CoreAudio blocks the stimulus forever in
131+
# AudioDeviceCreateIOProcID waiting for coreaudiod), then tccd
132+
# restarts;
133+
# - the display starts asleep (CGDisplayIsAsleep, no active display),
134+
# so ScreenCaptureKit lists windows but no display: wake it and keep
135+
# it awake for the job;
136+
# - there is no audio device: BlackHole provides input and output.
123137
- name: Blacksmith capture permissions and audio device
124138
if: startsWith(matrix.runner, 'blacksmith-')
125139
run: |
126140
set -euo pipefail
141+
sudo pmset -a displaysleep 0 sleep 0 disksleep 0
142+
caffeinate -u -t 2
143+
nohup caffeinate -dimsu > /dev/null 2>&1 &
127144
brew install --cask blackhole-2ch
128145
brew install switchaudio-osx
129146
sudo killall coreaudiod || true
@@ -151,19 +168,30 @@ jobs:
151168
for c in "$(rustup which cargo)" "$HOME/.cargo/bin/cargo"; do echo "$c"; done >> "$RUNNER_TEMP/capture-binaries"
152169
153170
db="/Library/Application Support/com.apple.TCC/TCC.db"
171+
user_db="$HOME/Library/Application Support/com.apple.TCC/TCC.db"
154172
approvals="$HOME/Library/Group Containers/group.com.apple.replayd/ScreenCaptureApprovals.plist"
155173
mkdir -p "$(dirname "$approvals")"
156174
now=$(date +%s)
157175
until=$(date -u -v+1y '+%a %b %d %H:%M:%S GMT %Y')
158176
sort -u "$RUNNER_TEMP/capture-binaries" | grep '^/' | while read -r client; do
159177
for service in kTCCServiceScreenCapture kTCCServiceMicrophone; do
160-
sudo sqlite3 "$db" "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);"
178+
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);"
179+
sudo sqlite3 "$db" "$row"
180+
sqlite3 "$user_db" "$row"
161181
done
162182
/usr/libexec/PlistBuddy -c "Add :$client date $until" "$approvals" >/dev/null 2>&1 || true
163183
echo "granted $client"
164184
done
185+
killall tccd || true
165186
sudo killall tccd || true
166187
sleep 2
188+
cat > "$RUNNER_TEMP/display.swift" <<'SWIFT'
189+
import CoreGraphics
190+
let main = CGMainDisplayID()
191+
print("display \(main): asleep \(CGDisplayIsAsleep(main)), active \(CGDisplayIsActive(main)), \(CGDisplayBounds(main)), screen capture \(CGPreflightScreenCaptureAccess())")
192+
SWIFT
193+
swiftc "$RUNNER_TEMP/display.swift" -o "$RUNNER_TEMP/display"
194+
"$RUNNER_TEMP/display"
167195
168196
- name: Real-capture recording oracle
169197
run: |

‎crates/verify/tests/recording.rs‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,19 @@ fn require_system_audio() -> bool {
318318
}
319319
}
320320

321+
/// `CAP_VERIFY_STIMULUS_AUDIO=0` keeps the stimulus silent, so the system
322+
/// audio track carries no markers and is not checked (unless system audio
323+
/// is required). On a host whose output device does reach
324+
/// ScreenCaptureKit but whose system-audio timing is not yet characterised
325+
/// (Blacksmith with BlackHole), this keeps the run equivalent to hosted
326+
/// runners, where the output devices give ScreenCaptureKit only silence.
327+
fn stimulus_plays_system_audio() -> bool {
328+
!matches!(
329+
std::env::var("CAP_VERIFY_STIMULUS_AUDIO").as_deref(),
330+
Ok("0") | Ok("false")
331+
)
332+
}
333+
321334
fn env_flag(name: &str) -> bool {
322335
matches!(std::env::var(name).as_deref(), Ok("1") | Ok("true"))
323336
}
@@ -1877,7 +1890,7 @@ async fn record(
18771890
seconds: spec.seconds,
18781891
holds: spec.holds.clone(),
18791892
busy: true,
1880-
audio: spec.system_audio,
1893+
audio: spec.system_audio && stimulus_plays_system_audio(),
18811894
warmup: match capture {
18821895
CaptureMode::Display => 2.0,
18831896
CaptureMode::Window => 2.0,

0 commit comments

Comments
 (0)