fix(streaming): stop streams dying mid-playback

- reaper no longer kills exited sessions immediately: a finished VOD
  transcode was reaped (segments deleted) while still being watched
- reuse completed VOD/recording sessions instead of re-transcoding
- live input: add -rw_timeout 30s and -reconnect_at_eof so a stalled
  upstream triggers reconnect instead of wedging ffmpeg forever
- reaper watchdog restarts live sessions whose playlist stopped updating
- waitForPlaylist fails fast on clean ffmpeg exit without output
- direct .ts proxy: close http.Client on stream end/error (socket leak)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Fable 5 committed 2026-06-10 14:34:31 +02:00
1 parent 92634dba13
commit 9595afcc99
2 files changed
+85 -9

No files matched your search

+28 -2
View File
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'package:shelf/shelf.dart';
import 'package:shelf_router/shelf_router.dart';
@@ -215,7 +216,11 @@ Handler createLiveStreamHandler(
if (useNvidiaGpu && quality != 'source') ...['-hwaccel', 'cuda'],
'-headers', 'User-Agent: VLC/3.0.18 LibVLC/3.0.18\r\n',
'-reconnect', '1', '-reconnect_streamed', '1',
'-reconnect_at_eof', '1',
'-reconnect_delay_max', '10',
// Abort reads stuck for 30s so a stalled upstream triggers the
// reconnect logic instead of wedging the process forever.
'-rw_timeout', '30000000',
'-i', targetUrl,
..._liveVideoArgs(quality, useNvidiaGpu),
..._audioArgs(withFilters: false),
@@ -276,11 +281,32 @@ Handler createLiveStreamHandler(
proxyRequest.headers['User-Agent'] = 'VLC/3.0.18 LibVLC/3.0.18';
proxyRequest.headers['Accept'] = '*/*';
final response = await client.send(proxyRequest);
final http.StreamedResponse response;
try {
response = await client.send(proxyRequest);
} catch (e) {
client.close();
return Response(502, body: 'Upstream connection failed');
}
// Close the client when the upstream stream ends or errors, otherwise
// each proxied stream leaks a socket.
final body = response.stream.transform<List<int>>(
StreamTransformer.fromHandlers(
handleDone: (sink) {
sink.close();
client.close();
},
handleError: (error, stackTrace, sink) {
sink.close();
client.close();
},
),
);
return Response(
response.statusCode,
body: response.stream,
body: body,
headers: {
'Content-Type': 'video/mp2t',
'Connection': 'keep-alive',