Files
wl-webrtc/src/fps_limit.rs
T
dailz 3e60258627 feat(portal): BWE-driven resolution adaptation + duplicate frame skipping
WebRTC client bandwidth estimate now drives both encoder bitrate and
resolution tier selection, replacing the previous static-target encoder.

- webrtc.rs: enable str0m BWE (seeded at 5 Mbps), surface
  EgressBitrateEstimate + KeyframeRequest events, expose
  get_bwe_estimate() / set_need_keyframe()
- state_portal.rs: wire bitrate/resolution channels between the WebRTC
  thread and the encode thread; tier ladder [1440p, 1080p, 720p] with
  downscale at 60% budget and upscale hysteresis (120% sustained 10s)
- avhw.rs: SwEncImport::poll_resolution_commands() rebuilds the import
  filter graph on UpdateResolution; SwEncEncode::recreate_encoder()
  rebuilds sws/enc_video/yuv_frame atomically; hash_sampled_y_plane()
  skips duplicate frames; VBV x264opts cap IDR bursts; H.264 level 4.0
  (muxer) / 4.2 (WebRTC)
- state.rs: sync wlr-screencopy GOP to fps*2 max 20 for parity
- fix: drain bitrate_rx + resolution_rx BEFORE the stride check in
  encode_cpu_frame() so the new (smaller-stride) frame produced after
  a resolution change does not hit the stale (larger) enc_width and
  crash the encode thread
- WebRTC GOP widened to fps*2 max 20 (was fps/2 max 10)
2026-06-13 22:46:33 +08:00

104 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);
}
}