@@ -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
0 commit comments