From 85deb540d25a3b01858c232a34ef6f948dae781c Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Sun, 14 Dec 2025 00:17:13 +0100 Subject: [PATCH] feat: implement Xtream Codes API service for managing IPTV connections, streams, and channel data. --- .../iptv/services/xtream_service.dart | 12 +++++-- web/player.html | 14 ++++++++ web/player_lite.html | 34 +++++++++++++------ 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/lib/features/iptv/services/xtream_service.dart b/lib/features/iptv/services/xtream_service.dart index 5b7f2d4..c34e2ae 100644 --- a/lib/features/iptv/services/xtream_service.dart +++ b/lib/features/iptv/services/xtream_service.dart @@ -72,16 +72,24 @@ class XtreamService { String getVodStreamUrl(String streamId, String containerExtension) { if (_currentPlaylist == null) throw Exception('No playlist configured'); + // Use FFmpeg transcoding to ensure audio compatibility (converts AC3/DTS to AAC) final url = '${_currentPlaylist!.dns}/movie/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.$containerExtension'; - return _wrapWithProxy(url); + + final baseUrl = html.window.location.origin; + final encodedUrl = Uri.encodeComponent(url); + return '$baseUrl/api/stream/$streamId?url=$encodedUrl'; } /// Generate stream URL for series episodes String getSeriesStreamUrl(String streamId, String containerExtension) { if (_currentPlaylist == null) throw Exception('No playlist configured'); + // Use FFmpeg transcoding to ensure audio compatibility final url = '${_currentPlaylist!.dns}/series/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.$containerExtension'; - return _wrapWithProxy(url); + + final baseUrl = html.window.location.origin; + final encodedUrl = Uri.encodeComponent(url); + return '$baseUrl/api/stream/$streamId?url=$encodedUrl'; } /// Authenticate and get server info diff --git a/web/player.html b/web/player.html index 6dc3064..b4cda85 100644 --- a/web/player.html +++ b/web/player.html @@ -511,6 +511,20 @@ log('Stream URL: ' + streamUrl.substring(0, 80) + '...'); + // SPECIAL: /api/stream/ endpoint outputs MPEG-TS, use mpegts.js directly + if (streamUrl.includes('/api/stream/')) { + log('Detected /api/stream/ endpoint - using MPEG-TS player for transcoded stream'); + showLoading('Initializing stream (transcoding audio)...'); + + if (mpegts.isSupported()) { + await playWithMpegTS(streamUrl); + return; + } else { + showError('MPEG-TS playback not supported on this device'); + return; + } + } + // Determine stream type and try appropriate player const lowerUrl = streamUrl.toLowerCase(); const ext = urlParams.get('ext') ? urlParams.get('ext').toLowerCase() : ''; diff --git a/web/player_lite.html b/web/player_lite.html index de43fd7..90aa7ab 100644 --- a/web/player_lite.html +++ b/web/player_lite.html @@ -239,8 +239,8 @@ return true; } - async function playWithMpegTS(url) { - log('Trying mpegts.js with: ' + url); + async function playWithMpegTS(url, isLive = true) { + log('Trying mpegts.js with: ' + url + ' (isLive: ' + isLive + ')'); showLoading('Loading MPEG-TS stream...'); if (!mpegts.isSupported()) { @@ -252,17 +252,21 @@ mpegts.getLoggingControl().enableError = true; } + // Configure player differently for VOD vs Live + const playerConfig = { + enableWorker: true, + enableStashBuffer: !isLive, // Enable for VOD, disable for live + stashInitialSize: isLive ? 384 * 1024 : 512 * 1024, + autoCleanupSourceBuffer: true, + fixAudioTimestampGap: isLive, // Only for live streams + liveBufferLatencyChasing: isLive, // Disable for VOD + }; + const player = mpegts.createPlayer({ type: 'mpegts', - isLive: true, + isLive: isLive, // Use parameter to enable/disable seeking url: url, - }, { - enableWorker: true, - enableStashBuffer: true, - stashInitialSize: 384 * 1024, - autoCleanupSourceBuffer: true, - fixAudioTimestampGap: true - }); + }, playerConfig); player.attachMediaElement(video); player.load(); @@ -357,6 +361,16 @@ log('Stream type: ' + streamType); const lowerUrl = streamUrl.toLowerCase(); + // SPECIAL: /api/stream/ endpoint outputs MPEG-TS, use mpegts.js directly + if (streamUrl.includes('/api/stream/')) { + log('Detected /api/stream/ endpoint - using MPEG-TS player for transcoded stream'); + showLoading('Initializing stream (transcoding audio)...'); + // For VOD, use isLive:false to enable seeking/duration + const isVodStream = (streamType === 'vod'); + await playWithMpegTS(streamUrl, !isVodStream); // true for live, false for vod + return; + } + // Use type parameter if available, otherwise detect from URL if (streamType === 'vod') { // VOD content - use direct HTML5 playback