Merge pull request #28 from R0m1k3/antigravity

feat: Implement Xtream service for IPTV API integration and add a web…
This commit is contained in:
LogiFlow authored and GitHub committed 2025-12-06 11:35:05 +01:00
commit b0a67de83f
2 files changed
+133 -25

No files matched your search

@@ -54,13 +54,12 @@ class XtreamService {
/// Generate stream URL for live TV /// Generate stream URL for live TV
/// ///
/// Uses .ts format for direct continuous streaming (avoids HLS segment 403 issues) /// Uses .m3u8 format for HLS streaming (compatible with hls.js player)
String getLiveStreamUrl(String streamId) { String getLiveStreamUrl(String streamId) {
if (_currentPlaylist == null) throw Exception('No playlist configured'); if (_currentPlaylist == null) throw Exception('No playlist configured');
// Use .ts format instead of .m3u8 for direct streaming // Use .m3u8 format for HLS streaming - the player.html handles this via hls.js
// This avoids the need toseparate segment requests which may be blocked by CORS or 403 final url = '${_currentPlaylist!.dns}/live/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.m3u8';
final url = '${_currentPlaylist!.dns}/live/${_currentPlaylist!.username}/${_currentPlaylist!.password}/$streamId.ts';
return _wrapWithProxy(url); return _wrapWithProxy(url);
} }
+130 -21
View File
@@ -56,6 +56,7 @@
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
color: #fff; color: #fff;
text-align: center;
} }
.spinner { .spinner {
@@ -65,6 +66,7 @@
border-top-color: transparent; border-top-color: transparent;
border-radius: 50%; border-radius: 50%;
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
margin: 0 auto 16px;
} }
@keyframes spin { @keyframes spin {
@@ -72,6 +74,20 @@
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
#debug {
position: absolute;
bottom: 10px;
left: 10px;
color: #0f0;
font-size: 12px;
font-family: monospace;
background: rgba(0, 0, 0, 0.7);
padding: 8px;
border-radius: 4px;
max-width: 80%;
word-break: break-all;
}
</style> </style>
</head> </head>
@@ -80,26 +96,38 @@
<video id="video" controls autoplay></video> <video id="video" controls autoplay></video>
<div id="loading"> <div id="loading">
<div class="spinner"></div> <div class="spinner"></div>
<div id="loading-text">Loading stream...</div>
</div> </div>
<div id="error"> <div id="error">
<div id="error-icon">⚠</div> <div id="error-icon">⚠</div>
<div id="error-message">Failed to load stream</div> <div id="error-message">Failed to load stream</div>
</div> </div>
<div id="debug"></div>
</div> </div>
<!-- Try HLS.js first, then fall back to mpegts.js -->
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/mpegts.js@latest/dist/mpegts.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/mpegts.js@latest/dist/mpegts.min.js"></script>
<script> <script>
const video = document.getElementById('video'); const video = document.getElementById('video');
const loading = document.getElementById('loading'); const loading = document.getElementById('loading');
const loadingText = document.getElementById('loading-text');
const error = document.getElementById('error'); const error = document.getElementById('error');
const errorMessage = document.getElementById('error-message'); const errorMessage = document.getElementById('error-message');
const debug = document.getElementById('debug');
// Get stream URL from query parameter // Get stream URL from query parameter
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const streamUrl = urlParams.get('url'); let streamUrl = urlParams.get('url');
function showLoading() { function log(msg) {
console.log(msg);
debug.textContent = msg;
}
function showLoading(msg) {
loading.style.display = 'block'; loading.style.display = 'block';
loadingText.textContent = msg || 'Loading stream...';
error.style.display = 'none'; error.style.display = 'none';
} }
@@ -113,30 +141,77 @@
errorMessage.textContent = message || 'Failed to load stream'; errorMessage.textContent = message || 'Failed to load stream';
} }
if (!streamUrl) { async function playWithHLS(url) {
showError('No stream URL provided'); log('Trying HLS.js with: ' + url);
} else if (mpegts.isSupported()) { showLoading('Loading HLS stream...');
showLoading();
if (!Hls.isSupported()) {
// Try native HLS (Safari)
if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = url;
video.addEventListener('canplay', () => {
hideLoading();
video.play().catch(e => log('Autoplay prevented: ' + e));
});
video.addEventListener('error', () => {
showError('HLS playback error');
});
return true;
}
return false;
}
const hls = new Hls({
xhrSetup: function (xhr, url) {
// Don't add extra headers - the proxy already handles this
}
});
hls.loadSource(url);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
log('HLS manifest parsed, starting playback');
hideLoading();
video.play().catch(e => log('Autoplay prevented: ' + e));
});
hls.on(Hls.Events.ERROR, (event, data) => {
log('HLS Error: ' + data.type + ' - ' + data.details);
if (data.fatal) {
showError('HLS Error: ' + data.details);
}
});
return true;
}
async function playWithMpegTS(url) {
log('Trying mpegts.js with: ' + url);
showLoading('Loading MPEG-TS stream...');
if (!mpegts.isSupported()) {
return false;
}
const player = mpegts.createPlayer({ const player = mpegts.createPlayer({
type: 'mse', // could be 'flv' or 'mpegts' type: 'mpegts',
isLive: true, isLive: true,
url: streamUrl, url: url,
}, { }, {
enableWorker: true, enableWorker: true,
enableStashBuffer: false, enableStashBuffer: true,
stashInitialSize: 128, stashInitialSize: 384 * 1024,
liveBufferLatencyChasing: true, autoCleanupSourceBuffer: true,
liveBufferLatencyMaxLatency: 1.5,
liveBufferLatencyMinRemain: 0.5,
}); });
player.attachMediaElement(video); player.attachMediaElement(video);
player.load(); player.load();
video.addEventListener('canplay', () => { video.addEventListener('canplay', () => {
log('MPEG-TS stream ready');
hideLoading(); hideLoading();
video.play().catch(e => console.log('Autoplay prevented:', e)); video.play().catch(e => log('Autoplay prevented: ' + e));
}); });
video.addEventListener('error', (e) => { video.addEventListener('error', (e) => {
@@ -144,16 +219,50 @@
}); });
player.on(mpegts.Events.ERROR, (errType, errDetail, errInfo) => { player.on(mpegts.Events.ERROR, (errType, errDetail, errInfo) => {
console.error('mpegts error:', errType, errDetail, errInfo); log('mpegts error: ' + errType + ' - ' + errDetail);
showError(`Stream error: ${errDetail}`); showError('Stream error: ' + errDetail);
}); });
player.on(mpegts.Events.LOADING_COMPLETE, () => { return true;
console.log('Loading complete');
});
} else {
showError('MPEG-TS playback is not supported in this browser');
} }
async function playDirect(url) {
log('Trying direct playback: ' + url);
showLoading('Loading stream directly...');
video.src = url;
video.addEventListener('canplay', () => {
hideLoading();
video.play().catch(e => log('Autoplay prevented: ' + e));
});
video.addEventListener('error', () => {
showError('Direct playback error');
});
}
async function initPlayer() {
if (!streamUrl) {
showError('No stream URL provided');
return;
}
log('Stream URL: ' + streamUrl.substring(0, 80) + '...');
// Determine stream type and try appropriate player
if (streamUrl.includes('.m3u8')) {
await playWithHLS(streamUrl);
} else if (streamUrl.includes('.ts')) {
const success = await playWithMpegTS(streamUrl);
if (!success) {
await playDirect(streamUrl);
}
} else {
// Try HLS first, then MPEG-TS
await playWithHLS(streamUrl);
}
}
initPlayer();
</script> </script>
</body> </body>