feat(overlay): add "Hide from screen recording" preference#522
Conversation
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>
There was a problem hiding this comment.
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.
| 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 ) | ||
|
|
||
| } | ||
|
|
||
| } |
There was a problem hiding this comment.
There are a few improvements we can make to applyContentProtection and syncContentProtection for better safety, platform compatibility, and consistency:
- Defensive Null/Undefined Guard:
checkboxTruecheckstypeof value === 'object' && value.includes(key). In JavaScript,typeof nullis"object", so if the preference value is evernull, it will throw aTypeError: Cannot read properties of null (reading 'includes')and crash. Providing a fallback default array|| []prevents this. - Platform Compatibility (Linux):
setContentProtectionis only supported on macOS and Windows. On Linux,win.setContentProtectionisundefined. Calling it will throw aTypeErrorwhich gets caught and logged as an error. To avoid throwing exceptions and flooding the logs on Linux, we should check ifwin.setContentProtectionis a function before calling it. - Consistency with
syncSettings: To align with how other settings are synced using the passedoptionsobject,syncContentProtectionshould accept an optionalvalueparameter.
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 )
}
}| // Sync screen-capture exclusion across all windows (#359) | ||
| windows.syncContentProtection() |
There was a problem hiding this comment.
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().
| // Sync screen-capture exclusion across all windows (#359) | |
| windows.syncContentProtection() | |
| // Sync screen-capture exclusion across all windows (#359) | |
| windows.syncContentProtection( options.app?.hideFromScreenCapture ) |
lacymorrow
left a comment
There was a problem hiding this comment.
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 choice —
BrowserWindow.setContentProtection()maps toSetWindowDisplayAffinityon Windows andNSWindowSharingNoneon macOS as documented. - Defensive —
isDestroyed()guard +try/catcharound the native call inapplyContentProtection(src/main/windows.js:84-102). - Live toggle —
syncContentProtection()is called fromcrossover.syncSettings()(src/main/crossover.js:412-413), so the preference takes effect without a restart. Matches the pattern of the rest ofsyncSettings. - Shadow-window coverage —
applyContentProtection(win)is invoked increate()(src/main/windows.js:183), andsyncContentProtection()iterateswindows.shadowWindowsexplicitly. 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,
checkboxTruehelper 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)
syncContentProtection()could reuse the existingwindows.each()helper for consistency, but the manual iteration matches other spots in the same file and is fine.Boolean(enabled)cast onsrc/main/windows.js:94is redundant —checkboxTrue()already returns a boolean — but it's harmless and arguably clearer at the call site.- Comment on
src/main/windows.js:82mentionsWDA_EXCLUDEFROMCAPTURE; Electron's implementation actually usesWDA_MONITORon 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.
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 toSetWindowDisplayAffinityon Windows andNSWindowSharingNoneon macOS.Changes
app.hideFromScreenCapture(off by default) insrc/main/preferences.jswindows.applyContentProtection(win)runs on window create for main + shadow windowswindows.syncContentProtection()is called fromcrossover.syncSettings()so toggling the preference takes effect immediately, no restart requiredsetContentProtectionare caught + logged (defensive; older platforms)Paperclip issue: LAC-1634
Test plan
Ctrl+Shift+Alt+Dand confirm shadow windows inherit the setting