feat: Implement GPU-accelerated live HLS streaming with FFmpeg, including iOS optimizations and IPTV redirect resolution.

This commit is contained in:
Michael committed 2026-03-09 14:10:13 +01:00
1 parent d0a2a1ee6a
commit c57273be2c
4 files changed
+40 -29

No files matched your search

+27 -21
View File
@@ -153,29 +153,22 @@ Handler createLiveStreamHandler(
'-i', targetUrl, '-i', targetUrl,
// Video // Video
if (useNvidiaGpu) ...[ if (useNvidiaGpu) ...[
'-c:v', '-c:v', 'h264_nvenc', '-preset', 'p1', '-tune', 'll',
'h264_nvenc', '-b:v', '3500k', '-maxrate', '4000k', '-bufsize', '7000k',
'-preset', '-g', '50', // Force keyframes every 2s
'p1',
'-tune',
'll',
'-b:v',
'3500k',
'-maxrate',
'4000k',
'-bufsize',
'7000k',
] else ...[ ] else ...[
'-c:v', '-c:v', 'libx264', '-preset', 'ultrafast', '-tune', 'zerolatency',
'copy', '-b:v', '3000k', '-maxrate', '3500k', '-bufsize', '6000k',
'-g', '50', // Force keyframes every 2s (CRITICAL for HLS)
], ],
// Audio // Audio
'-c:a', 'aac', '-b:a', '128k', '-ac', '2', '-c:a', 'aac', '-b:a', '128k', '-ac', '2',
// HLS Sliding Window // HLS Sliding Window
'-f', 'hls', '-f', 'hls',
'-hls_time', '2', // Small segments for low latency '-hls_time', '2',
'-hls_list_size', '5', // 10 seconds of history '-hls_list_size', '10', // Increased for safety
'-hls_flags', 'delete_segments+independent_segments', '-hls_flags', 'delete_segments+independent_segments+discont_start',
'-hls_segment_type', 'mpegts',
'-hls_segment_filename', 'seg_%03d.ts', '-hls_segment_filename', 'seg_%03d.ts',
'playlist.m3u8', 'playlist.m3u8',
]; ];
@@ -193,10 +186,16 @@ Handler createLiveStreamHandler(
process.exitCode.then((_) => _liveProcesses.remove(streamId)); 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'); final file = File('${streamDir.path}/playlist.m3u8');
int retries = 0; 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)); await Future.delayed(const Duration(milliseconds: 500));
retries++; retries++;
} }
@@ -404,6 +403,7 @@ Handler createVodStreamHandler(
'6000k', '6000k',
'-pix_fmt', '-pix_fmt',
'yuv420p', 'yuv420p',
'-g', '48', // Force keyframes
'-threads', '-threads',
'0', '0',
]); ]);
@@ -433,6 +433,8 @@ Handler createVodStreamHandler(
'event', 'event',
'-hls_allow_cache', '-hls_allow_cache',
'1', '1',
'-hls_flags',
'independent_segments',
'-hls_segment_type', '-hls_segment_type',
'mpegts', 'mpegts',
'-hls_segment_filename', '-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'); final playlistFile = File('${streamDir.path}/playlist.m3u8');
int retries = 0; 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)); await Future.delayed(const Duration(milliseconds: 500));
retries++; retries++;
} }
+10 -6
View File
@@ -59,6 +59,7 @@ void main(List<String> args) async {
} }
} }
} }
// Injecter après 5s pour attendre l'initialisation complète // Injecter après 5s pour attendre l'initialisation complète
Future.delayed(const Duration(seconds: 5), _injectPlaylistToScheduler); Future.delayed(const Duration(seconds: 5), _injectPlaylistToScheduler);
@@ -82,13 +83,15 @@ void main(List<String> args) async {
print('[getPlaylist] Total users in DB: ${users.length}'); print('[getPlaylist] Total users in DB: ${users.length}');
if (users.isNotEmpty) { if (users.isNotEmpty) {
final playlists = db.getPlaylists(users[0].id); 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 (playlists.isNotEmpty) playlist = playlists.first;
} }
} }
if (playlist != null) { if (playlist != null) {
print('[getPlaylist] Returning playlist: ${playlist.name} (DNS: ${playlist.serverUrl})'); print(
'[getPlaylist] Returning playlist: ${playlist.name} (DNS: ${playlist.serverUrl})');
return PlaylistConfig( return PlaylistConfig(
id: playlist.id, id: playlist.id,
name: playlist.name, name: playlist.name,
@@ -150,8 +153,8 @@ void main(List<String> args) async {
..get('/api/season-passes', seasonPassesApi.handleGetAll) ..get('/api/season-passes', seasonPassesApi.handleGetAll)
..post('/api/season-passes', seasonPassesApi.handlePost) ..post('/api/season-passes', seasonPassesApi.handlePost)
..delete('/api/season-passes/<id>', seasonPassesApi.handleDelete); ..delete('/api/season-passes/<id>', seasonPassesApi.handleDelete);
// NOTE: /api/xtream is handled by proxyHandler in the Cascade below // NOTE: /api/xtream is handled by proxyHandler in the Cascade below
// Do NOT mount here as it would intercept and block the actual proxy // Do NOT mount here as it would intercept and block the actual proxy
// Initialize Cleanup Service // Initialize Cleanup Service
final cleanupService = CleanupService(); final cleanupService = CleanupService();
@@ -299,6 +302,7 @@ Middleware _corsMiddleware() {
final _corsHeaders = { final _corsHeaders = {
'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', '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',
}; };
@@ -18,8 +18,8 @@ class XtreamService {
XtreamService() { XtreamService() {
_dio = Dio( _dio = Dio(
BaseOptions( BaseOptions(
connectTimeout: const Duration(seconds: 15), connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 15), receiveTimeout: const Duration(seconds: 30),
), ),
); );
+1
View File
@@ -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] Compatibilité Streaming iOS (HLS Live + Proxy Streaming)
- [x] Correction Chemins HLS Relatifs (Bug chemins absolus FFmpeg) - [x] Correction Chemins HLS Relatifs (Bug chemins absolus FFmpeg)
- [x] Résolution Erreur 502 Proxy (Nettoyage Headers + API Buffer) - [x] Résolution Erreur 502 Proxy (Nettoyage Headers + API Buffer)
- [x] Optimisation HLS iOS (Force Keyframes + Anti-Empty Playlist)
## Progress Log ## Progress Log