Skip to content

feat(overlay): add "Hide from screen recording" preference#522

Merged
lacymorrow merged 1 commit into
mainfrom
LAC-1634/hide-from-screen-recording
Jul 9, 2026
Merged

feat(overlay): add "Hide from screen recording" preference#522
lacymorrow merged 1 commit into
mainfrom
LAC-1634/hide-from-screen-recording

Conversation

@lacymorrow

Copy link
Copy Markdown
Owner

Closes #359

Summary

Adds an app preference Hide From Screen Recording that excludes the crosshair windows (main + all shadows) from screen captures — GeForce Instant Replay, OBS, screenshots, etc. The crosshair remains visible to the player; the recording just doesn't see it, which matches how Crosshair v2 behaves per the issue reporter.

Implemented via Electron's BrowserWindow.setContentProtection, which maps to SetWindowDisplayAffinity on Windows and NSWindowSharingNone on macOS.

Changes

  • New default app.hideFromScreenCapture (off by default) in src/main/preferences.js
  • New checkbox in the App settings section
  • windows.applyContentProtection(win) runs on window create for main + shadow windows
  • windows.syncContentProtection() is called from crossover.syncSettings() so toggling the preference takes effect immediately, no restart required
  • Errors from setContentProtection are caught + logged (defensive; older platforms)

Paperclip issue: LAC-1634

Test plan

  • Enable "Hide From Screen Recording" in Preferences → App
  • Verify crosshair is still visible on-screen
  • Start GeForce Instant Replay / OBS / Windows Game Bar recording; save/preview and confirm the crosshair does not appear in the recording
  • Take a screenshot (Snipping Tool / Print Screen); confirm the crosshair does not appear
  • Disable the preference; confirm the crosshair reappears in recordings without restarting the app
  • Duplicate the crosshair with Ctrl+Shift+Alt+D and confirm shadow windows inherit the setting

Exposes an app preference that toggles setContentProtection on all
crosshair windows so they can be excluded from screen captures (GeForce
Instant Replay, OBS, screenshots) while remaining visible on-screen.
Applied on window creation and re-applied on every settings sync so the
toggle takes effect without a restart.

Co-Authored-By: Paperclip <noreply@paperclip.ing>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new preference, hideFromScreenCapture, which utilizes Electron's setContentProtection to exclude crosshair windows from screen recordings and captures. The feedback focuses on improving robustness and consistency: specifically, adding defensive null guards for the preference lookup, checking for platform support before calling setContentProtection to prevent crashes on Linux, and passing the appropriate options parameter in crossover.js to align with the existing settings synchronization pattern.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/main/windows.js
Comment on lines +84 to +116
const applyContentProtection = ( win, enabled = checkboxTrue( preferences.value( 'app.hideFromScreenCapture' ), 'hideFromScreenCapture' ) ) => {

if ( !win || win.isDestroyed() ) {

return

}

try {

win.setContentProtection( Boolean( enabled ) )

} catch ( error ) {

log.error( `setContentProtection failed: ${error?.message}` )

}

}

// Apply the current hideFromScreenCapture preference to every crosshair window
const syncContentProtection = () => {

const enabled = checkboxTrue( preferences.value( 'app.hideFromScreenCapture' ), 'hideFromScreenCapture' )
applyContentProtection( windows.win, enabled )

for ( const currentWindow of windows.shadowWindows ) {

applyContentProtection( currentWindow, enabled )

}

}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

There are a few improvements we can make to applyContentProtection and syncContentProtection for better safety, platform compatibility, and consistency:

  1. Defensive Null/Undefined Guard: checkboxTrue checks typeof value === 'object' && value.includes(key). In JavaScript, typeof null is "object", so if the preference value is ever null, it will throw a TypeError: Cannot read properties of null (reading 'includes') and crash. Providing a fallback default array || [] prevents this.
  2. Platform Compatibility (Linux): setContentProtection is only supported on macOS and Windows. On Linux, win.setContentProtection is undefined. Calling it will throw a TypeError which gets caught and logged as an error. To avoid throwing exceptions and flooding the logs on Linux, we should check if win.setContentProtection is a function before calling it.
  3. Consistency with syncSettings: To align with how other settings are synced using the passed options object, syncContentProtection should accept an optional value parameter.
const applyContentProtection = ( win, enabled = checkboxTrue( preferences.value( 'app.hideFromScreenCapture' ) || [], 'hideFromScreenCapture' ) ) => {

	if ( !win || win.isDestroyed() || typeof win.setContentProtection !== 'function' ) {

		return

	}

	try {

		win.setContentProtection( Boolean( enabled ) )

	} catch ( error ) {

		log.error( `setContentProtection failed: ${error?.message}` )

	}

}

// Apply the current hideFromScreenCapture preference to every crosshair window
const syncContentProtection = ( value ) => {

	const enabled = checkboxTrue( value !== undefined ? value : ( preferences.value( 'app.hideFromScreenCapture' ) || [] ), 'hideFromScreenCapture' )
	applyContentProtection( windows.win, enabled )

	for ( const currentWindow of windows.shadowWindows ) {

		applyContentProtection( currentWindow, enabled )

	}

}

Comment thread src/main/crossover.js
Comment on lines +412 to +413
// Sync screen-capture exclusion across all windows (#359)
windows.syncContentProtection()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

To maintain consistency with the rest of the syncSettings function (which reads all configuration values from the passed options object rather than directly from the global preferences store), we should pass options.app?.hideFromScreenCapture to windows.syncContentProtection().

Suggested change
// Sync screen-capture exclusion across all windows (#359)
windows.syncContentProtection()
// Sync screen-capture exclusion across all windows (#359)
windows.syncContentProtection( options.app?.hideFromScreenCapture )

@lacymorrow lacymorrow left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Code Review — LGTM (approving via Paperclip; GitHub blocks self-approve)

Small, focused, well-implemented feature. setContentProtection() is the correct Electron API for excluding a window from screen captures; the implementation is defensive and covers main + shadow windows.

What's good

  • Correct API choiceBrowserWindow.setContentProtection() maps to SetWindowDisplayAffinity on Windows and NSWindowSharingNone on macOS as documented.
  • DefensiveisDestroyed() guard + try/catch around the native call in applyContentProtection (src/main/windows.js:84-102).
  • Live togglesyncContentProtection() is called from crossover.syncSettings() (src/main/crossover.js:412-413), so the preference takes effect without a restart. Matches the pattern of the rest of syncSettings.
  • Shadow-window coverageapplyContentProtection(win) is invoked in create() (src/main/windows.js:183), and syncContentProtection() iterates windows.shadowWindows explicitly. Both main and shadow windows are covered on create and on preference change.
  • Backward-compatible — new preference defaults to [] (unchecked); existing users see no behavior change.
  • Conventions followed — tabs, no semis, XO style, checkboxTrue helper used consistently. No new lint errors.

Security

No new attack surface. The boolean derives entirely from a local preference; no user-controlled string reaches native code. No secrets, no IPC changes, no injection risk.

Minor nits (non-blocking, ship as-is)

  1. syncContentProtection() could reuse the existing windows.each() helper for consistency, but the manual iteration matches other spots in the same file and is fine.
  2. Boolean(enabled) cast on src/main/windows.js:94 is redundant — checkboxTrue() already returns a boolean — but it's harmless and arguably clearer at the call site.
  3. Comment on src/main/windows.js:82 mentions WDA_EXCLUDEFROMCAPTURE; Electron's implementation actually uses WDA_MONITOR on Windows. Doc-only nit.

Test plan (from PR body) is appropriate

E2E automation of actual screen capture isn't feasible; manual verification against GeForce Instant Replay / OBS / Snipping Tool per the PR checklist is the right call.

Recommend merging after CI (Analyze / AppVeyor) turns green.

@lacymorrow
lacymorrow merged commit 4ac2cdf into main Jul 9, 2026
4 checks passed
@lacymorrow
lacymorrow deleted the LAC-1634/hide-from-screen-recording branch July 9, 2026 09:11
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.

[ISSUE] Can't record gameplay using Geforce Instant Replay whilst using crossover

1 participant