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
+64
View File
@@ -0,0 +1,64 @@
use anyhow::Result;
use wayland_client::globals::GlobalList;
use wayland_client::protocol::wl_buffer::WlBuffer;
use wayland_client::protocol::wl_output::WlOutput;
use wayland_client::QueueHandle;
use wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1;
use crate::state::{CaptureSource, OutputInfo, State};
/// wlr-screencopy capture backend.
///
/// Holds the current in-flight frame protocol object. The
/// `ZwlrScreencopyManagerV1` is stored separately in
/// `State::EverythingButFmt` because binding it requires a `Dispatch`
/// impl that lives in state.rs (T6b).
pub struct CapWlrScreencopy {
/// The active frame object for the current capture cycle.
/// Set by Dispatch impls after `manager.capture_output()`, cleared
/// by `on_done_with_frame()`.
pub current_frame: Option<ZwlrScreencopyFrameV1>,
}
impl CaptureSource for CapWlrScreencopy {
/// Unit type: wlr-screencopy is fully asynchronous — `alloc_frame()`
/// always returns `None`. The frame object is created by Dispatch
/// impls calling `manager.capture_output()`, not by this method.
type Frame = ();
fn new(
_gm: &GlobalList,
_output: &WlOutput,
_output_info: &OutputInfo,
_qh: &QueueHandle<State<Self>>,
) -> Result<Self> {
// Manager binding happens in state.rs during the ProbingOutputs →
// EverythingButFmt stage transition (T6b). It requires a Dispatch
// impl that doesn't exist yet, so we cannot call gm.bind() here.
Ok(Self {
current_frame: None,
})
}
fn alloc_frame(&mut self) -> Option<Self::Frame> {
// wlr-screencopy is asynchronous: the Dispatch impl creates a new
// ZwlrScreencopyFrameV1 which triggers the buffer allocation flow
// (buffer event → negotiate format → create DMA-BUF). This method
// always returns None.
None
}
fn queue_copy(&mut self, buffer: &WlBuffer, _qh: &QueueHandle<State<Self>>) {
if let Some(frame) = &self.current_frame {
frame.copy(buffer);
} else {
tracing::warn!("queue_copy: no current wlr-screencopy frame");
}
}
fn on_done_with_frame(&mut self, _frame: Self::Frame) {
if let Some(frame) = self.current_frame.take() {
frame.destroy();
}
}
}