fix: reduce CPU when paused and backgrounded on macOS
This commit is contained in:
@@ -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());
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user