feat: add streaming handler for live MPEG-TS proxying and VOD HLS transcoding with FFmpeg.

This commit is contained in:
Michael committed 2025-12-15 22:24:07 +01:00
1 parent b9d6d715d7
commit 887bfad433
1 file changed
+72 -27
+72 -27
View File
@@ -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',