import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:video_player/video_player.dart'; import 'package:chewie/chewie.dart'; import '../../../../core/models/playlist_config.dart'; import '../../../../core/models/iptv_models.dart'; import '../../../../core/theme/app_colors.dart'; import '../../../../features/iptv/services/xtream_service_mobile.dart'; /// Stream type enum for player enum StreamType { live, vod, series } /// Native video player screen for Android/iOS /// Uses video_player + chewie for HLS/MP4 playback class NativePlayerScreen extends ConsumerStatefulWidget { final String streamId; final String title; final StreamType streamType; final String containerExtension; final PlaylistConfig playlist; final List? channels; final int initialIndex; const NativePlayerScreen({ super.key, required this.streamId, required this.title, required this.playlist, this.streamType = StreamType.live, this.containerExtension = 'mp4', this.channels, this.initialIndex = 0, }); @override ConsumerState createState() => _NativePlayerScreenState(); } class _NativePlayerScreenState extends ConsumerState { VideoPlayerController? _videoController; ChewieController? _chewieController; bool _isLoading = true; String? _errorMessage; late int _currentIndex; late XtreamServiceMobile _xtreamService; @override void initState() { super.initState(); _currentIndex = widget.initialIndex; _xtreamService = XtreamServiceMobile(); _xtreamService.setPlaylist(widget.playlist); // Lock to landscape for video playback SystemChrome.setPreferredOrientations([ DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight, ]); SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); _initializePlayer(); } Future _initializePlayer() async { try { setState(() { _isLoading = true; _errorMessage = null; }); // Dispose previous controllers if switching channels await _disposeControllers(); // Determine effective Stream ID final currentStreamId = widget.channels != null && widget.channels!.isNotEmpty ? widget.channels![_currentIndex].streamId : widget.streamId; // Build stream URL based on type String streamUrl; if (widget.streamType == StreamType.live) { // Live TV - use HLS format for best mobile compatibility streamUrl = _xtreamService.getLiveStreamUrl(currentStreamId); } else if (widget.streamType == StreamType.vod) { // Movies streamUrl = _xtreamService.getVodStreamUrl(currentStreamId, widget.containerExtension); } else { // Series streamUrl = _xtreamService.getSeriesStreamUrl(currentStreamId, widget.containerExtension); } debugPrint('NativePlayer: Loading stream: $streamUrl'); // Initialize video controller _videoController = VideoPlayerController.networkUrl( Uri.parse(streamUrl), httpHeaders: { 'User-Agent': 'XtremFlow/1.0', }, ); await _videoController!.initialize(); // Initialize chewie controller with custom options _chewieController = ChewieController( videoPlayerController: _videoController!, autoPlay: true, looping: widget.streamType == StreamType.live, // Loop live streams allowFullScreen: true, allowMuting: true, showControls: true, showControlsOnInitialize: false, hideControlsTimer: const Duration(seconds: 3), materialProgressColors: ChewieProgressColors( playedColor: AppColors.primary, handleColor: AppColors.primary, backgroundColor: Colors.white24, bufferedColor: Colors.white38, ), placeholder: Container( color: Colors.black, child: const Center( child: CircularProgressIndicator(color: AppColors.primary), ), ), errorBuilder: (context, errorMessage) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.error_outline, color: Colors.red, size: 48), const SizedBox(height: 16), Text( 'Erreur de lecture', style: const TextStyle(color: Colors.white, fontSize: 18), ), const SizedBox(height: 8), Text( errorMessage, style: const TextStyle(color: Colors.white70, fontSize: 14), textAlign: TextAlign.center, ), ], ), ); }, ); setState(() { _isLoading = false; }); } catch (e) { debugPrint('NativePlayer error: $e'); setState(() { _isLoading = false; _errorMessage = e.toString(); }); } } Future _disposeControllers() async { _chewieController?.dispose(); _chewieController = null; await _videoController?.dispose(); _videoController = null; } void _playNext() { if (widget.channels == null || widget.channels!.isEmpty) return; final nextIndex = (_currentIndex + 1) % widget.channels!.length; _switchChannel(nextIndex); } void _playPrevious() { if (widget.channels == null || widget.channels!.isEmpty) return; final prevIndex = (_currentIndex - 1 + widget.channels!.length) % widget.channels!.length; _switchChannel(prevIndex); } void _switchChannel(int index) { setState(() { _currentIndex = index; }); _initializePlayer(); } @override void dispose() { _disposeControllers(); _xtreamService.dispose(); // Restore normal orientation SystemChrome.setPreferredOrientations([ DeviceOrientation.portraitUp, DeviceOrientation.portraitDown, DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight, ]); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); super.dispose(); } @override Widget build(BuildContext context) { final title = widget.channels != null && widget.channels!.isNotEmpty ? widget.channels![_currentIndex].name : widget.title; return Scaffold( backgroundColor: Colors.black, body: Stack( children: [ // Video Player if (_errorMessage != null) _buildErrorView() else if (_isLoading) _buildLoadingView() else if (_chewieController != null) Center( child: Chewie(controller: _chewieController!), ) else _buildLoadingView(), // Custom top bar for channel switching (live TV only) if (widget.channels != null && widget.channels!.isNotEmpty) Positioned( top: 0, left: 0, right: 0, child: SafeArea( child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.black.withOpacity(0.7), Colors.transparent, ], ), ), child: Row( children: [ IconButton( icon: const Icon(Icons.arrow_back, color: Colors.white), onPressed: () => Navigator.pop(context), ), Expanded( child: Text( title, style: const TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600, ), textAlign: TextAlign.center, maxLines: 1, overflow: TextOverflow.ellipsis, ), ), IconButton( icon: const Icon(Icons.skip_previous, color: Colors.white), onPressed: _playPrevious, ), IconButton( icon: const Icon(Icons.skip_next, color: Colors.white), onPressed: _playNext, ), ], ), ), ), ), // Back button for VOD/Series (no channel switching) if (widget.channels == null || widget.channels!.isEmpty) Positioned( top: 0, left: 0, child: SafeArea( child: IconButton( icon: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.black45, shape: BoxShape.circle, ), child: const Icon(Icons.arrow_back, color: Colors.white), ), onPressed: () => Navigator.pop(context), ), ), ), ], ), ); } Widget _buildLoadingView() { return const Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator(color: AppColors.primary), SizedBox(height: 16), Text( 'Chargement...', style: TextStyle(color: Colors.white70), ), ], ), ); } Widget _buildErrorView() { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.error_outline, size: 64, color: Colors.red), const SizedBox(height: 16), const Text( 'Erreur de lecture', style: TextStyle(color: Colors.white, fontSize: 18), ), const SizedBox(height: 8), Text( _errorMessage ?? 'Une erreur inconnue est survenue', style: const TextStyle(color: Colors.white70), textAlign: TextAlign.center, ), const SizedBox(height: 24), Row( mainAxisSize: MainAxisSize.min, children: [ ElevatedButton.icon( onPressed: () => Navigator.pop(context), icon: const Icon(Icons.arrow_back), label: const Text('Retour'), style: ElevatedButton.styleFrom( backgroundColor: Colors.grey[800], ), ), const SizedBox(width: 16), ElevatedButton.icon( onPressed: _initializePlayer, icon: const Icon(Icons.refresh), label: const Text('Réessayer'), style: ElevatedButton.styleFrom( backgroundColor: AppColors.primary, ), ), ], ), ], ), ); } }