feat: Install FFmpeg with NVIDIA NVENC/NVDEC support via apt-get instead of a static build.

This commit is contained in:
Michael committed 2026-01-03 10:26:17 +01:00
1 parent 33a331dce9
commit 71630d75e8
9 files changed
+135 -86

No files matched your search

+14 -8
View File
@@ -19,8 +19,9 @@ String _getFFmpegPath() {
if (Platform.isWindows) { if (Platform.isWindows) {
if (File('ffmpeg.exe').existsSync()) return 'ffmpeg.exe'; if (File('ffmpeg.exe').existsSync()) return 'ffmpeg.exe';
if (File('bin/ffmpeg.exe').existsSync()) return 'bin/ffmpeg.exe'; if (File('bin/ffmpeg.exe').existsSync()) return 'bin/ffmpeg.exe';
if (File('ffmpeg/bin/ffmpeg.exe').existsSync()) if (File('ffmpeg/bin/ffmpeg.exe').existsSync()) {
return 'ffmpeg/bin/ffmpeg.exe'; return 'ffmpeg/bin/ffmpeg.exe';
}
} }
return 'ffmpeg'; // Default to PATH return 'ffmpeg'; // Default to PATH
} }
@@ -41,7 +42,8 @@ void _logGpuStatus() {
print('[FFmpeg] NVIDIA GPU acceleration ENABLED (NVDEC/NVENC)'); print('[FFmpeg] NVIDIA GPU acceleration ENABLED (NVDEC/NVENC)');
} else { } else {
print( print(
'[FFmpeg] Using CPU processing (set NVIDIA_GPU=true to enable GPU)'); '[FFmpeg] Using CPU processing (set NVIDIA_GPU=true to enable GPU)',
);
} }
_gpuStatusLogged = true; _gpuStatusLogged = true;
} }
@@ -116,8 +118,9 @@ PlaylistConfig? _getCurrentPlaylist(Request request) {
// ========================================== // ==========================================
Handler createLiveStreamHandler( Handler createLiveStreamHandler(
Future<PlaylistConfig?> Function(Request) getPlaylist, Future<PlaylistConfig?> Function(Request) getPlaylist, {
{bool Function()? isGpuEnabled}) { bool Function()? isGpuEnabled,
}) {
final router = Router(); final router = Router();
// Route: /api/live/{streamId} (Catch all pattern to debug) // Route: /api/live/{streamId} (Catch all pattern to debug)
@@ -291,16 +294,18 @@ Handler createLiveStreamHandler(
final _lastLogTime = <String, int>{}; final _lastLogTime = <String, int>{};
Handler createVodStreamHandler( Handler createVodStreamHandler(
Future<PlaylistConfig?> Function(Request) getPlaylist, Future<PlaylistConfig?> Function(Request) getPlaylist, {
{bool Function()? isGpuEnabled}) { bool Function()? isGpuEnabled,
}) {
final router = Router(); final router = Router();
// Route: /api/vod/{streamId}/playlist.m3u8 // Route: /api/vod/{streamId}/playlist.m3u8
router.get('/<streamId>/playlist.m3u8', router.get('/<streamId>/playlist.m3u8',
(Request request, String streamId) async { (Request request, String streamId) async {
final playlist = await getPlaylist(request); final playlist = await getPlaylist(request);
if (playlist == null) if (playlist == null) {
return Response.internalServerError(body: 'No playlist'); return Response.internalServerError(body: 'No playlist');
}
final streamDir = Directory('${_hlsTempDir.path}/$streamId'); final streamDir = Directory('${_hlsTempDir.path}/$streamId');
@@ -514,7 +519,8 @@ Handler createVodStreamHandler(
if (!playlistFile.existsSync()) { if (!playlistFile.existsSync()) {
return Response.internalServerError( return Response.internalServerError(
body: 'Timeout waiting for transcoder'); body: 'Timeout waiting for transcoder',
);
} }
return Response.ok( return Response.ok(
+14 -7
View File
@@ -91,7 +91,8 @@ void main(List<String> args) async {
final user = req.context['user'] as User?; final user = req.context['user'] as User?;
if (user == null || !user.isAdmin) { if (user == null || !user.isAdmin) {
return Response.forbidden( return Response.forbidden(
jsonEncode({'error': 'Admin access required'})); jsonEncode({'error': 'Admin access required'}),
);
} }
final result = await cleanupService.runCleanup(); final result = await cleanupService.runCleanup();
@@ -173,13 +174,19 @@ void main(List<String> args) async {
// Create Streaming Router (Mount handlers on correct paths) // Create Streaming Router (Mount handlers on correct paths)
final streamingRouter = Router() final streamingRouter = Router()
..mount( ..mount(
'/api/live', '/api/live',
createLiveStreamHandler(getPlaylist, createLiveStreamHandler(
isGpuEnabled: db.isNvidiaGpuEnabled)) getPlaylist,
isGpuEnabled: db.isNvidiaGpuEnabled,
),
)
..mount( ..mount(
'/api/vod', '/api/vod',
createVodStreamHandler(getPlaylist, createVodStreamHandler(
isGpuEnabled: db.isNvidiaGpuEnabled)); getPlaylist,
isGpuEnabled: db.isNvidiaGpuEnabled,
),
);
// Main handler with API proxy and NEW Streaming Handlers // Main handler with API proxy and NEW Streaming Handlers
final handler = Cascade() final handler = Cascade()
+47 -33
View File
@@ -41,43 +41,52 @@ class AppTheme {
// Typography: "Cinematic Modern" (Outfit for Headings, Inter for UI) // Typography: "Cinematic Modern" (Outfit for Headings, Inter for UI)
textTheme: TextTheme( textTheme: TextTheme(
displayLarge: GoogleFonts.outfit( displayLarge: GoogleFonts.outfit(
fontSize: 56, fontSize: 56,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
letterSpacing: -1.0, letterSpacing: -1.0,
color: AppColors.textPrimary), color: AppColors.textPrimary,
),
displayMedium: GoogleFonts.outfit( displayMedium: GoogleFonts.outfit(
fontSize: 48, fontSize: 48,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
letterSpacing: -0.5, letterSpacing: -0.5,
color: AppColors.textPrimary), color: AppColors.textPrimary,
),
displaySmall: GoogleFonts.outfit( displaySmall: GoogleFonts.outfit(
fontSize: 32, fontSize: 32,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColors.textPrimary), color: AppColors.textPrimary,
),
headlineMedium: GoogleFonts.outfit( headlineMedium: GoogleFonts.outfit(
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColors.textPrimary), color: AppColors.textPrimary,
),
titleLarge: GoogleFonts.outfit( titleLarge: GoogleFonts.outfit(
fontSize: 20, fontSize: 20,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColors.textPrimary), color: AppColors.textPrimary,
),
titleMedium: GoogleFonts.inter( titleMedium: GoogleFonts.inter(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColors.textPrimary), color: AppColors.textPrimary,
),
bodyLarge: GoogleFonts.inter( bodyLarge: GoogleFonts.inter(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
color: AppColors.textSecondary), color: AppColors.textSecondary,
),
bodyMedium: GoogleFonts.inter( bodyMedium: GoogleFonts.inter(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,
color: AppColors.textSecondary), color: AppColors.textSecondary,
),
labelLarge: GoogleFonts.inter( labelLarge: GoogleFonts.inter(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColors.textPrimary), color: AppColors.textPrimary,
),
), ),
// AppBar: Transparent / Glass // AppBar: Transparent / Glass
@@ -99,7 +108,9 @@ class AppTheme {
filled: true, filled: true,
fillColor: AppColors.surface, fillColor: AppColors.surface,
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
horizontal: spacing24, vertical: spacing16), horizontal: spacing24,
vertical: spacing16,
),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(radiusMd), borderRadius: BorderRadius.circular(radiusMd),
borderSide: BorderSide.none, borderSide: BorderSide.none,
@@ -123,15 +134,18 @@ class AppTheme {
elevation: 0, elevation: 0,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusMd)), borderRadius: BorderRadius.circular(radiusMd),
),
textStyle: textStyle:
GoogleFonts.inter(fontWeight: FontWeight.w600, fontSize: 16), GoogleFonts.inter(fontWeight: FontWeight.w600, fontSize: 16),
).copyWith( ).copyWith(
overlayColor: WidgetStateProperty.resolveWith((states) { overlayColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.hovered)) if (states.contains(WidgetState.hovered)) {
return Colors.black.withOpacity(0.1); return Colors.black.withOpacity(0.1);
if (states.contains(WidgetState.pressed)) }
if (states.contains(WidgetState.pressed)) {
return Colors.black.withOpacity(0.2); return Colors.black.withOpacity(0.2);
}
return null; return null;
}), }),
), ),
+4 -2
View File
@@ -254,8 +254,10 @@ class _HeroCarouselState extends State<HeroCarousel> {
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const Icon(Icons.play_arrow_rounded, const Icon(
color: Colors.white), Icons.play_arrow_rounded,
color: Colors.white,
),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
'WATCH NOW', 'WATCH NOW',
@@ -121,8 +121,11 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
), ),
], ],
), ),
child: const Icon(Icons.play_arrow_rounded, child: const Icon(
color: Colors.white, size: 28), Icons.play_arrow_rounded,
color: Colors.white,
size: 28,
),
), ),
), ),
@@ -149,7 +152,8 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
border: isSelected border: isSelected
? Border.all( ? Border.all(
color: AppColors.primary.withOpacity(0.5)) color: AppColors.primary.withOpacity(0.5),
)
: null, : null,
), ),
child: Icon( child: Icon(
@@ -176,8 +180,11 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
child: CircleAvatar( child: CircleAvatar(
radius: 20, radius: 20,
backgroundColor: Colors.white.withOpacity(0.1), backgroundColor: Colors.white.withOpacity(0.1),
child: const Icon(Icons.person, child: const Icon(
color: Colors.white70, size: 20), Icons.person,
color: Colors.white70,
size: 20,
),
), ),
), ),
), ),
+41 -26
View File
@@ -243,7 +243,8 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
iframe.contentWindow?.postMessage(message, '*'); iframe.contentWindow?.postMessage(message, '*');
} else { } else {
print( print(
'[PlayerScreen] Error: Could not find iframe to send message: $message'); '[PlayerScreen] Error: Could not find iframe to send message: $message',
);
} }
} }
@@ -359,7 +360,9 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
right: 0, right: 0,
child: Container( child: Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 12), horizontal: 16,
vertical: 12,
),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
@@ -409,12 +412,6 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
], ],
), ),
), ),
_buildSimpleIconButton(
icon: _isMuted
? Icons.volume_off_rounded
: Icons.volume_up_rounded,
onTap: _toggleMute,
),
], ],
), ),
), ),
@@ -429,12 +426,15 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
right: 24, right: 24,
child: Container( child: Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 16), horizontal: 20,
vertical: 16,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF1A1A2E).withOpacity(0.9), color: const Color(0xFF1A1A2E).withOpacity(0.9),
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(24),
border: Border.all( border: Border.all(
color: Colors.white.withOpacity(0.1)), color: Colors.white.withOpacity(0.1),
),
), ),
child: Row( child: Row(
children: [ children: [
@@ -608,10 +608,14 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
Row( Row(
children: [ children: [
Text( Text(
_formatDuration(Duration( _formatDuration(
seconds: _currentPosition.toInt())), Duration(
seconds: _currentPosition.toInt(),
),
),
style: GoogleFonts.inter( style: GoogleFonts.inter(
color: Colors.white70), color: Colors.white70,
),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(
@@ -623,7 +627,8 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
Colors.white.withOpacity(0.2), Colors.white.withOpacity(0.2),
thumbColor: Colors.white, thumbColor: Colors.white,
thumbShape: const RoundSliderThumbShape( thumbShape: const RoundSliderThumbShape(
enabledThumbRadius: 8), enabledThumbRadius: 8,
),
overlayColor: overlayColor:
AppColors.primary.withOpacity(0.2), AppColors.primary.withOpacity(0.2),
), ),
@@ -632,18 +637,22 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
min: 0, min: 0,
max: _totalDuration, max: _totalDuration,
onChanged: (val) => setState( onChanged: (val) => setState(
() => _currentPosition = val), () => _currentPosition = val,
),
onChangeStart: (_) => onChangeStart: (_) =>
setState(() => _isSeeking = true), setState(() => _isSeeking = true),
onChangeEnd: (val) { onChangeEnd: (val) {
_sendMessage( _sendMessage(
{'type': 'seek', 'value': val}); {'type': 'seek', 'value': val},
);
Future.delayed( Future.delayed(
const Duration(milliseconds: 500), const Duration(milliseconds: 500),
() { () {
if (mounted) if (mounted) {
setState( setState(
() => _isSeeking = false); () => _isSeeking = false,
);
}
}); });
}, },
), ),
@@ -651,10 +660,14 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Text( Text(
_formatDuration(Duration( _formatDuration(
seconds: _totalDuration.toInt())), Duration(
seconds: _totalDuration.toInt(),
),
),
style: GoogleFonts.inter( style: GoogleFonts.inter(
color: Colors.white70), color: Colors.white70,
),
), ),
], ],
), ),
@@ -669,7 +682,7 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
onTap: () => _sendMessage({ onTap: () => _sendMessage({
'type': 'seek', 'type': 'seek',
'value': (_currentPosition - 10) 'value': (_currentPosition - 10)
.clamp(0, _totalDuration) .clamp(0, _totalDuration),
}), }),
transparent: true, transparent: true,
), ),
@@ -688,7 +701,7 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
onTap: () => _sendMessage({ onTap: () => _sendMessage({
'type': 'seek', 'type': 'seek',
'value': (_currentPosition + 10) 'value': (_currentPosition + 10)
.clamp(0, _totalDuration) .clamp(0, _totalDuration),
}), }),
transparent: true, transparent: true,
), ),
@@ -807,9 +820,11 @@ class _PlayerScreenState extends ConsumerState<PlayerScreen> {
return Consumer( return Consumer(
builder: (context, ref, child) { builder: (context, ref, child) {
final epgAsync = ref.watch(epgByPlaylistProvider( final epgAsync = ref.watch(
EpgRequestKey(playlist: widget.playlist, streamId: currentStreamId), epgByPlaylistProvider(
)); EpgRequestKey(playlist: widget.playlist, streamId: currentStreamId),
),
);
return epgAsync.when( return epgAsync.when(
data: (epgEntries) { data: (epgEntries) {
@@ -73,7 +73,8 @@ class PlaylistSelectionScreen extends ConsumerWidget {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon(Icons.error_outline, size: 64, color: AppColors.error), const Icon(Icons.error_outline,
size: 64, color: AppColors.error),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
'Error loading playlists', 'Error loading playlists',
@@ -235,7 +236,7 @@ class _PlaylistCardState extends State<_PlaylistCard> {
color: AppColors.primary.withOpacity(0.4), color: AppColors.primary.withOpacity(0.4),
blurRadius: 16, blurRadius: 16,
offset: const Offset(0, 4), offset: const Offset(0, 4),
) ),
] ]
: [], : [],
), ),
@@ -4,10 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import '../../../core/models/playlist_config.dart'; import '../../../core/models/playlist_config.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/utils/responsive_layout.dart'; import '../../../core/utils/responsive_layout.dart';
import '../../../core/widgets/hero_carousel.dart'; import '../../../core/widgets/hero_carousel.dart';
import '../../../core/widgets/themed_loading_screen.dart';
import '../../../core/widgets/glass_container.dart'; import '../../../core/widgets/glass_container.dart';
import '../../../core/widgets/tv_focusable_card.dart'; import '../../../core/widgets/tv_focusable_card.dart';
@@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import '../../../../core/models/playlist_config.dart'; import '../../../../core/models/playlist_config.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/widgets/components/hero_carousel.dart'; import '../../../../core/widgets/components/hero_carousel.dart';
import '../../../../core/widgets/glass_container.dart'; import '../../../../core/widgets/glass_container.dart';
import '../../../../features/iptv/models/xtream_models.dart'; import '../../../../features/iptv/models/xtream_models.dart';