feat: Introduce LitePlayerScreen for IPTV playback, including stream type definitions and mobile settings providers.

This commit is contained in:
Michael committed 2025-12-20 22:53:14 +01:00
1 parent b60efd6acb
commit af95742321
3 files changed
+163 -24

No files matched your search

@@ -74,6 +74,7 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
// Focus Nodes // Focus Nodes
final FocusNode _playPauseFocusNode = FocusNode(); final FocusNode _playPauseFocusNode = FocusNode();
final FocusNode _backFocusNode = FocusNode(); final FocusNode _backFocusNode = FocusNode();
final FocusNode _deinterlaceFocusNode = FocusNode();
Timer? _controlsTimer; Timer? _controlsTimer;
Timer? _clockTimer; Timer? _clockTimer;
@@ -309,7 +310,7 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
if (widget.streamType == StreamType.live) { if (widget.streamType == StreamType.live) {
// Only switch with arrows if controls are hidden // Only switch with arrows if controls are hidden
if (key == LogicalKeyboardKey.channelDown || !_showControls) { if (key == LogicalKeyboardKey.channelDown || !_showControls) {
_playPrevious(); _playNext();
_onUserInteraction(); _onUserInteraction();
return true; return true;
} }
@@ -322,7 +323,7 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
if (widget.streamType == StreamType.live) { if (widget.streamType == StreamType.live) {
// Only switch with arrows if controls are hidden // Only switch with arrows if controls are hidden
if (key == LogicalKeyboardKey.channelUp || !_showControls) { if (key == LogicalKeyboardKey.channelUp || !_showControls) {
_playNext(); _playPrevious();
_onUserInteraction(); _onUserInteraction();
return true; return true;
} }
@@ -347,6 +348,12 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
return true; 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 (key == LogicalKeyboardKey.escape || key == LogicalKeyboardKey.goBack) {
if (_showControls) { if (_showControls) {
setState(() => _showControls = false); setState(() => _showControls = false);
@@ -388,6 +395,7 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
_xtreamService?.dispose(); _xtreamService?.dispose();
_playPauseFocusNode.dispose(); _playPauseFocusNode.dispose();
_backFocusNode.dispose(); _backFocusNode.dispose();
_deinterlaceFocusNode.dispose();
SystemChrome.setPreferredOrientations([ SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp, DeviceOrientation.portraitUp,
@@ -461,6 +469,30 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
} }
} }
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final title = widget.channels != null && widget.channels!.isNotEmpty final title = widget.channels != null && widget.channels!.isNotEmpty
@@ -532,11 +564,13 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
style: const TextStyle( style: const TextStyle(
color: Colors.white, fontWeight: FontWeight.bold)), color: Colors.white, fontWeight: FontWeight.bold)),
const Spacer(), const Spacer(),
const SizedBox(width: 16), if (ref.watch(mobileSettingsProvider).showClock) ...[
Text( const SizedBox(width: 16),
_currentTime, Text(
style: const TextStyle(color: Colors.white70), _currentTime,
), style: const TextStyle(color: Colors.white70),
),
],
], ],
), ),
), ),
@@ -639,13 +673,14 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
_epg != null && _epg != null &&
_epg!.nowPlaying != null) _epg!.nowPlaying != null)
Positioned( Positioned(
bottom: 80, bottom: 180, // Moved up to avoid button overlap
left: 24, left: 24,
width: 350, width: 350,
child: Container( child: Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.black.withOpacity(0.7), color: Colors.black
.withOpacity(0.85), // Higher opacity for visibility
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.white24), border: Border.all(color: Colors.white24),
), ),
@@ -704,6 +739,78 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
), ),
), ),
// 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) // Progress Bar (VOD)
if (widget.streamType != StreamType.live) if (widget.streamType != StreamType.live)
Positioned( Positioned(
@@ -132,16 +132,12 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
// Enable software decoding fallback if hardware fails (handled by mpv usually, but ensuring 'auto' helps) // Enable software decoding fallback if hardware fails (handled by mpv usually, but ensuring 'auto' helps)
final decoderMode = ref.read(mobileSettingsProvider).decoderMode; final decoderMode = ref.read(mobileSettingsProvider).decoderMode;
final deinterlace = ref.read(mobileSettingsProvider).deinterlace;
(_player.platform as dynamic)?.setProperty('hwdec', decoderMode); (_player.platform as dynamic)?.setProperty('hwdec', decoderMode);
(_player.platform as dynamic)
?.setProperty('deinterlace', deinterlace ? 'yes' : 'no');
// Performance & Scaling Fixes for Android // Performance & Scaling Fixes for Android
(_player.platform as dynamic)?.setProperty('opengl-pbo', 'yes'); (_player.platform as dynamic)?.setProperty('opengl-pbo', 'yes');
(_player.platform as dynamic)?.setProperty('video-unscaled', 'no'); (_player.platform as dynamic)?.setProperty('video-unscaled', 'no');
debugPrint( debugPrint('MediaKitPlayer: Decoder Mode set to $decoderMode');
'MediaKitPlayer: Decoder Mode set to $decoderMode, Deinterlace: $deinterlace');
// ============ PERFORMANCE 3.0 (ANTI MICRO-COUPURE PROFILE) ============ // ============ PERFORMANCE 3.0 (ANTI MICRO-COUPURE PROFILE) ============
// Tuned to eliminate micro-stuttering during live TV playback // Tuned to eliminate micro-stuttering during live TV playback
@@ -207,17 +203,19 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
'live_start_index=-1,analyzeduration=10000000,probesize=5000000', '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', 'yes');
(_player.platform as dynamic)?.setProperty('hr-seek-framedrop', 'yes'); (_player.platform as dynamic)?.setProperty('hr-seek-framedrop', 'yes');
(_player.platform as dynamic)?.setProperty('vd-lavc-fast', 'yes');
(_player.platform as dynamic) (_player.platform as dynamic)
?.setProperty('vd-lavc-dr', 'yes'); // Direct rendering ?.setProperty('vd-lavc-threads', '0'); // Auto-detect
(_player.platform as dynamic) (_player.platform as dynamic)
?.setProperty('vd-lavc-fast', 'yes'); // Fast decoding ?.setProperty('vd-lavc-skiploopfilter', 'nonref'); // Balanced stability
(_player.platform as dynamic) (_player.platform as dynamic)?.setProperty('framedrop', 'vo');
?.setProperty('vd-lavc-threads', '4'); // Multithreading
(_player.platform as dynamic)?.setProperty( if (widget.streamType == StreamType.live) {
'vd-lavc-skiploopfilter', 'all'); // Skip loop filter for performance (_player.platform as dynamic)?.setProperty('profile', 'low-latency');
}
// ============ AUDIO CODEC SUPPORT FOR VOD ============ // ============ AUDIO CODEC SUPPORT FOR VOD ============
// V3: Explicit LAVC Downmix & Audiotrack // V3: Explicit LAVC Downmix & Audiotrack
@@ -670,7 +668,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
_reconnectAttempts = 0; // Reset reconnect counter on success _reconnectAttempts = 0; // Reset reconnect counter on success
}); });
// Start watchdog for live streams // Show watchdog for live streams
if (widget.streamType == StreamType.live) { if (widget.streamType == StreamType.live) {
_startLiveWatchdog(); _startLiveWatchdog();
} }
@@ -1235,7 +1233,7 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
_epg != null && _epg != null &&
_epg!.nowPlaying != null) _epg!.nowPlaying != null)
Positioned( Positioned(
bottom: 80, // Above progress/bottom bar bottom: 180, // Moved up to avoid overlap with central buttons
left: 24, left: 24,
width: 350, // Increased width for badge width: 350, // Increased width for badge
child: Row( child: Row(
@@ -1399,7 +1397,6 @@ class _NativePlayerScreenState extends ConsumerState<NativePlayerScreen>
), ),
), ),
), ),
// LIVE Badge removed from here
], ],
), ),
), ),
@@ -353,3 +353,38 @@ final mobileFavoritesProvider =
StateNotifierProvider<MobileFavoritesNotifier, Set<String>>((ref) { StateNotifierProvider<MobileFavoritesNotifier, Set<String>>((ref) {
return MobileFavoritesNotifier(); return MobileFavoritesNotifier();
}); });
/// Per-channel deinterlace settings (stored locally)
class DeinterlaceSettingsNotifier extends StateNotifier<Set<String>> {
static const String _boxName = 'deinterlace_settings';
Box? _box;
DeinterlaceSettingsNotifier() : super({}) {
_init();
}
Future<void> _init() async {
_box = await Hive.openBox(_boxName);
final settings =
_box?.get('deinterlace_on', defaultValue: <String>[]) as List?;
state = settings?.cast<String>().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<DeinterlaceSettingsNotifier, Set<String>>((ref) {
return DeinterlaceSettingsNotifier();
});