fix: normalize EOF playback state

close #828
This commit is contained in:
edde746
2026-04-09 05:55:03 +02:00
parent 966b6371f5
commit 8b40e0993f
3 changed files with 32 additions and 11 deletions
+6
View File
@@ -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)';
}
+24 -9
View File
@@ -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<void> _setWakelock(bool enabled) async {
if (_wakelockEnabled == enabled) return;
_wakelockEnabled = enabled;
try {
if (enabled) {
await WakelockPlus.enable();
@@ -80,6 +84,7 @@ Future<void> _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<VideoPlayerScreen> 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<VideoPlayerScreen> 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<VideoPlayerScreen> 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<VideoPlayerScreen> 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<VideoPlayerScreen> 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<VideoPlayerScreen> with WidgetsBindin
Future<void> _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<VideoPlayerScreen> 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<VideoPlayerScreen> 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
+2 -2
View File
@@ -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.