diff --git a/bin/api/streaming_handler.dart b/bin/api/streaming_handler.dart index 71ba35b..2629e8a 100644 --- a/bin/api/streaming_handler.dart +++ b/bin/api/streaming_handler.dart @@ -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', ]); diff --git a/lib/features/iptv/services/xtream_service.dart b/lib/features/iptv/services/xtream_service.dart index c220dc6..e297bd2 100644 --- a/lib/features/iptv/services/xtream_service.dart +++ b/lib/features/iptv/services/xtream_service.dart @@ -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; @@ -133,7 +133,7 @@ class XtreamService { 'password': _currentPlaylist!.password, 'action': 'get_live_categories', }, - options: Options(extra: _cacheOptions.toExtra()), + options: Options(extra: options), ); final List categories = response.data as List; @@ -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>> getLiveChannels() async { + Future>> 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 streams = response.data as List; @@ -212,7 +216,7 @@ class XtreamService { 'password': _currentPlaylist!.password, 'action': 'get_vod_categories', }, - options: Options(extra: _cacheOptions.toExtra()), + options: Options(extra: options), ); final List categories = response.data as List; @@ -232,9 +236,13 @@ class XtreamService { } /// Get all VOD items (movies) grouped by category - Future>> getVodItems() async { + Future>> 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 vods = response.data as List; @@ -283,7 +291,7 @@ class XtreamService { 'password': _currentPlaylist!.password, 'action': 'get_series_categories', }, - options: Options(extra: _cacheOptions.toExtra()), + options: Options(extra: options), ); final List categories = response.data as List; @@ -303,9 +311,13 @@ class XtreamService { } /// Get all series grouped by category - Future>> getSeries() async { + Future>> 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 seriesList = response.data as List; @@ -358,7 +370,7 @@ class XtreamService { 'password': _currentPlaylist!.password, 'action': 'get_vod_streams', }, - options: Options(extra: _cacheOptions.toExtra()), + options: Options(extra: options), ); final List allMovies = response.data as List; @@ -397,7 +409,7 @@ class XtreamService { 'password': _currentPlaylist!.password, 'action': 'get_vod_streams', }, - options: Options(extra: _cacheOptions.toExtra()), + options: Options(extra: options), ); final List allMovies = response.data as List; @@ -438,7 +450,7 @@ class XtreamService { 'password': _currentPlaylist!.password, 'action': 'get_series', }, - options: Options(extra: _cacheOptions.toExtra()), + options: Options(extra: options), ); final List allSeries = response.data as List; @@ -477,7 +489,7 @@ class XtreamService { 'password': _currentPlaylist!.password, 'action': 'get_series', }, - options: Options(extra: _cacheOptions.toExtra()), + options: Options(extra: options), ); final List allSeries = response.data as List; @@ -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); @@ -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; diff --git a/web/player_lite.html b/web/player_lite.html index f91a2f7..242291c 100644 --- a/web/player_lite.html +++ b/web/player_lite.html @@ -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); diff --git a/web/player_mobile.html b/web/player_mobile.html index b5419ef..425c868 100644 --- a/web/player_mobile.html +++ b/web/player_mobile.html @@ -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);