mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-22 14:38:06 +02:00
feat: Implement GPU-accelerated live HLS streaming with FFmpeg, including iOS optimizations and IPTV redirect resolution.
This commit is contained in:
1 parent
d0a2a1ee6a
commit
c57273be2c
4 files changed
+40
-29
No files matched your search
@@ -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
@@ -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),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user