fix: reduce CPU when paused and backgrounded on macOS

This commit is contained in:
edde746
2026-03-21 08:36:25 +01:00
parent 4ee4a766d6
commit 09ff47e19f
3 changed files with 38 additions and 6 deletions
+19 -3
View File
@@ -200,6 +200,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
// App lifecycle state tracking // App lifecycle state tracking
bool _wasPlayingBeforeInactive = false; bool _wasPlayingBeforeInactive = false;
bool _hiddenForBackground = false;
bool _autoPipEnabled = false; bool _autoPipEnabled = false;
/// Whether to skip lifecycle actions because PiP is active or about to start. /// Whether to skip lifecycle actions because PiP is active or about to start.
@@ -340,10 +341,9 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
// Don't pause - user may still be watching // Don't pause - user may still be watching
break; break;
case AppLifecycleState.hidden: case AppLifecycleState.hidden:
// App is being hidden (user is switching away) if (_shouldSkipForPip) break;
// Pause video since we don't support background playback (mobile only) // Pause video on mobile (we don't support background playback)
if (PlatformDetector.isMobile(context)) { if (PlatformDetector.isMobile(context)) {
if (_shouldSkipForPip) break;
if (player != null && _isPlayerInitialized) { if (player != null && _isPlayerInitialized) {
_wasPlayingBeforeInactive = player!.state.playing; _wasPlayingBeforeInactive = player!.state.playing;
if (_wasPlayingBeforeInactive) { if (_wasPlayingBeforeInactive) {
@@ -352,6 +352,13 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
} }
} }
} }
// Hide render layer to stop Vulkan present loop and gate native events
if (player != null && _isPlayerInitialized) {
player!.setVisible(false);
_hiddenForBackground = true;
_liveTimelineTimer?.cancel();
appLogger.d('Render layer hidden due to app being hidden');
}
break; break;
case AppLifecycleState.paused: case AppLifecycleState.paused:
if (_shouldSkipForPip) break; if (_shouldSkipForPip) break;
@@ -363,6 +370,15 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
appLogger.d('Media controls cleared and wakelock disabled due to app being paused/backgrounded'); appLogger.d('Media controls cleared and wakelock disabled due to app being paused/backgrounded');
break; break;
case AppLifecycleState.resumed: case AppLifecycleState.resumed:
// Restore render layer if it was hidden for background
if (_hiddenForBackground && player != null && _isPlayerInitialized) {
player!.setVisible(true);
_hiddenForBackground = false;
if (_liveSessionIdentifier != null) {
_startLiveTimelineUpdates();
}
appLogger.d('Render layer restored after app resumed');
}
// Restore media controls and wakelock when app is resumed // Restore media controls and wakelock when app is resumed
if (_isPlayerInitialized && mounted) { if (_isPlayerInitialized && mounted) {
unawaited(_restoreMediaControlsAfterResume()); unawaited(_restoreMediaControlsAfterResume());
+12 -3
View File
@@ -114,11 +114,13 @@ class MpvPlayerCore: MpvPlayerCoreBase {
} }
private var isVisible = false private var isVisible = false
private var pausedState = true
func setVisible(_ visible: Bool) { func setVisible(_ visible: Bool) {
guard let metalLayer, !isPipActive else { return } guard let metalLayer, !isPipActive else { return }
isVisible = visible isVisible = visible
isBackgrounded = !visible
if visible { if visible {
metalLayer.removeFromSuperlayer() metalLayer.removeFromSuperlayer()
@@ -135,6 +137,7 @@ class MpvPlayerCore: MpvPlayerCoreBase {
} }
func setPaused(_ paused: Bool) { func setPaused(_ paused: Bool) {
pausedState = paused
if paused { if paused {
endPlaybackActivity() endPlaybackActivity()
} else if isVisible { } else if isVisible {
@@ -220,15 +223,21 @@ class MpvPlayerCore: MpvPlayerCoreBase {
@objc private func windowOcclusionDidChange(_ notification: Notification) { @objc private func windowOcclusionDidChange(_ notification: Notification) {
guard let metalLayer, mpv != nil, !isPipActive else { return } guard let metalLayer, mpv != nil, !isPipActive else { return }
let isVisible = window?.occlusionState.contains(.visible) ?? true let windowVisible = window?.occlusionState.contains(.visible) ?? true
if !isVisible && !layerHiddenForOcclusion { if !windowVisible && !layerHiddenForOcclusion {
print("[MpvPlayerCore] Window occluded - hiding Metal layer") print("[MpvPlayerCore] Window occluded - hiding Metal layer")
metalLayer.isHidden = true metalLayer.isHidden = true
layerHiddenForOcclusion = true layerHiddenForOcclusion = true
} else if isVisible && layerHiddenForOcclusion { isBackgrounded = true
endPlaybackActivity()
} else if windowVisible && layerHiddenForOcclusion {
print("[MpvPlayerCore] Window visible - showing Metal layer") print("[MpvPlayerCore] Window visible - showing Metal layer")
layerHiddenForOcclusion = false layerHiddenForOcclusion = false
metalLayer.isHidden = false metalLayer.isHidden = false
isBackgrounded = false
if !pausedState {
beginPlaybackActivity()
}
} }
} }
@@ -80,9 +80,13 @@ class MpvPlayerCoreBase: NSObject {
var isInitialized = false var isInitialized = false
var isDisposing = false var isDisposing = false
var isPipActive = false var isPipActive = false
var isBackgrounded = false
var hdrEnabled = true var hdrEnabled = true
var lastSigPeak = 0.0 var lastSigPeak = 0.0
/// Properties that must still flow to Dart while backgrounded (state-critical).
private static let criticalProperties: Set<String> = ["pause", "eof-reached", "paused-for-cache"]
let queue = DispatchQueue(label: "mpv", qos: .userInitiated) let queue = DispatchQueue(label: "mpv", qos: .userInitiated)
private let queueKey = DispatchSpecificKey<Void>() private let queueKey = DispatchSpecificKey<Void>()
@@ -418,6 +422,7 @@ class MpvPlayerCoreBase: NSObject {
} }
case MPV_EVENT_LOG_MESSAGE: case MPV_EVENT_LOG_MESSAGE:
if isBackgrounded { break }
if let messagePointer = event.data?.assumingMemoryBound(to: mpv_event_log_message.self) { if let messagePointer = event.data?.assumingMemoryBound(to: mpv_event_log_message.self) {
let message = messagePointer.pointee let message = messagePointer.pointee
let prefix = message.prefix.map { safeString($0) } ?? "" let prefix = message.prefix.map { safeString($0) } ?? ""
@@ -438,6 +443,8 @@ class MpvPlayerCoreBase: NSObject {
} }
private func handlePropertyChange(name: String, property: mpv_event_property) { private func handlePropertyChange(name: String, property: mpv_event_property) {
if isBackgrounded && !Self.criticalProperties.contains(name) { return }
var value: Any? var value: Any?
switch property.format { switch property.format {