//! Wayland capture: ashpd ScreenCast portal → PipeWire fd → `pipewiresrc`. //! This module owns only the portal handshake and the source-element args; //! the shared encode/mux tail, gst spawn, and serving live in //! [`super::pipeline`]. use anyhow::{Context, Result}; use ashpd::{ WindowIdentifier, desktop::{ PersistMode, screencast::{CursorMode, Screencast, SourceType}, }, }; use nix::fcntl::{FcntlArg, FdFlag, fcntl}; use nix::unistd::close; use std::os::fd::{AsFd, IntoRawFd, OwnedFd, RawFd}; use super::pipeline::{self, CaptureHandle}; use super::quality::EffectiveQuality; use crate::cli::HostOpts; pub async fn start(opts: &HostOpts, quality: &EffectiveQuality) -> Result { // 1. Negotiate the screencast session with the portal. let proxy = Screencast::new() .await .context("could not reach the xdg-desktop-portal ScreenCast interface")?; let session = proxy.create_session().await?; let source = if opts.window { SourceType::Window } else { SourceType::Monitor }; proxy .select_sources( &session, CursorMode::Embedded, source.into(), false, None, PersistMode::DoNot, ) .await .context("select_sources failed")?; let response = proxy .start(&session, &WindowIdentifier::default()) .await .context("portal Start failed (did the user cancel the picker?)")? .response()?; let stream = response .streams() .first() .context("portal returned no screencast streams")?; let node_id = stream.pipe_wire_node_id(); let (w, h) = stream .size() .context("portal returned a stream with no size — pipewiresrc can't infer dimensions")?; let pw_fd: OwnedFd = proxy.open_pipe_wire_remote(&session).await?; tracing::info!(node_id, width = w, height = h, "portal handshake complete"); // The fd is CLOEXEC by default; the gst child needs to inherit it across // exec. We then leak it via into_raw_fd so its lifetime spans the spawn, // and close the parent's copy once gst is running (the pipeline's // after_spawn hook below). clear_cloexec(&pw_fd)?; let raw_fd: RawFd = pw_fd.into_raw_fd(); let source_args = vec![ "pipewiresrc".to_string(), format!("fd={raw_fd}"), format!("path={node_id}"), "do-timestamp=true".to_string(), ]; pipeline::spawn(opts, quality, Some((w as u32, h as u32)), source_args, move || { // Parent no longer needs the pipewire fd — gst inherited its own copy. let _ = close(raw_fd); }) .await } fn clear_cloexec(fd: &impl AsFd) -> Result<()> { let flags_int = fcntl(fd.as_fd(), FcntlArg::F_GETFD).context("F_GETFD on pipewire fd")?; let mut flags = FdFlag::from_bits_truncate(flags_int); flags.remove(FdFlag::FD_CLOEXEC); fcntl(fd.as_fd(), FcntlArg::F_SETFD(flags)).context("F_SETFD on pipewire fd")?; Ok(()) }