feat: Phase 1 MVP with audit fixes — Wayland screen capture + VAAPI encoding

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
This commit is contained in:
dailz
2026-04-05 23:35:00 +08:00
commit 6d49222de8
17 changed files with 6964 additions and 0 deletions

148
src/main.rs Normal file
View File

@@ -0,0 +1,148 @@
use std::os::unix::io::AsRawFd;
use anyhow::Result;
use clap::Parser;
use mio::unix::SourceFd;
use mio::{Events, Interest, Poll, Token};
use wayland_client::globals::registry_queue_init;
use wayland_client::Connection;
mod args;
mod avhw;
mod cap_wlr_screencopy;
mod fps_limit;
mod state;
mod transform;
use crate::args::Args;
use crate::cap_wlr_screencopy::CapWlrScreencopy;
use crate::state::State;
const TOKEN_WAYLAND: Token = Token(0);
const TOKEN_QUIT: Token = Token(1);
fn main() -> Result<()> {
let args = Args::parse();
tracing_subscriber::fmt()
.with_max_level(if args.verbose {
tracing::Level::DEBUG
} else {
tracing::Level::INFO
})
.init();
tracing::info!("wl-webrtc starting");
tracing::debug!("Args: {:?}", args);
if args.codec != "h264" {
anyhow::bail!("HEVC not supported in MVP. Use --codec h264");
}
// Connect to Wayland compositor
let conn = Connection::connect_to_env()?;
let (gm, mut queue) = registry_queue_init::<State<CapWlrScreencopy>>(&conn)?;
// Get the Wayland socket fd for mio polling.
// Use prepare_read() once to obtain the fd, then immediately drop the guard.
let wayland_fd = {
let guard = queue
.prepare_read()
.ok_or_else(|| anyhow::anyhow!("Failed to prepare Wayland read"))?;
guard.connection_fd().as_raw_fd()
};
// Create initial state
let qhandle = queue.handle();
let mut state = State::new(gm, args, qhandle);
// Dispatch initial round to bind all globals (screencopy manager, dmabuf, outputs)
queue.blocking_dispatch(&mut state)?;
// Set up mio event loop
let mut poll = Poll::new()?;
let mut events = Events::with_capacity(8);
// Register Wayland fd with mio
poll.registry().register(
&mut SourceFd(&wayland_fd),
TOKEN_WAYLAND,
Interest::READABLE,
)?;
// Register signal handler
let mut signals = signal_hook_mio::v1_0::Signals::new(&[
signal_hook::consts::SIGINT,
signal_hook::consts::SIGTERM,
])?;
poll.registry()
.register(&mut signals, TOKEN_QUIT, Interest::READABLE)?;
tracing::info!("Event loop started");
// Main event loop
let mut running = true;
while running {
// Wayland read pattern:
// 1. prepare_read() marks intent to read (also flushes outgoing)
// 2. poll() waits for data on Wayland fd or signals
// 3. If Wayland readable: read() consumes the guard, then dispatch_pending()
// 4. Dropping the guard without read() cancels the prepared read
let read_guard = queue.prepare_read();
poll.poll(&mut events, Some(std::time::Duration::from_millis(100)))
.unwrap_or_else(|e| {
tracing::error!("poll failed: {e}");
running = false;
});
for event in &events {
if event.token() == TOKEN_QUIT {
tracing::info!("Received quit signal");
running = false;
}
}
if events.iter().any(|e| e.token() == TOKEN_WAYLAND) {
if let Some(guard) = read_guard {
match guard.read() {
Ok(_) => {
queue.dispatch_pending(&mut state)?;
}
Err(e) => {
tracing::error!("Wayland read error: {e}");
running = false;
}
}
}
}
// If we didn't consume the read guard (no WAYLAND event), it drops here
// and cancels the prepared read. That's fine — we'll retry next iteration.
// After dispatch, try to start a new capture frame if we're in Streaming
// with no in-flight surface.
state.queue_alloc_frame();
// Check for fatal errors from the state machine
if state.errored {
tracing::error!("Fatal error in state machine, exiting");
running = false;
}
// Flush outgoing Wayland messages
conn.flush()?;
}
// Clean shutdown: flush encoder and write MP4 trailer
tracing::info!("Shutting down, flushing encoder...");
if let crate::state::EncConstructionStage::Streaming { enc, .. } = &mut state.stage {
if let Err(e) = enc.flush() {
tracing::error!("Failed to flush encoder: {e}");
}
}
tracing::info!("Done");
Ok(())
}