Phase 1 MVP implementation of wl-webrtc: Wayland screen capture tool with hardware-accelerated VAAPI H.264 encoding and WebTransport output. Includes all 9 runtime bug fixes from code audit (fix-audit-issues plan): CRITICAL: - C2: h264_metadata BSF with repeat_sps/repeat_pps in encode pipeline - C4: FpsLimit wired as timing gate in on_copy_complete HIGH: - C3+A2: DRM device discovery via dmabuf feedback MainDevice event, unified resolve_drm_path() helper (CLI > compositor > auto > fallback) - H2: Separate physical_size (mm) from mode_size (pixels) in wl_output - H1+A3: Multi-output warning + named-output-not-found error MEDIUM: - M5: tv_sec u32->u64 to avoid Y2106 timestamp truncation - M4: Guard against SHM Buffer event (DMA-BUF only) Key components: - src/avhw.rs: FFmpeg VAAPI encoder + filter graph + BSF pipeline - src/state.rs: Wayland event loop + output negotiation + screencopy - src/cap_wlr_screencopy.rs: wlr-screencopy capture source - src/fps_limit.rs: Frame rate limiting with configurable target - src/transform.rs: Frame format conversion utilities
78 lines
2.3 KiB
Rust
78 lines
2.3 KiB
Rust
use std::time::{Duration, Instant};
|
|
|
|
pub struct FpsLimit<T> {
|
|
on_deck: Option<(T, Instant)>,
|
|
min_interval: Duration,
|
|
}
|
|
|
|
impl<T> FpsLimit<T> {
|
|
pub fn new(fps: u32) -> Self {
|
|
Self {
|
|
on_deck: 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)
|
|
pub fn on_new_frame(&mut self, frame: T, timestamp: Instant) -> Option<T> {
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Flush the last buffered frame at end of stream
|
|
pub fn flush(&mut self) -> Option<T> {
|
|
self.on_deck.take().map(|(frame, _ts)| frame)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn first_frame_is_buffered() {
|
|
let mut limiter: FpsLimit<u32> = FpsLimit::new(30);
|
|
let now = Instant::now();
|
|
let result = limiter.on_new_frame(1u32, now);
|
|
assert!(result.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn frames_too_close_drops_old() {
|
|
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_output_old() {
|
|
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(40));
|
|
assert_eq!(result, Some(1));
|
|
}
|
|
|
|
#[test]
|
|
fn flush_returns_last_buffered() {
|
|
let mut limiter: FpsLimit<u32> = FpsLimit::new(30);
|
|
let now = Instant::now();
|
|
limiter.on_new_frame(1, now);
|
|
assert_eq!(limiter.flush(), Some(1));
|
|
assert_eq!(limiter.flush(), None);
|
|
}
|
|
}
|