fix(streaming): optimize quality, audio sync, and content update refresh

- Increased FFmpeg video quality to 8Mbps and audio to 192kbps.
- Added audio resync filters and better presets.
- Optimized HLS buffer settings for mobile players to reduce interruptions.
- Reduced API cache duration to 15m and added refresh support for faster content updates.
This commit is contained in:
Michael committed 2026-03-10 10:26:24 +01:00
1 parent 1377b70aee
commit 33d0e0a80d
4 files changed
+57 -45

No files matched your search

+14 -17
View File
@@ -153,16 +153,17 @@ Handler createLiveStreamHandler(
'-i', targetUrl, '-i', targetUrl,
// Video // Video
if (useNvidiaGpu) ...[ if (useNvidiaGpu) ...[
'-c:v', 'h264_nvenc', '-preset', 'p1', '-tune', 'll', '-c:v', 'h264_nvenc', '-preset', 'p4', '-tune', 'hq', // Better quality preset for NVENC
'-b:v', '3500k', '-maxrate', '4000k', '-bufsize', '7000k', '-b:v', '8000k', '-maxrate', '12000k', '-bufsize', '16000k', // Quality native / high bitrate
'-g', '50', // Force keyframes every 2s '-g', '50',
] else ...[ ] else ...[
'-c:v', 'libx264', '-preset', 'ultrafast', '-tune', 'zerolatency', '-c:v', 'libx264', '-preset', 'medium', '-tune', 'zerolatency', // Better detail than ultrafast
'-b:v', '3000k', '-maxrate', '3500k', '-bufsize', '6000k', '-b:v', '6000k', '-maxrate', '8000k', '-bufsize', '12000k',
'-g', '50', // Force keyframes every 2s (CRITICAL for HLS) '-g', '50',
], ],
// Audio // Audio: High quality + Sync filter
'-c:a', 'aac', '-b:a', '128k', '-ac', '2', '-c:a', 'aac', '-b:a', '192k', '-ac', '2', '-ar', '48000',
'-af', 'aresample=async=1',
// HLS Sliding Window // HLS Sliding Window
'-f', 'hls', '-f', 'hls',
'-hls_time', '2', '-hls_time', '2',
@@ -390,20 +391,16 @@ Handler createVodStreamHandler(
'-c:v', '-c:v',
'libx264', 'libx264',
'-preset', '-preset',
'ultrafast', 'medium', // Quality over raw speed for VOD
'-tune',
'zerolatency',
'-tune',
'fastdecode',
'-crf', '-crf',
'23', '18', // Near-native visual quality
'-maxrate', '-maxrate',
'3000k', '12000k',
'-bufsize', '-bufsize',
'6000k', '24000k',
'-pix_fmt', '-pix_fmt',
'yuv420p', 'yuv420p',
'-g', '48', // Force keyframes '-g', '48',
'-threads', '-threads',
'0', '0',
]); ]);
+30 -18
View File
@@ -28,8 +28,8 @@ class XtreamService {
// Use MemCacheStore on Web to avoid path errors // Use MemCacheStore on Web to avoid path errors
_cacheOptions = CacheOptions( _cacheOptions = CacheOptions(
store: kIsWeb ? MemCacheStore() : HiveCacheStore('./cache'), store: kIsWeb ? MemCacheStore() : HiveCacheStore('./cache'),
policy: CachePolicy.forceCache, policy: CachePolicy.refresh, // Allow refresh if stale
maxStale: const Duration(hours: 1), maxStale: const Duration(minutes: 15), // Reduced from 1h to 15m for better sync
priority: CachePriority.high, priority: CachePriority.high,
); );
@@ -112,7 +112,7 @@ class XtreamService {
'username': _currentPlaylist!.username, 'username': _currentPlaylist!.username,
'password': _currentPlaylist!.password, 'password': _currentPlaylist!.password,
}, },
options: Options(extra: _cacheOptions.toExtra()), options: Options(extra: options),
); );
return response.data as Map<String, dynamic>; return response.data as Map<String, dynamic>;
@@ -133,7 +133,7 @@ class XtreamService {
'password': _currentPlaylist!.password, 'password': _currentPlaylist!.password,
'action': 'get_live_categories', 'action': 'get_live_categories',
}, },
options: Options(extra: _cacheOptions.toExtra()), options: Options(extra: options),
); );
final List<dynamic> categories = response.data as List<dynamic>; final List<dynamic> categories = response.data as List<dynamic>;
@@ -157,9 +157,13 @@ class XtreamService {
/// Get all live TV channels grouped by category /// Get all live TV channels grouped by category
/// ///
/// Returns a Map where key is category name and value is list of channels /// Returns a Map where key is category name and value is list of channels
Future<Map<String, List<Channel>>> getLiveChannels() async { Future<Map<String, List<Channel>>> getLiveChannels({bool refresh = false}) async {
if (_currentPlaylist == null) throw Exception('No playlist configured'); if (_currentPlaylist == null) throw Exception('No playlist configured');
final options = refresh
? _cacheOptions.copyWith(policy: CachePolicy.refreshForceCache).toExtra()
: _cacheOptions.toExtra();
try { try {
// First, load categories to get the mapping // First, load categories to get the mapping
final categoryMap = await _getLiveCategories(); final categoryMap = await _getLiveCategories();
@@ -171,7 +175,7 @@ class XtreamService {
'password': _currentPlaylist!.password, 'password': _currentPlaylist!.password,
'action': 'get_live_streams', 'action': 'get_live_streams',
}, },
options: Options(extra: _cacheOptions.toExtra()), options: Options(extra: options),
); );
final List<dynamic> streams = response.data as List<dynamic>; final List<dynamic> streams = response.data as List<dynamic>;
@@ -212,7 +216,7 @@ class XtreamService {
'password': _currentPlaylist!.password, 'password': _currentPlaylist!.password,
'action': 'get_vod_categories', 'action': 'get_vod_categories',
}, },
options: Options(extra: _cacheOptions.toExtra()), options: Options(extra: options),
); );
final List<dynamic> categories = response.data as List<dynamic>; final List<dynamic> categories = response.data as List<dynamic>;
@@ -232,9 +236,13 @@ class XtreamService {
} }
/// Get all VOD items (movies) grouped by category /// Get all VOD items (movies) grouped by category
Future<Map<String, List<VodItem>>> getVodItems() async { Future<Map<String, List<VodItem>>> getVodItems({bool refresh = false}) async {
if (_currentPlaylist == null) throw Exception('No playlist configured'); if (_currentPlaylist == null) throw Exception('No playlist configured');
final options = refresh
? _cacheOptions.copyWith(policy: CachePolicy.refreshForceCache).toExtra()
: _cacheOptions.toExtra();
try { try {
final categoryMap = await _getVodCategories(); final categoryMap = await _getVodCategories();
@@ -245,7 +253,7 @@ class XtreamService {
'password': _currentPlaylist!.password, 'password': _currentPlaylist!.password,
'action': 'get_vod_streams', 'action': 'get_vod_streams',
}, },
options: Options(extra: _cacheOptions.toExtra()), options: Options(extra: options),
); );
final List<dynamic> vods = response.data as List<dynamic>; final List<dynamic> vods = response.data as List<dynamic>;
@@ -283,7 +291,7 @@ class XtreamService {
'password': _currentPlaylist!.password, 'password': _currentPlaylist!.password,
'action': 'get_series_categories', 'action': 'get_series_categories',
}, },
options: Options(extra: _cacheOptions.toExtra()), options: Options(extra: options),
); );
final List<dynamic> categories = response.data as List<dynamic>; final List<dynamic> categories = response.data as List<dynamic>;
@@ -303,9 +311,13 @@ class XtreamService {
} }
/// Get all series grouped by category /// Get all series grouped by category
Future<Map<String, List<Series>>> getSeries() async { Future<Map<String, List<Series>>> getSeries({bool refresh = false}) async {
if (_currentPlaylist == null) throw Exception('No playlist configured'); if (_currentPlaylist == null) throw Exception('No playlist configured');
final options = refresh
? _cacheOptions.copyWith(policy: CachePolicy.refreshForceCache).toExtra()
: _cacheOptions.toExtra();
try { try {
final categoryMap = await _getSeriesCategories(); final categoryMap = await _getSeriesCategories();
@@ -316,7 +328,7 @@ class XtreamService {
'password': _currentPlaylist!.password, 'password': _currentPlaylist!.password,
'action': 'get_series', 'action': 'get_series',
}, },
options: Options(extra: _cacheOptions.toExtra()), options: Options(extra: options),
); );
final List<dynamic> seriesList = response.data as List<dynamic>; final List<dynamic> seriesList = response.data as List<dynamic>;
@@ -358,7 +370,7 @@ class XtreamService {
'password': _currentPlaylist!.password, 'password': _currentPlaylist!.password,
'action': 'get_vod_streams', 'action': 'get_vod_streams',
}, },
options: Options(extra: _cacheOptions.toExtra()), options: Options(extra: options),
); );
final List<dynamic> allMovies = response.data as List<dynamic>; final List<dynamic> allMovies = response.data as List<dynamic>;
@@ -397,7 +409,7 @@ class XtreamService {
'password': _currentPlaylist!.password, 'password': _currentPlaylist!.password,
'action': 'get_vod_streams', 'action': 'get_vod_streams',
}, },
options: Options(extra: _cacheOptions.toExtra()), options: Options(extra: options),
); );
final List<dynamic> allMovies = response.data as List<dynamic>; final List<dynamic> allMovies = response.data as List<dynamic>;
@@ -438,7 +450,7 @@ class XtreamService {
'password': _currentPlaylist!.password, 'password': _currentPlaylist!.password,
'action': 'get_series', 'action': 'get_series',
}, },
options: Options(extra: _cacheOptions.toExtra()), options: Options(extra: options),
); );
final List<dynamic> allSeries = response.data as List<dynamic>; final List<dynamic> allSeries = response.data as List<dynamic>;
@@ -477,7 +489,7 @@ class XtreamService {
'password': _currentPlaylist!.password, 'password': _currentPlaylist!.password,
'action': 'get_series', 'action': 'get_series',
}, },
options: Options(extra: _cacheOptions.toExtra()), options: Options(extra: options),
); );
final List<dynamic> allSeries = response.data as List<dynamic>; final List<dynamic> allSeries = response.data as List<dynamic>;
@@ -515,7 +527,7 @@ class XtreamService {
'action': 'get_series_info', 'action': 'get_series_info',
'series_id': seriesId, 'series_id': seriesId,
}, },
options: Options(extra: _cacheOptions.toExtra()), options: Options(extra: options),
); );
return xm.SeriesInfo.fromJson(response.data as Map<String, dynamic>); return xm.SeriesInfo.fromJson(response.data as Map<String, dynamic>);
@@ -539,7 +551,7 @@ class XtreamService {
'action': 'get_vod_info', 'action': 'get_vod_info',
'vod_id': vodId, 'vod_id': vodId,
}, },
options: Options(extra: _cacheOptions.toExtra()), options: Options(extra: options),
); );
final data = response.data as Map<String, dynamic>; final data = response.data as Map<String, dynamic>;
+8 -5
View File
@@ -243,10 +243,12 @@
} }
const hls = new Hls({ const hls = new Hls({
maxBufferLength: 60, maxBufferLength: 30, // 30s is plenty for desktop stability
maxMaxBufferLength: 120, maxMaxBufferLength: 60,
enableWorker: true, enableWorker: true,
liveSyncDurationCount: 5 liveSyncDurationCount: 3,
liveMaxLatencyDurationCount: 10,
lowLatencyMode: true
}); });
window.hlsInstance = hls; window.hlsInstance = hls;
@@ -281,8 +283,9 @@
url: url url: url
}, { }, {
enableWorker: true, enableWorker: true,
stashInitialSize: 1024 * 1024, stashInitialSize: 512 * 1024, // Smaller stash for faster start and better audio sync
enableStashBuffer: true enableStashBuffer: true,
lazyLoad: false
}); });
player.attachMediaElement(video); player.attachMediaElement(video);
+5 -5
View File
@@ -229,12 +229,12 @@
if (isHLS && Hls.isSupported()) { if (isHLS && Hls.isSupported()) {
log('Using Hls.js optimized for mobile'); log('Using Hls.js optimized for mobile');
const hls = new Hls({ const hls = new Hls({
maxBufferLength: 90, maxBufferLength: 30,
maxMaxBufferLength: 180, maxMaxBufferLength: 60,
liveSyncDurationCount: 6, liveSyncDurationCount: 3,
liveMaxLatencyDurationCount: 12, liveMaxLatencyDurationCount: 10,
enableWorker: true, enableWorker: true,
lowLatencyMode: false lowLatencyMode: true
}); });
hls.loadSource(streamUrl); hls.loadSource(streamUrl);
hls.attachMedia(video); hls.attachMedia(video);