feat: Add IPTV player screen with FFmpeg transcoding, playback controls, and EPG overlay.

This commit is contained in:
Michael committed 2025-12-07 10:27:15 +01:00
1 parent 25de3f44cf
commit 76279aaba3
1 file changed
+33 -12
+33 -12
View File
@@ -64,7 +64,6 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
void initState() { void initState() {
super.initState(); super.initState();
_currentIndex = widget.initialIndex; _currentIndex = widget.initialIndex;
_viewId = 'iptv-player-${widget.streamId}-${DateTime.now().millisecondsSinceEpoch}';
_initializePlayer(); _initializePlayer();
_setupMessageListener(); _setupMessageListener();
@@ -260,12 +259,16 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
debugPrint('PlayerScreen: Player URL: $playerUrl'); debugPrint('PlayerScreen: Player URL: $playerUrl');
// Generate a new view ID for every load to force iframe recreation
_viewId = 'iptv-player-${widget.streamId}-${DateTime.now().millisecondsSinceEpoch}';
// Register platform view factory // Register platform view factory
// ignore: undefined_prefixed_name // ignore: undefined_prefixed_name
ui_web.platformViewRegistry.registerViewFactory( ui_web.platformViewRegistry.registerViewFactory(
_viewId, _viewId,
(int viewId) { (int viewId) {
final iframe = html.IFrameElement() final iframe = html.IFrameElement()
..id = _viewId // CRITICAL: Set ID for getElementById lookup
..src = playerUrl ..src = playerUrl
..style.border = 'none' ..style.border = 'none'
..style.width = '100%' ..style.width = '100%'
@@ -293,18 +296,28 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
void _showSettingsDialog() { void _showSettingsDialog() {
showDialog( showDialog(
context: context, context: context,
useRootNavigator: true, // Ensure it's on top
builder: (context) => PointerInterceptor( builder: (context) => PointerInterceptor(
child: Theme(
data: Theme.of(context).copyWith(dialogBackgroundColor: const Color(0xFF1C1C1E)),
child: AlertDialog( child: AlertDialog(
backgroundColor: const Color(0xFF1E1E1E), backgroundColor: const Color(0xFF1C1C1E).withOpacity(0.95),
title: const Text('Réglages du Lecteur', style: TextStyle(color: Colors.white)), shape: RoundedRectangleBorder(
content: Column( borderRadius: BorderRadius.circular(16),
side: BorderSide(color: Colors.white.withOpacity(0.1)),
),
title: const Text('Réglages', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600)),
content: SizedBox(
width: 400,
child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text('Format d\'image', style: TextStyle(color: Colors.white70, fontSize: 13, fontWeight: FontWeight.bold)), const Text('Format d\'image', style: TextStyle(color: Colors.white54, fontSize: 13, fontWeight: FontWeight.bold)),
const SizedBox(height: 8), const SizedBox(height: 12),
Wrap( Wrap(
spacing: 8, spacing: 8,
runSpacing: 8,
children: [ children: [
_buildAspectRatioChip('Original', 'contain'), _buildAspectRatioChip('Original', 'contain'),
_buildAspectRatioChip('Remplir', 'cover'), _buildAspectRatioChip('Remplir', 'cover'),
@@ -313,20 +326,25 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
), ),
if (_audioTracks.isNotEmpty) ...[ if (_audioTracks.isNotEmpty) ...[
const SizedBox(height: 24), const SizedBox(height: 24),
const Text('Pistes Audio', style: TextStyle(color: Colors.white70, fontSize: 13, fontWeight: FontWeight.bold)), const Text('Pistes Audio', style: TextStyle(color: Colors.white54, fontSize: 13, fontWeight: FontWeight.bold)),
const SizedBox(height: 8), const SizedBox(height: 12),
Container( Container(
constraints: const BoxConstraints(maxHeight: 200), constraints: const BoxConstraints(maxHeight: 200),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.05),
borderRadius: BorderRadius.circular(8),
),
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
children: _audioTracks.map((track) { children: _audioTracks.map((track) {
return ListTile( return ListTile(
title: Text(track['label'] ?? 'Piste ${track['id']}', style: const TextStyle(color: Colors.white)), title: Text(track['label'] ?? 'Piste ${track['id']}', style: const TextStyle(color: Colors.white, fontSize: 14)),
subtitle: Text(track['lang'] ?? '', style: const TextStyle(color: Colors.white54)), subtitle: Text(track['lang'] ?? '', style: const TextStyle(color: Colors.white54, fontSize: 12)),
onTap: () => _setAudioTrack(track['id']), onTap: () => _setAudioTrack(track['id']),
dense: true, dense: true,
contentPadding: EdgeInsets.zero, leading: const Icon(Icons.audiotrack, size: 20, color: Colors.white54),
leading: const Icon(Icons.audiotrack, color: Colors.white54), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
hoverColor: Colors.white.withOpacity(0.1),
); );
}).toList(), }).toList(),
), ),
@@ -335,14 +353,17 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
], ],
], ],
), ),
),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
style: TextButton.styleFrom(foregroundColor: Colors.white),
child: const Text('Fermer'), child: const Text('Fermer'),
), ),
], ],
), ),
), ),
),
); );
} }