fix(macos): restore video layer on focus

This commit is contained in:
edde746
2026-05-11 10:13:05 +02:00
parent 8d76dff353
commit 9b248a7a5b
7 changed files with 56 additions and 12 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
// MpvPluginShared conformance // MpvPluginShared conformance
var coreBase: MpvPlayerCoreBase? { playerCore } var coreBase: MpvPlayerCoreBase? { playerCore }
func setPlayerVisible(_ visible: Bool) { playerCore?.setVisible(visible) } func setPlayerVisible(_ visible: Bool, restoreOnWindowVisible _: Bool) { playerCore?.setVisible(visible) }
func updatePlayerFrame() { playerCore?.updateFrame() } func updatePlayerFrame() { playerCore?.updateFrame() }
// PiP // PiP
+5 -1
View File
@@ -163,8 +163,12 @@ abstract class Player {
/// On macOS, this controls the Metal layer visibility. /// On macOS, this controls the Metal layer visibility.
/// On other platforms, this may have no effect. /// 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. /// Returns true if the operation was successful.
Future<bool> setVisible(bool visible); Future<bool> setVisible(bool visible, {bool restoreOnWindowVisible = false});
/// Update the video frame/surface dimensions. /// Update the video frame/surface dimensions.
/// ///
+2 -2
View File
@@ -523,10 +523,10 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
} }
@override @override
Future<bool> setVisible(bool visible) async { Future<bool> setVisible(bool visible, {bool restoreOnWindowVisible = false}) async {
if (_disposed) return false; if (_disposed) return false;
try { try {
await invoke('setVisible', {'visible': visible}); await invoke('setVisible', {'visible': visible, 'restoreOnWindowVisible': restoreOnWindowVisible});
return true; return true;
} catch (e) { } catch (e) {
errorController.add(PlayerError('Failed to set visibility: $e')); errorController.add(PlayerError('Failed to set visibility: $e'));
@@ -115,7 +115,7 @@ extension _VideoPlayerLifecycleMethods on VideoPlayerScreenState {
} }
_hiddenForBackground = true; _hiddenForBackground = true;
await currentPlayer.setVisible(false); await currentPlayer.setVisible(false, restoreOnWindowVisible: Platform.isMacOS);
_recordLifecycleState('hidden', action: 'render_hidden'); _recordLifecycleState('hidden', action: 'render_hidden');
} }
@@ -133,7 +133,9 @@ extension _VideoPlayerLifecycleMethods on VideoPlayerScreenState {
// video-output refresh before any auto-resume logic runs. // video-output refresh before any auto-resume logic runs.
if (_hiddenForBackground && currentPlayer != null && _isPlayerInitialized) { if (_hiddenForBackground && currentPlayer != null && _isPlayerInitialized) {
await currentPlayer.setVisible(true); await currentPlayer.setVisible(true);
await currentPlayer.updateFrame(); if (!Platform.isMacOS) {
await currentPlayer.updateFrame();
}
if (!mounted || currentPlayer != player) return; if (!mounted || currentPlayer != player) return;
+38 -3
View File
@@ -100,14 +100,27 @@ class MpvPlayerCore: MpvPlayerCoreBase {
private var isVisible = false private var isVisible = false
private var pausedState = true 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 } 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 isVisible = visible
shouldRestoreOnWindowVisible = !visible && restoreOnWindowVisible
isBackgrounded = !visible isBackgrounded = !visible
if visible { if visible {
shouldRestoreOnWindowVisible = false
if let contentView = window?.contentView { if let contentView = window?.contentView {
contentView.wantsLayer = true contentView.wantsLayer = true
if let superlayer = contentView.layer { if let superlayer = contentView.layer {
@@ -120,7 +133,7 @@ class MpvPlayerCore: MpvPlayerCoreBase {
} }
setMetalLayerHidden(!visible) setMetalLayerHidden(!visible)
print("[MpvPlayerCore] setVisible(\(visible))") print("[MpvPlayerCore] setVisible(\(visible), restoreOnWindowVisible: \(restoreOnWindowVisible))")
} }
func setPaused(_ paused: Bool) { func setPaused(_ paused: Bool) {
@@ -211,7 +224,11 @@ class MpvPlayerCore: MpvPlayerCoreBase {
} else if windowVisible && layerHiddenForOcclusion { } else if windowVisible && layerHiddenForOcclusion {
print("[MpvPlayerCore] Window visible - showing Metal layer") print("[MpvPlayerCore] Window visible - showing Metal layer")
layerHiddenForOcclusion = false layerHiddenForOcclusion = false
setMetalLayerHidden(!isVisible) if shouldRestoreOnWindowVisible {
restoreMetalLayerAfterOcclusion()
} else {
setMetalLayerHidden(!isVisible)
}
isBackgrounded = false isBackgrounded = false
if !pausedState { if !pausedState {
beginPlaybackActivity() beginPlaybackActivity()
@@ -235,6 +252,24 @@ class MpvPlayerCore: MpvPlayerCoreBase {
print("[MpvPlayerCore] Ended playback activity assertion") 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) { private func attachMetalLayer(to superlayer: CALayer, frame: CGRect) {
guard let metalLayer else { return } guard let metalLayer else { return }
+3 -1
View File
@@ -13,7 +13,9 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
// MpvPluginShared conformance // MpvPluginShared conformance
var coreBase: MpvPlayerCoreBase? { playerCore } 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() } func updatePlayerFrame() { playerCore?.updateFrame() }
// PiP // PiP
@@ -12,7 +12,7 @@ protocol MpvPluginShared: AnyObject, MpvPlayerDelegate {
var eventSink: FlutterEventSink? { get } var eventSink: FlutterEventSink? { get }
var nameToId: [String: Int] { get set } var nameToId: [String: Int] { get set }
func setPlayerVisible(_ visible: Bool) func setPlayerVisible(_ visible: Bool, restoreOnWindowVisible: Bool)
func updatePlayerFrame() func updatePlayerFrame()
} }
@@ -86,9 +86,10 @@ extension MpvPluginShared {
code: "INVALID_ARGS", message: "Missing 'visible' argument", details: nil)) code: "INVALID_ARGS", message: "Missing 'visible' argument", details: nil))
return return
} }
let restoreOnWindowVisible = args["restoreOnWindowVisible"] as? Bool ?? false
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
self?.setPlayerVisible(visible) self?.setPlayerVisible(visible, restoreOnWindowVisible: restoreOnWindowVisible)
if visible { self?.updatePlayerFrame() } if visible { self?.updatePlayerFrame() }
result(nil) result(nil)
} }