Skip to content

feat(linux): enable Wayland window/display recording with PipeWire cursor metadata and evdev clicks - #2420

Open
EtherealBless wants to merge 1 commit into
CapSoftware:mainfrom
EtherealBless:feat/linux-wayland-recording-and-cursor
Open

EtherealBless wants to merge 1 commit into
CapSoftware:mainfrom
EtherealBless:feat/linux-wayland-recording-and-cursor

Conversation

@EtherealBless

@EtherealBless EtherealBless commented Oct 5, 2026 •

Copy link
Copy Markdown

Fixes #2419

Overview

This PR addresses several pain points for Linux Wayland users, enabling high-fidelity window recording (especially non-16:9 aspect ratios like mobile emulators), restoring cursor & click telemetry on Wayland, and fixing .cap project bundle imports.

Key Changes

1. PipeWire Window Format & Aspect Ratio Negotiation

  • Dynamic Format Handshake: Added WaylandCaptureInit oneshot notification to wait for PipeWire's negotiated format before configuring the output pipeline, passing actual window dimensions (target_width, target_height) to FFmpeg rather than forcing display resolution.
  • Aspect Ratio Preservation: Disabled forced output scaling for window captures in capture_pipeline.rs, preventing non-16:9 windows from stretching into 16:9.
  • Dynamic Rescaling: Scaler now re-adapts dynamically if frame geometry changes mid-stream.

2. DMA-BUF Memory Synchronization

  • Added DMA_BUF_IOCTL_SYNC (START / END) via DmaBufGuard to ensure CPU cache coherency when reading GPU buffers, preventing tearing and stale buffer reads.

3. Wayland Cursor & Click Capture

  • Cursor Position via SPA_META_Cursor: Extracted normalized cursor position from PipeWire buffer metadata instead of stubbing cursor events on Wayland.
  • Hardware Click Capture via evdev: Implemented EvdevInputListener in crates/recording/src/evdev_input.rs to read mouse buttons and keystrokes directly from /dev/input/event* devices, with graceful fallback to device_state.

4. .cap Bundle Import Resolution (editor_clips.rs & platform.rs)

  • On Linux, XDG Desktop Portal file choosers traverse into .cap directories instead of selecting them.
  • Implemented resolve_cap_project_path which traverses path.ancestors() to locate the project root (checking for recording-meta.json), allowing users to pick any file inside the bundle (or recording-meta.json) to import the project.
  • Added json and wildcard filters to the portal open panel.

Testing

  • Tested on EndeavourOS (Arch Linux) running KDE Plasma 6 (Wayland) on a 4K display.
  • Verified 9:16 mobile emulator recordings capture at native resolution without stretching.
  • Verified cursor tracking, click animations, and keyboard capture work as expected on Wayland.
  • Verified multi-clip merging of .cap projects via the file chooser dialog.

RetriggerConfidence Score: 0/5

The PR is not safe to merge until the input-scope, X11 click, resized-window, clean-capture, and unsafe event-read issues are addressed.

Findings

  1. P1 Security Keys leak outside recorded window ▶
  2. P1 X11 window clicks disappear ▶
  3. P1 Resized frames reach old encoder ▶
  4. P1 Wayland clean capture disabled ▶
  5. P1 Input events read unaligned ▶
  6. P2 Selected MP4 becomes bundle ▶
Fix with agent prompt
### Issue 1
crates/recording/src/cursor.rs:542-552
When recording a Wayland window, the evdev listener reads keys from all accessible input devices, but this path does not check whether the selected window has focus. If the user types in another application, those keystrokes can be saved in the recording’s keyboard events, including sensitive input.

**How this was verified:** Device-wide key events flow through `get_keys()` into the recording’s keyboard-event writer without a window-scope check.

### Issue 2
crates/recording/src/cursor.rs:498-507
For an X11 window recording with an accessible evdev device, this gate accepts mouse buttons only when Wayland cursor metadata is present. An X11 capture does not provide that metadata, so clicks are omitted even when the pointer is in the recorded window.

### Issue 3
crates/recording/src/sources/screen_capture/linux.rs:752-762
If PipeWire negotiates a new format after a window resize, this updates the frame dimensions, but the muxer was initialized with the dimensions from the first negotiation. Frames at the new size can then reach an encoder expecting the old size, causing encoding failures or lost frames.

### Issue 4
apps/desktop-gpui/src/app_windows.rs:2797-2799
This early return disables clean capture for Wayland display and area recordings that previously used it, so the application’s own windows can remain visible in recordings. The existing test still expects clean capture for both targets on Wayland and will fail.

### Issue 5
crates/recording/src/evdev_input.rs:130-132
The read buffer is a byte array, which is not guaranteed to meet `InputEvent`’s alignment requirement. Casting its address to an `InputEvent` reference is undefined behavior and can fault on strict-alignment Linux targets.

### Issue 6
apps/desktop-gpui/src/editor_clips.rs:1576
This resolves a selected file to its containing `.cap` project before checking whether the file is an MP4. Selecting an MP4 inside a bundle therefore imports the entire bundle instead of the selected video.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

The PR adds negotiated-size Wayland recording, PipeWire cursor metadata, evdev input capture, and .cap bundle import resolution. Review found regressions in input scoping, X11 clicks, resized-window encoding, and Wayland clean capture, plus an unsafe evdev read.

Reviews (1) · Last reviewed commit: "feat(linux): enable Wayland window/displ..."

Comment on lines +542 to +552
#[cfg(target_os = "linux")]
let current_keys = if let Some(evdev) = &evdev_listener {
let k = evdev.get_keys();
if !k.is_empty() || device_state.get_keys().is_empty() {
k
} else {
device_state.get_keys()
}
} else {
device_state.get_keys()
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Keys leak outside recorded window When recording a Wayland window, the evdev listener reads keys from all accessible input devices, but this path does not check whether the selected window has focus. If the user types in another application, those keystrokes can be saved in the recording’s keyboard events, including sensitive input.

How this was verified: Device-wide key events flow through get_keys() into the recording’s keyboard-event writer without a window-scope check.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/recording/src/cursor.rs
Line: 542-552

Comment:
**Keys leak outside recorded window** When recording a Wayland window, the evdev listener reads keys from all accessible input devices, but this path does not check whether the selected window has focus. If the user types in another application, those keystrokes can be saved in the recording’s keyboard events, including sensitive input.

**How this was verified:** Device-wide key events flow through `get_keys()` into the recording’s keyboard-event writer without a window-scope check.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +498 to +507
let mouse_buttons: [bool; 6] = if let Some(evdev) = &evdev_listener {
let in_window = target.window.is_none()
|| crate::sources::screen_capture::WAYLAND_CURSOR_POSITION
.read()
.is_some();
if in_window {
evdev.get_buttons()
} else {
[false; 6]
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 X11 window clicks disappear For an X11 window recording with an accessible evdev device, this gate accepts mouse buttons only when Wayland cursor metadata is present. An X11 capture does not provide that metadata, so clicks are omitted even when the pointer is in the recorded window.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/recording/src/cursor.rs
Line: 498-507

Comment:
**X11 window clicks disappear** For an X11 window recording with an accessible evdev device, this gate accepts mouse buttons only when Wayland cursor metadata is present. An X11 capture does not provide that metadata, so clicks are omitted even when the pointer is in the recorded window.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +752 to +762
let target_width = ensure_even(size.width);
let target_height = ensure_even(size.height);
if target_width > 0 && target_height > 0 {
let actual_info = VideoInfo::from_raw_ffmpeg(
pixel_format,
target_width,
target_height,
state.video_info.fps(),
);
state.video_info = actual_info;
state.notify_ready(actual_info);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Resized frames reach old encoder If PipeWire negotiates a new format after a window resize, this updates the frame dimensions, but the muxer was initialized with the dimensions from the first negotiation. Frames at the new size can then reach an encoder expecting the old size, causing encoding failures or lost frames.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/recording/src/sources/screen_capture/linux.rs
Line: 752-762

Comment:
**Resized frames reach old encoder** If PipeWire negotiates a new format after a window resize, this updates the frame dimensions, but the muxer was initialized with the dimensions from the first negotiation. Frames at the new size can then reach an encoder expecting the old size, causing encoding failures or lost frames.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +2797 to +2799
if wayland {
return false;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Wayland clean capture disabled This early return disables clean capture for Wayland display and area recordings that previously used it, so the application’s own windows can remain visible in recordings. The existing test still expects clean capture for both targets on Wayland and will fail.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/src/app_windows.rs
Line: 2797-2799

Comment:
**Wayland clean capture disabled** This early return disables clean capture for Wayland display and area recordings that previously used it, so the application’s own windows can remain visible in recordings. The existing test still expects clean capture for both targets on Wayland and will fail.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment thread crates/recording/src/evdev_input.rs Outdated
Comment on lines +130 to +132
let event = unsafe {
&*(buf.as_ptr().add(i * event_size) as *const InputEvent)
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Input events read unaligned The read buffer is a byte array, which is not guaranteed to meet InputEvent’s alignment requirement. Casting its address to an InputEvent reference is undefined behavior and can fault on strict-alignment Linux targets.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/recording/src/evdev_input.rs
Line: 130-132

Comment:
**Input events read unaligned** The read buffer is a byte array, which is not guaranteed to meet `InputEvent`’s alignment requirement. Casting its address to an `InputEvent` reference is undefined behavior and can fault on strict-alignment Linux targets.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment thread apps/desktop-gpui/src/editor_clips.rs Outdated
.detach();
return;
}
let source = resolve_cap_project_path(&source).unwrap_or(source);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Selected MP4 becomes bundle This resolves a selected file to its containing .cap project before checking whether the file is an MP4. Selecting an MP4 inside a bundle therefore imports the entire bundle instead of the selected video.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/src/editor_clips.rs
Line: 1576

Comment:
**Selected MP4 becomes bundle** This resolves a selected file to its containing `.cap` project before checking whether the file is an MP4. Selecting an MP4 inside a bundle therefore imports the entire bundle instead of the selected video.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@EtherealBless
EtherealBless force-pushed the feat/linux-wayland-recording-and-cursor branch from a79d74c to 7369cb7 Compare October 5, 2026 16:37

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug][Linux/Wayland] Window capture distorts aspect ratio, drops cursor/clicks, and cannot import .cap bundles via portal

1 participant