Skip to content

v0.1.10 — LustLess (Wayland capture on GNOME: version-tolerant PipeWire bindings)

Choose a tag to compare

@HenryGetz HenryGetz released this 14 Oct 19:12
· 5 commits to main since this release

Wayland capture ships on GNOME (Zorin Core, Ubuntu, Fedora)

Screen capture on a Wayland session now uses the compositor's ScreenCast portal instead of falling back to X11/XWayland root capture.

Screen capture on a Wayland session (GNOME: Zorin Core, Ubuntu, Fedora) now uses
the compositor's ScreenCast portal instead of falling back to X11/XWayland root
capture. The backend existed since v0.1.7 but could not ship, and the reason was
structural rather than a flag: the pipewire crate cannot build against the
Zorin 17 / Ubuntu 22.04 baseline. Its libspa bindings need symbols and struct
fields that do not exist in libspa 0.3.48 (spa_meta_first,
spa_meta_region_is_valid, spa_video_info_raw.flags), and
struct spa_video_info_raw changed layout between 0.3.48 and current PipeWire
(flags inserted after format, modifier widened), so no single linked build
could be correct on both 0.3.48 and 1.0.x — and one .deb has to cover both.

  • Our own version-tolerant PipeWire bindings replace the crate
    (crates/lustless-capture/src/pipewire_sys.rs): libpipewire-0.3.so.0 is
    dlopened and the 25 entry points that exist in 0.3.48 and 1.0.x are
    resolved at runtime. The two calls we need that are header macros
    (pw_core_get_registry, pw_registry_add_listener) are reached through the
    real spa_interface vtable — its layout and the pw_core_methods member
    order were verified identical across 0.3.48, 1.0.5 and master. Event tables
    carry trailing padding and the 0.3.x version so a newer library's copy stays
    inside our allocation. No spa struct ever crosses the boundary: only
    byte-level SPA pods do, which is what makes one build correct on both
    baselines. Consequences: the release build needs neither libpipewire-0.3-dev
    nor libclang-dev, no shipped binary has a libpipewire/libspa
    DT_NEEDED, and the packages declare PipeWire as a weak dependency
    (Recommends/optdepends) — absent PipeWire degrades to the X11 path
    instead of failing to install.
  • Our own SPA pod codec (pipewire_pod.rs) replaces the crate's
    PodSerializer/VideoInfoRaw: it builds the EnumFormat we negotiate and
    parses the format the compositor picks, with every constant read from the
    real headers and independently probed on 22.04 and 24.04 (all 24 print
    identically).
  • Backend fixes found while porting: the stream no longer asks for
    PW_STREAM_FLAG_DRIVER (wrong for a capture stream, and a way to get no
    frames); frame extraction honours chunk.offset/chunk.size/chunk.stride
    instead of assuming a packed stride, rejects DMA-BUF buffers with a log
    instead of reading them, and queues buffers back (the old code leaked
    every one); the PipeWire thread builds the whole graph itself instead of
    taking a command channel.
  • Portal permission persistence. The ScreenCast restore token is stored per
    user (<state_dir>/<uid>/portal-restore-token, 0600) and sent back with
    PersistMode, so the compositor re-uses the grant instead of re-prompting on
    every login. The portal session is opened on the session bus explicitly
    (Screencast::with_connection), which is what a root system service needs.
  • The daemon now chooses its backend from the logind session, not from its
    own environment. A system service has no session environment, so
    XDG_SESSION_TYPE was always absent: an X11 desktop would have picked the
    Wayland backend, and the X11 backend could not see $DISPLAY at all.
    sessions.rs reads the session's Type/Display/Remote/Leader and
    derives the environment (DISPLAY, XAUTHORITY, XDG_RUNTIME_DIR,
    DBUS_SESSION_BUS_ADDRESS); text and remote sessions are never captured.
  • X11 capture can connect now. A live run showed the daemon resolving the
    session correctly and then failing with $DISPLAY variable not set, because
    logind's per-session Display property is empty on real machines (verified on
    Ubuntu 24.04 and Zorin 17.3). DISPLAY/XAUTHORITY are recovered from the
    session leader's /proc/<pid>/environ, then from a bounded walk of its
    descendants, then from the ordered XAUTHORITY file probe — and only then
    does an X11 session fall back to :0.
    Not fixed by this: in the same live run the daemon never got past its logind
    gate (Skip capturing Screenstate (session inactive) on every tick) even
    though loginctl reported Active=yes for that session — so no screenstate
    has ever been observed to be captured on a real desktop
    , on either backend.
    See gap 0 in docs/impl/PARITY.md.

Verification: 350 workspace tests pass and clippy -D warnings is clean; the
FFI layer is covered by an integration test that compiles a stub
libpipewire-0.3.so.0 from the real headers and drives the wrappers end to end
(registry vtable, pod negotiation, padded-stride extraction, buffer queueing),
plus a test that resolves the real system library when one is installed. The
build was verified in the ubuntu:22.04 container without any PipeWire
headers, and the resulting release binaries were checked for the absence of a
libpipewire link dependency. Live VM evidence collected so far (daemon
session resolution, classifier artifact loading) is in docs/impl/TEST-MATRIX.md;
the remaining live step — a real compositor granting ScreenCast — is gap 0 in
docs/impl/PARITY.md.


What is proven, and what is not

Proven: 350 workspace tests pass, clippy -D warnings clean; the SPA pod
constants print identically on ubuntu:22.04 (libspa 0.3.48) and ubuntu:24.04
(1.0.x); all 25 symbols resolve against a real libpipewire-0.3.so.0 1.0.5; an
integration test compiles a stub libpipewire-0.3.so.0 from the real 0.3.48
headers and drives the wrappers end to end; the release builds in the
ubuntu:22.04 container with no libpipewire-0.3-dev and no
libclang-dev; no shipped binary has a libpipewire/libspa DT_NEEDED; and
a live install of 0.1.10 reached a running daemon, offline sign-in, a fresh
logind registry and classifier v0.1.10 ready with all six CE artifacts.

Not proven — read this before relying on screen accountability:

  1. No capture has ever been observed end-to-end, on either backend. In the
    one live run that got that far, every tick logged
    Skip capturing Screenstate (session inactive) while loginctl reported the
    session Active=yes, so the daemon never reached the portal at all. That is
    gap 1 in docs/impl/PARITY.md, and it outranks everything v0.1.10 added.
  2. The portal ScreenCast grant and real frame extraction are unverified against a
    compositor (gap 0); the FFI proof is against a stub built from the same
    headers, which is self-consistency rather than independent ground truth — the
    real library check covers symbol resolution only.
  3. X11 capture on a real desktop is unverified for the same reason as (1).

Packages

lustless_0.1.10_amd64.deb, lustless-0.1.10-1.x86_64.rpm,
lustless-0.1.10-1-x86_64.pkg.tar.zst, lustless-0.1.10-r0.apk, plus
install.sh and lustless.rsa.pub. PipeWire is a weak dependency now
(Recommends/optdepends): without it the client still installs and falls back
to X11 capture.