From 7372f51b197196ca972c1b0cfdc70066e91a6a53 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Sun, 7 Dec 2025 15:21:29 +0100 Subject: [PATCH 1/2] feat: add initial Dart server with API routing, authentication, streaming, and static file serving. --- bin/server.dart | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/bin/server.dart b/bin/server.dart index f5debc3..8a145c5 100644 --- a/bin/server.dart +++ b/bin/server.dart @@ -418,6 +418,8 @@ Handler _createStreamHandler() { // Get parameters final iptvUrl = request.url.queryParameters['url']; + final quality = request.url.queryParameters['quality'] ?? 'high'; + if (iptvUrl == null || iptvUrl.isEmpty) { return Response.badRequest(body: 'Missing url parameter'); } @@ -452,9 +454,39 @@ Handler _createStreamHandler() { // Output format: MPEG-TS for streaming (compatible with mpegts.js) '-f', 'mpegts', - // Video: Copy is BEST for performance (0% CPU) - // Browsers support H.264 well. - '-c:v', 'copy', + // Video: Transcode based on quality + // High = Copy (efficient), Low/Medium = Transcode to H.264 (compatible) + if (quality == 'high') { + ffmpegArgs.addAll(['-c:v', 'copy']); + } else { + // Transcode to H.264 (Baseline profile for max mobile compatibility) + ffmpegArgs.addAll([ + '-c:v', 'libx264', + '-preset', 'ultrafast', // Critical for live streaming + '-tune', 'zerolatency', + '-profile:v', 'baseline', + '-level', '3.0', + '-pix_fmt', 'yuv420p', // Ensure standard pixel format for web + '-g', '60', // Keyframe interval (2s at 30fps) for seeking + ]); + + if (quality == 'low') { + // Low: ~480p, 800k + ffmpegArgs.addAll([ + '-vf', 'scale=-2:480', + '-b:v', '800k', + '-maxrate', '800k', + '-bufsize', '1600k' + ]); + } else { + // Medium: Original res (or cap at 720p?), 2500k + ffmpegArgs.addAll([ + '-b:v', '2500k', + '-maxrate', '2500k', + '-bufsize', '5000k' + ]); + } + } // Audio: Transcode to AAC is SAFEST for browsers // (Many IPTV streams typically use MP2/AC3 which browsers hate) From b244f1f80715bb6feae7a3d800fc2f0d5e46184f Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Sun, 7 Dec 2025 15:29:00 +0100 Subject: [PATCH 2/2] feat: introduce core server with API, media streaming, Xtream proxy, and player UI --- bin/server.dart | 16 +++++++++++++--- lib/features/iptv/screens/player_screen.dart | 4 ++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/bin/server.dart b/bin/server.dart index 8a145c5..3a151cc 100644 --- a/bin/server.dart +++ b/bin/server.dart @@ -279,10 +279,17 @@ Future _streamVideoFile(Uri targetUrl, String? rangeHeader) async { // Forward the Range header for seeking support if (rangeHeader != null && rangeHeader.isNotEmpty) { - req.headers.set('Range', rangeHeader); + // Ensure we request bytes + if (!rangeHeader.startsWith('bytes=')) { + rangeHeader = 'bytes=$rangeHeader'; + } + req.headers.set(HttpHeaders.rangeHeader, rangeHeader); print('Video streaming with Range: $rangeHeader'); } + // Disable auto-decompression to strictly proxy the stream + client.autoUncompress = false; + final response = await req.close(); // Get content type from response @@ -303,11 +310,14 @@ Future _streamVideoFile(Uri targetUrl, String? rangeHeader) async { } // Add Content-Range if this is a partial response (206) - final contentRange = response.headers.value('content-range'); + final contentRange = response.headers.value(HttpHeaders.contentRangeHeader); if (contentRange != null) { - responseHeaders['content-range'] = contentRange; + responseHeaders[HttpHeaders.contentRangeHeader] = contentRange; } + // Propagate Accept-Ranges + responseHeaders[HttpHeaders.acceptRangesHeader] = 'bytes'; + // Return appropriate status code (200 for full, 206 for partial) return Response( response.statusCode, diff --git a/lib/features/iptv/screens/player_screen.dart b/lib/features/iptv/screens/player_screen.dart index e794e04..3de5d60 100644 --- a/lib/features/iptv/screens/player_screen.dart +++ b/lib/features/iptv/screens/player_screen.dart @@ -735,11 +735,11 @@ class _PlayerScreenState extends ConsumerState { // EPG Overlay if (widget.streamType == StreamType.live) Positioned( - bottom: 0, + bottom: 100, // Move up above progress bar area/OS safe area left: 0, right: 0, child: Container( - padding: const EdgeInsets.only(bottom: 20), + padding: const EdgeInsets.only(bottom: 8), decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.bottomCenter,