fix(macos): restore video layer on focus
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user