Files
wl-webrtc/src/fps_limit.rs
dailz a83d146ed3 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.
2026-05-29 22:09:35 +08:00

99 lines
2.9 KiB
Rust

use std::time::{Duration, Instant};
pub struct FpsLimit<T> {
on_deck: Option<T>,
last_output_time: Option<Instant>,
min_interval: Duration,
}
impl<T> FpsLimit<T> {
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(()) 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<T> {
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
}
}
pub fn flush(&mut self) -> Option<T> {
self.on_deck.take()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn first_frame_passes_immediately() {
let mut limiter: FpsLimit<u32> = FpsLimit::new(30);
let now = Instant::now();
let result = limiter.on_new_frame(1u32, now);
assert_eq!(result, Some(1));
}
#[test]
fn frames_too_close_are_dropped() {
let mut limiter: FpsLimit<u32> = FpsLimit::new(30);
let now = Instant::now();
limiter.on_new_frame(1, now);
let result = limiter.on_new_frame(2, now + Duration::from_millis(1));
assert!(result.is_none());
}
#[test]
fn frames_far_enough_pass() {
let mut limiter: FpsLimit<u32> = FpsLimit::new(30);
let now = Instant::now();
limiter.on_new_frame(1, now);
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<u32> = 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]
fn flush_returns_last_buffered() {
let mut limiter: FpsLimit<u32> = FpsLimit::new(30);
let now = Instant::now();
limiter.on_new_frame(1, now);
limiter.on_new_frame(2, now + Duration::from_millis(1));
assert_eq!(limiter.flush(), Some(2));
assert_eq!(limiter.flush(), None);
}
}