diff --git a/Dockerfile b/Dockerfile index 96aa345..322bdce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,7 +19,7 @@ RUN flutter config --enable-web # Copy dependency files first for better caching # Note: pubspec.lock will be generated by flutter pub get if missing # Cache buster - INCREMENT THIS VALUE TO FORCE REBUILD ON DEPLOY -ARG CACHEBUST=2025-12-13-v6 +ARG CACHEBUST=2025-12-13-v7 COPY pubspec.yaml ./ # Get dependencies (generates pubspec.lock automatically) diff --git a/lib/features/iptv/screens/player_screen.dart b/lib/features/iptv/screens/player_screen.dart index d252913..09f9592 100644 --- a/lib/features/iptv/screens/player_screen.dart +++ b/lib/features/iptv/screens/player_screen.dart @@ -62,6 +62,7 @@ class _PlayerScreenState extends ConsumerState { String? _currentStreamUrl; String _statusMessage = 'Loading...'; String? _errorMessage; + bool _isMuted = false; @override void initState() { @@ -115,8 +116,9 @@ class _PlayerScreenState extends ConsumerState { final encodedUrl = Uri.encodeComponent(streamUrl); final playerSettings = ref.read(iptvSettingsProvider); + final streamTypeParam = widget.streamType == StreamType.live ? 'live' : 'vod'; var playerSrc = playerSettings.playerType == PlayerType.lite - ? 'player_lite.html?url=$encodedUrl' + ? 'player_lite.html?url=$encodedUrl&type=$streamTypeParam' : 'player.html?url=$encodedUrl'; if (startTimeOverride != null) { @@ -140,6 +142,13 @@ class _PlayerScreenState extends ConsumerState { _isInitialized = true; _isLoading = false; }); + + // Force unmute on load (after a short delay to ensure iframe is ready) + Future.delayed(const Duration(milliseconds: 500), () { + if (mounted) { + _sendMessage({'type': 'set_volume', 'value': 1.0}); + } + }); } } catch (e) { if (mounted) { @@ -218,6 +227,11 @@ class _PlayerScreenState extends ConsumerState { } } + void _toggleMute() { + setState(() => _isMuted = !_isMuted); + _sendMessage({'type': 'set_volume', 'value': _isMuted ? 0.0 : 1.0}); + } + void _previousChannel() { if (widget.channels != null && widget.channels!.isNotEmpty) { setState(() { @@ -298,10 +312,29 @@ class _PlayerScreenState extends ConsumerState { ), ), ), + const SizedBox(width: 12), + // Mute/Unmute Button + Material( + color: Colors.black54, + shape: const CircleBorder(), + child: InkWell( + onTap: _toggleMute, + customBorder: const CircleBorder(), + child: Padding( + padding: const EdgeInsets.all(12), + child: Icon( + _isMuted ? Icons.volume_off_rounded : Icons.volume_up_rounded, + color: Colors.white, + ), + ), + ), + ), const SizedBox(width: 16), Expanded( child: Text( - widget.title, + widget.channels != null + ? widget.channels![_currentIndex].name + : widget.title, style: const TextStyle( color: Colors.white, fontSize: 18, diff --git a/web/player_lite.html b/web/player_lite.html index 87acdf6..de43fd7 100644 --- a/web/player_lite.html +++ b/web/player_lite.html @@ -108,6 +108,7 @@ const urlParams = new URLSearchParams(window.location.search); let streamUrl = urlParams.get('url'); const startTime = parseFloat(urlParams.get('t') || '0'); + const streamType = urlParams.get('type') || ''; // 'live' or 'vod' function log(msg) { console.log('[LitePlayer] ' + msg); @@ -289,33 +290,60 @@ showLoading('Loading video...'); video.preload = 'auto'; + let playStarted = false; video.addEventListener('waiting', () => { log('Buffering...'); showLoading('Buffering...'); }); - video.addEventListener('playing', hideLoading); - - video.addEventListener('canplay', () => { + video.addEventListener('playing', () => { + playStarted = true; hideLoading(); - video.play().catch(e => log('Autoplay prevented: ' + e)); - }, { once: true }); - - video.addEventListener('error', () => { - const err = video.error; - let msg = 'Playback error'; - if (err) { - switch (err.code) { - case MediaError.MEDIA_ERR_NETWORK: msg = 'Network error'; break; - case MediaError.MEDIA_ERR_DECODE: msg = 'Video decode error'; break; - case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED: msg = 'Format not supported'; break; - } - } - showError(msg); }); + const onCanPlay = () => { + playStarted = true; + hideLoading(); + // FORCE AUDIO SETTINGS + video.muted = false; + video.volume = 1.0; + + video.play().catch(e => { + log('Autoplay prevented: ' + e); + // If autoplay prevents sound/video, we should show a UI to user but native controls are there + // Try playing muted if failed + video.muted = true; + video.play().then(() => { + log('Autoplay started muted'); + showError('Click to unmute'); // User can verify context + }).catch(e2 => log('Autoplay failed completely: ' + e2)); + }); + }; + + const onError = async () => { + if (!playStarted) { + log('Direct playback failed, trying fallbacks...'); + video.removeEventListener('canplay', onCanPlay); + video.removeEventListener('error', onError); + + // Fallback 1: HLS (common for streams) + log('Trying HLS fallback...'); + if (await playWithHLS(url)) return; + + // Fallback 2: MpegTS (for TS streams) + log('Comparing HLS failed, trying MpegTS fallback...'); + if (await playWithMpegTS(url)) return; + + showError('Format not supported. Try Standard player mode.'); + } + }; + + video.addEventListener('canplay', onCanPlay, { once: true }); + video.addEventListener('error', onError, { once: true }); + video.src = url; + video.load(); return true; } @@ -326,20 +354,35 @@ } log('Stream URL: ' + streamUrl); + log('Stream type: ' + streamType); const lowerUrl = streamUrl.toLowerCase(); - // Route to appropriate player - if (lowerUrl.includes('.m3u8') || lowerUrl.includes('/hls/')) { - log('Detected HLS stream'); - await playWithHLS(streamUrl); - } else if (lowerUrl.includes('.ts') || lowerUrl.includes('/live/') || lowerUrl.includes('mpegts')) { - log('Detected MPEG-TS (via extension or URL)'); - log('Using mpegts.js'); - await playWithMpegTS(streamUrl); - } else { - // Try direct playback (mp4, etc.) - log('Trying direct playback'); + // Use type parameter if available, otherwise detect from URL + if (streamType === 'vod') { + // VOD content - use direct HTML5 playback + log('VOD mode: Using direct playback'); await playDirect(streamUrl); + } else if (streamType === 'live') { + // Live TV - check for HLS first, then MPEG-TS + if (lowerUrl.includes('.m3u8') || lowerUrl.includes('/hls/')) { + log('Live HLS detected'); + await playWithHLS(streamUrl); + } else { + log('Live MPEG-TS mode'); + await playWithMpegTS(streamUrl); + } + } else { + // Fallback: detect from URL pattern + if (lowerUrl.includes('.m3u8') || lowerUrl.includes('/hls/')) { + await playWithHLS(streamUrl); + } else if (lowerUrl.includes('.mp4') || lowerUrl.includes('.mkv') || + lowerUrl.includes('/movie/') || lowerUrl.includes('/series/')) { + await playDirect(streamUrl); + } else if (lowerUrl.includes('.ts') || lowerUrl.includes('/live/')) { + await playWithMpegTS(streamUrl); + } else { + await playDirect(streamUrl); + } } }