From b0de7bce86329ae0943efec6c90dec4e4c7c96f2 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Sun, 14 Dec 2025 21:48:37 +0100 Subject: [PATCH] feat: introduce Xtream Codes API service for IPTV content access and streaming. --- bin/api/hls_handler.dart | 20 ++++++----- .../iptv/services/xtream_service.dart | 4 +-- web/player.html | 9 +++++ web/player_lite.html | 35 +++++++++---------- 4 files changed, 39 insertions(+), 29 deletions(-) diff --git a/bin/api/hls_handler.dart b/bin/api/hls_handler.dart index 4d62dc6..a17e837 100644 --- a/bin/api/hls_handler.dart +++ b/bin/api/hls_handler.dart @@ -152,18 +152,20 @@ Handler createStreamInitHandler() { final isLiveStream = iptvUrl.endsWith('.ts') || iptvUrl.contains('/live/'); if (isLiveStream) { - // LIVE TV: Fast transcoding with ultrafast preset + // LIVE TV: Transcode to H.264 for browser compatibility + // Source may be HEVC/H.265 which browsers don't support + // Using superfast preset for minimal CPU usage ffmpegArgs.addAll([ '-c:v', 'libx264', - '-preset', 'ultrafast', - '-tune', 'zerolatency', - '-profile:v', 'main', // Main profile for better quality than baseline - '-level', '4.0', + '-preset', 'superfast', // Fast encoding to prevent buffering + '-tune', 'zerolatency', // Minimize latency + '-profile:v', 'baseline', // Maximum browser compatibility + '-level', '3.1', '-pix_fmt', 'yuv420p', - '-g', '48', // 2s GOP at 24fps - '-b:v', '4000k', // 4Mbps for good quality live - '-maxrate', '4500k', - '-bufsize', '8000k', + '-g', '30', // Keyframe every second at 30fps + '-b:v', '2500k', // 2.5Mbps - balance quality/speed + '-maxrate', '3000k', + '-bufsize', '5000k', ]); } else { // VOD: Higher quality transcoding diff --git a/lib/features/iptv/services/xtream_service.dart b/lib/features/iptv/services/xtream_service.dart index 503d964..81f21f7 100644 --- a/lib/features/iptv/services/xtream_service.dart +++ b/lib/features/iptv/services/xtream_service.dart @@ -60,14 +60,14 @@ class XtreamService { /// Generate stream URL for live TV /// /// Returns the proxy URL for direct MPEG-TS streaming. - /// Player will use mpegts.js for playback (no server-side transcoding). + /// Player uses mpegts.js for playback. String getLiveStreamUrl(String streamId) { if (_currentPlaylist == null) throw Exception('No playlist configured'); // Direct .ts URL for MPEG-TS streaming final iptvUrl = '${_currentPlaylist!.dns}/live/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.ts'; - // Use proxy for direct streaming (no FFmpeg transcoding) + // Use proxy for direct streaming return _wrapWithProxy(iptvUrl); } diff --git a/web/player.html b/web/player.html index 79b79be..2826249 100644 --- a/web/player.html +++ b/web/player.html @@ -541,6 +541,15 @@ // Determine stream type and try appropriate player const lowerUrl = streamUrl.toLowerCase(); const ext = urlParams.get('ext') ? urlParams.get('ext').toLowerCase() : ''; + const streamTypeParam = urlParams.get('type'); + + // VOD via proxy (/api/xtream/) - use direct playback + if (streamUrl.includes('/api/xtream/') && streamTypeParam === 'vod') { + log('VOD via proxy - using direct playback'); + showLoading('Loading video...'); + await playDirect(streamUrl); + return; + } // HLS streams first if (lowerUrl.includes('.m3u8') || lowerUrl.includes('/hls/')) { diff --git a/web/player_lite.html b/web/player_lite.html index 9e9280f..fda7f50 100644 --- a/web/player_lite.html +++ b/web/player_lite.html @@ -252,20 +252,28 @@ mpegts.getLoggingControl().enableError = true; } - // Configure player differently for VOD vs Live + // Optimized configuration for stable Live TV playback const playerConfig = { enableWorker: true, - enableStashBuffer: !isLive, // Enable for VOD, disable for live - stashInitialSize: isLive ? 384 * 1024 : 512 * 1024, + enableStashBuffer: true, // Enable buffering for stability + stashInitialSize: 1024 * 1024, // 1MB initial buffer autoCleanupSourceBuffer: true, - fixAudioTimestampGap: isLive, // Only for live streams - liveBufferLatencyChasing: isLive, // Disable for VOD + autoCleanupMaxBackwardDuration: 60, // Keep 1 min of buffer + autoCleanupMinBackwardDuration: 30, // Cleanup after 30s + fixAudioTimestampGap: true, // Fix audio sync issues + liveBufferLatencyChasing: false, // Disable for stability (prevents skipping) + liveSync: false, // Don't auto-sync to edge + lazyLoad: false, // Load immediately + lazyLoadMaxDuration: 0, + lazyLoadRecoverDuration: 0, }; const player = mpegts.createPlayer({ type: 'mpegts', - isLive: isLive, // Use parameter to enable/disable seeking + isLive: isLive, url: url, + hasAudio: true, + hasVideo: true, }, playerConfig); player.attachMediaElement(video); @@ -361,20 +369,11 @@ log('Stream type: ' + streamType); const lowerUrl = streamUrl.toLowerCase(); - // LIVE TV: Simple MPEG-TS playback with mpegts.js + // LIVE TV: Direct MPEG-TS playback with mpegts.js if (streamType === 'live') { - log('Live TV mode: Using MPEG-TS direct playback'); + log('Live TV mode - using mpegts.js direct'); showLoading('Connecting to live stream...'); - - // Check for HLS first (.m3u8) - if (lowerUrl.includes('.m3u8') || lowerUrl.includes('/hls/')) { - log('Live HLS detected'); - await playWithHLS(streamUrl); - } else { - // Default: MPEG-TS with mpegts.js - log('Using mpegts.js for MPEG-TS'); - await playWithMpegTS(streamUrl, true); // isLive=true - } + await playWithMpegTS(streamUrl, true); return; }