mirror of
https://github.com/R0m1k3/xtremobile.git
synced 2026-10-11 17:29:26 +02:00
feat: Introduce LitePlayerScreen for IPTV playback, including stream type definitions and mobile settings providers.
This commit is contained in:
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();
|
||||
});
|
||||
Reference in new issue
Block a user