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:
148
src/main.rs
Normal file
148
src/main.rs
Normal 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(())
|
||||
}
|
||||
Reference in New Issue
Block a user