mirror of
https://github.com/R0m1k3/xtremobile.git
synced 2026-10-24 15:48:14 +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
|
// 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();
|
||||||
|
});
|
||||||
Reference in new issue
Block a user