fix(macos): stop CPU spin when display sleeps with video paused
When the display/system sleeps with a video paused, the window server stops completing CAMetalLayer presents and mpv's MoltenVK swapchain wedges, pinning one core at 100% until the app exits. occlusionState does not reliably change on display sleep, so observe NSWorkspace screensDidSleep/ Wake to hide the metal layer (and drop the playback activity) while dark, restoring on wake with a forced redraw when paused to avoid a stale frame. Pairs with MPVKit v1.0.10, which gates libmpv presentation on the layer's hidden state (check_visible) so presents actually stop while hidden.
This commit is contained in:
@@ -801,7 +801,7 @@
|
|||||||
repositoryURL = "https://github.com/edde746/MPVKit";
|
repositoryURL = "https://github.com/edde746/MPVKit";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = exactVersion;
|
kind = exactVersion;
|
||||||
version = 1.0.9;
|
version = 1.0.10;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|||||||
@@ -40,8 +40,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/edde746/MPVKit",
|
"location" : "https://github.com/edde746/MPVKit",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "206ba4a9f8f8191f18cabfdc025bfa512729af58",
|
"revision" : "bdc3a651c17c01d3985828bdc07b716786992259",
|
||||||
"version" : "1.0.9"
|
"version" : "1.0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -40,8 +40,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/edde746/MPVKit",
|
"location" : "https://github.com/edde746/MPVKit",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "206ba4a9f8f8191f18cabfdc025bfa512729af58",
|
"revision" : "bdc3a651c17c01d3985828bdc07b716786992259",
|
||||||
"version" : "1.0.9"
|
"version" : "1.0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -891,7 +891,7 @@
|
|||||||
repositoryURL = "https://github.com/edde746/MPVKit";
|
repositoryURL = "https://github.com/edde746/MPVKit";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = exactVersion;
|
kind = exactVersion;
|
||||||
version = 1.0.9;
|
version = 1.0.10;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/edde746/MPVKit",
|
"location" : "https://github.com/edde746/MPVKit",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "206ba4a9f8f8191f18cabfdc025bfa512729af58",
|
"revision" : "bdc3a651c17c01d3985828bdc07b716786992259",
|
||||||
"version" : "1.0.9"
|
"version" : "1.0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/edde746/MPVKit",
|
"location" : "https://github.com/edde746/MPVKit",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "206ba4a9f8f8191f18cabfdc025bfa512729af58",
|
"revision" : "bdc3a651c17c01d3985828bdc07b716786992259",
|
||||||
"version" : "1.0.9"
|
"version" : "1.0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,8 +8,14 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
|||||||
private weak var window: NSWindow?
|
private weak var window: NSWindow?
|
||||||
private var playbackActivity: NSObjectProtocol?
|
private var playbackActivity: NSObjectProtocol?
|
||||||
private var layerHiddenForOcclusion = false
|
private var layerHiddenForOcclusion = false
|
||||||
|
private var layerHiddenForScreenSleep = false
|
||||||
private var isDisposed = false
|
private var isDisposed = false
|
||||||
|
|
||||||
|
/// True while any reason (occlusion, screen sleep) requires the layer hidden.
|
||||||
|
private var hasLayerHideReason: Bool {
|
||||||
|
layerHiddenForOcclusion || layerHiddenForScreenSleep
|
||||||
|
}
|
||||||
|
|
||||||
func initialize(in window: NSWindow) -> Bool {
|
func initialize(in window: NSWindow) -> Bool {
|
||||||
guard !isInitialized else {
|
guard !isInitialized else {
|
||||||
print("[MpvPlayerCore] Already initialized")
|
print("[MpvPlayerCore] Already initialized")
|
||||||
@@ -73,6 +79,24 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
|||||||
object: window
|
object: window
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Display/system sleep does not reliably change occlusionState, so observe
|
||||||
|
// NSWorkspace screen sleep/wake directly to gate presentation (prevents a
|
||||||
|
// pinned CPU core after long display sleep). This is a DIFFERENT
|
||||||
|
// notification center than NotificationCenter.default used above.
|
||||||
|
let workspaceCenter = NSWorkspace.shared.notificationCenter
|
||||||
|
workspaceCenter.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(screensDidSleep),
|
||||||
|
name: NSWorkspace.screensDidSleepNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
workspaceCenter.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(screensDidWake),
|
||||||
|
name: NSWorkspace.screensDidWakeNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
|
||||||
isInitialized = true
|
isInitialized = true
|
||||||
print("[MpvPlayerCore] Initialized successfully with MPV")
|
print("[MpvPlayerCore] Initialized successfully with MPV")
|
||||||
return true
|
return true
|
||||||
@@ -107,8 +131,9 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
|||||||
|
|
||||||
if visible && isVisible && !shouldRestoreOnWindowVisible {
|
if visible && isVisible && !shouldRestoreOnWindowVisible {
|
||||||
isBackgrounded = false
|
isBackgrounded = false
|
||||||
if metalLayer?.isHidden == true {
|
if metalLayer?.isHidden == true && !hasLayerHideReason {
|
||||||
setMetalLayerHidden(false)
|
setMetalLayerHidden(false)
|
||||||
|
redrawIfPausedAndVisible()
|
||||||
}
|
}
|
||||||
beginPlaybackActivity()
|
beginPlaybackActivity()
|
||||||
print("[MpvPlayerCore] setVisible(true) skipped - already visible")
|
print("[MpvPlayerCore] setVisible(true) skipped - already visible")
|
||||||
@@ -132,7 +157,10 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
|||||||
endPlaybackActivity()
|
endPlaybackActivity()
|
||||||
}
|
}
|
||||||
|
|
||||||
setMetalLayerHidden(!visible)
|
setMetalLayerHidden(!visible || hasLayerHideReason)
|
||||||
|
if visible {
|
||||||
|
redrawIfPausedAndVisible()
|
||||||
|
}
|
||||||
print("[MpvPlayerCore] setVisible(\(visible), restoreOnWindowVisible: \(restoreOnWindowVisible))")
|
print("[MpvPlayerCore] setVisible(\(visible), restoreOnWindowVisible: \(restoreOnWindowVisible))")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,6 +217,7 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
|||||||
|
|
||||||
endPlaybackActivity()
|
endPlaybackActivity()
|
||||||
NotificationCenter.default.removeObserver(self)
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
NSWorkspace.shared.notificationCenter.removeObserver(self)
|
||||||
disposeSharedState(destroySynchronously: false)
|
disposeSharedState(destroySynchronously: false)
|
||||||
|
|
||||||
metalLayer?.removeFromSuperlayer()
|
metalLayer?.removeFromSuperlayer()
|
||||||
@@ -224,10 +253,13 @@ 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
|
||||||
if shouldRestoreOnWindowVisible {
|
if !layerHiddenForScreenSleep {
|
||||||
restoreMetalLayerAfterOcclusion()
|
if shouldRestoreOnWindowVisible {
|
||||||
} else {
|
restoreMetalLayerAfterOcclusion()
|
||||||
setMetalLayerHidden(!isVisible)
|
} else {
|
||||||
|
setMetalLayerHidden(!isVisible)
|
||||||
|
}
|
||||||
|
redrawIfPausedAndVisible()
|
||||||
}
|
}
|
||||||
isBackgrounded = false
|
isBackgrounded = false
|
||||||
if !pausedState {
|
if !pausedState {
|
||||||
@@ -236,6 +268,54 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func screensDidSleep(_ notification: Notification) {
|
||||||
|
guard metalLayer != nil, mpv != nil, !layerHiddenForScreenSleep else { return }
|
||||||
|
print("[MpvPlayerCore] Screens did sleep - hiding Metal layer")
|
||||||
|
layerHiddenForScreenSleep = true
|
||||||
|
// Hide even during PiP: nothing is visible while the displays are dark, and
|
||||||
|
// the hidden layer is what gates libmpv presentation (MPVKit >= 1.0.10).
|
||||||
|
setMetalLayerHidden(true)
|
||||||
|
isBackgrounded = true
|
||||||
|
endPlaybackActivity()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func screensDidWake(_ notification: Notification) {
|
||||||
|
guard metalLayer != nil, mpv != nil, layerHiddenForScreenSleep else { return }
|
||||||
|
print("[MpvPlayerCore] Screens did wake - restoring Metal layer")
|
||||||
|
layerHiddenForScreenSleep = false
|
||||||
|
|
||||||
|
if isPipActive {
|
||||||
|
// Layer is hosted by the PiP window; just unhide it there. Attach/frame
|
||||||
|
// logic is owned by the PiP controller.
|
||||||
|
setMetalLayerHidden(false)
|
||||||
|
isBackgrounded = false
|
||||||
|
} else if !layerHiddenForOcclusion {
|
||||||
|
if shouldRestoreOnWindowVisible {
|
||||||
|
restoreMetalLayerAfterOcclusion()
|
||||||
|
} else {
|
||||||
|
setMetalLayerHidden(!isVisible)
|
||||||
|
}
|
||||||
|
isBackgrounded = !isVisible
|
||||||
|
}
|
||||||
|
// else: window still occluded; windowOcclusionDidChange owns the restore.
|
||||||
|
|
||||||
|
if !layerHiddenForOcclusion && !pausedState {
|
||||||
|
beginPlaybackActivity()
|
||||||
|
}
|
||||||
|
redrawIfPausedAndVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// With MPVKit >= 1.0.10, frames produced while the layer was hidden are
|
||||||
|
/// skipped at the swapchain, so a paused video would otherwise show a stale
|
||||||
|
/// frame after unhiding (upstream regression mpv#16693). Playing video
|
||||||
|
/// repaints itself on the next frame; only paused needs a forced draw. The
|
||||||
|
/// isHidden guard makes multi-path wake ordering safe — only the path that
|
||||||
|
/// actually unhides the layer triggers the single redraw.
|
||||||
|
private func redrawIfPausedAndVisible() {
|
||||||
|
guard pausedState, metalLayer?.isHidden == false else { return }
|
||||||
|
forceDraw()
|
||||||
|
}
|
||||||
|
|
||||||
private func beginPlaybackActivity() {
|
private func beginPlaybackActivity() {
|
||||||
guard playbackActivity == nil else { return }
|
guard playbackActivity == nil else { return }
|
||||||
playbackActivity = ProcessInfo.processInfo.beginActivity(
|
playbackActivity = ProcessInfo.processInfo.beginActivity(
|
||||||
@@ -267,7 +347,7 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
|||||||
}
|
}
|
||||||
isVisible = true
|
isVisible = true
|
||||||
shouldRestoreOnWindowVisible = false
|
shouldRestoreOnWindowVisible = false
|
||||||
setMetalLayerHidden(false)
|
setMetalLayerHidden(hasLayerHideReason)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func attachMetalLayer(to superlayer: CALayer, frame: CGRect) {
|
private func attachMetalLayer(to superlayer: CALayer, frame: CGRect) {
|
||||||
|
|||||||
Reference in New Issue
Block a user