mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-21 22:18:04 +02:00
feat: Add streaming handlers for live and VOD content using FFmpeg.
This commit is contained in:
1 parent
e27fa6fec3
commit
478107f0a4
3 files changed
+227
-66
No files matched your search
+186
-66
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user