From a83d146ed33e4cbcc9abce4b586b6244cab3d9ca Mon Sep 17 00:00:00 2001 From: dailz Date: Fri, 29 May 2026 22:09:35 +0800 Subject: [PATCH] fix: FPS limiter never passes frames when input > target rate The old FpsLimit compared timestamps between CONSECUTIVE frames. When PipeWire delivers at 60fps (16ms intervals) and target is 30fps (33ms min_interval), the gap between consecutive frames is always 16ms < 33ms, so EVERY frame was rejected after the first. Fix: track last_output_time and compare against that instead of the previous frame's timestamp. Now frames pass when enough time has elapsed since the last OUTPUT, not since the last INPUT. Also adds PipeWire process callback counter logging and frame diagnostic STATS in state_portal.rs for debugging. --- src/cap_portal.rs | 7 +++++ src/fps_limit.rs | 65 +++++++++++++++++++++++++++++++---------------- 2 files changed, 50 insertions(+), 22 deletions(-) diff --git a/src/cap_portal.rs b/src/cap_portal.rs index 2112e21..5332057 100644 --- a/src/cap_portal.rs +++ b/src/cap_portal.rs @@ -405,7 +405,14 @@ fn pipewire_thread(ctx: PwThreadCtx) { let format_info = format_info.clone(); let frame_tx = frame_tx.clone(); let dropped = dropped; + let process_count = Rc::new(Cell::new(0u64)); + let process_count_clone = process_count.clone(); move |stream, _| { + let count = process_count_clone.get() + 1; + process_count_clone.set(count); + if count <= 5 || count % 60 == 0 { + tracing::info!("PipeWire process callback #{count}"); + } // 从流中出队原始 buffer(包含帧数据的元信息) let raw_buf = unsafe { stream.dequeue_raw_buffer() }; if raw_buf.is_null() { diff --git a/src/fps_limit.rs b/src/fps_limit.rs index a617227..efdfd98 100644 --- a/src/fps_limit.rs +++ b/src/fps_limit.rs @@ -1,7 +1,8 @@ use std::time::{Duration, Instant}; pub struct FpsLimit { - on_deck: Option<(T, Instant)>, + on_deck: Option, + last_output_time: Option, min_interval: Duration, } @@ -9,30 +10,32 @@ impl FpsLimit { pub fn new(fps: u32) -> Self { Self { on_deck: None, + last_output_time: None, min_interval: Duration::from_secs_f64(1.0 / fps as f64), } } /// Feed a new frame. Returns: - /// - Some(previous_frame) if enough time elapsed since previous frame - /// - None if frame is buffered (first frame) or previous is dropped (too close) + /// - Some(()) if enough time elapsed since the last output — proceed to encode current frame + /// - None if too close to the last output — drop current frame pub fn on_new_frame(&mut self, frame: T, timestamp: Instant) -> Option { - let old = self.on_deck.replace((frame, timestamp)); - match old { - None => None, // First frame — buffer it - Some((old_frame, old_ts)) => { - if timestamp.duration_since(old_ts) >= self.min_interval { - Some(old_frame) // Enough time — output previous - } else { - None // Too close — discard previous, keep new - } - } + let ready = match self.last_output_time { + None => true, + Some(last) => timestamp.duration_since(last) >= self.min_interval, + }; + + if ready { + self.last_output_time = Some(timestamp); + self.on_deck = Some(frame); + self.on_deck.take() + } else { + let _ = self.on_deck.replace(frame); + None } } - /// Flush the last buffered frame at end of stream pub fn flush(&mut self) -> Option { - self.on_deck.take().map(|(frame, _ts)| frame) + self.on_deck.take() } } @@ -41,15 +44,15 @@ mod tests { use super::*; #[test] - fn first_frame_is_buffered() { + fn first_frame_passes_immediately() { let mut limiter: FpsLimit = FpsLimit::new(30); let now = Instant::now(); let result = limiter.on_new_frame(1u32, now); - assert!(result.is_none()); + assert_eq!(result, Some(1)); } #[test] - fn frames_too_close_drops_old() { + fn frames_too_close_are_dropped() { let mut limiter: FpsLimit = FpsLimit::new(30); let now = Instant::now(); limiter.on_new_frame(1, now); @@ -58,12 +61,29 @@ mod tests { } #[test] - fn frames_far_enough_output_old() { + fn frames_far_enough_pass() { let mut limiter: FpsLimit = FpsLimit::new(30); let now = Instant::now(); limiter.on_new_frame(1, now); - let result = limiter.on_new_frame(2, now + Duration::from_millis(40)); - assert_eq!(result, Some(1)); + let result = limiter.on_new_frame(2, now + Duration::from_millis(34)); + assert_eq!(result, Some(2)); + } + + #[test] + fn high_fps_input_downsampled_correctly() { + let mut limiter: FpsLimit = FpsLimit::new(30); + let base = Instant::now(); + let mut outputs = Vec::new(); + + for i in 0..10u32 { + let t = base + Duration::from_millis(i as u64 * 16); + if let Some(f) = limiter.on_new_frame(i, t) { + outputs.push(f); + } + } + + assert!(outputs.len() >= 3, "expected at least 3 outputs, got {} ({:?})", outputs.len(), outputs); + assert_eq!(outputs[0], 0); } #[test] @@ -71,7 +91,8 @@ mod tests { let mut limiter: FpsLimit = FpsLimit::new(30); let now = Instant::now(); limiter.on_new_frame(1, now); - assert_eq!(limiter.flush(), Some(1)); + limiter.on_new_frame(2, now + Duration::from_millis(1)); + assert_eq!(limiter.flush(), Some(2)); assert_eq!(limiter.flush(), None); } }