From 6e1a2e05687b4b99d170c02065bc1f7106ac1a27 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sat, 1 Nov 2025 09:35:58 +0100 Subject: [PATCH] fix: orientation issues when exiting player --- lib/screens/video_player_screen.dart | 175 +++++++++++++++++++-------- 1 file changed, 126 insertions(+), 49 deletions(-) diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index 6513249f..59c25d71 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart'; import '../models/plex_metadata.dart'; @@ -12,6 +13,7 @@ import '../utils/app_logger.dart'; import '../services/settings_service.dart'; import '../utils/orientation_helper.dart'; import '../utils/video_player_navigation.dart'; +import '../utils/platform_detector.dart'; class VideoPlayerScreen extends StatefulWidget { final PlexMetadata metadata; @@ -34,14 +36,16 @@ class VideoPlayerScreen extends StatefulWidget { } class _VideoPlayerScreenState extends State { - late final Player player; - late final VideoController controller; + Player? player; + VideoController? controller; + bool _isPlayerInitialized = false; Timer? _progressTimer; PlexMetadata? _nextEpisode; PlexMetadata? _previousEpisode; bool _isLoadingNext = false; bool _showPlayNextDialog = false; PlexClientProvider? _cachedClientProvider; + bool _isPhone = false; @override void initState() { @@ -79,8 +83,13 @@ class _VideoPlayerScreenState extends State { _cachedClientProvider = null; } - // Ensure landscape orientation is set even after navigation - _setLandscapeOrientation(); + // Cache device type for safe access in dispose() + try { + _isPhone = PlatformDetector.isPhone(context); + } catch (e) { + appLogger.w('Failed to determine device type', error: e); + _isPhone = false; // Default to tablet/desktop (all orientations) + } } void _setLandscapeOrientation() { @@ -88,37 +97,60 @@ class _VideoPlayerScreenState extends State { } Future _initializePlayer() async { - // Load buffer size from settings - final settingsService = await SettingsService.getInstance(); - final bufferSizeMB = settingsService.getBufferSize(); - final bufferSizeBytes = bufferSizeMB * 1024 * 1024; + try { + // Load buffer size from settings + final settingsService = await SettingsService.getInstance(); + final bufferSizeMB = settingsService.getBufferSize(); + final bufferSizeBytes = bufferSizeMB * 1024 * 1024; - // Create player with configuration - player = Player( - configuration: PlayerConfiguration( - libass: true, - bufferSize: bufferSizeBytes, - ), - ); - controller = VideoController(player); + // Create player with configuration + player = Player( + configuration: PlayerConfiguration( + libass: true, + bufferSize: bufferSizeBytes, + ), + ); + controller = VideoController(player!); - // Get the video URL and start playback - _startPlayback(); + // Notify that player is ready + if (mounted) { + setState(() { + _isPlayerInitialized = true; + }); + } - // Set fullscreen mode and landscape orientation - _setLandscapeOrientation(); + // Get the video URL and start playback + _startPlayback(); - // Listen to playback state changes - player.stream.playing.listen(_onPlayingStateChanged); + // Set fullscreen mode and landscape orientation + if (mounted) { + try { + _setLandscapeOrientation(); + } catch (e) { + appLogger.w('Failed to set landscape orientation', error: e); + // Don't crash if orientation fails - video can still play + } + } - // Listen to completion - player.stream.completed.listen(_onVideoCompleted); + // Listen to playback state changes + player!.stream.playing.listen(_onPlayingStateChanged); - // Start periodic progress updates - _startProgressTracking(); + // Listen to completion + player!.stream.completed.listen(_onVideoCompleted); - // Load next/previous episodes - _loadAdjacentEpisodes(); + // Start periodic progress updates + _startProgressTracking(); + + // Load next/previous episodes + _loadAdjacentEpisodes(); + } catch (e) { + appLogger.e('Failed to initialize player', error: e); + if (mounted) { + setState(() { + _isPlayerInitialized = false; + }); + } + } } Future _loadAdjacentEpisodes() async { @@ -158,11 +190,11 @@ class _VideoPlayerScreenState extends State { if (videoUrl != null) { // Open video without auto-playing - await player.open(Media(videoUrl), play: false); + await player!.open(Media(videoUrl), play: false); // Wait for media to be ready (duration > 0) int attempts = 0; - while (player.state.duration.inMilliseconds == 0 && attempts < 50) { + while (player!.state.duration.inMilliseconds == 0 && attempts < 100) { await Future.delayed(const Duration(milliseconds: 100)); attempts++; } @@ -173,11 +205,11 @@ class _VideoPlayerScreenState extends State { final resumePosition = Duration( milliseconds: widget.metadata.viewOffset!, ); - await player.seek(resumePosition); + await player!.seek(resumePosition); } // Start playback after seeking - await player.play(); + await player!.play(); // Wait for tracks to be loaded, then apply preferred tracks _waitForTracksAndApply(); @@ -207,16 +239,36 @@ class _VideoPlayerScreenState extends State { // Restore system UI and orientation preferences OrientationHelper.restoreSystemUI(); - OrientationHelper.restoreDefaultOrientations(context); - player.dispose(); + // Restore orientation based on cached device type (no context needed) + try { + if (_isPhone) { + // Phone: portrait only + SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + ]); + } else { + // Tablet/Desktop: all orientations + SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ]); + } + } catch (e) { + appLogger.w('Failed to restore orientation in dispose', error: e); + } + + player?.dispose(); super.dispose(); } void _startProgressTracking() { // Send progress update every 10 seconds _progressTimer = Timer.periodic(const Duration(seconds: 10), (timer) { - if (player.state.playing) { + if (player?.state.playing ?? false) { _sendProgress('playing'); } }); @@ -497,7 +549,7 @@ class _VideoPlayerScreenState extends State { appLogger.i( 'Final audio selection: ${trackToSelect.title ?? "Track ${trackToSelect.id}"} (${trackToSelect.language ?? "unknown"})', ); - player.setAudioTrack(trackToSelect); + player!.setAudioTrack(trackToSelect); } else { appLogger.d('No audio tracks available'); } @@ -570,21 +622,21 @@ class _VideoPlayerScreenState extends State { ? 'OFF' : '${subtitleToSelect.title ?? "Track ${subtitleToSelect.id}"} (${subtitleToSelect.language ?? "unknown"})'; appLogger.i('Final subtitle selection: $finalSubtitle'); - player.setSubtitleTrack(subtitleToSelect); + player!.setSubtitleTrack(subtitleToSelect); // Set playback rate if preferred rate was provided if (widget.preferredPlaybackRate != null) { appLogger.d( 'Setting preferred playback rate: ${widget.preferredPlaybackRate}x', ); - player.setRate(widget.preferredPlaybackRate!); + player!.setRate(widget.preferredPlaybackRate!); } appLogger.d('Track selection complete'); } // Check if tracks are already available in current state - final currentTracks = player.state.tracks; + final currentTracks = player!.state.tracks; if (currentTracks.audio.isNotEmpty || currentTracks.subtitle.isNotEmpty) { await processTracks(currentTracks); return; @@ -592,7 +644,7 @@ class _VideoPlayerScreenState extends State { // If not, listen to tracks stream for when they become available bool applied = false; - final subscription = player.stream.tracks.listen((tracks) async { + final subscription = player!.stream.tracks.listen((tracks) async { // Check if tracks are loaded (have at least one track) and not yet applied if (!applied && (tracks.audio.isNotEmpty || tracks.subtitle.isNotEmpty)) { applied = true; @@ -612,8 +664,11 @@ class _VideoPlayerScreenState extends State { } void _sendProgress(String state) { - final position = player.state.position.inMilliseconds; - final duration = player.state.duration.inMilliseconds; + // Don't send misleading data if player isn't available + if (player == null) return; + + final position = player!.state.position.inMilliseconds; + final duration = player!.state.duration.inMilliseconds; if (duration > 0) { final clientProvider = _cachedClientProvider; @@ -659,18 +714,31 @@ class _VideoPlayerScreenState extends State { /// Navigates to a new episode, preserving playback state and track selections Future _navigateToEpisode(PlexMetadata episodeMetadata) async { + // If player isn't available, navigate without preserving settings + if (player == null) { + if (mounted) { + navigateToVideoPlayer( + context, + metadata: episodeMetadata, + userProfile: widget.userProfile, + usePushReplacement: true, + ); + } + return; + } + // Capture current track selection BEFORE pausing - final currentAudioTrack = player.state.track.audio; - final currentSubtitleTrack = player.state.track.subtitle; + final currentAudioTrack = player!.state.track.audio; + final currentSubtitleTrack = player!.state.track.subtitle; // Pause and stop current playback - player.pause(); + player!.pause(); _progressTimer?.cancel(); _sendProgress('stopped'); // Navigate to the episode using pushReplacement to destroy current player if (mounted) { - final currentRate = player.state.rate; + final currentRate = player!.state.rate; navigateToVideoPlayer( context, metadata: episodeMetadata, @@ -685,8 +753,17 @@ class _VideoPlayerScreenState extends State { @override Widget build(BuildContext context) { + // Show loading indicator while player initializes + if (!_isPlayerInitialized || controller == null || player == null) { + return const Scaffold( + backgroundColor: Colors.black, + body: Center(child: CircularProgressIndicator(color: Colors.white)), + ); + } + return PopScope( - canPop: false, // Disable swipe-back gesture to prevent interference with timeline scrubbing + canPop: + false, // Disable swipe-back gesture to prevent interference with timeline scrubbing onPopInvokedWithResult: (didPop, result) { // Allow programmatic back navigation from UI controls if (!didPop) { @@ -700,9 +777,9 @@ class _VideoPlayerScreenState extends State { // Video player Center( child: Video( - controller: controller, + controller: controller!, controls: (state) => plexVideoControlsBuilder( - player, + player!, widget.metadata, onNext: _nextEpisode != null ? _playNext : null, onPrevious: _previousEpisode != null ? _playPrevious : null,