mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-11 17:30:00 +02:00
220 lines
8.7 KiB
HTML
220 lines
8.7 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>XtremFlow Player</title>
|
|
<style>
|
|
body, html { margin: 0; padding: 0; width: 100%; height: 100%; background: #000; overflow: hidden; }
|
|
#video { width: 100%; height: 100%; object-fit: contain; }
|
|
#error-overlay { display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); color: white; display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 10; font-family: sans-serif; }
|
|
.hidden { display: none !important; }
|
|
</style>
|
|
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.4.12/dist/hls.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/mpegts.js@1.7.3/dist/mpegts.min.js"></script>
|
|
</head>
|
|
<body>
|
|
|
|
<video id="video" autoplay playsinline></video>
|
|
|
|
<div id="error-overlay" class="hidden">
|
|
<h2 id="error-title">Playback Error</h2>
|
|
<p id="error-msg">Unknown error</p>
|
|
</div>
|
|
|
|
<script>
|
|
const video = document.getElementById('video');
|
|
const errorOverlay = document.getElementById('error-overlay');
|
|
const errorMsg = document.getElementById('error-msg');
|
|
|
|
let hlsKey = null;
|
|
let mpegtsPlayer = null;
|
|
|
|
// Parse URL params
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const streamUrl = urlParams.get('url');
|
|
const streamType = urlParams.get('type'); // 'live' or 'vod'
|
|
const startTime = parseFloat(urlParams.get('t')) || 0;
|
|
|
|
function log(msg) {
|
|
console.log(`[Player] ${msg}`);
|
|
}
|
|
|
|
function showError(msg) {
|
|
log('Error: ' + msg);
|
|
errorMsg.innerText = msg;
|
|
errorOverlay.classList.remove('hidden');
|
|
// Notify Flutter
|
|
window.parent.postMessage({ type: 'error', message: msg }, '*');
|
|
}
|
|
|
|
// --- Communication with Flutter ---
|
|
window.addEventListener('message', (event) => {
|
|
const data = event.data;
|
|
if (!data) return;
|
|
|
|
switch(data.type) {
|
|
case 'play': video.play().catch(e => log('Play fail: ' + e)); break;
|
|
case 'pause': video.pause(); break;
|
|
case 'seek':
|
|
if (Number.isFinite(data.value)) video.currentTime = data.value;
|
|
break;
|
|
case 'set_volume':
|
|
video.volume = Math.max(0, Math.min(1, data.value));
|
|
break;
|
|
}
|
|
});
|
|
|
|
// Report progress to Flutter
|
|
setInterval(() => {
|
|
if (!video.paused) {
|
|
window.parent.postMessage({
|
|
type: 'playback_position',
|
|
currentTime: video.currentTime,
|
|
duration: video.duration || 0
|
|
}, '*');
|
|
|
|
window.parent.postMessage({
|
|
type: 'playback_status',
|
|
status: 'playing'
|
|
}, '*');
|
|
} else {
|
|
window.parent.postMessage({
|
|
type: 'playback_status',
|
|
status: 'paused'
|
|
}, '*');
|
|
}
|
|
}, 1000);
|
|
|
|
// --- Player Logic ---
|
|
|
|
function initPlayer() {
|
|
if (!streamUrl) {
|
|
showError('No stream URL provided');
|
|
return;
|
|
}
|
|
|
|
if (data.playlist) {
|
|
const playlistUrl = window.location.origin + data.playlist;
|
|
log('Got HLS playlist: ' + playlistUrl);
|
|
await playWithHLS(playlistUrl);
|
|
return;
|
|
} else {
|
|
showError('Invalid stream response');
|
|
return;
|
|
}
|
|
} catch (err) {
|
|
log('Error fetching playlist: ' + err);
|
|
showError('Stream init failed');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 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/')) {
|
|
await playWithHLS(streamUrl);
|
|
return;
|
|
}
|
|
|
|
// MPEG-TS streams (live or forced TS via ext param)
|
|
// Checked BEFORE generic MP4/MKV to handle transcoded streams correctly
|
|
if (ext === '.ts' || lowerUrl.includes('.ts')) {
|
|
log('Detected MPEG-TS (via extension or URL)');
|
|
|
|
// Check if mpegts.js is supported (may not work on mobile)
|
|
if (mpegts.isSupported()) {
|
|
log('Using mpegts.js');
|
|
const success = await playWithMpegTS(streamUrl);
|
|
if (success) return;
|
|
log('mpegts.js failed, trying fallbacks...');
|
|
}
|
|
|
|
// Fallback 1: Try HLS.js for better mobile compatibility
|
|
log('Trying HLS.js as fallback for MPEG-TS');
|
|
const hlsSuccess = await playWithHLS(streamUrl);
|
|
if (hlsSuccess) return;
|
|
|
|
// Fallback 2: Try direct play as last resort
|
|
log('Trying direct play as last resort');
|
|
await playDirect(streamUrl);
|
|
return;
|
|
}
|
|
|
|
// MP4 and WebM - play directly (supports seeking via Range)
|
|
if (lowerUrl.includes('.mp4') || lowerUrl.includes('.webm') || lowerUrl.includes('.mov')) {
|
|
log('Detected MP4/WebM/MOV, playing directly with seek support');
|
|
await playDirect(streamUrl);
|
|
return;
|
|
}
|
|
|
|
// MKV, AVI - not natively supported, try anyway with fallback
|
|
if (lowerUrl.includes('.mkv') || lowerUrl.includes('.avi') || lowerUrl.includes('.wmv') || lowerUrl.includes('.flv')) {
|
|
log('Detected MKV/AVI - limited browser support, trying direct play...');
|
|
showLoading('Loading video (format may not support seeking)...');
|
|
|
|
// Try direct first, some browsers/servers may transcode on the fly
|
|
video.src = streamUrl;
|
|
|
|
let playStarted = false;
|
|
const onCanPlay = () => {
|
|
playStarted = true;
|
|
hideLoading();
|
|
video.play().catch(e => {
|
|
log('Autoplay prevented: ' + e);
|
|
window.parent.postMessage({ type: 'playback_status', status: 'paused' }, '*');
|
|
});
|
|
};
|
|
|
|
const onError = async () => {
|
|
if (!playStarted) {
|
|
log('Direct play failed for MKV/AVI, trying HLS fallback...');
|
|
video.removeEventListener('canplay', onCanPlay);
|
|
video.removeEventListener('error', onError);
|
|
// Try HLS as fallback (in case URL was actually HLS disguised)
|
|
const hlsSuccess = await playWithHLS(streamUrl);
|
|
if (!hlsSuccess) {
|
|
showError('Format not supported by browser. Try MP4 version if available.');
|
|
}
|
|
}
|
|
};
|
|
|
|
video.addEventListener('canplay', onCanPlay, { once: true });
|
|
video.addEventListener('error', onError, { once: true });
|
|
return;
|
|
}
|
|
|
|
// Unknown format - try direct first (for VOD with Range support), then HLS
|
|
log('Unknown format, trying direct first then HLS fallback');
|
|
video.src = streamUrl;
|
|
|
|
video.addEventListener('canplay', () => {
|
|
hideLoading();
|
|
video.play().catch(e => {
|
|
log('Autoplay prevented: ' + e);
|
|
window.parent.postMessage({ type: 'playback_status', status: 'paused' }, '*');
|
|
});
|
|
}, { once: true });
|
|
|
|
video.addEventListener('error', async () => {
|
|
log('Direct play failed, trying HLS...');
|
|
await playWithHLS(streamUrl);
|
|
}, { once: true });
|
|
}
|
|
|
|
initPlayer();
|
|
</script>
|
|
</body>
|
|
|
|
</html> |