From 880a9c92aa470547bc3bfcf8cac0f4ea9244a5d8 Mon Sep 17 00:00:00 2001 From: Tommy Chow Date: Sat, 8 Aug 2026 11:47:54 -0700 Subject: [PATCH] fix(video): sync android pip state and playback across both players MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three related PiP gaps on Android: `handleAndroidPipChanged` lived on `NativeVideoPlayerInterface`, and `notifyPip` type-checked against it, so the WebView store never learned it was in PiP — `_isInPipMode` stayed false for the whole session and the overlay kept auto-hiding behind the PiP window. Moved to the shared `VideoPlayerInterface` and implemented in both stores. The WebView store's two JS PiP channel bodies collapse into one `_setPipActive` state machine, now shared with the Android callback. `NativeVideoStore` never called `_pip.setIsPlaying()`, so with the `mediaOnlyPause` layout the PiP window's button was stuck on whatever glyph it started with, and `onPipAction` — which toggles blind — could do the opposite of what the button showed. `VideoStore.handleAppResume()` did nothing in video mode, while the native store calls `play()`. Backgrounding can suspend the webview's playback with no way for the page to restart itself, leaving it dead until a manual refresh. Resumes now unless the user was the one who paused. Not fixed: PiP with the WebView player. Twitch's embed refuses to play below 400x300 CSS pixels and a PiP window is far under that, Android resizes the activity before firing onPictureInPictureModeChanged so any correction arrives too late, and the embed latches once tripped. The native player has no such constraint. Refs #563. Co-Authored-By: Claude Opus 5 --- lib/screens/channel/channel.dart | 11 ++++------- .../channel/video/native_video_player_interface.dart | 4 ---- lib/screens/channel/video/native_video_store.dart | 12 ++++++++++++ .../channel/video/video_player_interface.dart | 5 +++++ 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/lib/screens/channel/channel.dart b/lib/screens/channel/channel.dart index c66dae36..c65c0e4b 100644 --- a/lib/screens/channel/channel.dart +++ b/lib/screens/channel/channel.dart @@ -7,7 +7,6 @@ import 'package:frosty/screens/channel/chat/stores/chat_store.dart'; import 'package:frosty/screens/channel/chat/stores/chat_tabs_store.dart'; import 'package:frosty/screens/channel/chat/widgets/chat_tabs.dart'; import 'package:frosty/screens/channel/video/native_video.dart'; -import 'package:frosty/screens/channel/video/native_video_player_interface.dart'; import 'package:frosty/screens/channel/video/native_video_store.dart'; import 'package:frosty/screens/channel/video/stream_info_bar.dart'; import 'package:frosty/screens/channel/video/video.dart'; @@ -772,12 +771,10 @@ class _VideoChatState extends State // If on Android, use PiPSwitcher to enable PiP functionality. if (Platform.isAndroid) { - void notifyPip(bool isInPip) { - final store = _videoStore; - if (store is NativeVideoPlayerInterface) { - store.handleAndroidPipChanged(isInPip); - } - } + // Both players need this — the WebView store used to be skipped by a + // NativeVideoPlayerInterface type check, so it never learned it was in + // PiP on Android. + void notifyPip(bool isInPip) => _videoStore.handleAndroidPipChanged(isInPip); return PipWidget( pipLayout: PipActionsLayout.mediaOnlyPause, diff --git a/lib/screens/channel/video/native_video_player_interface.dart b/lib/screens/channel/video/native_video_player_interface.dart index 766d5a3c..a01e34ec 100644 --- a/lib/screens/channel/video/native_video_player_interface.dart +++ b/lib/screens/channel/video/native_video_player_interface.dart @@ -5,8 +5,4 @@ import 'package:frosty/screens/channel/video/video_player_interface.dart'; abstract class NativeVideoPlayerInterface extends VideoPlayerInterface { /// Whether the current quality is an HLS audio-only variant. bool get isAudioOnlyMode; - - /// Called from platform PiP callbacks on Android. Lets the store sync - /// its overlay state with the OS PiP window. - void handleAndroidPipChanged(bool isInPip); } diff --git a/lib/screens/channel/video/native_video_store.dart b/lib/screens/channel/video/native_video_store.dart index e0b789bc..e32a276c 100644 --- a/lib/screens/channel/video/native_video_store.dart +++ b/lib/screens/channel/video/native_video_store.dart @@ -499,9 +499,19 @@ abstract class NativeVideoStoreBase }); } + /// Keeps the Android PiP window's play/pause remote action in sync with + /// actual playback. Without this the button is stuck on whatever glyph the + /// `mediaOnlyPause` layout started with, and `onPipAction` — which toggles + /// blind — then does the opposite of what the button shows. + void _syncPipPlaybackState({required bool isPlaying}) { + if (!Platform.isAndroid) return; + _pip.setIsPlaying(isPlaying); + } + void _onPlaying() { _loading = false; _paused = false; + _syncPipPlaybackState(isPlaying: true); _isStalled = false; _hasPlayedOnce = true; _initializing = false; @@ -543,6 +553,7 @@ abstract class NativeVideoStoreBase void _onPlayerError(Map? eventData) { _loading = false; _paused = true; + _syncPipPlaybackState(isPlaying: false); _isStalled = false; _initializing = false; _isQualitySwitching = false; @@ -667,6 +678,7 @@ abstract class NativeVideoStoreBase void _onPaused() { _healthyPlaybackTimer?.cancel(); _paused = true; + _syncPipPlaybackState(isPlaying: false); _isStalled = false; _isQualitySwitching = false; _stallRecoveryTimer?.cancel(); diff --git a/lib/screens/channel/video/video_player_interface.dart b/lib/screens/channel/video/video_player_interface.dart index b2507f1f..9838e7ad 100644 --- a/lib/screens/channel/video/video_player_interface.dart +++ b/lib/screens/channel/video/video_player_interface.dart @@ -39,5 +39,10 @@ abstract class VideoPlayerInterface { Future setStreamQuality(String quality); Future updateStreamInfo({bool forceUpdate}); void handleAppResume(); + + /// Called from the platform PiP callbacks on Android so the store can sync + /// its overlay state, and its playback, with the OS PiP window. + void handleAndroidPipChanged(bool isInPip); + void dispose(); }