From c57273be2c22f2fa7980aa5e60a6249f8e50e3d6 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Mon, 9 Mar 2026 14:10:13 +0100 Subject: [PATCH] feat: Implement GPU-accelerated live HLS streaming with FFmpeg, including iOS optimizations and IPTV redirect resolution. --- bin/api/streaming_handler.dart | 48 +++++++++++-------- bin/server.dart | 16 ++++--- .../iptv/services/xtream_service.dart | 4 +- task.md | 1 + 4 files changed, 40 insertions(+), 29 deletions(-) diff --git a/bin/api/streaming_handler.dart b/bin/api/streaming_handler.dart index 327dcd7..71ba35b 100644 --- a/bin/api/streaming_handler.dart +++ b/bin/api/streaming_handler.dart @@ -153,29 +153,22 @@ Handler createLiveStreamHandler( '-i', targetUrl, // Video if (useNvidiaGpu) ...[ - '-c:v', - 'h264_nvenc', - '-preset', - 'p1', - '-tune', - 'll', - '-b:v', - '3500k', - '-maxrate', - '4000k', - '-bufsize', - '7000k', + '-c:v', 'h264_nvenc', '-preset', 'p1', '-tune', 'll', + '-b:v', '3500k', '-maxrate', '4000k', '-bufsize', '7000k', + '-g', '50', // Force keyframes every 2s ] else ...[ - '-c:v', - 'copy', + '-c:v', 'libx264', '-preset', 'ultrafast', '-tune', 'zerolatency', + '-b:v', '3000k', '-maxrate', '3500k', '-bufsize', '6000k', + '-g', '50', // Force keyframes every 2s (CRITICAL for HLS) ], // Audio '-c:a', 'aac', '-b:a', '128k', '-ac', '2', // HLS Sliding Window '-f', 'hls', - '-hls_time', '2', // Small segments for low latency - '-hls_list_size', '5', // 10 seconds of history - '-hls_flags', 'delete_segments+independent_segments', + '-hls_time', '2', + '-hls_list_size', '10', // Increased for safety + '-hls_flags', 'delete_segments+independent_segments+discont_start', + '-hls_segment_type', 'mpegts', '-hls_segment_filename', 'seg_%03d.ts', 'playlist.m3u8', ]; @@ -193,10 +186,16 @@ Handler createLiveStreamHandler( process.exitCode.then((_) => _liveProcesses.remove(streamId)); } - // Wait for playlist + // Wait for playlist AND at least one segment reference final file = File('${streamDir.path}/playlist.m3u8'); int retries = 0; - while (!file.existsSync() && retries < 40) { + while (retries < 60) { + if (file.existsSync()) { + final content = file.readAsStringSync(); + // Live HLS needs at least 2 or 3 segments to be stable on iOS + // but we start as soon as we have one for speed + if (content.contains('.ts')) break; + } await Future.delayed(const Duration(milliseconds: 500)); retries++; } @@ -404,6 +403,7 @@ Handler createVodStreamHandler( '6000k', '-pix_fmt', 'yuv420p', + '-g', '48', // Force keyframes '-threads', '0', ]); @@ -433,6 +433,8 @@ Handler createVodStreamHandler( 'event', '-hls_allow_cache', '1', + '-hls_flags', + 'independent_segments', '-hls_segment_type', 'mpegts', '-hls_segment_filename', @@ -463,10 +465,14 @@ Handler createVodStreamHandler( }); } - // Wait for playlist to appear (max 30s) + // Wait for playlist AND at least one segment to be referenced final playlistFile = File('${streamDir.path}/playlist.m3u8'); int retries = 0; - while (!playlistFile.existsSync() && retries < 60) { + while (retries < 60) { + if (playlistFile.existsSync()) { + final content = playlistFile.readAsStringSync(); + if (content.contains('.ts')) break; // At least one segment is ready + } await Future.delayed(const Duration(milliseconds: 500)); retries++; } diff --git a/bin/server.dart b/bin/server.dart index 08d69cb..6eef50f 100644 --- a/bin/server.dart +++ b/bin/server.dart @@ -59,6 +59,7 @@ void main(List args) async { } } } + // Injecter après 5s pour attendre l'initialisation complète Future.delayed(const Duration(seconds: 5), _injectPlaylistToScheduler); @@ -82,13 +83,15 @@ void main(List args) async { print('[getPlaylist] Total users in DB: ${users.length}'); if (users.isNotEmpty) { final playlists = db.getPlaylists(users[0].id); - print('[getPlaylist] User ${users[0].username} has ${playlists.length} playlists'); + print( + '[getPlaylist] User ${users[0].username} has ${playlists.length} playlists'); if (playlists.isNotEmpty) playlist = playlists.first; } } if (playlist != null) { - print('[getPlaylist] Returning playlist: ${playlist.name} (DNS: ${playlist.serverUrl})'); + print( + '[getPlaylist] Returning playlist: ${playlist.name} (DNS: ${playlist.serverUrl})'); return PlaylistConfig( id: playlist.id, name: playlist.name, @@ -150,8 +153,8 @@ void main(List args) async { ..get('/api/season-passes', seasonPassesApi.handleGetAll) ..post('/api/season-passes', seasonPassesApi.handlePost) ..delete('/api/season-passes/', seasonPassesApi.handleDelete); - // NOTE: /api/xtream is handled by proxyHandler in the Cascade below - // Do NOT mount here as it would intercept and block the actual proxy + // NOTE: /api/xtream is handled by proxyHandler in the Cascade below + // Do NOT mount here as it would intercept and block the actual proxy // Initialize Cleanup Service final cleanupService = CleanupService(); @@ -299,6 +302,7 @@ Middleware _corsMiddleware() { final _corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', - 'Access-Control-Allow-Headers': 'Origin, Content-Type, Accept, Authorization', + 'Access-Control-Allow-Headers': + 'Origin, Content-Type, Accept, Authorization, Range', + 'Access-Control-Expose-Headers': 'Content-Length, Content-Range', }; - diff --git a/lib/features/iptv/services/xtream_service.dart b/lib/features/iptv/services/xtream_service.dart index 93362a0..33a9463 100644 --- a/lib/features/iptv/services/xtream_service.dart +++ b/lib/features/iptv/services/xtream_service.dart @@ -18,8 +18,8 @@ class XtreamService { XtreamService() { _dio = Dio( BaseOptions( - connectTimeout: const Duration(seconds: 15), - receiveTimeout: const Duration(seconds: 15), + connectTimeout: const Duration(seconds: 30), + receiveTimeout: const Duration(seconds: 30), ), ); diff --git a/task.md b/task.md index 7a24ace..b4099d4 100644 --- a/task.md +++ b/task.md @@ -23,6 +23,7 @@ Refonte du Guide TV pour inclure les catégories et corriger l'EPG. - [x] Compatibilité Streaming iOS (HLS Live + Proxy Streaming) - [x] Correction Chemins HLS Relatifs (Bug chemins absolus FFmpeg) - [x] Résolution Erreur 502 Proxy (Nettoyage Headers + API Buffer) +- [x] Optimisation HLS iOS (Force Keyframes + Anti-Empty Playlist) ## Progress Log