From 0ca6f96710b862d998cfae2e6571afa9d13b8778 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 10 Mar 2026 18:41:46 +0100 Subject: [PATCH] feat: implement Turbo-Start stream acceleration - Add direct MPEG-TS support and optimized HLS tuning - Implement low-latency sync and instant bitrate selection - Achieve TiviMate-like zapping speed by reducing initial buffers --- lib/features/iptv/screens/player_screen.dart | 15 +++++++++--- .../iptv/services/xtream_service.dart | 15 ++++++++++-- web/player.html | 14 ++++++++++- web/player_lite.html | 23 +++++++++++++------ web/player_mobile.html | 18 +++++++++++---- 5 files changed, 67 insertions(+), 18 deletions(-) diff --git a/lib/features/iptv/screens/player_screen.dart b/lib/features/iptv/screens/player_screen.dart index 21288aa..95f0ae2 100644 --- a/lib/features/iptv/screens/player_screen.dart +++ b/lib/features/iptv/screens/player_screen.dart @@ -132,20 +132,29 @@ class _PlayerScreenState extends ConsumerState { } // Store URL for Lite Player + final isMobile = MediaQuery.of(context).size.width < 600; + final isLiveTV = widget.streamType == StreamType.live; + + // TURBO-START: Use direct MPEG-TS for Live TV on Desktop/Android for instant zapping + if (isLiveTV && !isMobile) { + streamUrl = service.getLiveStreamUrlTs(currentStreamId); + } + _currentStreamUrl = streamUrl; final encodedUrl = Uri.encodeComponent(streamUrl); // Force player choice based on stream type: // - Live TV: Player Lite (simple TS playback with mpegts.js) // - VOD/Series: Player Standard (fuller controls, HLS support) - final isMobile = MediaQuery.of(context).size.width < 600; - final isLiveTV = widget.streamType == StreamType.live; + // Cache buster to force reload of updated player.html final cacheBuster = DateTime.now().millisecondsSinceEpoch; final streamTypeParam = widget.streamType == StreamType.live ? 'live' : 'vod'; final playerFile = isMobile ? 'player_mobile.html' : (isLiveTV ? 'player_lite.html' : 'player.html'); - var playerSrc = '$playerFile?url=$encodedUrl&type=$streamTypeParam&v=$cacheBuster'; + + // Inject Turbo-Start flag + var playerSrc = '$playerFile?url=$encodedUrl&type=$streamTypeParam&turbo=true&v=$cacheBuster'; if (startTimeOverride != null) { playerSrc += '&t=$startTimeOverride'; diff --git a/lib/features/iptv/services/xtream_service.dart b/lib/features/iptv/services/xtream_service.dart index 144a272..211931a 100644 --- a/lib/features/iptv/services/xtream_service.dart +++ b/lib/features/iptv/services/xtream_service.dart @@ -89,13 +89,24 @@ class XtreamService { _currentPlaylist = playlist; } - /// Generate stream URL for live TV + /// Generate stream URL for live TV (HLS) String getLiveStreamUrl(String streamId) { if (_currentPlaylist == null) throw Exception('No playlist configured'); - // Use HLS (.m3u8) with a subpath to ensure relative segments resolve correctly return '$_backendBaseUrl/api/live/$streamId/playlist.m3u8'; } + /// Generate stream URL for live TV (Direct MPEG-TS) + /// Faster zapping on compatible players (mpegts.js) + String getLiveStreamUrlTs(String streamId) { + if (_currentPlaylist == null) throw Exception('No playlist configured'); + // Direct link to the .ts stream through our proxy + final dns = _currentPlaylist!.dns; + final user = _currentPlaylist!.username; + final pass = _currentPlaylist!.password; + final url = '$dns/live/$user/$pass/$streamId.ts'; + return '$_backendBaseUrl/api/xtream/${Uri.encodeComponent(url)}'; + } + /// Generate stream URL for VOD (movies) String getVodStreamUrl(String streamId, String containerExtension) { if (_currentPlaylist == null) throw Exception('No playlist configured'); diff --git a/web/player.html b/web/player.html index 5a69706..97067a4 100644 --- a/web/player.html +++ b/web/player.html @@ -250,7 +250,19 @@ return false; } - const hls = new Hls({ maxBufferLength: 45, enableWorker: true }); + const isTurbo = urlParams.get('turbo') === 'true'; + + const hls = new Hls({ + maxBufferLength: isTurbo ? 30 : 45, + enableWorker: true, + liveSyncDurationCount: isTurbo ? 2 : 4, + // Turbo-Start specific + startLevel: isTurbo ? 0 : -1, + initialLiveManifestSize: 1, + abrEwmaDefaultEstimate: 500000, + manifestLoadingRetryDelay: 500, + levelLoadingRetryDelay: 500 + }); window.hlsInstance = hls; hls.loadSource(url); hls.attachMedia(video); diff --git a/web/player_lite.html b/web/player_lite.html index 2c80cd7..eaaf4ff 100644 --- a/web/player_lite.html +++ b/web/player_lite.html @@ -242,13 +242,21 @@ return false; } + const isTurbo = urlParams.get('turbo') === 'true'; + const hls = new Hls({ - maxBufferLength: 60, // Increased for better stability - maxMaxBufferLength: 120, + maxBufferLength: isTurbo ? 30 : 60, + maxMaxBufferLength: isTurbo ? 60 : 120, enableWorker: true, - liveSyncDurationCount: 5, // More stable sync - liveMaxLatencyDurationCount: 15, - lowLatencyMode: true + liveSyncDurationCount: isTurbo ? 2 : 5, // Aggressive sync for Turbo + liveMaxLatencyDurationCount: isTurbo ? 10 : 15, + lowLatencyMode: true, + // Turbo-Start specific + startLevel: isTurbo ? 0 : -1, // Lowest bitrate first + initialLiveManifestSize: 1, + abrEwmaDefaultEstimate: 500000, // Start with conservative estimate + manifestLoadingRetryDelay: 500, + levelLoadingRetryDelay: 500 }); window.hlsInstance = hls; @@ -277,14 +285,15 @@ if (!mpegts.isSupported()) return false; + const isTurbo = urlParams.get('turbo') === 'true'; const player = mpegts.createPlayer({ type: 'mpegts', isLive: isLive, url: url }, { enableWorker: true, - stashInitialSize: 512 * 1024, // Smaller stash for faster start and better audio sync - enableStashBuffer: true, + stashInitialSize: isTurbo ? 128 * 1024 : 512 * 1024, // Tiny stash for Turbo zapping + enableStashBuffer: !isTurbo, // Direct pass-through for Live TV Turbo lazyLoad: false }); diff --git a/web/player_mobile.html b/web/player_mobile.html index dc42b68..344664c 100644 --- a/web/player_mobile.html +++ b/web/player_mobile.html @@ -228,13 +228,21 @@ // Android / Chrome - HLS.js if (isHLS && Hls.isSupported()) { log('Using Hls.js optimized for mobile'); + const isTurbo = urlParams.get('turbo') === 'true'; + const hls = new Hls({ - maxBufferLength: 60, // More buffer for mobile stability - maxMaxBufferLength: 120, - liveSyncDurationCount: 5, - liveMaxLatencyDurationCount: 15, + maxBufferLength: isTurbo ? 30 : 60, + maxMaxBufferLength: isTurbo ? 60 : 120, + liveSyncDurationCount: isTurbo ? 2 : 5, + liveMaxLatencyDurationCount: isTurbo ? 10 : 15, enableWorker: true, - lowLatencyMode: false // Disable low latency for continuity + lowLatencyMode: !isTurbo, // Prioritize stability unless Turbo requested + // Turbo-Start specific + startLevel: isTurbo ? 0 : -1, + initialLiveManifestSize: 1, + abrEwmaDefaultEstimate: 500000, + manifestLoadingRetryDelay: 500, + levelLoadingRetryDelay: 500 }); hls.loadSource(streamUrl); hls.attachMedia(video);