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