Skip to content

fix(recording): capture the selected window itself on macOS - #2469

Open
MinitJain wants to merge 3 commits into
CapSoftware:mainfrom
MinitJain:fix/macos-window-capture
Open

MinitJain wants to merge 3 commits into
CapSoftware:mainfrom
MinitJain:fix/macos-window-capture

Conversation

@MinitJain

@MinitJain MinitJain commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Addresses #1754 (macOS).

Problem

On macOS, Window mode records a fixed region of the screen rather than the selected window. If the window is moved during recording, or another app is placed over it, the recording shows whatever now occupies the original rectangle.

Root cause

target_to_display_and_crop (capture_pipeline.rs) turns a ScreenCaptureTarget::Window into its display plus the window's bounds at start time. The macOS capturer (sources/screen_capture/macos.rs) then builds a display SCContentFilter and applies those bounds with set_src_rect, so the window id is never used for capture. The restart path (rebuild_capturer) does the same.

As discussed with @richiemcilroy on Discord, the crop was intended at the time; this PR moves Window mode to capturing the window itself.

Change

  • Config gains a macOS-only window: Option<WindowId>, passed through ScreenCaptureConfig::init.
  • When it is set, the macOS capturer builds SCContentFilter::with_desktop_independent_window for that window and skips the display src_rect crop. The same filter is used in rebuild_capturer.
  • Studio mode always passes the window id.
  • Instant mode passes it only when no camera is attached (see limitations).
  • Display, Area and Camera Only targets go through the same code as before. Windows and Linux are untouched.

The output size is still derived from the window's bounds at start, as before.

Intended behavior

In Window mode on macOS, the recording contains only the selected window, regardless of where it moves on screen or what overlaps it.

Known limitations

  • Studio cursor drift: custom cursor positions are still normalised against the window's start bounds, so the cursor overlay is misplaced after the window moves. Fixing this needs a macOS window-relative cursor path in cursor.rs (similar to the Linux X11 one) and is left for a follow-up.
  • Resizing is untested: the output size is fixed at the window's size at start; how ScreenCaptureKit scales a resized window into it has not been verified.
  • System audio may change: with a window filter, ScreenCaptureKit likely limits captured system audio to the window's owning app instead of the whole system mix (feat(capture): add cross-platform window capture, app audio, and standalone CLI builds #2368 adds a separate audio stream for this reason). Not verified.
  • Fallback to screen-region capture: if the window is missing from ScreenCaptureKit's shareable content at start or when the capturer is rebuilt (e.g. after the window closes), capture falls back to the previous display crop, with a warning logged.
  • Instant mode with camera keeps the old behavior: macOS Instant mode has no camera compositing; the camera preview only appears because the display capture records it. To avoid dropping it, Instant + camera + Window still uses the display crop.
  • Separate popup windows may be omitted: context menus, dropdowns and some dialogs are separate windows on macOS and are likely not included by a single-window filter. Not verified.

Tests

Automated (all passing locally on macOS):

  • cargo fmt --all -- --check
  • cargo clippy -p cap-recording --all-targets (no warnings)
  • cargo test -p cap-recording --lib: 563 passed, 4 ignored. Includes a new test, macos_window_target_captures_window_unless_camera_attached.

The tests only cover how the capture configuration is selected. Real macOS recording behavior has not been verified yet.

Manual testing still required

  • Studio, Window mode: move the window, drag it to another display, overlap it with another app, resize, minimise/restore, close it, open a context menu
  • Instant without camera: same checks
  • Instant with camera: camera preview still appears in the recording
  • Studio cursor overlay behavior after moving the window (expected to drift)
  • System audio from another app while recording a window
  • Display and Area recordings unchanged
  • CLI: cap record start --window <id>

Related work

RetriggerConfidence Score: 5/5

No blocking finding remains after the excluded-window fix.

Summary

macOS Window recordings now use the selected window’s own capture filter in Studio and in Instant without a camera. Initial capture and restart both honor excluded-window settings.

  • macOS Window recordings follow the selected window.

Reviews (4) · Last reviewed commit: "Merge branch 'main' into fix/macos-windo..." · Reviewed by Greptile

Window targets on macOS were recorded as a display capture cropped to the
window's bounds at start time, so moving the window or overlapping it with
other apps changed what was recorded (CapSoftware#1754).

Carry the window id into the macOS capture config and build a
desktop-independent window SCContentFilter for it, skipping the display
crop. The same filter is used when the capturer is rebuilt after a system
stop. Display, Area and Camera Only targets are unchanged.

Instant mode keeps the display crop when a camera is attached, because
macOS Instant recordings have no camera compositing and the camera preview
only appears through the display capture.

Known limitations:
- Studio custom cursor positions are still normalised against the window's
  start bounds, so the cursor overlay drifts after the window moves.
- Output size is fixed at the window's size at start; resize behaviour is
  untested.
- With system audio on, ScreenCaptureKit likely limits audio to the
  window's owning app.
- If the window is missing from shareable content (initially or on
  restart), capture falls back to the previous display crop.

Window-filter approach also explored in CapSoftware#2368.
@MinitJain

Copy link
Copy Markdown
Contributor Author

@greptileai

let captures_window = window_filter.is_some();

let content_filter = match window_filter {
Some(filter) => filter,

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 Excluded windows get recorded

The new window filter ignores excluded_windows. A user can select an excluded app from the window dropdown because that list does not apply saved exclusions. Studio, or Instant without a camera, then records that window despite the “Hide windows from recordings” setting. Sharing the recording can expose its contents.

Check the selected ID against excluded_windows before creating the window filter. Apply the same check in rebuild_capturer, which also bypasses the exclusions.

How this was verified: Saved exclusions reach the capturer, but both window-filter branches use the selected window without checking them.

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

Comment:
**Excluded windows get recorded**

The new window filter ignores `excluded_windows`. A user can select an excluded app from the window dropdown because that list does not apply saved exclusions. Studio, or Instant without a camera, then records that window despite the “Hide windows from recordings” setting. Sharing the recording can expose its contents.

Check the selected ID against `excluded_windows` before creating the window filter. Apply the same check in `rebuild_capturer`, which also bypasses the exclusions.

**How this was verified:** Saved exclusions reach the capturer, but both window-filter branches use the selected window without checking them.

---

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

The window-only SCContentFilter ignored the "Hide windows from recordings"
exclusions, so selecting an excluded window in Window mode recorded it.

When the selected window is in the exclusion list, skip the window filter
and use the display filter with exclusions, which hides it as before. The
check runs both when capture starts and when the capturer is rebuilt.
@MinitJain

Copy link
Copy Markdown
Contributor Author

@greptileai

@richiemcilroy

Copy link
Copy Markdown
Member

Very nice! Will test this out when I'm at my Mac

@MinitJain

Copy link
Copy Markdown
Contributor Author

@greptileai

@MinitJain

Copy link
Copy Markdown
Contributor Author

@richiemcilroy CI is green now. I merged main to pick up the clippy fix from #2463. Ready whenever you get to test it 🙏

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.

2 participants