analysis/plans/studio-mode-frame-capture-optimization.md
The studio mode recording pipeline suffers from severe frame drops during capture, with observed drop rates ranging from 9% to 99% depending on resolution and frame rate. This document analyzes the root causes and proposes a phased fix approach.
Studio mode recording flows through:
crates/recording/src/sources/screen_capture/macos.rs) - ScreenCaptureKit frame acquisitioncrates/recording/src/output_pipeline/core.rs) - Frame buffering between capture and muxcore.rs) - Timestamp processing and frame forwardingcrates/recording/src/output_pipeline/macos_fragmented_m4s.rs) - Encoder channel and H264 encodingTest Run 1 (High System Load):
| Resolution | Target FPS | Actual FPS | Frames | Drop Rate |
|---|---|---|---|---|
| 3024x1964 | 30 | 4.8 | 48/300 | 84.0% |
| 3024x1964 | 60 | 7.3 | 74/600 | 87.7% |
| 5952x3348 | 30 | 0.4 | 4/300 | 98.7% |
| 5952x3348 | 60 | 0.5 | 5/600 | 99.2% |
Test Run 2 (Lower System Load):
| Resolution | Target FPS | Actual FPS | Frames | Drop Rate |
|---|---|---|---|---|
| 3024x1964 | 30 | 27.0 | 272/300 | 9.3% |
| 3024x1964 | 60 | 48.8 | 491/600 | 18.2% |
| 5952x3348 | 30 | 10.2 | 103/300 | 65.7% |
| 5952x3348 | 60 | 0.4 | 4/600 | 99.3% |
Key Observations:
Location: crates/recording/src/output_pipeline/macos_fragmented_m4s.rs:23-28
fn get_muxer_buffer_size() -> usize {
std::env::var("CAP_MUXER_BUFFER_SIZE")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(3) // <-- ONLY 3 FRAMES!
}
Windows comparison: crates/recording/src/output_pipeline/win_fragmented_m4s.rs:25
const DEFAULT_MUXER_BUFFER_SIZE: usize = 240; // 80x larger
Impact:
try_send at lines 396-411 silently drops frames when buffer is full:match state.video_tx.try_send(Some((frame.sample_buf, adjusted_timestamp))) {
Ok(()) => { self.frame_drops.record_frame(); }
Err(e) => match e {
std::sync::mpsc::TrySendError::Full(_) => {
self.frame_drops.record_drop(); // Silent drop!
}
// ...
},
}
Evidence: Test logs show only 48-74 frames reaching muxer out of 300-600 expected.
Location: crates/recording/src/sources/screen_capture/macos.rs:127-132
fn get_screen_buffer_size() -> usize {
std::env::var("CAP_SCREEN_BUFFER_SIZE")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(4) // Only 4 frames
}
Impact:
Location: crates/recording/src/output_pipeline/core.rs:30, 259-294
const LARGE_FORWARD_JUMP_SECS: f64 = 0.5; // 500ms threshold
fn handle_forward_jump(&mut self, last: Duration, current: Duration, jump_secs: f64) {
// ...
let expected_increment = Duration::from_millis(33); // Assumes 30fps
let adjusted = last.saturating_add(expected_increment);
let compensation_secs = current.as_secs_f64() - adjusted.as_secs_f64();
self.accumulated_compensation_secs -= compensation_secs; // Accumulates negative!
// ...
}
Evidence from logs:
Large forward timestamp jump detected (system sleep/wake?), resyncing timeline
stream="video" forward_secs=0.599994292 accumulated_compensation_secs="-8.288"
Problems:
Location: crates/recording/src/output_pipeline/core.rs:976-1034
if was_cancelled {
info!("mux-video cancelled, draining remaining frames from channel");
let drain_timeout = Duration::from_secs(2);
let max_drain_frames = 30u64; // Only 30 frames!
// ...
}
Impact:
Evidence: Logs show mux-video drain complete: 1 frames processed despite frames being buffered.
Location: crates/recording/src/sources/screen_capture/macos.rs:58-97
fn get_pixel_buffer_pool_size() -> usize {
std::env::var("CAP_PIXEL_BUFFER_POOL_SIZE")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(20) // 20 buffers
}
Impact at 5952x3348:
PixelBufferCopier synchronously copies each frame through mutex-protected sessionEvidence: 5952x3348 @ 60fps shows 99%+ frame drops on both test runs.
Objective: Align macOS muxer buffer size with Windows to provide adequate buffering for encoder throughput variations.
Files to modify:
crates/recording/src/output_pipeline/macos_fragmented_m4s.rs:27Implementation:
fn get_muxer_buffer_size() -> usize {
std::env::var("CAP_MUXER_BUFFER_SIZE")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(60) // 1 second at 60fps, up from 3
}
Acceptance Criteria:
cargo fmt before completingObjective: Increase screen capture channel buffer to provide more tolerance for processing jitter.
Files to modify:
crates/recording/src/sources/screen_capture/macos.rs:131Implementation:
fn get_screen_buffer_size() -> usize {
std::env::var("CAP_SCREEN_BUFFER_SIZE")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(15) // 500ms at 30fps, up from 4
}
Acceptance Criteria:
cargo fmt before completingObjective: Ensure all buffered frames are processed on recording stop.
Files to modify:
crates/recording/src/output_pipeline/core.rs:982Implementation:
let max_drain_frames = 500u64; // Match video source channel capacity + headroom
Acceptance Criteria:
cargo fmt before completingObjective: Eliminate false "Large forward timestamp jump" warnings by establishing baseline from first frame instead of pipeline creation time.
Files to modify:
crates/recording/src/output_pipeline/core.rs - TimestampAnomalyTrackerImplementation approach:
first_frame_baseline: Option<Duration> field to TimestampAnomalyTrackerAcceptance Criteria:
cargo fmt before completingObjective: Use actual frame rate for timestamp gap expectations instead of hardcoded 33ms.
Files to modify:
crates/recording/src/output_pipeline/core.rs:271Implementation:
expected_frame_duration: Duration field to TimestampAnomalyTrackerhandle_forward_jump:let expected_increment = self.expected_frame_duration;
Acceptance Criteria:
cargo fmt before completingObjective: Automatically scale buffer sizes based on resolution to handle high-resolution capture.
Files to modify:
crates/recording/src/sources/screen_capture/macos.rscrates/recording/src/output_pipeline/macos_fragmented_m4s.rsImplementation approach:
width * height6K: 2x buffers
Acceptance Criteria:
cargo fmt before completingObjective: Scale pixel buffer pool based on resolution and frame rate to prevent pool exhaustion.
Files to modify:
crates/recording/src/sources/screen_capture/macos.rs:58-97Implementation approach:
frame_size * pool_countAcceptance Criteria:
cargo fmt before completingObjective: When encoder can't keep up, deliberately skip frames at regular intervals instead of random drops from buffer overflow.
Files to modify:
crates/recording/src/output_pipeline/macos_fragmented_m4s.rsImplementation approach:
Acceptance Criteria:
cargo fmt before completingObjective: Improve hardware vs software encoder selection and configuration.
Files to modify:
crates/encoder/src/h264.rsImplementation approach:
requires_software_encoder() thresholdsAcceptance Criteria:
cargo fmt before completingcargo run -p cap-test -- suite recording| Variable | Default | Description |
|---|---|---|
CAP_MUXER_BUFFER_SIZE | 3 (macOS), 240 (Windows) | Muxer channel buffer |
CAP_SCREEN_BUFFER_SIZE | 4 | Screen capture channel buffer |
CAP_VIDEO_SOURCE_BUFFER_SIZE | 300 | Video source channel capacity |
CAP_PIXEL_BUFFER_POOL_SIZE | 20 | Pixel buffer pool count |
CAP_MAX_QUEUE_DEPTH | 8 | ScreenCaptureKit queue depth |
All changes use environment variables with fallback to current defaults. To rollback:
CAP_MUXER_BUFFER_SIZE=3 to restore original macOS bufferCAP_SCREEN_BUFFER_SIZE=4 to restore original screen buffercrates/recording| Resolution | Target FPS | Current Drop Rate | Expected Drop Rate |
|---|---|---|---|
| 3024x1964 | 30 | 9-84% | < 5% |
| 3024x1964 | 60 | 18-88% | < 10% |
| 5952x3348 | 30 | 66-99% | < 30% |
| 5952x3348 | 60 | 99% | < 50% (with software encoder) |