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