diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index aa11be93..40f12c34 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -801,7 +801,7 @@ repositoryURL = "https://github.com/edde746/MPVKit"; requirement = { kind = exactVersion; - version = 1.0.9; + version = 1.0.10; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3af7a777..538f1fc1 100644 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -40,8 +40,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/edde746/MPVKit", "state" : { - "revision" : "206ba4a9f8f8191f18cabfdc025bfa512729af58", - "version" : "1.0.9" + "revision" : "bdc3a651c17c01d3985828bdc07b716786992259", + "version" : "1.0.10" } }, { diff --git a/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3af7a777..538f1fc1 100644 --- a/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -40,8 +40,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/edde746/MPVKit", "state" : { - "revision" : "206ba4a9f8f8191f18cabfdc025bfa512729af58", - "version" : "1.0.9" + "revision" : "bdc3a651c17c01d3985828bdc07b716786992259", + "version" : "1.0.10" } }, { diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 887943f1..193cdfa4 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -891,7 +891,7 @@ repositoryURL = "https://github.com/edde746/MPVKit"; requirement = { kind = exactVersion; - version = 1.0.9; + version = 1.0.10; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ed26da49..9968b615 100644 --- a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -13,8 +13,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/edde746/MPVKit", "state" : { - "revision" : "206ba4a9f8f8191f18cabfdc025bfa512729af58", - "version" : "1.0.9" + "revision" : "bdc3a651c17c01d3985828bdc07b716786992259", + "version" : "1.0.10" } }, { diff --git a/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved index ed26da49..9968b615 100644 --- a/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -13,8 +13,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/edde746/MPVKit", "state" : { - "revision" : "206ba4a9f8f8191f18cabfdc025bfa512729af58", - "version" : "1.0.9" + "revision" : "bdc3a651c17c01d3985828bdc07b716786992259", + "version" : "1.0.10" } }, { diff --git a/macos/Runner/MpvPlayer/MpvPlayerCore.swift b/macos/Runner/MpvPlayer/MpvPlayerCore.swift index 7c479f8d..5387a3f7 100644 --- a/macos/Runner/MpvPlayer/MpvPlayerCore.swift +++ b/macos/Runner/MpvPlayer/MpvPlayerCore.swift @@ -8,8 +8,14 @@ class MpvPlayerCore: MpvPlayerCoreBase { private weak var window: NSWindow? private var playbackActivity: NSObjectProtocol? private var layerHiddenForOcclusion = false + private var layerHiddenForScreenSleep = 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 { guard !isInitialized else { print("[MpvPlayerCore] Already initialized") @@ -73,6 +79,24 @@ class MpvPlayerCore: MpvPlayerCoreBase { 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 print("[MpvPlayerCore] Initialized successfully with MPV") return true @@ -107,8 +131,9 @@ class MpvPlayerCore: MpvPlayerCoreBase { if visible && isVisible && !shouldRestoreOnWindowVisible { isBackgrounded = false - if metalLayer?.isHidden == true { + if metalLayer?.isHidden == true && !hasLayerHideReason { setMetalLayerHidden(false) + redrawIfPausedAndVisible() } beginPlaybackActivity() print("[MpvPlayerCore] setVisible(true) skipped - already visible") @@ -132,7 +157,10 @@ class MpvPlayerCore: MpvPlayerCoreBase { endPlaybackActivity() } - setMetalLayerHidden(!visible) + setMetalLayerHidden(!visible || hasLayerHideReason) + if visible { + redrawIfPausedAndVisible() + } print("[MpvPlayerCore] setVisible(\(visible), restoreOnWindowVisible: \(restoreOnWindowVisible))") } @@ -189,6 +217,7 @@ class MpvPlayerCore: MpvPlayerCoreBase { endPlaybackActivity() NotificationCenter.default.removeObserver(self) + NSWorkspace.shared.notificationCenter.removeObserver(self) disposeSharedState(destroySynchronously: false) metalLayer?.removeFromSuperlayer() @@ -224,10 +253,13 @@ class MpvPlayerCore: MpvPlayerCoreBase { } else if windowVisible && layerHiddenForOcclusion { print("[MpvPlayerCore] Window visible - showing Metal layer") layerHiddenForOcclusion = false - if shouldRestoreOnWindowVisible { - restoreMetalLayerAfterOcclusion() - } else { - setMetalLayerHidden(!isVisible) + if !layerHiddenForScreenSleep { + if shouldRestoreOnWindowVisible { + restoreMetalLayerAfterOcclusion() + } else { + setMetalLayerHidden(!isVisible) + } + redrawIfPausedAndVisible() } isBackgrounded = false 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() { guard playbackActivity == nil else { return } playbackActivity = ProcessInfo.processInfo.beginActivity( @@ -267,7 +347,7 @@ class MpvPlayerCore: MpvPlayerCoreBase { } isVisible = true shouldRestoreOnWindowVisible = false - setMetalLayerHidden(false) + setMetalLayerHidden(hasLayerHideReason) } private func attachMetalLayer(to superlayer: CALayer, frame: CGRect) {