From 8b40e0993fe6df702bbbdda3969bb995122dedaa Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Thu, 9 Apr 2026 05:50:09 +0200 Subject: [PATCH] fix: normalize EOF playback state close #828 --- lib/mpv/player/player_state.dart | 6 ++++ lib/screens/video_player_screen.dart | 33 +++++++++++++++------ lib/services/playback_progress_tracker.dart | 4 +-- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/lib/mpv/player/player_state.dart b/lib/mpv/player/player_state.dart index 85ab996f..971ef961 100644 --- a/lib/mpv/player/player_state.dart +++ b/lib/mpv/player/player_state.dart @@ -117,6 +117,12 @@ class PlayerState { ); } + /// Whether media is actively playing (not paused, not at EOF). + /// + /// mpv keeps [playing] true at EOF, so raw [playing] alone is unreliable + /// for gating wakelock, media controls, progress tracking, etc. + bool get isActive => playing && !completed; + @override String toString() => 'PlayerState(playing: $playing, position: $position, duration: $duration, seekable: $seekable)'; } diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index cd944376..7c79a5f0 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -72,7 +72,11 @@ import '../i18n/strings.g.dart'; import '../watch_together/providers/watch_together_provider.dart'; import '../watch_together/widgets/watch_together_overlay.dart'; +bool? _wakelockEnabled; + Future _setWakelock(bool enabled) async { + if (_wakelockEnabled == enabled) return; + _wakelockEnabled = enabled; try { if (enabled) { await WakelockPlus.enable(); @@ -80,6 +84,7 @@ Future _setWakelock(bool enabled) async { await WakelockPlus.disable(); } } catch (e) { + _wakelockEnabled = null; appLogger.w('Wakelock ${enabled ? 'enable' : 'disable'} failed: $e'); } } @@ -403,7 +408,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin // Pause first so Android MPV does not keep decoding against a transient // background surface while the app is locking or hiding. if (PlatformDetector.isMobile(context)) { - _wasPlayingBeforeInactive = currentPlayer.state.playing; + _wasPlayingBeforeInactive = currentPlayer.state.isActive; if (_wasPlayingBeforeInactive) { try { await currentPlayer.pause(); @@ -890,7 +895,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin _updateMediaControlsPlaybackState(); } else if (event is TogglePlayPauseEvent) { appLogger.d('Media control: Toggle play/pause event received'); - if (currentPlayer!.state.playing) { + if (currentPlayer!.state.isActive) { currentPlayer.pause(); } else { _seekBackForRewind(currentPlayer); @@ -929,7 +934,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin // Listen to position updates for media controls and Discord _mediaControlsPositionSubscription = player!.streams.position.listen((position) { _mediaControlsManager?.updatePlaybackState( - isPlaying: player!.state.playing, + isPlaying: player!.state.isActive, position: position, speed: player!.state.rate, ); @@ -2066,9 +2071,19 @@ class VideoPlayerScreenState extends State with WidgetsBindin // Live TV streams are continuous — ignore spurious EOF events caused by // inter-segment gaps in the chunked MKV transcode stream. if (widget.isLive) return; + if (!completed) return; - if (completed && - _nextEpisode != null && + // mpv does not flip the `pause` property on EOF, so _onPlayingStateChanged + // never fires false. Normalize all playback-dependent state. + _setWakelock(false); + _progressTracker?.sendProgress('paused'); + _updateMediaControlsPlaybackState(); + DiscordRPCService.instance.pausePlayback(); + if (_autoPipEnabled) { + _videoPIPManager?.updateAutoPipState(isPlaying: false); + } + + if (_nextEpisode != null && !_showPlayNextDialog && !_showStillWatchingPrompt && !_completionTriggered) { @@ -2098,7 +2113,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin if (autoPlayEnabled) { _startAutoPlayTimer(); } - } else if (completed && _nextEpisode == null && !_completionTriggered) { + } else if (_nextEpisode == null && !_completionTriggered) { _completionTriggered = true; _handleBackButton(); } @@ -2167,7 +2182,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin Future _restoreMediaControlsAfterResume() async { if (!_isPlayerInitialized || !mounted) return; - _setWakelock(true); + _setWakelock(player?.state.isActive ?? false); final manager = _mediaControlsManager; final currentPlayer = player; @@ -2196,7 +2211,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin } _updateMediaControlsPlaybackState(); - appLogger.d('Media controls restored and wakelock re-enabled on app resume'); + appLogger.d('Media controls restored on app resume'); } /// Wrapper method to update media controls playback state @@ -2204,7 +2219,7 @@ class VideoPlayerScreenState extends State with WidgetsBindin if (player == null) return; _mediaControlsManager?.updatePlaybackState( - isPlaying: player!.state.playing, + isPlaying: player!.state.isActive, position: player!.state.position, speed: player!.state.rate, force: true, // Force update since this is an explicit state change diff --git a/lib/services/playback_progress_tracker.dart b/lib/services/playback_progress_tracker.dart index 24597798..b6d083d7 100644 --- a/lib/services/playback_progress_tracker.dart +++ b/lib/services/playback_progress_tracker.dart @@ -67,12 +67,12 @@ class PlaybackProgressTracker { } // Send initial progress immediately (don't wait for first timer tick) - if (player.state.playing) { + if (player.state.isActive) { _sendProgress('playing'); } _progressTimer = Timer.periodic(updateInterval, (timer) { - if (player.state.playing) { + if (player.state.isActive) { _pausedTickCounter = 0; // Skip ticks when backing off after consecutive failures to avoid // flooding the network with doomed requests during an outage.