From 5be7c0c3ff0059cb5e1bba3feb8b1ba463330bf7 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Sat, 6 Dec 2025 16:43:19 +0100 Subject: [PATCH] feat: implement IPTV player screen with FFmpeg transcoding for live streams, playback position tracking, and dedicated settings --- bin/server.dart | 79 ++++++++++++------- .../iptv/providers/settings_provider.dart | 9 +++ lib/features/iptv/screens/player_screen.dart | 3 +- 3 files changed, 63 insertions(+), 28 deletions(-) diff --git a/bin/server.dart b/bin/server.dart index 04baa1a..e710bb2 100644 --- a/bin/server.dart +++ b/bin/server.dart @@ -414,6 +414,7 @@ Handler _createStreamHandler() { final quality = request.url.queryParameters['quality'] ?? 'medium'; final buffer = request.url.queryParameters['buffer'] ?? 'medium'; final timeout = request.url.queryParameters['timeout'] ?? 'medium'; + final mode = request.url.queryParameters['mode'] ?? 'auto'; // direct, transcode, auto // Map quality to bitrate/CRF final (int bitrate, int crf) = switch (quality) { @@ -470,9 +471,11 @@ Handler _createStreamHandler() { '-y', // Overwrite output files '-loglevel', 'warning', // Reduce verbose output '-err_detect', 'ignore_err', // Ignore decoding errors - '-fflags', '+genpts+discardcorrupt', // Generate PTS, discard corrupt frames - '-analyzeduration', isVod ? '10000000' : '2000000', // 10s for VOD, 2s for live - '-probesize', isVod ? '5000000' : '1000000', // 5MB for VOD, 1MB for live + '-fflags', '+genpts+discardcorrupt+nobuffer', // Generate PTS, discard corrupt, reduce buffering + '-flags', 'low_delay', // Low delay mode + '-thread_queue_size', '4096', // Prevent queue overflow on slow CPU + '-analyzeduration', isVod ? '10000000' : '1000000', // 10s for VOD, 1s for live (faster start) + '-probesize', isVod ? '5000000' : '500000', // 5MB for VOD, 500KB for live (faster start) '-user_agent', 'VLC/3.0.18 LibVLC/3.0.18', '-headers', 'Accept: */*\r\nConnection: keep-alive\r\n', ]; @@ -488,46 +491,68 @@ Handler _createStreamHandler() { ]); } - ffmpegArgs.addAll([ - '-i', iptvUrl, - // Video: transcode to H.264 for browser compatibility - '-c:v', 'libx264', - '-preset', 'veryfast', // Better quality than ultrafast, still fast - '-profile:v', 'main', // Main profile for better quality HD - '-level', '4.0', // Higher level for HD content - '-pix_fmt', 'yuv420p', // Required for browser compatibility - '-crf', '$crf', // Quality-based encoding from settings - '-b:v', '${bitrate}k', // Video bitrate from settings - '-maxrate', '${(bitrate * 1.25).round()}k', // Allow bursts - '-bufsize', '${bufferSize}k', // Buffer size from settings - ]); + ffmpegArgs.addAll(['-i', iptvUrl]); + + // Check mode: direct (copy) vs transcode + final usePassthrough = mode == 'direct'; - // Add latency flags only for live streams + if (usePassthrough) { + // PASSTHROUGH MODE: Copy streams without re-encoding (0% CPU) + // Only works if source is H.264/AAC compatible with browsers + ffmpegArgs.addAll([ + '-c:v', 'copy', // Copy video stream as-is + '-c:a', 'copy', // Copy audio stream as-is + '-bsf:v', 'h264_mp4toannexb', // Convert to Annex B format for HLS + ]); + } else { + // TRANSCODE MODE: Re-encode for browser compatibility + ffmpegArgs.addAll([ + // Video: transcode to H.264 for browser compatibility + '-c:v', 'libx264', + '-preset', 'ultrafast', // Fastest encoding, lowest CPU (was: veryfast) + '-tune', 'zerolatency', // Low latency for live streaming + '-profile:v', 'main', // Main profile for better quality HD + '-level', '4.0', // Higher level for HD content + '-pix_fmt', 'yuv420p', // Required for browser compatibility + '-crf', '$crf', // Quality-based encoding from settings + '-b:v', '${bitrate}k', // Video bitrate from settings + '-maxrate', '${(bitrate * 1.5).round()}k', // Allow larger bursts + '-bufsize', '${bufferSize}k', // Buffer size from settings + ]); + } + + // Add timestamp handling for live streams if (!isVod) { ffmpegArgs.addAll([ - '-tune', 'zerolatency', // Low latency for live streaming - '-fps_mode', 'passthrough', // Pass through timestamps as-is (replaces deprecated -vsync) + '-fps_mode', 'passthrough', // Pass through timestamps as-is ]); } else { ffmpegArgs.addAll([ - '-fps_mode', 'cfr', // Constant frame rate for VOD (replaces deprecated -vsync) + '-fps_mode', 'cfr', // Constant frame rate for VOD '-g', '48', // Keyframe every 2 seconds at 24fps '-keyint_min', '48', ]); } + // Audio handling for transcode mode + if (!usePassthrough) { + ffmpegArgs.addAll([ + '-c:a', 'aac', + '-b:a', '128k', + '-ar', '44100', + '-ac', '2', // Force stereo + ]); + } + ffmpegArgs.addAll([ '-avoid_negative_ts', 'make_zero', // Handle negative timestamps - // Audio: transcode to AAC - '-c:a', 'aac', - '-b:a', '128k', - '-ar', '44100', - '-ac', '2', // Force stereo + '-max_muxing_queue_size', '1024', // Prevent queue overflow // HLS output settings - optimized for stability '-f', 'hls', '-hls_time', isVod ? '4' : '$segmentDuration', // Segment duration from settings for live - '-hls_list_size', isVod ? '0' : '10', // Keep all segments for VOD, 10 for live - '-hls_flags', isVod ? 'independent_segments' : 'delete_segments+append_list+omit_endlist+program_date_time', + '-hls_list_size', isVod ? '0' : '20', // Keep all segments for VOD, 20 for live (was 10) + '-hls_flags', isVod ? 'independent_segments' : 'append_list+omit_endlist+program_date_time', // Removed delete_segments to avoid 404s + '-hls_delete_threshold', '5', // Keep 5 extra segments before deleting '-hls_start_number_source', 'datetime', // Better segment continuity '-hls_segment_filename', '${outputDir.path}/segment_%d.ts', outputPath, diff --git a/lib/features/iptv/providers/settings_provider.dart b/lib/features/iptv/providers/settings_provider.dart index c3b4996..83c9443 100644 --- a/lib/features/iptv/providers/settings_provider.dart +++ b/lib/features/iptv/providers/settings_provider.dart @@ -166,6 +166,15 @@ class IptvSettings { } } + /// Get transcoding mode string for FFmpeg URL parameter + String get modeString { + switch (transcodingMode) { + case TranscodingMode.disabled: return 'direct'; // Passthrough, 0% CPU + case TranscodingMode.forced: return 'transcode'; + case TranscodingMode.auto: return 'auto'; + } + } + // ===== Filter Methods ===== static List _parseKeywords(String filter) { diff --git a/lib/features/iptv/screens/player_screen.dart b/lib/features/iptv/screens/player_screen.dart index e8a4937..0fa6551 100644 --- a/lib/features/iptv/screens/player_screen.dart +++ b/lib/features/iptv/screens/player_screen.dart @@ -134,8 +134,9 @@ class _PlayerScreenState extends ConsumerState { ConnectionTimeout.medium => 'medium', ConnectionTimeout.long => 'long', }; + final modeParam = settings.modeString; // direct, transcode, or auto - final streamEndpoint = '$baseUrl/api/stream/${widget.streamId}?url=$encodedUrl&quality=$qualityParam&buffer=$bufferParam&timeout=$timeoutParam'; + final streamEndpoint = '$baseUrl/api/stream/${widget.streamId}?url=$encodedUrl&quality=$qualityParam&buffer=$bufferParam&timeout=$timeoutParam&mode=$modeParam'; debugPrint('PlayerScreen: Starting FFmpeg stream: $streamEndpoint');