diff --git a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt index 5d4aa2dc..a1a3cd20 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/exoplayer/ExoPlayerCore.kt @@ -3294,6 +3294,11 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener { check(Looper.myLooper() == Looper.getMainLooper()) Log.d(TAG, "Disposing") + surfaceContainer?.let { container -> + container.visibility = View.INVISIBLE + Log.d(TAG, "Hiding surface container during dispose") + } + stopFrameWatchdog() cancelDecoderHangCheck() stopPositionUpdates() diff --git a/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerCore.kt b/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerCore.kt index 94ef22fa..fad44576 100644 --- a/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerCore.kt +++ b/android/app/src/main/kotlin/com/edde746/plezy/mpv/MpvPlayerCore.kt @@ -813,6 +813,11 @@ class MpvPlayerCore(private val activity: Activity) : SurfaceHolder.Callback { check(Looper.myLooper() == Looper.getMainLooper()) Log.d(TAG, "Disposing") + surfaceContainer?.let { container -> + container.visibility = View.INVISIBLE + Log.d(TAG, "Hiding surface container during dispose") + } + handler.removeCallbacksAndMessages(null) // Clean up frame rate and audio focus. diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index 18b46655..8e94399e 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -1057,6 +1057,32 @@ class VideoPlayerScreenState extends State with WidgetsBindin /// latch, MediaSession pause-suppression window) — see [FrameRateMatcher]. final FrameRateMatcher _frameRate = FrameRateMatcher(); + Future _pauseAndHidePlayerForRouteExit() async { + final currentPlayer = player; + if (currentPlayer == null || !_isPlayerInitialized) return null; + + final exitPosition = currentPlayer.state.position; + if (currentPlayer.state.isActive) { + try { + await currentPlayer.pause(); + } catch (e, st) { + appLogger.w('Failed to pause player during route exit', error: e, stackTrace: st); + } + } + + if (!mounted || currentPlayer != player) return exitPosition; + + if (Platform.isAndroid && PlatformDetector.isTV()) { + try { + await currentPlayer.setVisible(false); + } catch (e, st) { + appLogger.w('Failed to hide Android TV player surface during route exit', error: e, stackTrace: st); + } + } + + return exitPosition; + } + /// Handle back button press /// For non-host participants in Watch Together, shows leave session confirmation Future _handleBackButton() async { @@ -1079,7 +1105,10 @@ class VideoPlayerScreenState extends State with WidgetsBindin final navigator = Navigator.of(context); if (navigator.canPop()) { _isExiting.value = true; - await _sendStoppedProgressOnce(); + final exitPosition = await _pauseAndHidePlayerForRouteExit(); + if (!mounted) return; + await _sendStoppedProgressOnce(positionOverride: exitPosition); + if (!mounted) return; await _restoreSystemUiAndOrientation(); if (!mounted) return; navigator.pop(true); @@ -1094,7 +1123,10 @@ class VideoPlayerScreenState extends State with WidgetsBindin final navigator = Navigator.of(context); if (navigator.canPop()) { _isExiting.value = true; - await _sendStoppedProgressOnce(); + final exitPosition = await _pauseAndHidePlayerForRouteExit(); + if (!mounted) return; + await _sendStoppedProgressOnce(positionOverride: exitPosition); + if (!mounted) return; await _restoreSystemUiAndOrientation(); if (!mounted) return; navigator.pop(true); diff --git a/lib/widgets/side_navigation_rail.dart b/lib/widgets/side_navigation_rail.dart index a39bc4d4..fc8ab9a3 100644 --- a/lib/widgets/side_navigation_rail.dart +++ b/lib/widgets/side_navigation_rail.dart @@ -580,7 +580,6 @@ class SideNavigationRailState extends State with MountedSetS final horizontalPadding = horizontalPaddingForContext(context, isCollapsed: isCollapsed); final itemHorizontalPadding = itemHorizontalPaddingForContext(context, isCollapsed: isCollapsed); final hasLiveTv = context.watch().hasLiveTv; - final surfaceOpacity = PlatformDetector.isTV() ? 0.0 : 1.0; // Listen to fullscreen + groupLibrariesByServer setting so the rail // rebuilds when the user toggles "Group libraries by server" in Appearance. @@ -645,7 +644,7 @@ class SideNavigationRailState extends State with MountedSetS children: [ Positioned.fill( child: AnimatedOpacity( - opacity: surfaceOpacity, + opacity: 1.0, duration: t.normal, curve: Curves.easeOutCubic, child: ColoredBox(color: t.surface), diff --git a/test/widgets/side_navigation_rail_test.dart b/test/widgets/side_navigation_rail_test.dart index 8d15609c..214be6e7 100644 --- a/test/widgets/side_navigation_rail_test.dart +++ b/test/widgets/side_navigation_rail_test.dart @@ -177,7 +177,7 @@ void main() { expect((selectedItemContainer.decoration as BoxDecoration?)?.color, isNull); }); - testWidgets('expanded TV rail keeps its surface transparent', (tester) async { + testWidgets('expanded TV rail draws an opaque surface', (tester) async { TvDetectionService.debugSetAppleTVOverride(true); addTearDown(() => TvDetectionService.debugSetAppleTVOverride(null)); await SettingsService.getInstance(); @@ -227,7 +227,7 @@ void main() { find.descendant(of: find.byType(SideNavigationRail), matching: find.byType(AnimatedOpacity)), ) .singleWhere((widget) => widget.child is ColoredBox); - expect(surfaceOpacity.opacity, 0.0); + expect(surfaceOpacity.opacity, 1.0); }); testWidgets('expanded rail keeps selected background outside sidebar keyboard focus', (tester) async {