From 09ff47e19f0b58239573528cbf5939dbec839c84 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sat, 21 Mar 2026 08:36:25 +0100 Subject: [PATCH] fix: reduce CPU when paused and backgrounded on macOS --- lib/screens/video_player_screen.dart | 22 ++++++++++++++++--- macos/Runner/MpvPlayer/MpvPlayerCore.swift | 15 ++++++++++--- .../apple/MpvPlayer/MpvPlayerCoreBase.swift | 7 ++++++ 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index 5774a58d..8e9519d0 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -200,6 +200,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin // App lifecycle state tracking bool _wasPlayingBeforeInactive = false; + bool _hiddenForBackground = false; bool _autoPipEnabled = false; /// Whether to skip lifecycle actions because PiP is active or about to start. @@ -340,10 +341,9 @@ class VideoPlayerScreenState extends State with WidgetsBindin // Don't pause - user may still be watching break; case AppLifecycleState.hidden: - // App is being hidden (user is switching away) - // Pause video since we don't support background playback (mobile only) + if (_shouldSkipForPip) break; + // Pause video on mobile (we don't support background playback) if (PlatformDetector.isMobile(context)) { - if (_shouldSkipForPip) break; if (player != null && _isPlayerInitialized) { _wasPlayingBeforeInactive = player!.state.playing; if (_wasPlayingBeforeInactive) { @@ -352,6 +352,13 @@ class VideoPlayerScreenState extends State 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; case AppLifecycleState.paused: if (_shouldSkipForPip) break; @@ -363,6 +370,15 @@ class VideoPlayerScreenState extends State with WidgetsBindin appLogger.d('Media controls cleared and wakelock disabled due to app being paused/backgrounded'); break; 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 if (_isPlayerInitialized && mounted) { unawaited(_restoreMediaControlsAfterResume()); diff --git a/macos/Runner/MpvPlayer/MpvPlayerCore.swift b/macos/Runner/MpvPlayer/MpvPlayerCore.swift index 8daa89fc..f4f44f3b 100644 --- a/macos/Runner/MpvPlayer/MpvPlayerCore.swift +++ b/macos/Runner/MpvPlayer/MpvPlayerCore.swift @@ -114,11 +114,13 @@ class MpvPlayerCore: MpvPlayerCoreBase { } private var isVisible = false + private var pausedState = true func setVisible(_ visible: Bool) { guard let metalLayer, !isPipActive else { return } isVisible = visible + isBackgrounded = !visible if visible { metalLayer.removeFromSuperlayer() @@ -135,6 +137,7 @@ class MpvPlayerCore: MpvPlayerCoreBase { } func setPaused(_ paused: Bool) { + pausedState = paused if paused { endPlaybackActivity() } else if isVisible { @@ -220,15 +223,21 @@ class MpvPlayerCore: MpvPlayerCoreBase { @objc private func windowOcclusionDidChange(_ notification: Notification) { guard let metalLayer, mpv != nil, !isPipActive else { return } - let isVisible = window?.occlusionState.contains(.visible) ?? true - if !isVisible && !layerHiddenForOcclusion { + let windowVisible = window?.occlusionState.contains(.visible) ?? true + if !windowVisible && !layerHiddenForOcclusion { print("[MpvPlayerCore] Window occluded - hiding Metal layer") metalLayer.isHidden = true layerHiddenForOcclusion = true - } else if isVisible && layerHiddenForOcclusion { + isBackgrounded = true + endPlaybackActivity() + } else if windowVisible && layerHiddenForOcclusion { print("[MpvPlayerCore] Window visible - showing Metal layer") layerHiddenForOcclusion = false metalLayer.isHidden = false + isBackgrounded = false + if !pausedState { + beginPlaybackActivity() + } } } diff --git a/shared/apple/MpvPlayer/MpvPlayerCoreBase.swift b/shared/apple/MpvPlayer/MpvPlayerCoreBase.swift index d517b587..3ab4f32d 100644 --- a/shared/apple/MpvPlayer/MpvPlayerCoreBase.swift +++ b/shared/apple/MpvPlayer/MpvPlayerCoreBase.swift @@ -80,9 +80,13 @@ class MpvPlayerCoreBase: NSObject { var isInitialized = false var isDisposing = false var isPipActive = false + var isBackgrounded = false var hdrEnabled = true var lastSigPeak = 0.0 + /// Properties that must still flow to Dart while backgrounded (state-critical). + private static let criticalProperties: Set = ["pause", "eof-reached", "paused-for-cache"] + let queue = DispatchQueue(label: "mpv", qos: .userInitiated) private let queueKey = DispatchSpecificKey() @@ -418,6 +422,7 @@ class MpvPlayerCoreBase: NSObject { } case MPV_EVENT_LOG_MESSAGE: + if isBackgrounded { break } if let messagePointer = event.data?.assumingMemoryBound(to: mpv_event_log_message.self) { let message = messagePointer.pointee let prefix = message.prefix.map { safeString($0) } ?? "" @@ -438,6 +443,8 @@ class MpvPlayerCoreBase: NSObject { } private func handlePropertyChange(name: String, property: mpv_event_property) { + if isBackgrounded && !Self.criticalProperties.contains(name) { return } + var value: Any? switch property.format {