From 1e334dcc33e210f363b26de9f88968be35e6a92f Mon Sep 17 00:00:00 2001 From: ManthanNimodiya Date: Fri, 3 Jul 2026 18:55:31 +0530 Subject: [PATCH] fix(recording): encode segmented video with real capture timestamps to fix white editor preview and cursor desync --- crates/enc-ffmpeg/src/mux/segmented_stream.rs | 18 +++++++----------- crates/rendering/src/lib.rs | 7 +++++-- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/crates/enc-ffmpeg/src/mux/segmented_stream.rs b/crates/enc-ffmpeg/src/mux/segmented_stream.rs index 5605044b45..9fdda46428 100644 --- a/crates/enc-ffmpeg/src/mux/segmented_stream.rs +++ b/crates/enc-ffmpeg/src/mux/segmented_stream.rs @@ -84,7 +84,6 @@ pub struct SegmentedVideoEncoder { segment_start_time: Option, last_frame_timestamp: Option, frames_in_segment: u32, - encoded_frame_count: u64, completed_segments: Vec, @@ -281,7 +280,6 @@ impl SegmentedVideoEncoder { segment_start_time: None, last_frame_timestamp: None, frames_in_segment: 0, - encoded_frame_count: 0, completed_segments: Vec::new(), pending_segment_indices: Vec::new(), frames_since_pending_flush: 0, @@ -341,10 +339,14 @@ impl SegmentedVideoEncoder { self.last_frame_timestamp = Some(timestamp); - let encoder_timestamp = self.next_encoder_timestamp(); + // Encode with the real capture timestamp: the editor timeline, cursor + // events and the mic track are all addressed in wall-clock time, so + // frames must keep it too (WGC-style captures only deliver frames when + // the screen changes — a synthetic frame-index clock compresses those + // gaps and desyncs everything downstream). Non-monotonic capture + // timestamps are handled by normalize_input_pts inside the encoder. self.encoder - .queue_frame(frame, encoder_timestamp, &mut self.output)?; - self.encoded_frame_count += 1; + .queue_frame(frame, timestamp, &mut self.output)?; self.frames_in_segment += 1; if is_first_frame { @@ -367,12 +369,6 @@ impl SegmentedVideoEncoder { Ok(()) } - fn next_encoder_timestamp(&self) -> Duration { - let frame_rate_num = self.codec_info.frame_rate_num.max(1) as f64; - let frame_rate_den = self.codec_info.frame_rate_den.max(1) as f64; - Duration::from_secs_f64(self.encoded_frame_count as f64 * frame_rate_den / frame_rate_num) - } - fn notify_segment(&self, event: SegmentCompletedEvent) { if let Some(tx) = &self.segment_tx && let Err(e) = tx.send(event) diff --git a/crates/rendering/src/lib.rs b/crates/rendering/src/lib.rs index 442ad18c84..e331e4a081 100644 --- a/crates/rendering/src/lib.rs +++ b/crates/rendering/src/lib.rs @@ -258,7 +258,6 @@ pub struct RecordingSegmentDecoders { pub segment_offset: f64, } -const SCREEN_MAX_FALLBACK_DISTANCE: u32 = 4; const CAMERA_MAX_FALLBACK_DISTANCE: u32 = 2; pub struct SegmentVideoPaths { @@ -328,6 +327,11 @@ impl RecordingSegmentDecoders { }; let screen_future = async { + // Screen capture is change-driven (WGC delivers nothing while the + // screen is static), so the nearest decoded frame is routinely many + // frame-times away from the requested time and is still the right + // content. Keep the default (wide) fallback distance; a tight cap + // here blanks the preview during static stretches. spawn_decoder( "screen", display_path, @@ -336,7 +340,6 @@ impl RecordingSegmentDecoders { force_ffmpeg, ) .await - .map(|decoder| decoder.with_max_fallback_distance(SCREEN_MAX_FALLBACK_DISTANCE)) .map_err(|e| format!("Screen:{e}")) };