8044a42f98
Live medium-quality stream errored with "negotiation problem" on the host and rendered a squashed, garbled picture in the viewer. Two causes, both from inserting videoscale before videoconvert with PAR+range caps: - videoscale was scaling pipewiresrc's raw output directly. The portal source's format/memory (e.g. DMABuf) isn't something software videoscale negotiates — the original pipeline always fed pipewiresrc through videoconvert first. Move videoscale *after* videoconvert so it operates on system-memory NV12/I420. - `pixel-aspect-ratio=1/1` + a width range over-constrained negotiation and risked a non-square-PAR / distorted result. Instead compute an exact even WxH from the known source dimensions (Wayland: portal size; X11: root/window geometry), preserving aspect, and pin it fully in the caps. This is also downscale-only now — a source already at/below the target height is left native instead of upscaled. Unknown dims (rare X11 geometry failure) fall back to the height-only + square-pixel + even width-range negotiation. source_dims threaded through pipeline::spawn from both backends. Smoke test updated to mirror the new ordering (1920x1080 -> 852x480, videoscale after videoconvert) and still asserts an even sub-source width. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
87 lines
3.0 KiB
Rust
87 lines
3.0 KiB
Rust
//! 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<CaptureHandle> {
|
|
// 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(())
|
|
}
|