diff --git a/lib/mobile/features/iptv/screens/lite_player_screen.dart b/lib/mobile/features/iptv/screens/lite_player_screen.dart index 3b96872..ec436b4 100644 --- a/lib/mobile/features/iptv/screens/lite_player_screen.dart +++ b/lib/mobile/features/iptv/screens/lite_player_screen.dart @@ -74,6 +74,7 @@ class _LitePlayerScreenState extends ConsumerState // Focus Nodes final FocusNode _playPauseFocusNode = FocusNode(); final FocusNode _backFocusNode = FocusNode(); + final FocusNode _deinterlaceFocusNode = FocusNode(); Timer? _controlsTimer; Timer? _clockTimer; @@ -309,7 +310,7 @@ class _LitePlayerScreenState extends ConsumerState if (widget.streamType == StreamType.live) { // Only switch with arrows if controls are hidden if (key == LogicalKeyboardKey.channelDown || !_showControls) { - _playPrevious(); + _playNext(); _onUserInteraction(); return true; } @@ -322,7 +323,7 @@ class _LitePlayerScreenState extends ConsumerState if (widget.streamType == StreamType.live) { // Only switch with arrows if controls are hidden if (key == LogicalKeyboardKey.channelUp || !_showControls) { - _playNext(); + _playPrevious(); _onUserInteraction(); return true; } @@ -347,6 +348,12 @@ class _LitePlayerScreenState extends ConsumerState return true; } + // Toggle Deinterlace - secret key 'D' or digit 2 + if (key == LogicalKeyboardKey.keyD || key == LogicalKeyboardKey.digit2) { + _togglePerChannelDeinterlace(); + return true; + } + if (key == LogicalKeyboardKey.escape || key == LogicalKeyboardKey.goBack) { if (_showControls) { setState(() => _showControls = false); @@ -388,6 +395,7 @@ class _LitePlayerScreenState extends ConsumerState _xtreamService?.dispose(); _playPauseFocusNode.dispose(); _backFocusNode.dispose(); + _deinterlaceFocusNode.dispose(); SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, @@ -461,6 +469,30 @@ class _LitePlayerScreenState extends ConsumerState } } + void _togglePerChannelDeinterlace() { + final currentStreamId = + widget.channels != null && widget.channels!.isNotEmpty + ? widget.channels![_currentIndex].streamId + : widget.streamId; + + final notifier = ref.read(deinterlaceSettingsProvider.notifier); + notifier.toggle(currentStreamId); + + final newState = notifier.isEnabled(currentStreamId); + + if (mounted) { + setState(() {}); // Refresh UI + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text('Désentrelacement: ${newState ? "ACTIVER" : "DÉSACTIVER"}'), + duration: const Duration(seconds: 2), + backgroundColor: newState ? Colors.green : Colors.grey[800], + ), + ); + } + } + @override Widget build(BuildContext context) { final title = widget.channels != null && widget.channels!.isNotEmpty @@ -532,11 +564,13 @@ class _LitePlayerScreenState extends ConsumerState style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold)), const Spacer(), - const SizedBox(width: 16), - Text( - _currentTime, - style: const TextStyle(color: Colors.white70), - ), + if (ref.watch(mobileSettingsProvider).showClock) ...[ + const SizedBox(width: 16), + Text( + _currentTime, + style: const TextStyle(color: Colors.white70), + ), + ], ], ), ), @@ -639,13 +673,14 @@ class _LitePlayerScreenState extends ConsumerState _epg != null && _epg!.nowPlaying != null) Positioned( - bottom: 80, + bottom: 180, // Moved up to avoid button overlap left: 24, width: 350, child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.black.withOpacity(0.7), + color: Colors.black + .withOpacity(0.85), // Higher opacity for visibility borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.white24), ), @@ -704,6 +739,78 @@ class _LitePlayerScreenState extends ConsumerState ), ), + // Deinterlace Toggle Button (Only for Live) + if (widget.streamType == StreamType.live) + Positioned( + bottom: 20, // Moved down and left to be visible and out of the way + right: 24, + child: TVFocusable( + focusNode: _deinterlaceFocusNode, + onPressed: _togglePerChannelDeinterlace, + onFocus: _resetControlsTimer, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.7), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: ref + .watch(deinterlaceSettingsProvider.notifier) + .isEnabled(widget.channels != null && + widget.channels!.isNotEmpty + ? widget.channels![_currentIndex].streamId + : widget.streamId) + ? AppColors.primary + : Colors.white24, + width: 2, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.waves, + color: ref + .watch(deinterlaceSettingsProvider.notifier) + .isEnabled(widget.channels != null && + widget.channels!.isNotEmpty + ? widget.channels![_currentIndex].streamId + : widget.streamId) + ? AppColors.primary + : Colors.white70, + size: 20, + ), + const SizedBox(width: 8), + const Text( + "Désentrelacer", + style: TextStyle( + color: Colors.white, + fontSize: 13, + fontWeight: FontWeight.bold), + ), + const SizedBox(width: 8), + SizedBox( + height: 24, + child: Switch( + value: ref + .watch(deinterlaceSettingsProvider.notifier) + .isEnabled(widget.channels != null && + widget.channels!.isNotEmpty + ? widget.channels![_currentIndex].streamId + : widget.streamId), + onChanged: (val) => _togglePerChannelDeinterlace(), + activeColor: AppColors.primary, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ), + ], + ), + ), + ), + ), + // Progress Bar (VOD) if (widget.streamType != StreamType.live) Positioned( diff --git a/lib/mobile/features/iptv/screens/native_player_screen.dart b/lib/mobile/features/iptv/screens/native_player_screen.dart index 2034622..7c40f6e 100644 --- a/lib/mobile/features/iptv/screens/native_player_screen.dart +++ b/lib/mobile/features/iptv/screens/native_player_screen.dart @@ -132,16 +132,12 @@ class _NativePlayerScreenState extends ConsumerState // Enable software decoding fallback if hardware fails (handled by mpv usually, but ensuring 'auto' helps) final decoderMode = ref.read(mobileSettingsProvider).decoderMode; - final deinterlace = ref.read(mobileSettingsProvider).deinterlace; (_player.platform as dynamic)?.setProperty('hwdec', decoderMode); - (_player.platform as dynamic) - ?.setProperty('deinterlace', deinterlace ? 'yes' : 'no'); // Performance & Scaling Fixes for Android (_player.platform as dynamic)?.setProperty('opengl-pbo', 'yes'); (_player.platform as dynamic)?.setProperty('video-unscaled', 'no'); - debugPrint( - 'MediaKitPlayer: Decoder Mode set to $decoderMode, Deinterlace: $deinterlace'); + debugPrint('MediaKitPlayer: Decoder Mode set to $decoderMode'); // ============ PERFORMANCE 3.0 (ANTI MICRO-COUPURE PROFILE) ============ // Tuned to eliminate micro-stuttering during live TV playback @@ -207,17 +203,19 @@ class _NativePlayerScreenState extends ConsumerState 'live_start_index=-1,analyzeduration=10000000,probesize=5000000', ); - // Prevent stalls (readahead already set above) + // Optimized for performance and stability on progressive & interlaced streams (_player.platform as dynamic)?.setProperty('hr-seek', 'yes'); (_player.platform as dynamic)?.setProperty('hr-seek-framedrop', 'yes'); + (_player.platform as dynamic)?.setProperty('vd-lavc-fast', 'yes'); (_player.platform as dynamic) - ?.setProperty('vd-lavc-dr', 'yes'); // Direct rendering + ?.setProperty('vd-lavc-threads', '0'); // Auto-detect (_player.platform as dynamic) - ?.setProperty('vd-lavc-fast', 'yes'); // Fast decoding - (_player.platform as dynamic) - ?.setProperty('vd-lavc-threads', '4'); // Multithreading - (_player.platform as dynamic)?.setProperty( - 'vd-lavc-skiploopfilter', 'all'); // Skip loop filter for performance + ?.setProperty('vd-lavc-skiploopfilter', 'nonref'); // Balanced stability + (_player.platform as dynamic)?.setProperty('framedrop', 'vo'); + + if (widget.streamType == StreamType.live) { + (_player.platform as dynamic)?.setProperty('profile', 'low-latency'); + } // ============ AUDIO CODEC SUPPORT FOR VOD ============ // V3: Explicit LAVC Downmix & Audiotrack @@ -670,7 +668,7 @@ class _NativePlayerScreenState extends ConsumerState _reconnectAttempts = 0; // Reset reconnect counter on success }); - // Start watchdog for live streams + // Show watchdog for live streams if (widget.streamType == StreamType.live) { _startLiveWatchdog(); } @@ -1235,7 +1233,7 @@ class _NativePlayerScreenState extends ConsumerState _epg != null && _epg!.nowPlaying != null) Positioned( - bottom: 80, // Above progress/bottom bar + bottom: 180, // Moved up to avoid overlap with central buttons left: 24, width: 350, // Increased width for badge child: Row( @@ -1399,7 +1397,6 @@ class _NativePlayerScreenState extends ConsumerState ), ), ), - // LIVE Badge removed from here ], ), ), diff --git a/lib/mobile/providers/mobile_settings_providers.dart b/lib/mobile/providers/mobile_settings_providers.dart index 870392c..59eeeb6 100644 --- a/lib/mobile/providers/mobile_settings_providers.dart +++ b/lib/mobile/providers/mobile_settings_providers.dart @@ -353,3 +353,38 @@ final mobileFavoritesProvider = StateNotifierProvider>((ref) { return MobileFavoritesNotifier(); }); + +/// Per-channel deinterlace settings (stored locally) +class DeinterlaceSettingsNotifier extends StateNotifier> { + static const String _boxName = 'deinterlace_settings'; + Box? _box; + + DeinterlaceSettingsNotifier() : super({}) { + _init(); + } + + Future _init() async { + _box = await Hive.openBox(_boxName); + final settings = + _box?.get('deinterlace_on', defaultValue: []) as List?; + state = settings?.cast().toSet() ?? {}; + } + + void toggle(String streamId) { + final updated = {...state}; + if (updated.contains(streamId)) { + updated.remove(streamId); + } else { + updated.add(streamId); + } + state = updated; + _box?.put('deinterlace_on', updated.toList()); + } + + bool isEnabled(String streamId) => state.contains(streamId); +} + +final deinterlaceSettingsProvider = + StateNotifierProvider>((ref) { + return DeinterlaceSettingsNotifier(); +});