mirror of
https://github.com/R0m1k3/xtremobile.git
synced 2026-10-23 15:18:15 +02:00
Fix: Movie infinite scroll + Player controls auto-hide
- Added in-memory caching for movies/series pagination in XtreamServiceMobile - Fixed player controls appearing then instantly disappearing on touch - Updated refresh methods to clear cache on pull-to-refresh - Added releases/ to gitignore (APKs too large for GitHub)
This commit is contained in:
1 parent
3888a21990
commit
ccda497236
8 files changed
+330
-83
No files matched your search
@@ -55,10 +55,28 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen> {
|
||||
Duration _position = Duration.zero;
|
||||
Duration _duration = Duration.zero;
|
||||
bool _isSeeking = false;
|
||||
bool _useSoftwareDecoder = false;
|
||||
|
||||
Timer? _clockTimer;
|
||||
Timer? _controlsTimer; // Auto-hide timer
|
||||
String _currentTime = '';
|
||||
|
||||
void _resetControlsTimer() {
|
||||
_controlsTimer?.cancel();
|
||||
if (_showControls && _isPlaying) {
|
||||
_controlsTimer = Timer(const Duration(seconds: 4), () {
|
||||
if (mounted) {
|
||||
setState(() => _showControls = false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _onUserInteraction() {
|
||||
setState(() => _showControls = true);
|
||||
_resetControlsTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -79,12 +97,40 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen> {
|
||||
// Enable software decoding fallback if hardware fails (handled by mpv usually, but ensuring 'auto' helps)
|
||||
(_player.platform as dynamic)?.setProperty('hwdec', 'auto');
|
||||
|
||||
// Reverted to Basic Configuration (Stock MPV) for maximum stability
|
||||
// The previous advanced caching was causing streams to cut off after ~30s.
|
||||
// We only enable basic cache and let MPV handle the rest automatically.
|
||||
|
||||
// Reliable Network Configuration (Fix for 30s cutoff)
|
||||
// - Increase timeout significantly
|
||||
// - Allow large cache duration (not just bytes)
|
||||
// - Disable SSL verification to prevent handshake drops
|
||||
// - Enable reconnect
|
||||
|
||||
// Reliable Network Configuration (Fix for 30s cutoff)
|
||||
(_player.platform as dynamic)?.setProperty('cache', 'yes');
|
||||
(_player.platform as dynamic)?.setProperty('cache-secs', '120'); // Cache up to 2 minutes
|
||||
(_player.platform as dynamic)?.setProperty('demuxer-max-bytes', '50000000'); // 50MB max buffer
|
||||
(_player.platform as dynamic)?.setProperty('network-timeout', '120'); // 2 minutes timeout
|
||||
(_player.platform as dynamic)?.setProperty('tls-verify', 'no'); // Ignore SSL errors
|
||||
(_player.platform as dynamic)?.setProperty('http-header-fields', 'User-Agent: XtremFlow/1.0');
|
||||
|
||||
_controller = VideoController(_player);
|
||||
|
||||
// Listen to player state
|
||||
_player.stream.playing.listen((playing) {
|
||||
if (mounted) {
|
||||
setState(() => _isPlaying = playing);
|
||||
if (playing) {
|
||||
// Smooth Loading: Hide loading screen 1s AFTER playback actually starts
|
||||
// This masks initial glitches/re-buffering
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
if (mounted && _isPlaying) {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
});
|
||||
_resetControlsTimer(); // Start auto-hide timer
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -100,13 +146,31 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen> {
|
||||
}
|
||||
});
|
||||
|
||||
_player.stream.error.listen((error) {
|
||||
if (mounted && error.isNotEmpty) {
|
||||
setState(() {
|
||||
_errorMessage = error;
|
||||
_isLoading = false;
|
||||
});
|
||||
_player.stream.error.listen((error) async {
|
||||
if (!mounted) return;
|
||||
|
||||
// Filter out non-errors or non-fatal messages if necessary
|
||||
if (error.isEmpty) return;
|
||||
|
||||
debugPrint('MediaKitPlayer Error Stream: $error');
|
||||
|
||||
// Smart Retry for Codec/Decoder errors
|
||||
// If we haven't tried SW decoding yet, retry with it enabled.
|
||||
if (!_useSoftwareDecoder) {
|
||||
debugPrint('MediaKitPlayer: Error encountered. Auto-retrying with Software Decoding...');
|
||||
setState(() => _useSoftwareDecoder = true);
|
||||
|
||||
// Short delay to allow player cleanup
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
if (mounted) _loadStream(); // Retry connection
|
||||
return;
|
||||
}
|
||||
|
||||
// If SW decoding was already on or failed too, show error
|
||||
setState(() {
|
||||
_errorMessage = error;
|
||||
_isLoading = false;
|
||||
});
|
||||
});
|
||||
|
||||
// Lock to landscape for video playback
|
||||
@@ -137,6 +201,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen> {
|
||||
@override
|
||||
void dispose() {
|
||||
_clockTimer?.cancel();
|
||||
_controlsTimer?.cancel();
|
||||
_player.dispose();
|
||||
_xtreamService?.dispose();
|
||||
|
||||
@@ -211,6 +276,10 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen> {
|
||||
|
||||
debugPrint('MediaKitPlayer: Loading stream: $streamUrl');
|
||||
|
||||
// Enable software decoding fallback if hardware fails (handled by mpv usually, but ensuring 'auto' helps)
|
||||
// If we are in fallback mode, force software ('no')
|
||||
(_player.platform as dynamic)?.setProperty('hwdec', _useSoftwareDecoder ? 'no' : 'auto');
|
||||
|
||||
await _player.open(
|
||||
Media(streamUrl, httpHeaders: {'User-Agent': 'XtremFlow/1.0'}),
|
||||
play: true,
|
||||
@@ -218,9 +287,24 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen> {
|
||||
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
// Reset fallback flag on success? No, keep it if it works to avoid future crashes on same stream
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint('MediaKitPlayer error: $e');
|
||||
|
||||
// Automatic Retry with Software Decoding if it fails the first time
|
||||
if (!_useSoftwareDecoder) {
|
||||
debugPrint('MediaKitPlayer: Retrying with Software Decoding forced...');
|
||||
setState(() {
|
||||
_useSoftwareDecoder = true;
|
||||
// _isLoading remains true
|
||||
});
|
||||
// Delay slightly to let player reset state if needed
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
_loadStream(); // Recursive retry
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = e.toString();
|
||||
@@ -264,12 +348,15 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen> {
|
||||
} else {
|
||||
_player.play();
|
||||
}
|
||||
_onUserInteraction();
|
||||
}
|
||||
|
||||
void _toggleControls() {
|
||||
setState(() {
|
||||
_showControls = !_showControls;
|
||||
});
|
||||
// Always show controls on tap, then start auto-hide timer
|
||||
if (!_showControls) {
|
||||
setState(() => _showControls = true);
|
||||
}
|
||||
_resetControlsTimer();
|
||||
}
|
||||
|
||||
|
||||
@@ -286,14 +373,15 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen> {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: _toggleControls,
|
||||
onPanDown: (_) => _onUserInteraction(),
|
||||
onPanUpdate: (_) => _onUserInteraction(),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Video Player
|
||||
if (_errorMessage != null)
|
||||
_buildErrorView()
|
||||
else if (_isLoading)
|
||||
_buildLoadingView()
|
||||
else
|
||||
Center(
|
||||
child: Video(
|
||||
@@ -302,6 +390,14 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen> {
|
||||
),
|
||||
),
|
||||
|
||||
// Loading Overlay (Stays on top until smooth playback starts)
|
||||
// We moved _buildLoadingView out of the if/else chain above to overlay it
|
||||
if (_isLoading && _errorMessage == null)
|
||||
Container(
|
||||
color: Colors.black,
|
||||
child: _buildLoadingView(),
|
||||
),
|
||||
|
||||
// Custom controls overlay
|
||||
if (_showControls && _errorMessage == null && !_isLoading)
|
||||
_buildControlsOverlay(title),
|
||||
@@ -637,7 +733,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen> {
|
||||
label: const Text('Réessayer'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
foregroundColor: Colors.black,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
Reference in new issue
Block a user