From 76279aaba3ac15fa778cb6fcec03c0e2eec0bfba Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Sun, 7 Dec 2025 10:27:15 +0100 Subject: [PATCH] feat: Add IPTV player screen with FFmpeg transcoding, playback controls, and EPG overlay. --- lib/features/iptv/screens/player_screen.dart | 107 +++++++++++-------- 1 file changed, 64 insertions(+), 43 deletions(-) diff --git a/lib/features/iptv/screens/player_screen.dart b/lib/features/iptv/screens/player_screen.dart index d870fdf..6cf8193 100644 --- a/lib/features/iptv/screens/player_screen.dart +++ b/lib/features/iptv/screens/player_screen.dart @@ -64,7 +64,6 @@ class _PlayerScreenState extends ConsumerState { void initState() { super.initState(); _currentIndex = widget.initialIndex; - _viewId = 'iptv-player-${widget.streamId}-${DateTime.now().millisecondsSinceEpoch}'; _initializePlayer(); _setupMessageListener(); @@ -260,12 +259,16 @@ class _PlayerScreenState extends ConsumerState { 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 // ignore: undefined_prefixed_name ui_web.platformViewRegistry.registerViewFactory( _viewId, (int viewId) { final iframe = html.IFrameElement() + ..id = _viewId // CRITICAL: Set ID for getElementById lookup ..src = playerUrl ..style.border = 'none' ..style.width = '100%' @@ -293,54 +296,72 @@ class _PlayerScreenState extends ConsumerState { void _showSettingsDialog() { showDialog( context: context, + useRootNavigator: true, // Ensure it's on top builder: (context) => PointerInterceptor( - child: AlertDialog( - backgroundColor: const Color(0xFF1E1E1E), - title: const Text('Réglages du Lecteur', style: TextStyle(color: Colors.white)), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('Format d\'image', style: TextStyle(color: Colors.white70, fontSize: 13, fontWeight: FontWeight.bold)), - const SizedBox(height: 8), - Wrap( - spacing: 8, + child: Theme( + data: Theme.of(context).copyWith(dialogBackgroundColor: const Color(0xFF1C1C1E)), + child: AlertDialog( + backgroundColor: const Color(0xFF1C1C1E).withOpacity(0.95), + shape: RoundedRectangleBorder( + 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, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildAspectRatioChip('Original', 'contain'), - _buildAspectRatioChip('Remplir', 'cover'), - _buildAspectRatioChip('Étirer', 'fill'), + const Text('Format d\'image', style: TextStyle(color: Colors.white54, fontSize: 13, fontWeight: FontWeight.bold)), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + _buildAspectRatioChip('Original', 'contain'), + _buildAspectRatioChip('Remplir', 'cover'), + _buildAspectRatioChip('Étirer', 'fill'), + ], + ), + if (_audioTracks.isNotEmpty) ...[ + const SizedBox(height: 24), + const Text('Pistes Audio', style: TextStyle(color: Colors.white54, fontSize: 13, fontWeight: FontWeight.bold)), + const SizedBox(height: 12), + Container( + constraints: const BoxConstraints(maxHeight: 200), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.05), + borderRadius: BorderRadius.circular(8), + ), + child: SingleChildScrollView( + child: Column( + children: _audioTracks.map((track) { + return ListTile( + 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, fontSize: 12)), + onTap: () => _setAudioTrack(track['id']), + dense: true, + leading: const Icon(Icons.audiotrack, size: 20, color: Colors.white54), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + hoverColor: Colors.white.withOpacity(0.1), + ); + }).toList(), + ), + ), + ), + ], ], ), - if (_audioTracks.isNotEmpty) ...[ - const SizedBox(height: 24), - const Text('Pistes Audio', style: TextStyle(color: Colors.white70, fontSize: 13, fontWeight: FontWeight.bold)), - const SizedBox(height: 8), - Container( - constraints: const BoxConstraints(maxHeight: 200), - child: SingleChildScrollView( - child: Column( - children: _audioTracks.map((track) { - return ListTile( - title: Text(track['label'] ?? 'Piste ${track['id']}', style: const TextStyle(color: Colors.white)), - subtitle: Text(track['lang'] ?? '', style: const TextStyle(color: Colors.white54)), - onTap: () => _setAudioTrack(track['id']), - dense: true, - contentPadding: EdgeInsets.zero, - leading: const Icon(Icons.audiotrack, color: Colors.white54), - ); - }).toList(), - ), - ), - ), - ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + style: TextButton.styleFrom(foregroundColor: Colors.white), + child: const Text('Fermer'), + ), ], ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Fermer'), - ), - ], ), ), );