Skip to content

Fix cursor hidden and audio leak after closing media viewer#1357

Open
SnoElement wants to merge 2 commits intoovertake:masterfrom
SnoElement:fix/media-viewer-cursor-and-audio
Open

Fix cursor hidden and audio leak after closing media viewer#1357
SnoElement wants to merge 2 commits intoovertake:masterfrom
SnoElement:fix/media-viewer-cursor-and-audio

Conversation

@SnoElement
Copy link
Copy Markdown

@SnoElement SnoElement commented Mar 30, 2026

Fixes #1303

Also fixes #269

Changes

Bug 1 - Cursor stays hidden system-wide after media viewer closes

Root cause: macOS cursor visibility uses a reference-counted system - NSCursor.hide() and NSCursor.unhide() must be balanced. Two separate code paths in SVideoController called NSCursor.hide():

  1. updateIdleTimer() - hides the cursor after 1 second of mouse idle
  2. updateControlVisibility(true) (fullscreen mouseDown/mouseUp path) - also calls NSCursor.hide() independently

But viewDidDisappear() only called NSCursor.unhide() once, leaving the reference count unbalanced when both paths had fired. Additionally, GalleryViewer.close(animated: false) ordered the window out without ever calling item.disappear(), so viewDidDisappear() (and its NSCursor.unhide()) never ran at all on non-animated closes (for example pressing Escape).

Fixes (SVideoController.swift, GalleryViewer.swift):

  • SVideoController: Introduced isCursorHidden: Bool to track cursor state. Every NSCursor.hide() is guarded by !isCursorHidden and every NSCursor.unhide() is guarded by isCursorHidden, guaranteeing exactly one matched pair per lifecycle regardless of which code paths fire.
  • GalleryViewer.close(animated:): Call pager.selectedItem?.disappear(for: nil) before window.orderOut(nil) in the non-animated path, ensuring viewDidDisappear always runs on close.

Bug 2 - Audio continues playing after navigating away or closing the viewer

Root cause: In MediaPlayerContext.seekingCompleted(), the current playback action (for example .play) is read from self.state and captured into the audioRenderer.flushBuffers(completion:) closure. While the flush is in flight, if pause() is called (for example the user navigates to the next item or closes the viewer), self.state's action is updated to .pause - but the completion closure uses the stale captured .play, sets state to .playing, and starts the audio renderer anyway.

This is particularly noticeable with large remote videos on slow connections, where buffering causes a long flush that outlasts viewer dismissal.

Fix (packages/TelegramMedia/Sources/MediaPlayer.swift):

  • In the flushBuffers completion closure, re-read the current action from self.state (when still in .seeking) rather than using the value captured at seek time, so any pause() call made during the flush is correctly honoured.

Bug 3 - Full-screen media viewer behaves incorrectly across multiple monitors

Root cause: GalleryViewer always created its full-screen window on NSScreen.main and forcibly reclaimed key-window status on resign. When the app focus moved to another display, the viewer could close unexpectedly or leave Telegram in a bad state that required relaunch.

Fixes (GalleryViewer.swift, AppDelegate.swift):

  • Open the gallery window on the app window's current screen instead of hardcoding NSScreen.main.
  • Stop forcibly re-focusing the gallery window on resign-key.
  • When app and viewer screens diverge, close the viewer cleanly instead of leaving stale viewer state behind.

Bug 1 — cursor stays hidden system-wide after media viewer closes (fixes overtake#1303):
- SVideoController: track cursor visibility with isCursorHidden flag so every
  NSCursor.hide() is paired with exactly one NSCursor.unhide(). The idle timer
  and the fullscreen mouseDown path both called NSCursor.hide() independently,
  but viewDidDisappear only called NSCursor.unhide() once, leaving the macOS
  reference count unbalanced.
- GalleryViewer.close(animated:false): call selectedItem?.disappear() before
  ordering the window out, so viewDidDisappear (and cursor restore) always runs.

Bug 2 — audio continues playing after navigating away or closing viewer:
- MediaPlayer.seekingCompleted: re-read the current playback action from
  self.state inside the async audioRenderer.flushBuffers completion closure
  instead of using the value captured at seek time. A pause() call made while
  the flush is in flight was being silently ignored because the closure held a
  stale .play action, causing audio to start even after the viewer was dismissed.
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Mar 30, 2026

CLA assistant check
All committers have signed the CLA.

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.

Mouse cursor disappears after full screen video Do not close full image view on click on another screen while user have multiple displays setup

2 participants