Skip to content

fix(recording): keep the countdown's last digit out of frame 0 on Linux - #1267

Merged
EtienneLescot merged 1 commit into
mainfrom
fix/linux-countdown-frame0
Oct 15, 2026
Merged

EtienneLescot merged 1 commit into
mainfrom
fix/linux-countdown-frame0

Conversation

@EtienneLescot

@EtienneLescot EtienneLescot commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Summary

On Linux the countdown overlay's last digit ends up in the first frames of a full-screen recording. This PR makes the Linux hide wait until the overlay has really painted its empty state before the window is hidden, so the capture can no longer pick up the "1". It's a minimal bugfix meant to be cherry-picked onto release/v2.1.0. Windows and macOS keep the immediate hide.

Observed

Found during the manual e2e pass of the installed v2.1.0-rc.1 .deb on Ubuntu 24.04, GNOME Shell 46, native Wayland, Electron 41.2.1, 1920×1080 @ 100%.

Flow: HUD record → GNOME portal picker (answered) → 3-2-1 countdown overlay → capture.

  • In 2 of 2 full-screen takes, frame 0 of the screen MP4 contains the overlay: a dark translucent circle with a white "1". Frames 10 and 30 are clean.
  • The overlay is fading out across the first frames: frames 0→3 of one take go from nearly opaque to almost gone, and the other take's frame 0 is already faint. That fade is the compositor's window-close animation, not a single stale frame.
  • Window-source takes are unaffected because the overlay sits outside the window.
  • App logs show [capture-linux] portal stream started → capture started {"width":1920,"height":1080,"fps":60}, with nothing about the overlay.

Root cause

On Linux nothing keeps the overlay out of a capture (setContentProtection does nothing there), so the only protection is ordering. The capture side is fine: a prepared portal session holds the grant without connecting a pw_stream, so no frames exist before arm(). The hide is what goes wrong:

  1. countdown-overlay-hide sent the overlay null and called hide() in the same tick. Chromium stops producing frames for a hidden window, so the renderer never painted its empty state, and the last buffer the compositor held still showed the "1".
  2. GNOME Shell animates a closing window out from its last buffer (a fade of ~150 ms).
  3. The HUD starts the take as soon as the hide IPC resolves (storage check → start-native-linux-recording → arm() → first frame). That chain finishes well inside the animation, so the first frames record the fading "1".

Fix

  • electron/countdownOverlay.ts (new, small, testable without Electron): hideCountdownOverlay(win, runId, { platform, isSuperseded }).
    • On Linux, when the overlay is visible, it starts listening on the overlay's own webContents.ipc for countdown-overlay-cleared, sends null, waits for that report for the same runId, and only then calls hide(). Whatever the compositor animates after that is transparent, however long the animation runs.
    • The wait is capped at 500 ms. If the overlay never reports (say its renderer has hung), it hides anyway and logs a warning, so a take is never blocked. That fallback is the old behaviour.
    • If a newer countdown re-showed the overlay during the wait (a show counter in handlers.ts), the hide is skipped so the new countdown stays on screen.
    • On every other platform it sends null and calls hide() synchronously, exactly as before.
  • CountdownOverlay.tsx: commits each value with flushSync, so the DOM is already empty before any frame is counted. On null it waits 3 animation frames (one for Chromium to produce the empty frame, one for it to reach the compositor, one of margin), then calls notifyCountdownOverlayCleared(runId).
  • preload.ts / electron-env.d.ts: onCountdownOverlayValue now also passes runId (backward compatible). Adds notifyCountdownOverlayCleared.
  • handlers.ts: the hide handler now awaits hideCountdownOverlay. The renderer already awaited it (safeHideCountdownOverlay) before startRecording, so useScreenRecorder needs no change.

On Linux the take starts about 50 ms later at 60 Hz, after a 3 s countdown.

Follow-up, not in this PR: the overlay is not centred on Wayland

A Wayland client cannot position its own toplevel, so the x/y that createCountdownOverlayWindow computes are ignored and mutter places the window itself: top-left (≈210,160) on one take, lower-middle on another. The obvious fix would make the overlay window cover the whole work area and centre the circle in CSS. That isn't small or safe for a release cherry-pick:

  • It would add a full-screen always-on-top window.
  • Click-through (setIgnoreMouseEvents) is not reliable on Linux. The HUD does not use it there.
  • It could interact with mutter's auto-maximize.

Better handled in a separate PR.

Related issue

No issue filed. Found during the manual e2e pass of v2.1.0-rc.1 on Ubuntu 24.04 / GNOME 46 Wayland.

Type of change

  • Bug fix
  • Feature
  • Enhancement
  • Documentation
  • Refactor / maintenance
  • Performance
  • Security

Release impact

  • Patch
  • Minor
  • Major / breaking change
  • No release note needed

Desktop impact

  • Windows
  • macOS
  • Linux
  • Installer / packaging
  • Not platform-specific

Screenshots / video

Frame 0 of the affected takes (before the fix): the "1" in a dark circle, top-left in one take and faint, lower-middle, in the other. The overlay fades out over frames 0–3.

Testing

Unit tests (platform pinned through the platform argument, not process.platform):

  • electron/countdownOverlay.test.ts, for the ordering on Linux:
    • the hide waits for the cleared report, and the promise the HUD awaits before starting the capture stays pending until then;
    • the listener is attached before the clear is sent;
    • a report from another run is ignored;
    • after the 500 ms fallback it hides anyway and removes the listener;
    • a newer show supersedes the hide;
    • a window destroyed during the wait is left alone;
    • an invisible overlay hides at once;
    • win32 and darwin keep the synchronous hide.
  • src/components/launch/CountdownOverlay.test.tsx (jsdom):
    • the digit is already gone from the DOM when the first frame is requested (this fails if flushSync is replaced with a batched setValue);
    • the report goes out only after CLEARED_AFTER_FRAMES frames, with the right runId;
    • nothing is reported while a digit is showing.
  • Mutation checks: with the old immediate hide, 5 of the 9 module tests fail; without flushSync, the component test fails.

Commands:

  • npx vitest --run electron/countdownOverlay.test.ts src/components/launch/CountdownOverlay.test.tsx src/hooks/useScreenRecorder electron/windows.test.ts electron/lingeringOverlay.test.ts: 47/47 pass.
  • npx vitest --run --changed origin/main: pass.
  • npx tsc --noEmit and npx tsc -p tsconfig.test.json --noEmit: no errors outside electron/mcp/*. Those errors come from @modelcontextprotocol/sdk missing in my local node_modules, are unrelated to this change, and CI installs it.
  • npm run lint: 0 errors. The warnings were all there before this change.

Manual verification on GNOME Wayland (not run here; needs the real desktop)

  1. Build or install this branch on Ubuntu 24.04 / GNOME 46, Wayland session.
  2. Make a full-screen take: HUD record → answer the portal picker with the whole screen → let the 3-2-1 countdown run → record a few seconds → stop.
  3. Extract the first frames of the screen MP4 and check that none of them contains the countdown circle or the "1":
    cd ~/.config/openscreen/recordings
    F=$(ls -t recording-*.mp4 | grep -v webcam | head -1)
    ffmpeg -v error -i "$F" -vf "select=eq(n\,0)" -vsync 0 f0.png
    ffmpeg -v error -i "$F" -vf "select=lt(n\,12)" -vsync 0 f_%02d.png   # frames 0–11, covers the fade window
  4. Repeat 2–3 times; the bug reproduced in 2 of 2 takes before the fix.
  5. Optional regression check: cancel a countdown midway and start a new one right away. The new countdown must stay visible to the end.
  6. Windows/macOS: the countdown must hide exactly as before (no behaviour change intended).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved countdown overlay dismissal to prevent the previous digit from briefly reappearing, particularly on Linux.
    • Ensured the overlay stays visible until it has cleared, while avoiding stale or duplicate hide actions.

On Linux nothing excludes the countdown overlay from a capture, so the
only protection is ordering. The hide sent the overlay its cleared value
and hid the window in the same tick, so the renderer never painted the
empty state: the compositor's last buffer still showed the "1", GNOME
Shell faded that buffer out over ~150 ms, and the capture armed right
after the hide recorded the fade in its first frames.

On Linux the hide now clears the digit, waits for the overlay renderer
to report that empty frames have been painted (bounded by a 500 ms
fallback), and only then hides the window, so whatever the compositor
animates afterwards is transparent. A countdown that re-shows the
overlay during that wait keeps it up. Windows and macOS keep the
immediate hide.
@coderabbitai

coderabbitai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 0894a577-36b2-4c88-a27f-75dce8455a28
📥 Commits

Reviewing files that changed from the base of the PR and between 1b7ed86 and 4a46206.

📒 Files selected for processing: 7
  • electron/countdownOverlay.test.ts
  • electron/countdownOverlay.ts
  • electron/electron-env.d.ts
  • electron/ipc/handlers.ts
  • electron/preload.ts
  • src/components/launch/CountdownOverlay.test.tsx
  • src/components/launch/CountdownOverlay.tsx

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 1 remain after this review.

📝 Walkthrough

The countdown overlay now reports when its digit has cleared. On Linux, the main process waits for a matching report or a timeout before hiding the overlay. Other platforms retain synchronous hiding.

Changes: Countdown overlay hide flow

Layer / File(s) Summary
Renderer clear-frame reporting
electron/electron-env.d.ts, electron/preload.ts, src/components/launch/CountdownOverlay.tsx, src/components/launch/CountdownOverlay.test.tsx
Countdown value callbacks now include a run ID. After a null value, the renderer commits the cleared display, waits three animation frames, and reports the run ID. Tests cover the delayed report and displayed-value behavior.
Main-process hide coordination
electron/countdownOverlay.ts, electron/ipc/handlers.ts, electron/countdownOverlay.test.ts
On Linux, the hide operation waits for a matching cleared-frame report or the configured timeout. It does not hide if the window was destroyed or the hide was superseded. Other platforms and invisible overlays hide immediately. Tests cover these paths and listener cleanup.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant IPCHandlers
  participant hideCountdownOverlay
  participant CountdownOverlay
  participant Preload
  IPCHandlers->>hideCountdownOverlay: Request overlay hide
  hideCountdownOverlay->>CountdownOverlay: Send null countdown value
  CountdownOverlay->>CountdownOverlay: Clear display and wait three animation frames
  CountdownOverlay->>Preload: Report cleared runId
  Preload->>hideCountdownOverlay: Send cleared-frame event
  hideCountdownOverlay->>IPCHandlers: Hide overlay after matching report
Loading

Suggested reviewers: abduznik

Merge Risk: ⚪ Minimal · up to 4a462

No actionable merge-blocking issue is established for the countdown overlay change. Normal checks can proceed; manual GNOME Wayland verification remains outstanding.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 6 functions across 7 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the primary Linux recording fix: preventing the countdown's last digit from appearing in the first captured frame.
Description check Passed The description is complete and relevant. It covers the bug, root cause, implementation, platform impact, release impact, testing, known limitations, and manual verification steps. It also explains th…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@EtienneLescot

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@EtienneLescot
EtienneLescot merged commit e1f2cac into main Oct 15, 2026
19 checks passed
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.

1 participant