Wayland capture: ashpd portal → gst-launch → ffmpeg → MPEG-TS

Implements the Wayland host pipeline from plan §4.5:

  ashpd ScreenCast portal
    -> CreateSession + SelectSources + Start + OpenPipeWireRemote
    -> (pipewire fd, node_id, width, height)
  gst-launch-1.0 pipewiresrc fd=N path=NODE_ID ! videoconvert
    ! video/x-raw,format=NV12 ! fdsink fd=1
  ffmpeg
    -f rawvideo -pix_fmt nv12 -video_size WxH -i pipe:0
    -f pulse -i default
    -c:v libx264 -preset ultrafast -tune zerolatency
    -c:a aac -f mpegts -listen 1 http://127.0.0.1:<rand>

Phase 1 ships software x264 per plan §7; VAAPI is Phase 2.

src/host/wayland.rs is the new module. capture.rs becomes a thin
dispatcher with a CaptureHandle enum (Wayland today, X11 next).
host/mod.rs swaps the 150ms sleep for a poll-until-listener-ready
helper, and calls handle.shutdown().await for an orderly SIGTERM /
1s grace / SIGKILL teardown. The Drop impl is the panic backstop.

The pipewire fd handoff clears CLOEXEC before gst-launch spawn and
closes the parent's copy of the raw fd after the child has it.

Also deletes the empty src/host/tunnel.rs and src/viewer/tunnel.rs
placeholder files — the generic bridge in common/tunnel.rs is doing
the work, and there's no host- or viewer-specific tunnel concern
worth a module yet.
This commit is contained in:
2026-05-15 15:22:35 -04:00
parent 6ad92081aa
commit 3a551c2287
6 changed files with 269 additions and 43 deletions
+24 -28
View File
@@ -1,43 +1,39 @@
//! Capture-and-encode pipeline.
//!
//! Phase 1 stub: the actual ffmpeg / gst-launch spawn lives here. Returns a
//! [`CaptureHandle`] whose `Drop` impl kills the children so a panic on the
//! bridge side can't leave a black-hole encoder running.
//!
//! Phase 2 will fill in X11 (x11grab) and Wayland (ashpd portal +
//! pipewiresrc → fdsink | ffmpeg) bodies; per-app PipeWire routing hangs off
//! the Wayland branch.
//! Capture dispatcher. Selects the per-display-server pipeline and returns its
//! [`CaptureHandle`]. Each backend owns its own children and tears them down
//! via its own Drop / shutdown.
use anyhow::{Result, bail};
use crate::cli::HostOpts;
use crate::common::display::DisplayServer;
use crate::host::wayland;
pub struct CaptureHandle {
port: u16,
// Future: child handles, PipewireState, etc.
pub enum CaptureHandle {
Wayland(wayland::CaptureHandle),
}
impl CaptureHandle {
pub fn local_port(&self) -> u16 {
self.port
}
}
impl Drop for CaptureHandle {
fn drop(&mut self) {
// Future: SIGTERM children with a short grace period, then SIGKILL.
// Tear down any PipeWire null sinks / loopbacks created in spawn().
}
}
pub async fn spawn(display: DisplayServer, _opts: &HostOpts) -> Result<CaptureHandle> {
match display {
DisplayServer::X11 => {
bail!("X11 capture pipeline not yet implemented (Phase 2)");
match self {
CaptureHandle::Wayland(h) => h.local_port(),
}
}
pub async fn shutdown(self) {
match self {
CaptureHandle::Wayland(h) => h.shutdown().await,
}
}
}
pub async fn spawn(display: DisplayServer, opts: &HostOpts) -> Result<CaptureHandle> {
match display {
DisplayServer::Wayland => {
bail!("Wayland capture pipeline not yet implemented (Phase 2)");
let h = wayland::start(opts).await?;
Ok(CaptureHandle::Wayland(h))
}
DisplayServer::X11 => {
bail!("X11 capture pipeline not yet implemented (Phase 2 follow-up)");
}
DisplayServer::Unknown => unreachable!("caller guarantees display != Unknown"),
}