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)
104 lines
2.9 KiB
Rust
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);
|
|
}
|
|
}
|