mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-18 20:59:59 +02:00
feat: add streaming handler for live MPEG-TS proxying and VOD HLS transcoding with FFmpeg.
This commit is contained in:
1 parent
b9d6d715d7
commit
887bfad433
1 file changed
+72
-27
@@ -54,34 +54,71 @@ Handler createLiveStreamHandler(Future<PlaylistConfig?> Function(Request) getPla
|
|||||||
return Response.internalServerError(body: 'No playlist configured');
|
return Response.internalServerError(body: 'No playlist configured');
|
||||||
}
|
}
|
||||||
|
|
||||||
final targetUrl = Uri.parse('${playlist.dns}/live/${playlist.username}/${playlist.password}/$streamId.ts');
|
final targetUrl = '${playlist.dns}/live/${playlist.username}/${playlist.password}/$streamId.ts';
|
||||||
print('[Live] Proxying: $targetUrl');
|
print('[Live] Streaming via FFmpeg: $targetUrl');
|
||||||
|
|
||||||
|
// Start FFmpeg to proxy and stabilize the stream
|
||||||
|
// We use "-c copy" to avoid transcoding (Low CPU) but gain FFmpeg's network robustness
|
||||||
|
final ffmpegArgs = [
|
||||||
|
'-hide_banner', '-loglevel', 'error',
|
||||||
|
'-headers', 'User-Agent: VLC/3.0.18 LibVLC/3.0.18\r\n',
|
||||||
|
|
||||||
|
// Robustness flags (Same as VOD)
|
||||||
|
'-reconnect', '1',
|
||||||
|
'-reconnect_at_eof', '1',
|
||||||
|
'-reconnect_streamed', '1',
|
||||||
|
'-reconnect_delay_max', '5',
|
||||||
|
'-rw_timeout', '15000000',
|
||||||
|
'-analyzeduration', '5000000', // Faster probe for live
|
||||||
|
'-probesize', '5000000',
|
||||||
|
|
||||||
|
'-i', targetUrl,
|
||||||
|
|
||||||
|
'-c', 'copy', // Direct stream copy (Very low CPU)
|
||||||
|
'-f', 'mpegts',
|
||||||
|
'pipe:1' // Output to stdout
|
||||||
|
];
|
||||||
|
|
||||||
final client = http.Client();
|
|
||||||
final streamRequest = http.Request('GET', targetUrl);
|
|
||||||
|
|
||||||
// Spoof User-Agent to avoid 403/405 from providers
|
|
||||||
streamRequest.headers['User-Agent'] = 'VLC/3.0.18 LibVLC/3.0.18';
|
|
||||||
streamRequest.headers['Connection'] = 'keep-alive';
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final streamResponse = await client.send(streamRequest);
|
final process = await Process.start('ffmpeg', ffmpegArgs);
|
||||||
print('[Live] Upstream response: ${streamResponse.statusCode} ${streamResponse.reasonPhrase}');
|
|
||||||
|
// Cleanup handling: Kill FFmpeg when client disconnects
|
||||||
|
final controller = StreamController<List<int>>(
|
||||||
|
onCancel: () {
|
||||||
|
print('[Live] Client disconnected, killing FFmpeg for $streamId');
|
||||||
|
process.kill();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pipe stdout to controller
|
||||||
|
process.stdout.listen(
|
||||||
|
(data) => controller.add(data),
|
||||||
|
onDone: () {
|
||||||
|
print('[Live] FFmpeg stream ended for $streamId');
|
||||||
|
controller.close();
|
||||||
|
},
|
||||||
|
onError: (e) {
|
||||||
|
print('[Live] FFmpeg stream error: $e');
|
||||||
|
controller.addError(e);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Log errors
|
||||||
|
process.stderr.transform(utf8.decoder).listen((data) {
|
||||||
|
print('[FFmpeg Live $streamId] $data');
|
||||||
|
});
|
||||||
|
|
||||||
// Filter headers to avoid encoding issues with shelf
|
return Response.ok(
|
||||||
final responseHeaders = Map<String, Object>.from(streamResponse.headers);
|
controller.stream,
|
||||||
responseHeaders.remove('content-length');
|
headers: {
|
||||||
responseHeaders.remove('content-encoding');
|
'Content-Type': 'video/mp2t',
|
||||||
responseHeaders.remove('transfer-encoding');
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
return Response(
|
},
|
||||||
streamResponse.statusCode,
|
|
||||||
body: streamResponse.stream,
|
|
||||||
headers: responseHeaders,
|
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('[Live] Upstream connection error: $e');
|
print('[Live] Failed to start FFmpeg: $e');
|
||||||
return Response.internalServerError(body: 'Upstream error: $e');
|
return Response.internalServerError(body: 'Stream start error: $e');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -96,6 +133,8 @@ Handler createLiveStreamHandler(Future<PlaylistConfig?> Function(Request) getPla
|
|||||||
// 2. VOD HANDLER (FFmpeg HLS Transcoding)
|
// 2. VOD HANDLER (FFmpeg HLS Transcoding)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
|
final _lastLogTime = <String, int>{};
|
||||||
|
|
||||||
Handler createVodStreamHandler(Future<PlaylistConfig?> Function(Request) getPlaylist) {
|
Handler createVodStreamHandler(Future<PlaylistConfig?> Function(Request) getPlaylist) {
|
||||||
final router = Router();
|
final router = Router();
|
||||||
|
|
||||||
@@ -116,7 +155,12 @@ Handler createVodStreamHandler(Future<PlaylistConfig?> Function(Request) getPlay
|
|||||||
final content = playlistFile.readAsStringSync();
|
final content = playlistFile.readAsStringSync();
|
||||||
// Check if playlist has segments (healthy transcoding)
|
// Check if playlist has segments (healthy transcoding)
|
||||||
if (content.contains('.ts')) {
|
if (content.contains('.ts')) {
|
||||||
print('[VOD] Reusing existing session for $streamId');
|
// Only log once per second to avoid spam
|
||||||
|
final now = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
if (!_lastLogTime.containsKey(streamId) || (now - _lastLogTime[streamId]! > 2000)) {
|
||||||
|
print('[VOD] Reusing existing session for $streamId');
|
||||||
|
_lastLogTime[streamId] = now;
|
||||||
|
}
|
||||||
// Serve existing playlist
|
// Serve existing playlist
|
||||||
return Response.ok(
|
return Response.ok(
|
||||||
playlistFile.openRead(),
|
playlistFile.openRead(),
|
||||||
@@ -175,15 +219,16 @@ Handler createVodStreamHandler(Future<PlaylistConfig?> Function(Request) getPlay
|
|||||||
'-reconnect_at_eof', '1',
|
'-reconnect_at_eof', '1',
|
||||||
'-reconnect_streamed', '1',
|
'-reconnect_streamed', '1',
|
||||||
'-reconnect_delay_max', '5',
|
'-reconnect_delay_max', '5',
|
||||||
'-rw_timeout', '15000000', // 15s timeout for read/write
|
'-rw_timeout', '15000000',
|
||||||
'-analyzeduration', '10000000', // Increase probe duration
|
'-analyzeduration', '2000000', // Reduced from 10M to 2M for faster start
|
||||||
'-probesize', '10000000',
|
'-probesize', '2000000', // Reduced from 10M to 2M for faster start
|
||||||
|
|
||||||
'-i', targetUrl,
|
'-i', targetUrl,
|
||||||
|
|
||||||
// Video: H.264 Ultrafast (Low CPU)
|
// Video: H.264 Ultrafast (Low CPU)
|
||||||
'-c:v', 'libx264',
|
'-c:v', 'libx264',
|
||||||
'-preset', 'ultrafast',
|
'-preset', 'ultrafast',
|
||||||
|
'-tune', 'zerolatency', // Optimize for low latency startup
|
||||||
'-tune', 'fastdecode',
|
'-tune', 'fastdecode',
|
||||||
'-crf', '23',
|
'-crf', '23',
|
||||||
'-maxrate', '3000k',
|
'-maxrate', '3000k',
|
||||||
|
|||||||
Reference in new issue
Block a user