diff --git a/lib/screens/video_player/video_player.dart b/lib/screens/video_player/video_player.dart index e751dace7..52f827fca 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'; @@ -30,27 +29,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 c9372b07c..54ea00aef 100644 --- a/lib/wrappers/media_control_wrapper.dart +++ b/lib/wrappers/media_control_wrapper.dart @@ -74,6 +74,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; @@ -259,6 +262,7 @@ class MediaControlsWrapper extends BaseAudioHandler implements VideoPlayerContro )); smtc?.setPosition(value.position); smtc?.setPlaybackStatus(value.playing ? PlaybackStatus.playing : PlaybackStatus.paused); + _applyWakelock(_shouldKeepScreenOn(value.playing)); if (value.completed && !_audioQueueTransitioning) { _onAudioTrackCompleted(); } @@ -308,6 +312,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(); @@ -317,7 +347,7 @@ class MediaControlsWrapper extends BaseAudioHandler implements VideoPlayerContro updatePosition: position, controls: [MediaControl.play], )); - await WakelockPlus.disable(); + await _applyWakelock(false); final playerState = _player; if (playerState != null) { final model = ref.read(playBackModel); @@ -331,14 +361,12 @@ 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) { - await WakelockPlus.enable(); - } + await _applyWakelock(_shouldKeepScreenOn(true)); await _player?.play(); + final playBackItem = ref.read(playBackModel.select((value) => value?.item)); + final currentPosition = await ref.read(playBackModel.select((value) => value?.startDuration())); if (_isNewPlayback || !playbackState.value.playing) { _isNewPlayback = false; @@ -446,7 +474,7 @@ class MediaControlsWrapper extends BaseAudioHandler implements VideoPlayerContro if (playbackModel == null) return; ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(state: VideoPlayerState.disposed)); - WakelockPlus.disable(); + _applyWakelock(false); _player?.stop(); ref.read(windowTitleProvider.notifier).setPlayTitle(null); @@ -497,15 +525,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) { - await WakelockPlus.enable(); - } - } else { - await WakelockPlus.disable(); - } + await _applyWakelock(_shouldKeepScreenOn(playing)); final playerState = _player; if (playerState != null) {