feat: add initial server infrastructure with API, static file serving, Xtream proxy, and streaming capabilities.

This commit is contained in:
Michael committed 2025-12-06 17:28:25 +01:00
1 parent 0fc4a15f40
commit cfadc1d3bb
2 files changed
+66 -35

No files matched your search

+45 -15
View File
@@ -480,14 +480,16 @@ Handler _createStreamHandler() {
'-headers', 'Accept: */*\r\nConnection: keep-alive\r\n', '-headers', 'Accept: */*\r\nConnection: keep-alive\r\n',
]; ];
// Add reconnect flags only for live streams (not VOD) // Add reconnect flags and longer timeout for live streams (not VOD)
if (!isVod) { if (!isVod) {
ffmpegArgs.addAll([ ffmpegArgs.addAll([
'-reconnect', '1', '-reconnect', '1',
'-reconnect_streamed', '1', '-reconnect_streamed', '1',
'-reconnect_delay_max', '5', '-reconnect_delay_max', '10', // Increased from 5s
'-reconnect_on_network_error', '1', '-reconnect_on_network_error', '1',
'-reconnect_on_http_error', '4xx,5xx', '-reconnect_on_http_error', '4xx,5xx',
'-rw_timeout', '${timeoutSeconds * 1000000}', // Read/write timeout in microseconds
'-stimeout', '${timeoutSeconds * 1000000}', // Socket timeout
]); ]);
} }
@@ -554,11 +556,10 @@ Handler _createStreamHandler() {
// HLS output settings - optimized for stability // HLS output settings - optimized for stability
'-f', 'hls', '-f', 'hls',
'-hls_time', isVod ? '4' : '$segmentDuration', // Segment duration from settings for live '-hls_time', isVod ? '4' : '$segmentDuration', // Segment duration from settings for live
'-hls_list_size', isVod ? '0' : '20', // Keep all segments for VOD, 20 for live (was 10) '-hls_list_size', isVod ? '0' : '30', // Keep all for VOD, 30 for live (more buffer to prevent 404s)
'-hls_flags', isVod ? 'independent_segments' : 'append_list+omit_endlist+program_date_time', // Removed delete_segments to avoid 404s '-hls_flags', isVod ? 'independent_segments' : 'append_list+omit_endlist', // Simplified flags
'-hls_delete_threshold', '5', // Keep 5 extra segments before deleting '-hls_start_number_source', 'epoch', // Use epoch for consistent segment naming
'-hls_start_number_source', 'datetime', // Better segment continuity '-hls_segment_filename', '${outputDir.path}/segment_%05d.ts', // 5-digit numbering
'-hls_segment_filename', '${outputDir.path}/segment_%d.ts',
outputPath, outputPath,
]); ]);
@@ -584,22 +585,51 @@ Handler _createStreamHandler() {
_activeStreams.remove(streamId); _activeStreams.remove(streamId);
}); });
// Wait for FFmpeg to create the initial playlist (transcoding takes longer) // Wait for FFmpeg to create the initial playlist with retries
await Future.delayed(const Duration(seconds: 5)); // Different sources may take varying times to buffer and produce first segment
// Check if stream started successfully
final playlistFile = File(outputPath); final playlistFile = File(outputPath);
if (!await playlistFile.exists()) { bool playlistReady = false;
const maxWaitSeconds = 15; // Maximum wait time for first segment
const checkIntervalMs = 500; // Check every 500ms
for (int waited = 0; waited < maxWaitSeconds * 1000; waited += checkIntervalMs) {
await Future.delayed(const Duration(milliseconds: checkIntervalMs));
// Check if process died early
if (!_activeStreams.containsKey(streamId)) {
final stderr = stderrBuffer.toString();
print('FFmpeg process died early for $streamId: $stderr');
return Response.internalServerError(
body: jsonEncode({
'error': 'FFmpeg process died early',
'details': stderr.length > 500 ? stderr.substring(0, 500) : stderr,
}),
headers: {'content-type': 'application/json'},
);
}
// Check if playlist file exists and has content
if (await playlistFile.exists()) {
final content = await playlistFile.readAsString();
if (content.contains('#EXTINF')) {
playlistReady = true;
print('FFmpeg ready for $streamId after ${waited}ms');
break;
}
}
}
if (!playlistReady) {
// FFmpeg failed - get error message // FFmpeg failed - get error message
final stderr = stderrBuffer.toString(); final stderr = stderrBuffer.toString();
print('FFmpeg failed to create playlist for $streamId'); print('FFmpeg failed to create playlist for $streamId after ${maxWaitSeconds}s');
print('FFmpeg stderr: $stderr'); print('FFmpeg stderr: $stderr');
process.kill(); process.kill();
_activeStreams.remove(streamId); _activeStreams.remove(streamId);
return Response.internalServerError( return Response.internalServerError(
body: jsonEncode({ body: jsonEncode({
'error': 'FFmpeg failed to start stream', 'error': 'FFmpeg failed to start stream (timeout)',
'details': stderr.length > 200 ? stderr.substring(0, 200) : stderr, 'details': stderr.length > 500 ? stderr.substring(0, 500) : stderr,
}), }),
headers: {'content-type': 'application/json'}, headers: {'content-type': 'application/json'},
); );
+21 -20
View File
@@ -237,28 +237,29 @@ class _ChannelCardState extends State<ChannelCard>
), ),
], ],
), ),
child: const Row( child: child,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.circle,
size: 6,
color: Colors.white,
),
SizedBox(width: 4),
Text(
'LIVE',
style: TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.w700,
letterSpacing: 0.5,
),
),
],
),
); );
}, },
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.circle,
size: 6,
color: Colors.white,
),
SizedBox(width: 4),
Text(
'LIVE',
style: TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.w700,
letterSpacing: 0.5,
),
),
],
),
); );
} }
} }