feat: Add streaming handlers for live and VOD content using FFmpeg.

This commit is contained in:
Michael committed 2026-01-01 20:05:40 +01:00
1 parent e27fa6fec3
commit 478107f0a4
3 files changed
+227 -66

No files matched your search

+186 -66
View File
@@ -25,6 +25,28 @@ String _getFFmpegPath() {
return 'ffmpeg'; // Default to PATH return 'ffmpeg'; // Default to PATH
} }
/// Check if NVIDIA GPU acceleration is enabled via environment variable
bool _isNvidiaGpuEnabled() {
final envValue = Platform.environment['NVIDIA_GPU'] ??
Platform.environment['nvidia_gpu'] ??
'false';
return envValue.toLowerCase() == 'true' || envValue == '1';
}
/// Log GPU status on first use
bool _gpuStatusLogged = false;
void _logGpuStatus() {
if (!_gpuStatusLogged) {
if (_isNvidiaGpuEnabled()) {
print('[FFmpeg] NVIDIA GPU acceleration ENABLED (NVDEC/NVENC)');
} else {
print(
'[FFmpeg] Using CPU processing (set NVIDIA_GPU=true to enable GPU)');
}
_gpuStatusLogged = true;
}
}
/// Initialize streaming subsystem /// Initialize streaming subsystem
Future<void> initStreaming() async { Future<void> initStreaming() async {
if (!_hlsTempDir.existsSync()) { if (!_hlsTempDir.existsSync()) {
@@ -122,33 +144,71 @@ Handler createLiveStreamHandler(
'${playlist.dns}/live/${playlist.username}/${playlist.password}/$streamId.ts'; '${playlist.dns}/live/${playlist.username}/${playlist.password}/$streamId.ts';
print('[Live] Streaming via FFmpeg: $targetUrl'); print('[Live] Streaming via FFmpeg: $targetUrl');
// Start FFmpeg to proxy and stabilize the stream // Log GPU status once
// We use "-c copy" to avoid transcoding (Low CPU) but gain FFmpeg's network robustness _logGpuStatus();
final ffmpegArgs = [
'-hide_banner', '-loglevel', 'error', // Check if NVIDIA GPU is enabled
// Full headers to mimic a real video player final useNvidiaGpu = _isNvidiaGpuEnabled();
// Build FFmpeg arguments
final ffmpegArgs = <String>[
'-hide_banner',
'-loglevel',
'error',
];
// NVIDIA GPU Hardware Acceleration (NVDEC for decoding)
if (useNvidiaGpu) {
ffmpegArgs.addAll([
'-hwaccel', 'cuda', // Use NVIDIA CUDA for decoding
'-hwaccel_output_format', 'cuda', // Keep frames on GPU
]);
}
// Input headers and robustness flags
ffmpegArgs.addAll([
'-headers', '-headers',
'User-Agent: VLC/3.0.18 LibVLC/3.0.18\r\nAccept: */*\r\nConnection: keep-alive\r\nReferer: ${playlist.dns}/\r\n', 'User-Agent: VLC/3.0.18 LibVLC/3.0.18\r\nAccept: */*\r\nConnection: keep-alive\r\nReferer: ${playlist.dns}/\r\n',
'-reconnect',
'1',
'-reconnect_at_eof',
'1',
'-reconnect_streamed',
'1',
'-reconnect_delay_max',
'5',
'-rw_timeout',
'15000000',
'-fflags',
'+nobuffer+fastseek+genpts',
'-analyzeduration',
'3000000',
'-probesize',
'5000000',
'-i',
targetUrl,
]);
// HTTP redirect handling and robustness flags // Output encoding options
'-reconnect', '1', if (useNvidiaGpu) {
'-reconnect_at_eof', '1', // With GPU: Use NVENC for any re-encoding needed
'-reconnect_streamed', '1', ffmpegArgs.addAll([
'-reconnect_delay_max', '5', '-c:v', 'copy', // Still copy video (no transcode needed for live)
'-rw_timeout', '15000000', '-c:a', 'aac', // Audio to AAC (browser compatible)
'-fflags', '+nobuffer+fastseek+genpts', // Fast start + fix timestamps '-b:a', '192k',
'-analyzeduration', '3000000', // 3s probe (reliable detection) '-ac', '2',
'-probesize', '5000000', // 5MB probe (original - ensures audio) ]);
} else {
// Without GPU: Standard CPU processing
ffmpegArgs.addAll([
'-c:v', 'copy', // Video: Direct copy (Very low CPU)
'-c:a', 'aac', // Audio: Transcode to AAC
'-b:a', '192k',
'-ac', '2',
]);
}
'-i', targetUrl, ffmpegArgs.addAll(['-f', 'mpegts', 'pipe:1']);
'-c:v', 'copy', // Video: Direct copy (Very low CPU)
'-c:a', 'aac', // Audio: Transcode to AAC (browser compatible)
'-b:a', '192k', // Good audio quality
'-ac', '2', // Stereo downmix (handles 5.1 surround)
'-f', 'mpegts',
'pipe:1', // Output to stdout
];
try { try {
final ffmpegPath = _getFFmpegPath(); final ffmpegPath = _getFFmpegPath();
@@ -285,58 +345,118 @@ Handler createVodStreamHandler(
'${playlist.dns}/$basePath/${playlist.username}/${playlist.password}/$streamId.mkv'; '${playlist.dns}/$basePath/${playlist.username}/${playlist.password}/$streamId.mkv';
print('[VOD] Starting Transcode ($contentType): $targetUrl'); print('[VOD] Starting Transcode ($contentType): $targetUrl');
// Start FFmpeg // Log GPU status and check if enabled
final ffmpegArgs = [ _logGpuStatus();
'-hide_banner', '-loglevel', 'error', final useNvidiaGpu = _isNvidiaGpuEnabled();
'-headers', 'User-Agent: VLC/3.0.18 LibVLC/3.0.18\r\n',
// Robustness for network streams // Build FFmpeg arguments
'-reconnect', '1', final ffmpegArgs = <String>[
'-reconnect_at_eof', '1', '-hide_banner',
'-reconnect_streamed', '1', '-loglevel',
'-reconnect_delay_max', '5', 'error',
'-rw_timeout', '15000000', ];
// NVIDIA GPU Hardware Acceleration
if (useNvidiaGpu) {
ffmpegArgs.addAll([
'-hwaccel',
'cuda',
'-hwaccel_output_format',
'cuda',
]);
}
// Input and robustness flags
ffmpegArgs.addAll([
'-headers',
'User-Agent: VLC/3.0.18 LibVLC/3.0.18\r\n',
'-reconnect',
'1',
'-reconnect_at_eof',
'1',
'-reconnect_streamed',
'1',
'-reconnect_delay_max',
'5',
'-rw_timeout',
'15000000',
'-analyzeduration', '-analyzeduration',
'2000000', // Reduced from 10M to 2M for faster start '2000000',
'-probesize', '2000000', // Reduced from 10M to 2M for faster start '-probesize',
'2000000',
'-i',
targetUrl,
]);
'-i', targetUrl, // Video encoding (GPU or CPU)
if (useNvidiaGpu) {
// NVIDIA NVENC - Hardware encoding (50x+ faster, much lower CPU)
ffmpegArgs.addAll([
'-c:v', 'h264_nvenc', // Use NVIDIA NVENC encoder
'-preset', 'p4', // Good quality/speed balance (p1=fastest, p7=best)
'-tune', 'hq', // High quality mode
'-rc', 'vbr', // Variable bitrate
'-cq', '23', // Quality level (similar to CRF)
'-maxrate', '4000k',
'-bufsize', '8000k',
'-pix_fmt', 'yuv420p',
]);
} else {
// CPU encoding (libx264)
ffmpegArgs.addAll([
'-c:v',
'libx264',
'-preset',
'ultrafast',
'-tune',
'zerolatency',
'-tune',
'fastdecode',
'-crf',
'23',
'-maxrate',
'3000k',
'-bufsize',
'6000k',
'-pix_fmt',
'yuv420p',
'-threads',
'0',
]);
}
// Video: H.264 Ultrafast (Low CPU) // Audio encoding (same for both)
'-c:v', 'libx264', ffmpegArgs.addAll([
'-preset', 'ultrafast', '-c:a',
'-tune', 'zerolatency', // Optimize for low latency startup 'aac',
'-tune', 'fastdecode', '-b:a',
'-crf', '23', '192k',
'-maxrate', '3000k', '-ar',
'-bufsize', '6000k', '48000',
'-pix_fmt', 'yuv420p',
'-threads', '0',
// Audio: AAC (Browser compatible, improved quality)
'-c:a', 'aac',
'-b:a', '192k',
'-ar', '48000',
// Advanced Audio Filtering:
// 1. "pan": Downmix 5.1/7.1 to Stereo with center mix (dialogue) boosted.
// 2. "dynaudnorm": Dynamic Audio Normalizer to boost quiet dialogue and limit loud explosions (Night Mode effect).
'-af', '-af',
'pan=stereo|FL=1.0*FL+0.707*FC+0.5*BL+0.5*SL+0.5*LFE|FR=1.0*FR+0.707*FC+0.5*BR+0.5*SR+0.5*LFE,dynaudnorm=f=150:g=15', 'pan=stereo|FL=1.0*FL+0.707*FC+0.5*BL+0.5*SL+0.5*LFE|FR=1.0*FR+0.707*FC+0.5*BR+0.5*SR+0.5*LFE,dynaudnorm=f=150:g=15',
]);
'-f', 'hls', // HLS output settings
'-hls_time', '4', ffmpegArgs.addAll([
'-hls_list_size', '0', '-f',
'hls',
'-hls_time',
'4',
'-hls_list_size',
'0',
'-hls_playlist_type', '-hls_playlist_type',
'event', // Event type allows immediate playback while transcoding 'event',
'-hls_allow_cache', '1', '-hls_allow_cache',
'-hls_segment_type', 'mpegts', '1',
'-hls_segment_filename', '${streamDir.path}/segment_%03d.ts', '-hls_segment_type',
'-start_number', '0', 'mpegts',
'-hls_segment_filename',
// Write playlist to stdout or file? File is easier for static serving '${streamDir.path}/segment_%03d.ts',
'-start_number',
'0',
'${streamDir.path}/playlist.m3u8', '${streamDir.path}/playlist.m3u8',
]; ]);
final ffmpegPath = _getFFmpegPath(); final ffmpegPath = _getFFmpegPath();
Process.start(ffmpegPath, ffmpegArgs).then((process) { Process.start(ffmpegPath, ffmpegArgs).then((process) {
@@ -63,6 +63,8 @@ class _SettingsKeys {
static const String showClock = 'show_clock'; static const String showClock = 'show_clock';
static const String preferredAspectRatio = 'aspect_ratio'; static const String preferredAspectRatio = 'aspect_ratio';
static const String playerType = 'player_type'; static const String playerType = 'player_type';
// Server/GPU Settings
static const String enableNvidiaGpu = 'enable_nvidia_gpu';
} }
/// Settings state for IPTV preferences with persistence /// Settings state for IPTV preferences with persistence
@@ -86,6 +88,9 @@ class IptvSettings {
final String preferredAspectRatio; final String preferredAspectRatio;
final PlayerType playerType; final PlayerType playerType;
// Server/GPU Settings
final bool enableNvidiaGpu;
const IptvSettings({ const IptvSettings({
// Filters // Filters
this.liveTvCategoryFilter = '', this.liveTvCategoryFilter = '',
@@ -103,6 +108,8 @@ class IptvSettings {
this.showClock = false, this.showClock = false,
this.preferredAspectRatio = 'contain', this.preferredAspectRatio = 'contain',
this.playerType = PlayerType.standard, this.playerType = PlayerType.standard,
// Server/GPU defaults
this.enableNvidiaGpu = false,
}); });
IptvSettings copyWith({ IptvSettings copyWith({
@@ -119,6 +126,7 @@ class IptvSettings {
bool? showClock, bool? showClock,
String? preferredAspectRatio, String? preferredAspectRatio,
PlayerType? playerType, PlayerType? playerType,
bool? enableNvidiaGpu,
}) { }) {
return IptvSettings( return IptvSettings(
liveTvCategoryFilter: liveTvCategoryFilter ?? this.liveTvCategoryFilter, liveTvCategoryFilter: liveTvCategoryFilter ?? this.liveTvCategoryFilter,
@@ -134,6 +142,7 @@ class IptvSettings {
showClock: showClock ?? this.showClock, showClock: showClock ?? this.showClock,
preferredAspectRatio: preferredAspectRatio ?? this.preferredAspectRatio, preferredAspectRatio: preferredAspectRatio ?? this.preferredAspectRatio,
playerType: playerType ?? this.playerType, playerType: playerType ?? this.playerType,
enableNvidiaGpu: enableNvidiaGpu ?? this.enableNvidiaGpu,
); );
} }
@@ -334,6 +343,7 @@ class IptvSettingsNotifier extends StateNotifier<IptvSettings> {
? PlayerType ? PlayerType
.values[remoteSettings[_SettingsKeys.playerType] as int] .values[remoteSettings[_SettingsKeys.playerType] as int]
: null, : null,
enableNvidiaGpu: getValue<bool>(_SettingsKeys.enableNvidiaGpu),
); );
} }
_initialized = true; _initialized = true;
@@ -377,6 +387,7 @@ class IptvSettingsNotifier extends StateNotifier<IptvSettings> {
_SettingsKeys.showClock: state.showClock, _SettingsKeys.showClock: state.showClock,
_SettingsKeys.preferredAspectRatio: state.preferredAspectRatio, _SettingsKeys.preferredAspectRatio: state.preferredAspectRatio,
_SettingsKeys.playerType: state.playerType.index, _SettingsKeys.playerType: state.playerType.index,
_SettingsKeys.enableNvidiaGpu: state.enableNvidiaGpu,
}; };
print('[SettingsProvider] _saveToApi: Saving settings...'); print('[SettingsProvider] _saveToApi: Saving settings...');
@@ -469,6 +480,13 @@ class IptvSettingsNotifier extends StateNotifier<IptvSettings> {
_saveToApi(); _saveToApi();
} }
// ===== Server/GPU Setters =====
void setEnableNvidiaGpu(bool value) {
state = state.copyWith(enableNvidiaGpu: value);
_saveToApi();
}
// Legacy methods // Legacy methods
void setCategoryFilter(String filter) => setLiveTvFilter(filter); void setCategoryFilter(String filter) => setLiveTvFilter(filter);
void clearCategoryFilter() => clearLiveTvFilter(); void clearCategoryFilter() => clearLiveTvFilter();
@@ -174,6 +174,29 @@ class StreamingSettingsTab extends ConsumerWidget {
), ),
], ],
), ),
const SizedBox(height: 16),
// Server/GPU Section
_buildSectionCard(
context: context,
title: 'Serveur / GPU',
icon: Icons.memory,
children: [
SwitchListTile(
title: Text(
'Accélération NVIDIA GPU',
style: GoogleFonts.roboto(fontSize: 14, color: Colors.white),
),
subtitle: Text(
'Utiliser NVENC/NVDEC pour le transcodage (requiert GPU NVIDIA)',
style: GoogleFonts.roboto(fontSize: 12, color: Colors.white54),
),
value: settings.enableNvidiaGpu,
activeThumbColor: Colors.green,
onChanged: (v) => notifier.setEnableNvidiaGpu(v),
),
],
),
const SizedBox(height: 32), const SizedBox(height: 32),
// Info card // Info card