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
final FocusNode _playPauseFocusNode = FocusNode();
final FocusNode _backFocusNode = FocusNode();
final FocusNode _deinterlaceFocusNode = FocusNode();
Timer? _controlsTimer;
Timer? _clockTimer;
@@ -309,7 +310,7 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
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<LitePlayerScreen>
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<LitePlayerScreen>
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<LitePlayerScreen>
_xtreamService?.dispose();
_playPauseFocusNode.dispose();
_backFocusNode.dispose();
_deinterlaceFocusNode.dispose();
SystemChrome.setPreferredOrientations([
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
Widget build(BuildContext context) {
final title = widget.channels != null && widget.channels!.isNotEmpty
@@ -532,11 +564,13 @@ class _LitePlayerScreenState extends ConsumerState<LitePlayerScreen>
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<LitePlayerScreen>
_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<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)
if (widget.streamType != StreamType.live)
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)
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<NativePlayerScreen>
'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<NativePlayerScreen>
_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<NativePlayerScreen>
_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<NativePlayerScreen>
),
),
),
// LIVE Badge removed from here
],
),
),
@@ -353,3 +353,38 @@ final mobileFavoritesProvider =
StateNotifierProvider<MobileFavoritesNotifier, Set<String>>((ref) {
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();
});