mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-23 06:58:05 +02:00
feat: Implement initial server with API, streaming, Xtream proxy, and static file serving.
This commit is contained in:
1 parent
63846c090a
commit
f5f70f73a6
4 files changed
+34
-13
No files matched your search
+31
-10
@@ -342,33 +342,48 @@ Handler _createXtreamProxyHandler() {
|
|||||||
|
|
||||||
/// Stream video file using HttpClient with Range support for seeking
|
/// Stream video file using HttpClient with Range support for seeking
|
||||||
Future<Response> _streamVideoFile(Uri targetUrl, String? rangeHeader) async {
|
Future<Response> _streamVideoFile(Uri targetUrl, String? rangeHeader) async {
|
||||||
|
final maxRedirects = 5;
|
||||||
|
var currentUrl = targetUrl;
|
||||||
|
|
||||||
|
for (var i = 0; i < maxRedirects; i++) {
|
||||||
try {
|
try {
|
||||||
final client = HttpClient();
|
final client = HttpClient();
|
||||||
// Increase timeouts for large video files
|
// Increase timeouts for large video files
|
||||||
client.connectionTimeout = const Duration(seconds: 60);
|
client.connectionTimeout = const Duration(seconds: 60);
|
||||||
client.idleTimeout = const Duration(minutes: 5); // Keep connection alive longer
|
client.idleTimeout = const Duration(minutes: 5);
|
||||||
|
|
||||||
|
final req = await client.getUrl(currentUrl);
|
||||||
|
req.followRedirects = false; // We handle redirects manually to preserve Range header
|
||||||
|
|
||||||
final req = await client.getUrl(targetUrl);
|
|
||||||
req.headers.set('User-Agent', 'VLC/3.0.18 LibVLC/3.0.18');
|
req.headers.set('User-Agent', 'VLC/3.0.18 LibVLC/3.0.18');
|
||||||
req.headers.set('Accept', '*/*');
|
req.headers.set('Accept', '*/*');
|
||||||
req.headers.set('Connection', 'keep-alive');
|
req.headers.set('Connection', 'keep-alive');
|
||||||
req.headers.set('Accept-Encoding', 'identity'); // Don't compress video
|
req.headers.set('Accept-Encoding', 'identity');
|
||||||
|
|
||||||
// Forward the Range header for seeking support
|
// Forward the Range header for seeking support
|
||||||
if (rangeHeader != null && rangeHeader.isNotEmpty) {
|
if (rangeHeader != null && rangeHeader.isNotEmpty) {
|
||||||
// Ensure we request bytes
|
|
||||||
if (!rangeHeader.startsWith('bytes=')) {
|
if (!rangeHeader.startsWith('bytes=')) {
|
||||||
rangeHeader = 'bytes=$rangeHeader';
|
rangeHeader = 'bytes=$rangeHeader';
|
||||||
}
|
}
|
||||||
req.headers.set(HttpHeaders.rangeHeader, rangeHeader);
|
req.headers.set(HttpHeaders.rangeHeader, rangeHeader);
|
||||||
print('Video streaming with Range: $rangeHeader');
|
print('Video streaming with Range: $rangeHeader (Redirect $i)');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable auto-decompression to strictly proxy the stream
|
|
||||||
client.autoUncompress = false;
|
client.autoUncompress = false;
|
||||||
|
|
||||||
final response = await req.close();
|
final response = await req.close();
|
||||||
|
|
||||||
|
// Handle Redirects (301, 302, 303, 307, 308)
|
||||||
|
if (response.statusCode >= 300 && response.statusCode < 400) {
|
||||||
|
final location = response.headers.value(HttpHeaders.locationHeader);
|
||||||
|
if (location != null) {
|
||||||
|
print('Redirecting stream to: $location');
|
||||||
|
currentUrl = Uri.parse(location);
|
||||||
|
await response.drain(); // Drain body to release connection
|
||||||
|
continue; // Loop to next request
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get content type from response
|
// Get content type from response
|
||||||
final contentType = response.headers.contentType?.mimeType ?? 'video/mp4';
|
final contentType = response.headers.contentType?.mimeType ?? 'video/mp4';
|
||||||
|
|
||||||
@@ -377,11 +392,11 @@ Future<Response> _streamVideoFile(Uri targetUrl, String? rangeHeader) async {
|
|||||||
'content-type': contentType,
|
'content-type': contentType,
|
||||||
'access-control-allow-origin': '*',
|
'access-control-allow-origin': '*',
|
||||||
'accept-ranges': 'bytes',
|
'accept-ranges': 'bytes',
|
||||||
'cache-control': 'no-cache', // Allow browser to cache but revalidate
|
'cache-control': 'no-cache',
|
||||||
'connection': 'keep-alive',
|
'connection': 'keep-alive',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add Content-Length if available (critical for seeking)
|
// Add Content-Length if available
|
||||||
if (response.contentLength > 0) {
|
if (response.contentLength > 0) {
|
||||||
responseHeaders['content-length'] = response.contentLength.toString();
|
responseHeaders['content-length'] = response.contentLength.toString();
|
||||||
}
|
}
|
||||||
@@ -395,20 +410,26 @@ Future<Response> _streamVideoFile(Uri targetUrl, String? rangeHeader) async {
|
|||||||
// Propagate Accept-Ranges
|
// Propagate Accept-Ranges
|
||||||
responseHeaders[HttpHeaders.acceptRangesHeader] = 'bytes';
|
responseHeaders[HttpHeaders.acceptRangesHeader] = 'bytes';
|
||||||
|
|
||||||
// Return appropriate status code (200 for full, 206 for partial)
|
// Return appropriate status code
|
||||||
return Response(
|
return Response(
|
||||||
response.statusCode,
|
response.statusCode,
|
||||||
body: response,
|
body: response,
|
||||||
headers: responseHeaders,
|
headers: responseHeaders,
|
||||||
);
|
);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Video streaming error: $e');
|
print('Video streaming exception: $e');
|
||||||
|
if (i == maxRedirects - 1) {
|
||||||
return Response.internalServerError(
|
return Response.internalServerError(
|
||||||
body: jsonEncode({'error': 'Video streaming error', 'message': e.toString()}),
|
body: jsonEncode({'error': 'Video streaming error', 'message': e.toString()}),
|
||||||
headers: {'content-type': 'application/json'},
|
headers: {'content-type': 'application/json'},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.internalServerError(body: 'Too many redirects');
|
||||||
|
}
|
||||||
|
|
||||||
/// Rewrite URLs in M3U8 playlist to go through the proxy
|
/// Rewrite URLs in M3U8 playlist to go through the proxy
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -264,11 +264,11 @@ class _LiveTVTabState extends ConsumerState<LiveTVTab>
|
|||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
colors: [
|
colors: [
|
||||||
AppColors.surface,
|
AppColors.surfaceVariant,
|
||||||
AppColors.surface.withOpacity(0.8),
|
AppColors.surfaceVariant.withOpacity(0.8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
border: Border.all(color: AppColors.border.withOpacity(0.5)),
|
border: Border.all(color: Colors.white.withOpacity(0.2)),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Reference in new issue
Block a user