From 9b248a7a5ba5208a4da7fc2ea268ecbd07d9c1f1 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Mon, 11 May 2026 10:13:05 +0200 Subject: [PATCH] fix(macos): restore video layer on focus --- ios/Runner/MpvPlayer/MpvPlayerPlugin.swift | 2 +- lib/mpv/player/player.dart | 6 ++- lib/mpv/player/player_base.dart | 4 +- lib/screens/video_player/parts/lifecycle.dart | 6 ++- macos/Runner/MpvPlayer/MpvPlayerCore.swift | 41 +++++++++++++++++-- macos/Runner/MpvPlayer/MpvPlayerPlugin.swift | 4 +- .../MpvPlayer/MpvPlayerPluginShared.swift | 5 ++- 7 files changed, 56 insertions(+), 12 deletions(-) diff --git a/ios/Runner/MpvPlayer/MpvPlayerPlugin.swift b/ios/Runner/MpvPlayer/MpvPlayerPlugin.swift index 1019667b..a09fc46f 100644 --- a/ios/Runner/MpvPlayer/MpvPlayerPlugin.swift +++ b/ios/Runner/MpvPlayer/MpvPlayerPlugin.swift @@ -14,7 +14,7 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS // MpvPluginShared conformance var coreBase: MpvPlayerCoreBase? { playerCore } - func setPlayerVisible(_ visible: Bool) { playerCore?.setVisible(visible) } + func setPlayerVisible(_ visible: Bool, restoreOnWindowVisible _: Bool) { playerCore?.setVisible(visible) } func updatePlayerFrame() { playerCore?.updateFrame() } // PiP diff --git a/lib/mpv/player/player.dart b/lib/mpv/player/player.dart index 18360a06..c9ad1931 100644 --- a/lib/mpv/player/player.dart +++ b/lib/mpv/player/player.dart @@ -163,8 +163,12 @@ abstract class Player { /// On macOS, this controls the Metal layer visibility. /// On other platforms, this may have no effect. /// + /// When [restoreOnWindowVisible] is true, macOS may restore the layer as soon + /// as AppKit reports the window visible again instead of waiting for Dart's + /// lifecycle resume callback. + /// /// Returns true if the operation was successful. - Future setVisible(bool visible); + Future setVisible(bool visible, {bool restoreOnWindowVisible = false}); /// Update the video frame/surface dimensions. /// diff --git a/lib/mpv/player/player_base.dart b/lib/mpv/player/player_base.dart index 06a5df51..be242442 100644 --- a/lib/mpv/player/player_base.dart +++ b/lib/mpv/player/player_base.dart @@ -523,10 +523,10 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player { } @override - Future setVisible(bool visible) async { + Future setVisible(bool visible, {bool restoreOnWindowVisible = false}) async { if (_disposed) return false; try { - await invoke('setVisible', {'visible': visible}); + await invoke('setVisible', {'visible': visible, 'restoreOnWindowVisible': restoreOnWindowVisible}); return true; } catch (e) { errorController.add(PlayerError('Failed to set visibility: $e')); diff --git a/lib/screens/video_player/parts/lifecycle.dart b/lib/screens/video_player/parts/lifecycle.dart index 859afed4..f9ec1acc 100644 --- a/lib/screens/video_player/parts/lifecycle.dart +++ b/lib/screens/video_player/parts/lifecycle.dart @@ -115,7 +115,7 @@ extension _VideoPlayerLifecycleMethods on VideoPlayerScreenState { } _hiddenForBackground = true; - await currentPlayer.setVisible(false); + await currentPlayer.setVisible(false, restoreOnWindowVisible: Platform.isMacOS); _recordLifecycleState('hidden', action: 'render_hidden'); } @@ -133,7 +133,9 @@ extension _VideoPlayerLifecycleMethods on VideoPlayerScreenState { // video-output refresh before any auto-resume logic runs. if (_hiddenForBackground && currentPlayer != null && _isPlayerInitialized) { await currentPlayer.setVisible(true); - await currentPlayer.updateFrame(); + if (!Platform.isMacOS) { + await currentPlayer.updateFrame(); + } if (!mounted || currentPlayer != player) return; diff --git a/macos/Runner/MpvPlayer/MpvPlayerCore.swift b/macos/Runner/MpvPlayer/MpvPlayerCore.swift index 593e79f0..7c479f8d 100644 --- a/macos/Runner/MpvPlayer/MpvPlayerCore.swift +++ b/macos/Runner/MpvPlayer/MpvPlayerCore.swift @@ -100,14 +100,27 @@ class MpvPlayerCore: MpvPlayerCoreBase { private var isVisible = false private var pausedState = true + private var shouldRestoreOnWindowVisible = false - func setVisible(_ visible: Bool) { + func setVisible(_ visible: Bool, restoreOnWindowVisible: Bool = false) { guard metalLayer != nil, !isPipActive else { return } + if visible && isVisible && !shouldRestoreOnWindowVisible { + isBackgrounded = false + if metalLayer?.isHidden == true { + setMetalLayerHidden(false) + } + beginPlaybackActivity() + print("[MpvPlayerCore] setVisible(true) skipped - already visible") + return + } + isVisible = visible + shouldRestoreOnWindowVisible = !visible && restoreOnWindowVisible isBackgrounded = !visible if visible { + shouldRestoreOnWindowVisible = false if let contentView = window?.contentView { contentView.wantsLayer = true if let superlayer = contentView.layer { @@ -120,7 +133,7 @@ class MpvPlayerCore: MpvPlayerCoreBase { } setMetalLayerHidden(!visible) - print("[MpvPlayerCore] setVisible(\(visible))") + print("[MpvPlayerCore] setVisible(\(visible), restoreOnWindowVisible: \(restoreOnWindowVisible))") } func setPaused(_ paused: Bool) { @@ -211,7 +224,11 @@ class MpvPlayerCore: MpvPlayerCoreBase { } else if windowVisible && layerHiddenForOcclusion { print("[MpvPlayerCore] Window visible - showing Metal layer") layerHiddenForOcclusion = false - setMetalLayerHidden(!isVisible) + if shouldRestoreOnWindowVisible { + restoreMetalLayerAfterOcclusion() + } else { + setMetalLayerHidden(!isVisible) + } isBackgrounded = false if !pausedState { beginPlaybackActivity() @@ -235,6 +252,24 @@ class MpvPlayerCore: MpvPlayerCoreBase { print("[MpvPlayerCore] Ended playback activity assertion") } + private func restoreMetalLayerAfterOcclusion() { + if let metalLayer, let contentView = window?.contentView { + contentView.wantsLayer = true + if let superlayer = contentView.layer { + let targetFrame = contentView.bounds + let needsAttach = metalLayer.superlayer !== superlayer || superlayer.sublayers?.first !== metalLayer + if needsAttach { + attachMetalLayer(to: superlayer, frame: targetFrame) + } else if !metalLayer.frame.equalTo(targetFrame) { + updateFrame(targetFrame) + } + } + } + isVisible = true + shouldRestoreOnWindowVisible = false + setMetalLayerHidden(false) + } + private func attachMetalLayer(to superlayer: CALayer, frame: CGRect) { guard let metalLayer else { return } diff --git a/macos/Runner/MpvPlayer/MpvPlayerPlugin.swift b/macos/Runner/MpvPlayer/MpvPlayerPlugin.swift index 1fd1ad04..69412ebd 100644 --- a/macos/Runner/MpvPlayer/MpvPlayerPlugin.swift +++ b/macos/Runner/MpvPlayer/MpvPlayerPlugin.swift @@ -13,7 +13,9 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS // MpvPluginShared conformance var coreBase: MpvPlayerCoreBase? { playerCore } - func setPlayerVisible(_ visible: Bool) { playerCore?.setVisible(visible) } + func setPlayerVisible(_ visible: Bool, restoreOnWindowVisible: Bool) { + playerCore?.setVisible(visible, restoreOnWindowVisible: restoreOnWindowVisible) + } func updatePlayerFrame() { playerCore?.updateFrame() } // PiP diff --git a/shared/apple/MpvPlayer/MpvPlayerPluginShared.swift b/shared/apple/MpvPlayer/MpvPlayerPluginShared.swift index 3baf381c..5e0ab9be 100644 --- a/shared/apple/MpvPlayer/MpvPlayerPluginShared.swift +++ b/shared/apple/MpvPlayer/MpvPlayerPluginShared.swift @@ -12,7 +12,7 @@ protocol MpvPluginShared: AnyObject, MpvPlayerDelegate { var eventSink: FlutterEventSink? { get } var nameToId: [String: Int] { get set } - func setPlayerVisible(_ visible: Bool) + func setPlayerVisible(_ visible: Bool, restoreOnWindowVisible: Bool) func updatePlayerFrame() } @@ -86,9 +86,10 @@ extension MpvPluginShared { code: "INVALID_ARGS", message: "Missing 'visible' argument", details: nil)) return } + let restoreOnWindowVisible = args["restoreOnWindowVisible"] as? Bool ?? false DispatchQueue.main.async { [weak self] in - self?.setPlayerVisible(visible) + self?.setPlayerVisible(visible, restoreOnWindowVisible: restoreOnWindowVisible) if visible { self?.updatePlayerFrame() } result(nil) }