From b193a2352494acf4d4d49495adbf648ef3cb52d1 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Wed, 25 Feb 2026 08:16:38 +0100 Subject: [PATCH] fix: video stuck on still frame after long pause close #536 --- lib/mpv/player/player_native.dart | 3 +-- lib/screens/video_player_screen.dart | 4 ++++ lib/services/playback_progress_tracker.dart | 16 ++++++++++++++++ linux/runner/mpv/mpv_plugin.cc | 5 +++++ 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/lib/mpv/player/player_native.dart b/lib/mpv/player/player_native.dart index 2ff10169..494a0038 100644 --- a/lib/mpv/player/player_native.dart +++ b/lib/mpv/player/player_native.dart @@ -282,8 +282,7 @@ class PlayerNative extends PlayerBase { Future updateFrame() async { checkDisposed(); if (!initialized) return; - // Only iOS and macOS use Metal layer that needs frame updates - if (Platform.isIOS || Platform.isMacOS) { + if (Platform.isIOS || Platform.isMacOS || Platform.isLinux) { await methodChannel.invokeMethod('updateFrame'); } } diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index 5fe69e32..efe4b534 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -1698,6 +1698,10 @@ class VideoPlayerScreenState extends State with WidgetsBindin // Toggle wakelock based on playback state if (isPlaying) { WakelockPlus.enable(); + // Force a texture refresh on resume to unstick stale frames + // (Linux/macOS texture registrars can miss frame-available + // notifications after extended pause periods) + player?.updateFrame(); } else { WakelockPlus.disable(); } diff --git a/lib/services/playback_progress_tracker.dart b/lib/services/playback_progress_tracker.dart index 2428f308..24597798 100644 --- a/lib/services/playback_progress_tracker.dart +++ b/lib/services/playback_progress_tracker.dart @@ -43,6 +43,9 @@ class PlaybackProgressTracker { /// Timer ticks to skip before retrying after failures (exponential backoff). int _ticksToSkip = 0; + /// Counts timer ticks while paused to send periodic "paused" heartbeats. + int _pausedTickCounter = 0; + PlaybackProgressTracker({ required this.client, required this.metadata, @@ -70,6 +73,7 @@ class PlaybackProgressTracker { _progressTimer = Timer.periodic(updateInterval, (timer) { if (player.state.playing) { + _pausedTickCounter = 0; // Skip ticks when backing off after consecutive failures to avoid // flooding the network with doomed requests during an outage. if (_ticksToSkip > 0) { @@ -77,6 +81,18 @@ class PlaybackProgressTracker { return; } _sendProgress('playing'); + } else { + // Send periodic "paused" updates to keep the Plex session alive + // (~60s with default 10s interval) + _pausedTickCounter++; + if (_pausedTickCounter >= 6) { + _pausedTickCounter = 0; + if (_ticksToSkip > 0) { + _ticksToSkip--; + return; + } + _sendProgress('paused'); + } } }); diff --git a/linux/runner/mpv/mpv_plugin.cc b/linux/runner/mpv/mpv_plugin.cc index cb152d01..c2028f6d 100644 --- a/linux/runner/mpv/mpv_plugin.cc +++ b/linux/runner/mpv/mpv_plugin.cc @@ -324,6 +324,11 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel, response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } + } else if (strcmp(method, "updateFrame") == 0) { + if (self->visible && self->texture) { + mpv_texture_mark_frame_available(self->texture); + } + response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } else if (strcmp(method, "isInitialized") == 0) { gboolean initialized = self->player && self->initialized; response = FL_METHOD_RESPONSE(