From 46ea3ff5fad35eb5bf0578defbb9d0226a3387c4 Mon Sep 17 00:00:00 2001 From: aviadlevy Date: Fri, 3 Jul 2026 17:13:28 +0300 Subject: [PATCH 1/2] fix: keep screen awake after returning to playback from background MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Android the keep-screen-on window flag (FLAG_KEEP_SCREEN_ON) is dropped across a background→foreground cycle, so the screen would sleep after the idle timeout while a video kept playing. The wakelock was only set in play() and cleared in pause() (set-and-forget), and the lifecycle resume handler meant to re-apply it was dead code: its `playing` flag was never set to true. Route the wakelock through a single idempotent path in MediaControlsWrapper driven by the actual player state (on when playing and not an AudioModel), and add reassertWakelock() which force re-applies it. Call it from AppLifecycleState.resumed so the flag is restored whenever the app returns to the foreground while playback continues. --- lib/screens/video_player/video_player.dart | 20 ++------- lib/wrappers/media_control_wrapper.dart | 50 +++++++++++++++------- 2 files changed, 39 insertions(+), 31 deletions(-) diff --git a/lib/screens/video_player/video_player.dart b/lib/screens/video_player/video_player.dart index 384287531..b6091af35 100644 --- a/lib/screens/video_player/video_player.dart +++ b/lib/screens/video_player/video_player.dart @@ -9,7 +9,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/media_playback_model.dart'; import 'package:fladder/models/playback/playback_model.dart'; import 'package:fladder/models/playback/tv_playback_model.dart'; -import 'package:fladder/providers/pip_provider.dart'; import 'package:fladder/providers/settings/video_player_settings_provider.dart'; import 'package:fladder/providers/video_player_provider.dart'; import 'package:fladder/screens/video_player/components/video_player_guide_wrapper.dart'; @@ -31,27 +30,16 @@ class _VideoPlayerState extends ConsumerState with WidgetsBindingOb double lastScale = 0.0; bool errorPlaying = false; - bool playing = false; late PlaybackModel? currentPlaybackModel = ref.read(playBackModel); @override void didChangeAppLifecycleState(AppLifecycleState state) { - //Don't pause on desktop focus loss + //Don't manage the wakelock on desktop focus loss if (!(AdaptiveLayout.of(context).isDesktop || kIsWeb)) { - // Don't pause when entering PiP — playback must continue. - final inPip = ref.read(pipStateProvider).asData?.value ?? false; - switch (state) { - case AppLifecycleState.resumed: - if (playing) ref.read(videoPlayerProvider).play(); - break; - case AppLifecycleState.hidden: - case AppLifecycleState.paused: - case AppLifecycleState.detached: - if (playing && !inPip) ref.read(videoPlayerProvider).pause(); - break; - default: - break; + if (state == AppLifecycleState.resumed) { + // Android drops the keep-screen-on flag on resume; re-apply it. + ref.read(videoPlayerProvider).reassertWakelock(); } } } diff --git a/lib/wrappers/media_control_wrapper.dart b/lib/wrappers/media_control_wrapper.dart index 2300e3afb..db973851c 100644 --- a/lib/wrappers/media_control_wrapper.dart +++ b/lib/wrappers/media_control_wrapper.dart @@ -75,6 +75,9 @@ class MediaControlsWrapper extends BaseAudioHandler implements VideoPlayerContro bool _isAudioQueueMode = false; bool _audioQueueTransitioning = false; + /// Last requested wakelock state; used to skip redundant calls during steady playback. + bool _wakelockEnabled = false; + AudioPrefetchBuffer? _prefetchBuffer; List _mpvPlaylistItems = []; int _mpvPlaylistCurrentIndex = 0; @@ -264,6 +267,7 @@ class MediaControlsWrapper extends BaseAudioHandler implements VideoPlayerContro )); smtc?.setPosition(value.position); smtc?.setPlaybackStatus(value.playing ? PlaybackStatus.playing : PlaybackStatus.paused); + unawaited(_applyWakelock(_shouldKeepScreenOn(value.playing))); if (value.completed && !_audioQueueTransitioning) { _onAudioTrackCompleted(); } @@ -313,6 +317,32 @@ class MediaControlsWrapper extends BaseAudioHandler implements VideoPlayerContro return loadPreviousVideo(); } + /// Video needs the screen awake; audio can keep playing with the screen off. + bool _shouldKeepScreenOn(bool playing) { + final item = ref.read(playBackModel.select((value) => value?.item)); + return playing && item is! AudioModel; + } + + /// Single source of truth for the wakelock. Idempotent during steady + /// playback; pass [force] to re-apply even when [_wakelockEnabled] already + /// matches (needed after Android silently clears the window flag on resume). + Future _applyWakelock(bool shouldEnable, {bool force = false}) async { + if (!force && shouldEnable == _wakelockEnabled) return; + _wakelockEnabled = shouldEnable; + if (shouldEnable) { + await WakelockPlus.enable(); + } else { + await WakelockPlus.disable(); + } + } + + /// Re-applies the wakelock based on the current playback state. Called when + /// the app returns to the foreground, because Android drops the + /// keep-screen-on window flag across a background→foreground cycle and + /// nothing else restores it while playback continues. + Future reassertWakelock() async => + _applyWakelock(_shouldKeepScreenOn(_player?.lastState.playing ?? false), force: true); + @override Future pause() async { await _player?.pause(); @@ -322,7 +352,7 @@ class MediaControlsWrapper extends BaseAudioHandler implements VideoPlayerContro updatePosition: position, controls: [MediaControl.play], )); - unawaited(WakelockPlus.disable()); + unawaited(_applyWakelock(false)); final playerState = _player; if (playerState != null) { final model = ref.read(playBackModel); @@ -336,13 +366,11 @@ class MediaControlsWrapper extends BaseAudioHandler implements VideoPlayerContro @override Future play() async { - // Only enable wakelock for video; audio can continue with screen off final playBackItem = ref.read(playBackModel.select((value) => value?.item)); - if (playBackItem is! AudioModel) { - unawaited(WakelockPlus.enable()); - } else { + if (playBackItem is AudioModel) { _isStopped = false; } + unawaited(_applyWakelock(_shouldKeepScreenOn(true))); await _player?.play(); @@ -456,7 +484,7 @@ class MediaControlsWrapper extends BaseAudioHandler implements VideoPlayerContro _isStopped = true; ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(state: VideoPlayerState.disposed)); - unawaited(WakelockPlus.disable()); + unawaited(_applyWakelock(false)); _player?.stop(); ref.read(windowTitleProvider.notifier).setPlayTitle(null); @@ -510,15 +538,7 @@ class MediaControlsWrapper extends BaseAudioHandler implements VideoPlayerContro controls: [playing ? MediaControl.pause : MediaControl.play], )); - if (playing) { - // Only enable wakelock for video; audio can continue with screen off - final playBackItem = ref.read(playBackModel.select((value) => value?.item)); - if (playBackItem is! AudioModel) { - unawaited(WakelockPlus.enable()); - } - } else { - unawaited(WakelockPlus.disable()); - } + unawaited(_applyWakelock(_shouldKeepScreenOn(playing))); final playerState = _player; if (playerState != null) { From a067f8c866a33c0ed63de03e2f63edab835409c9 Mon Sep 17 00:00:00 2001 From: aviadlevy Date: Sun, 9 Aug 2026 17:02:29 +0300 Subject: [PATCH 2/2] chore: reduce comments around the wakelock helpers Review feedback on #1047: the helper names already say what they do, so the doc comments on _wakelockEnabled, _shouldKeepScreenOn and reassertWakelock were noise. Keeps only the note on _applyWakelock's force flag, since "re-apply even though the cached state matches" is not derivable from the code. --- lib/wrappers/media_control_wrapper.dart | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/wrappers/media_control_wrapper.dart b/lib/wrappers/media_control_wrapper.dart index db973851c..e5d4cb78b 100644 --- a/lib/wrappers/media_control_wrapper.dart +++ b/lib/wrappers/media_control_wrapper.dart @@ -74,8 +74,6 @@ class MediaControlsWrapper extends BaseAudioHandler implements VideoPlayerContro bool _isNewPlayback = false; bool _isAudioQueueMode = false; bool _audioQueueTransitioning = false; - - /// Last requested wakelock state; used to skip redundant calls during steady playback. bool _wakelockEnabled = false; AudioPrefetchBuffer? _prefetchBuffer; @@ -317,15 +315,13 @@ class MediaControlsWrapper extends BaseAudioHandler implements VideoPlayerContro return loadPreviousVideo(); } - /// Video needs the screen awake; audio can keep playing with the screen off. bool _shouldKeepScreenOn(bool playing) { final item = ref.read(playBackModel.select((value) => value?.item)); return playing && item is! AudioModel; } - /// Single source of truth for the wakelock. Idempotent during steady - /// playback; pass [force] to re-apply even when [_wakelockEnabled] already - /// matches (needed after Android silently clears the window flag on resume). + /// [force] re-applies even when the cached state already matches, since + /// Android silently clears the keep-screen-on flag while we still think it's set. Future _applyWakelock(bool shouldEnable, {bool force = false}) async { if (!force && shouldEnable == _wakelockEnabled) return; _wakelockEnabled = shouldEnable; @@ -336,10 +332,6 @@ class MediaControlsWrapper extends BaseAudioHandler implements VideoPlayerContro } } - /// Re-applies the wakelock based on the current playback state. Called when - /// the app returns to the foreground, because Android drops the - /// keep-screen-on window flag across a background→foreground cycle and - /// nothing else restores it while playback continues. Future reassertWakelock() async => _applyWakelock(_shouldKeepScreenOn(_player?.lastState.playing ?? false), force: true);