diff --git a/Dockerfile b/Dockerfile index 71aa518..5bee59e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ ENV DART_VM_OPTIONS="--old_gen_heap_size=4096" RUN flutter config --enable-web # Force cache invalidation when source changes (update this value to force rebuild) -ARG CACHEBUST=2026-03-09-epg-v1 +ARG CACHEBUST=2026-03-09-mobile-v1 # Copy dependency files first for better caching COPY pubspec.yaml ./ diff --git a/lib/features/iptv/screens/player_screen.dart b/lib/features/iptv/screens/player_screen.dart index bc997c6..65541d9 100644 --- a/lib/features/iptv/screens/player_screen.dart +++ b/lib/features/iptv/screens/player_screen.dart @@ -25,7 +25,7 @@ class PlayerScreen extends ConsumerStatefulWidget { final PlaylistConfig playlist; final StreamType streamType; final String containerExtension; - final List? channels; + final List? channels; final double? startTime; final Duration? duration; @@ -57,6 +57,7 @@ class _PlayerScreenState extends ConsumerState { StreamSubscription? _messageSubscription; final String _aspectRatio = 'contain'; bool _isSeeking = false; + late final String _viewIdPrefix = DateTime.now().millisecondsSinceEpoch.toString(); String _viewId = 'iptv-player'; String? _currentStreamUrl; String _statusMessage = 'Loading...'; @@ -68,8 +69,13 @@ class _PlayerScreenState extends ConsumerState { void initState() { super.initState(); if (widget.channels != null) { - _currentIndex = - widget.channels!.indexWhere((c) => c.streamId == widget.streamId); + _currentIndex = widget.channels!.indexWhere((c) { + try { + return (c.streamId ?? c.id ?? '').toString() == widget.streamId; + } catch (_) { + return false; + } + }); if (_currentIndex == -1) _currentIndex = 0; } // Show controls at start, then auto-hide after timeout @@ -97,12 +103,16 @@ class _PlayerScreenState extends ConsumerState { try { // Determine Stream ID (Initial or Current Index) - final currentStreamId = isChannelSwitch && widget.channels != null - ? widget.channels![_currentIndex].streamId - : widget.streamId; + String currentStreamId = widget.streamId; + if (isChannelSwitch && widget.channels != null) { + try { + final item = widget.channels![_currentIndex]; + currentStreamId = (item.streamId ?? item.id ?? '').toString(); + } catch (_) {} + } - _viewId = - 'iptv-player-$currentStreamId-${DateTime.now().millisecondsSinceEpoch}'; + // Stable view ID for the instance to prevent iframe reload on rebuild (orientation change) + _viewId = 'iptv-player-$currentStreamId-$_viewIdPrefix'; final service = ref.read(xtreamServiceProvider(widget.playlist)); String streamUrl = ''; @@ -274,10 +284,19 @@ class _PlayerScreenState extends ConsumerState { } void _toggleFullscreen() { - if (html.document.fullscreenElement != null) { - html.document.exitFullscreen(); - } else { - html.document.documentElement?.requestFullscreen(); + try { + if (html.document.fullscreenElement != null) { + html.document.exitFullscreen(); + } else { + final element = html.document.documentElement; + if (element != null) { + element.requestFullscreen(); + } + } + } catch (e) { + print('[PlayerScreen] Fullscreen error: $e'); + // Fallback: Message the player to try internal fullscreen + _sendMessage({'type': 'request_fullscreen'}); } } @@ -390,9 +409,13 @@ class _PlayerScreenState extends ConsumerState { children: [ Text( widget.channels != null - ? widget - .channels![_currentIndex] - .name + ? (widget.channels![_currentIndex] + .name ?? + widget.channels![ + _currentIndex] + .title ?? + 'Unknown') + .toString() : widget.title, style: GoogleFonts.outfit( color: Colors.white, diff --git a/lib/features/iptv/widgets/lite_player_view.dart b/lib/features/iptv/widgets/lite_player_view.dart deleted file mode 100644 index 2ebed49..0000000 --- a/lib/features/iptv/widgets/lite_player_view.dart +++ /dev/null @@ -1,238 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:video_player/video_player.dart'; -import 'package:chewie/chewie.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'dart:async'; -import '../../../core/theme/app_colors.dart'; -import '../../../core/widgets/glass_container.dart'; - -class LitePlayerView extends ConsumerStatefulWidget { - final String streamUrl; - final bool isLive; - final VoidCallback? onNext; - final VoidCallback? onPrevious; - - const LitePlayerView({ - super.key, - required this.streamUrl, - required this.isLive, - this.onNext, - this.onPrevious, - }); - - @override - ConsumerState createState() => _LitePlayerViewState(); -} - -class _LitePlayerViewState extends ConsumerState { - late VideoPlayerController _videoController; - ChewieController? _chewieController; - bool _error = false; - - @override - void initState() { - super.initState(); - _initializePlayer(); - } - - @override - void didUpdateWidget(LitePlayerView oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.streamUrl != widget.streamUrl) { - _disposeControllers(); - _initializePlayer(); - } - } - - Future _initializePlayer() async { - try { - setState(() => _error = false); - _videoController = - VideoPlayerController.networkUrl(Uri.parse(widget.streamUrl)); - await _videoController.initialize(); - - _chewieController = ChewieController( - videoPlayerController: _videoController, - autoPlay: true, - looping: false, - isLive: widget.isLive, - aspectRatio: _videoController.value.aspectRatio > 0 - ? _videoController.value.aspectRatio - : 16 / 9, - materialProgressColors: ChewieProgressColors( - playedColor: AppColors.primary, - handleColor: Colors.white, - backgroundColor: Colors.white24, - bufferedColor: Colors.white54, - ), - - // Fullscreen configuration for Web/Mobile - allowFullScreen: true, - allowMuting: true, - showControls: true, - useRootNavigator: true, // Crucial for escaping layout constraints - - // Orientation settings are often ignored or cause issues on web - // but we keep them for native mobile if needed. - // On web, the browser handles this. - deviceOrientationsOnEnterFullScreen: [ - DeviceOrientation.landscapeLeft, - DeviceOrientation.landscapeRight, - ], - deviceOrientationsAfterFullScreen: [ - DeviceOrientation.portraitUp, - ], - - // Custom Controls to include Zapping - additionalOptions: (context) { - return [ - if (widget.onNext != null) - OptionItem( - onTap: (context) { - widget.onNext!(); - Navigator.of(context).pop(); // Close options menu - }, - iconData: Icons.skip_next, - title: 'Next Channel', - ), - if (widget.onPrevious != null) - OptionItem( - onTap: (context) { - widget.onPrevious!(); - Navigator.of(context).pop(); // Close options menu - }, - iconData: Icons.skip_previous, - title: 'Previous Channel', - ), - ]; - }, - ); - - _chewieController!.addListener(() { - if (mounted) setState(() {}); - }); - - setState(() {}); - } catch (e) { - debugPrint('Error initializing Lite Player: $e'); - setState(() => _error = true); - } - } - - void _disposeControllers() { - _videoController.dispose(); - _chewieController?.dispose(); - _chewieController = null; - } - - @override - void dispose() { - _disposeControllers(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - if (_error) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.error_outline, color: Colors.red, size: 48), - const SizedBox(height: 16), - const Text('Error loading video', - style: TextStyle(color: Colors.white)), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () { - _disposeControllers(); - _initializePlayer(); - }, - child: const Text('Retry'), - ), - ], - ), - ); - } - - if (_chewieController != null && - _chewieController!.videoPlayerController.value.isInitialized) { - return Stack( - children: [ - Chewie(controller: _chewieController!), - - // Right-Side Overlays (Zapping + Fullscreen) - Positioned( - right: 16, - top: 0, - bottom: 0, - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Fullscreen Toggle - IconButton( - onPressed: () { - // For Web, ensure we use the built-in fullscreen transition - // which is more compatible with Safari/Chrome security - if (_chewieController?.isFullScreen == true) { - _chewieController?.exitFullScreen(); - } else { - // On web, sometimes we need to toggle more than once - // to ensure the browser captures the intent - _chewieController?.enterFullScreen(); - } - }, - icon: Icon( - _chewieController?.isFullScreen == true - ? Icons.fullscreen_exit - : Icons.fullscreen, - color: Colors.white, - size: 32), - style: IconButton.styleFrom( - backgroundColor: Colors.black87, - padding: const EdgeInsets.all(12), - ), - ), - const SizedBox(height: 24), - - // Zapping Controls - if (widget.onPrevious != null || widget.onNext != null) - GlassContainer( - borderRadius: 16, - opacity: 0.3, - padding: const EdgeInsets.symmetric(vertical: 8), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (widget.onPrevious != null) - IconButton( - onPressed: widget.onPrevious, - icon: const Icon(Icons.keyboard_arrow_up, - color: Colors.white, size: 36), - tooltip: 'Previous Channel', - ), - const SizedBox(height: 8), - if (widget.onNext != null) - IconButton( - onPressed: widget.onNext, - icon: const Icon(Icons.keyboard_arrow_down, - color: Colors.white, size: 36), - tooltip: 'Next Channel', - ), - ], - ), - ), - ], - ), - ), - ), - ], - ); - } - - return const Center( - child: CircularProgressIndicator(color: AppColors.primary)); - } -} diff --git a/lib/mobile/features/iptv/screens/mobile_player_screen.dart b/lib/mobile/features/iptv/screens/mobile_player_screen.dart index e02209e..8e402f2 100644 --- a/lib/mobile/features/iptv/screens/mobile_player_screen.dart +++ b/lib/mobile/features/iptv/screens/mobile_player_screen.dart @@ -3,8 +3,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../features/iptv/screens/player_screen.dart'; export '../../../../features/iptv/screens/player_screen.dart' show StreamType; import '../../../../core/models/playlist_config.dart'; -import '../../../../features/iptv/widgets/lite_player_view.dart'; -import '../../../../features/iptv/providers/xtream_provider.dart'; class MobilePlayerScreen extends ConsumerWidget { final String streamId; @@ -12,7 +10,8 @@ class MobilePlayerScreen extends ConsumerWidget { final StreamType streamType; final String containerExtension; final PlaylistConfig playlist; - final List? channels; // New: List of channels for zapping + final List? channels; + final double? startTime; const MobilePlayerScreen({ super.key, @@ -22,100 +21,22 @@ class MobilePlayerScreen extends ConsumerWidget { this.streamType = StreamType.live, this.containerExtension = 'mp4', this.channels, + this.startTime, }); @override Widget build(BuildContext context, WidgetRef ref) { - final service = ref.read(xtreamServiceProvider(playlist)); - String streamUrl = ''; - - if (streamType == StreamType.live) { - streamUrl = service.getLiveStreamUrl(streamId); - } else if (streamType == StreamType.vod) { - streamUrl = service.getVodStreamUrl(streamId, containerExtension); - } else if (streamType == StreamType.series) { - streamUrl = service.getSeriesStreamUrl(streamId, containerExtension); - } - - // Zapping logic - VoidCallback? onNext; - VoidCallback? onPrevious; - - if (channels != null && channels!.isNotEmpty) { - final currentIndex = channels!.indexWhere((c) { - try { - // Supports Channel/Movie (.streamId) and Episode (.id) - final cId = (c.streamId ?? c.id).toString(); - return cId == streamId; - } catch (_) { - return false; - } - }); - - if (currentIndex != -1) { - void navigateTo(int index) { - final item = channels![index % channels!.length]; - String nId = ''; - String nTitle = ''; - String nExt = containerExtension; - - try { - // ID Extraction - nId = (item.streamId ?? item.id).toString(); - // Title Extraction (name for channels/movies, title for episodes) - nTitle = item.name ?? item.title ?? 'Unknown'; - - // Format series title predictably - if (streamType == StreamType.series && title.contains(' - ')) { - final prefix = title.split(' - ').first; - nTitle = '$prefix - Episode ${item.episodeNum ?? ""}'; - } - - nExt = item.containerExtension ?? containerExtension; - } catch (_) {} - - if (nId.isEmpty) return; - - Navigator.pushReplacement( - context, - MaterialPageRoute( - builder: (context) => MobilePlayerScreen( - streamId: nId, - title: nTitle, - playlist: playlist, - streamType: streamType, - containerExtension: nExt, - channels: channels, - ), - ), - ); - } - - onNext = () => navigateTo(currentIndex + 1); - onPrevious = () => navigateTo(currentIndex - 1 + channels!.length); - } - } - - return Scaffold( - backgroundColor: Colors.black, - appBar: AppBar( - backgroundColor: Colors.transparent, - elevation: 0, - title: Text(title, - style: const TextStyle(color: Colors.white, fontSize: 16)), - leading: IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.white), - onPressed: () => Navigator.pop(context), - ), - ), - body: SizedBox.expand( - child: LitePlayerView( - streamUrl: streamUrl, - isLive: streamType == StreamType.live, - onNext: onNext, - onPrevious: onPrevious, - ), - ), + // Use the premium PlayerScreen which handles IFrames and custom overlays + // This removes the redundant AppBar and fixes full-screen/controls issues + return PlayerScreen( + key: ValueKey('player-$streamId'), + streamId: streamId, + title: title, + playlist: playlist, + streamType: streamType, + containerExtension: containerExtension, + channels: channels, + startTime: startTime, ); } } diff --git a/web/player.html b/web/player.html index 6652827..efdddc7 100644 --- a/web/player.html +++ b/web/player.html @@ -432,6 +432,15 @@ video.muted = vol === 0; } break; + case 'request_fullscreen': + if (video.requestFullscreen) { + video.requestFullscreen(); + } else if (video.webkitRequestFullscreen) { + video.webkitRequestFullscreen(); + } else if (video.msRequestFullscreen) { + video.msRequestFullscreen(); + } + break; } }); diff --git a/web/player_lite.html b/web/player_lite.html index bfb8838..2b55c09 100644 --- a/web/player_lite.html +++ b/web/player_lite.html @@ -499,6 +499,14 @@ if (isFinite(video.duration)) { video.currentTime = parseFloat(data.value); } + } else if (data.type === 'request_fullscreen') { + if (video.requestFullscreen) { + video.requestFullscreen(); + } else if (video.webkitRequestFullscreen) { + video.webkitRequestFullscreen(); + } else if (video.msRequestFullscreen) { + video.msRequestFullscreen(); + } } });