From 66a020416e6f7c0472dc99c6e88f8edd97dd603e Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Sun, 14 Dec 2025 18:47:00 +0100 Subject: [PATCH] feat: implement IPTV player screen for live, VOD, and series streams with integrated controls and state management. --- bin/api/hls_handler.dart | 52 +++++++------ lib/features/iptv/screens/player_screen.dart | 20 +++-- .../iptv/services/xtream_service.dart | 14 ++-- .../iptv/widgets/streaming_settings_tab.dart | 23 +----- web/player_lite.html | 74 +++++++------------ 5 files changed, 79 insertions(+), 104 deletions(-) diff --git a/bin/api/hls_handler.dart b/bin/api/hls_handler.dart index 322834f..4d62dc6 100644 --- a/bin/api/hls_handler.dart +++ b/bin/api/hls_handler.dart @@ -147,28 +147,34 @@ Handler createStreamInitHandler() { '-hls_segment_filename', '${tempDir.path}/segment_%03d.ts', ]; - // Detect if this is likely a Live TV stream (TS format) vs VOD (MKV/MP4) + // Video encoding: Always transcode for HLS timestamp compatibility + // Using ultrafast preset for Live TV to minimize latency final isLiveStream = iptvUrl.endsWith('.ts') || iptvUrl.contains('/live/'); - // Video encoding strategy: - // - Live TV (.ts): Try to COPY video stream for maximum quality (no re-encoding loss) - // - VOD (MKV/MP4): Transcode to ensure timestamp compatibility if (isLiveStream) { - // LIVE TV: Copy video stream for best quality - // Most IPTV live streams are already H.264 and browser-compatible - ffmpegArgs.addAll([ - '-c:v', 'copy', // No re-encoding = best quality - ]); - } else { - // VOD: Transcode to fix timestamp issues in MKV containers + // LIVE TV: Fast transcoding with ultrafast preset ffmpegArgs.addAll([ '-c:v', 'libx264', - '-preset', 'fast', // Better quality than ultrafast - '-tune', 'film', // Better for movies/series - '-profile:v', 'high', // High profile for better compression - '-level', '4.1', // Supports 1080p60 + '-preset', 'ultrafast', + '-tune', 'zerolatency', + '-profile:v', 'main', // Main profile for better quality than baseline + '-level', '4.0', '-pix_fmt', 'yuv420p', - '-g', '48', // 2s GOP + '-g', '48', // 2s GOP at 24fps + '-b:v', '4000k', // 4Mbps for good quality live + '-maxrate', '4500k', + '-bufsize', '8000k', + ]); + } else { + // VOD: Higher quality transcoding + ffmpegArgs.addAll([ + '-c:v', 'libx264', + '-preset', 'fast', + '-tune', 'film', + '-profile:v', 'high', + '-level', '4.1', + '-pix_fmt', 'yuv420p', + '-g', '48', ]); if (quality == 'low') { @@ -179,26 +185,24 @@ Handler createStreamInitHandler() { '-bufsize', '3000k', ]); } else { - // High quality (default) - 6Mbps for 1080p ffmpegArgs.addAll([ '-b:v', '6000k', '-maxrate', '8000k', '-bufsize', '12000k', - '-crf', '18', // High quality CRF + '-crf', '18', ]); } } - // Audio: Always transcode to AAC (browsers don't support AC3/DTS) - // Higher bitrate for better audio quality + // Audio: Always transcode to AAC ffmpegArgs.addAll([ '-c:a', 'aac', - '-b:a', '192k', // Higher audio bitrate - '-ar', '48000', // Standard sample rate + '-b:a', '192k', + '-ar', '48000', '-ac', '2', - - playlistPath, ]); + + ffmpegArgs.add(playlistPath); // Start FFmpeg process final process = await Process.start('ffmpeg', ffmpegArgs); diff --git a/lib/features/iptv/screens/player_screen.dart b/lib/features/iptv/screens/player_screen.dart index 6e5732a..23a8287 100644 --- a/lib/features/iptv/screens/player_screen.dart +++ b/lib/features/iptv/screens/player_screen.dart @@ -120,9 +120,14 @@ class _PlayerScreenState extends ConsumerState { final encodedUrl = Uri.encodeComponent(streamUrl); final streamTypeParam = widget.streamType == StreamType.live ? 'live' : 'vod'; - var playerSrc = playerSettings.playerType == PlayerType.lite - ? 'player_lite.html?url=$encodedUrl&type=$streamTypeParam' - : 'player.html?url=$encodedUrl'; + + // Force player choice based on stream type: + // - Live TV: Player Lite (simple TS playback with mpegts.js) + // - VOD/Series: Player Standard (fuller controls, HLS support) + final isLiveTV = widget.streamType == StreamType.live; + var playerSrc = isLiveTV + ? 'player_lite.html?url=$encodedUrl&type=live' + : 'player.html?url=$encodedUrl&type=vod'; if (startTimeOverride != null) { playerSrc += '&t=$startTimeOverride'; @@ -271,10 +276,13 @@ class _PlayerScreenState extends ConsumerState { @override Widget build(BuildContext context) { - final settings = ref.watch(iptvSettingsProvider); + // Use stream type to determine player UI: + // - Live TV: Lite player UI (minimal overlay) + // - VOD/Series: Standard player UI (full controls) + final isLiveTV = widget.streamType == StreamType.live; - // LITE PLAYER MODE: Simple iframe with native HTML5 controls, minimal Flutter overlay - if (settings.playerType == PlayerType.lite) { + // LITE PLAYER MODE for Live TV: Simple iframe with native HTML5 controls, minimal Flutter overlay + if (isLiveTV) { return Scaffold( backgroundColor: Colors.black, body: MouseRegion( diff --git a/lib/features/iptv/services/xtream_service.dart b/lib/features/iptv/services/xtream_service.dart index cab804d..503d964 100644 --- a/lib/features/iptv/services/xtream_service.dart +++ b/lib/features/iptv/services/xtream_service.dart @@ -57,20 +57,18 @@ class XtreamService { _currentPlaylist = playlist; } - /// Generate stream URL for live TV via FFmpeg transcoding + /// Generate stream URL for live TV /// - /// Returns the start URL for FFmpeg transcoding endpoint. - /// The endpoint will start FFmpeg and return the local HLS URL. + /// Returns the proxy URL for direct MPEG-TS streaming. + /// Player will use mpegts.js for playback (no server-side transcoding). String getLiveStreamUrl(String streamId) { if (_currentPlaylist == null) throw Exception('No playlist configured'); - // Use .ts format for direct MPEG-TS streaming (FFmpeg handles this better) + // Direct .ts URL for MPEG-TS streaming final iptvUrl = '${_currentPlaylist!.dns}/live/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.ts'; - // Use FFmpeg transcoding endpoint - final baseUrl = html.window.location.origin; - final encodedUrl = Uri.encodeComponent(iptvUrl); - return '$baseUrl/api/stream/$streamId?url=$encodedUrl'; + // Use proxy for direct streaming (no FFmpeg transcoding) + return _wrapWithProxy(iptvUrl); } /// Generate stream URL for VOD (movies) diff --git a/lib/features/iptv/widgets/streaming_settings_tab.dart b/lib/features/iptv/widgets/streaming_settings_tab.dart index 968c66e..8b2c53c 100644 --- a/lib/features/iptv/widgets/streaming_settings_tab.dart +++ b/lib/features/iptv/widgets/streaming_settings_tab.dart @@ -34,26 +34,9 @@ class StreamingSettingsTab extends ConsumerWidget { ), const SizedBox(height: 24), - // Player Type Section - _buildSectionCard( - context: context, - title: 'Lecteur Vidéo', - icon: Icons.play_circle_outline, - children: [ - _buildDropdownTile( - title: 'Type de lecteur', - subtitle: 'Changer si vous rencontrez des problèmes de lecture', - value: settings.playerType, - items: PlayerType.values, - onChanged: (v) => notifier.setPlayerType(v!), - labelBuilder: (v) => switch (v) { - PlayerType.standard => 'Standard (Avancé)', - PlayerType.lite => 'Lite (Compatibilité max)', - }, - ), - ], - ), - const SizedBox(height: 16), + // Note: Player type is now automatically determined: + // - Live TV uses Player Lite (MPEG-TS) + // - Movies/Series use Player Standard // Quality Section _buildSectionCard( diff --git a/web/player_lite.html b/web/player_lite.html index adca23a..9e9280f 100644 --- a/web/player_lite.html +++ b/web/player_lite.html @@ -361,59 +361,41 @@ log('Stream type: ' + streamType); const lowerUrl = streamUrl.toLowerCase(); - // SPECIAL: /api/stream/ endpoint handling - ALWAYS returns JSON with HLS playlist - if (streamUrl.includes('/api/stream/')) { - // Both Live TV and VOD use /api/stream/ which returns JSON with HLS playlist URL - log((streamType === 'live' ? 'Live TV' : 'VOD') + ' via /api/stream/ - fetching HLS...'); - showLoading(streamType === 'live' ? 'Connecting to live stream...' : 'Initializing stream...'); + // LIVE TV: Simple MPEG-TS playback with mpegts.js + if (streamType === 'live') { + log('Live TV mode: Using MPEG-TS direct playback'); + showLoading('Connecting to live stream...'); - try { - const response = await fetch(streamUrl); - const data = await response.json(); - - if (data.playlist) { - const playlistUrl = window.location.origin + data.playlist; - log('Got HLS playlist: ' + playlistUrl); - showLoading('Loading HLS stream...'); - await playWithHLS(playlistUrl); - return; - } else { - showError('Invalid stream response'); - return; - } - } catch (err) { - log('Error fetching playlist: ' + err); - showError('Failed to initialize stream'); - return; - } - } - - // 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 + // Check for HLS first (.m3u8) if (lowerUrl.includes('.m3u8') || lowerUrl.includes('/hls/')) { log('Live HLS detected'); await playWithHLS(streamUrl); } else { - log('Live MPEG-TS mode'); - await playWithMpegTS(streamUrl); + // Default: MPEG-TS with mpegts.js + log('Using mpegts.js for MPEG-TS'); + await playWithMpegTS(streamUrl, true); // isLive=true } + return; + } + + // VOD: Direct HTML5 playback with proxy + if (streamType === 'vod') { + log('VOD mode: Using direct playback'); + showLoading('Loading video...'); + await playDirect(streamUrl); + return; + } + + // 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, true); } 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); - } + await playDirect(streamUrl); } }