mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-23 15:08:07 +02:00
feat: add mobile-optimized video player and streaming API handler
This commit is contained in:
1 parent
db2eab0e66
commit
36a7658cf9
2 files changed
+58
-35
No files matched your search
@@ -154,24 +154,26 @@ Handler createLiveStreamHandler(
|
|||||||
// Video
|
// Video
|
||||||
if (useNvidiaGpu) ...[
|
if (useNvidiaGpu) ...[
|
||||||
'-c:v', 'h264_nvenc', '-preset', 'p4', '-tune',
|
'-c:v', 'h264_nvenc', '-preset', 'p4', '-tune',
|
||||||
'hq', // Better quality preset for NVENC
|
'hq',
|
||||||
'-b:v', '8000k', '-maxrate', '12000k', '-bufsize',
|
'-b:v', '8000k', '-maxrate', '12000k', '-bufsize', '16000k',
|
||||||
'16000k', // Quality native / high bitrate
|
'-profile:v', 'high', '-level', '4.0',
|
||||||
|
'-pix_fmt', 'yuv420p',
|
||||||
'-g', '50',
|
'-g', '50',
|
||||||
] else ...[
|
] else ...[
|
||||||
'-c:v', 'libx264', '-preset', 'medium', '-tune',
|
'-c:v', 'libx264', '-preset', 'medium', '-tune', 'zerolatency',
|
||||||
'zerolatency', // Better detail than ultrafast
|
'-profile:v', 'high', '-level', '4.0',
|
||||||
'-b:v', '6000k', '-maxrate', '8000k', '-bufsize', '12000k',
|
'-b:v', '6000k', '-maxrate', '8000k', '-bufsize', '12000k',
|
||||||
|
'-pix_fmt', 'yuv420p',
|
||||||
'-g', '50',
|
'-g', '50',
|
||||||
],
|
],
|
||||||
// Audio: High quality + Sync filter
|
// Audio: High quality + Sync filter
|
||||||
'-c:a', 'aac', '-b:a', '192k', '-ac', '2', '-ar', '48000',
|
'-c:a', 'aac', '-b:a', '192k', '-ac', '2', '-ar', '48000',
|
||||||
'-af', 'aresample=async=1',
|
'-af', 'aresample=async=1',
|
||||||
// HLS Sliding Window
|
// HLS Sliding Window — larger window so iOS never requests a deleted segment
|
||||||
'-f', 'hls',
|
'-f', 'hls',
|
||||||
'-hls_time', '2',
|
'-hls_time', '2',
|
||||||
'-hls_list_size', '10', // Increased for safety
|
'-hls_list_size', '20',
|
||||||
'-hls_flags', 'delete_segments+independent_segments+discont_start',
|
'-hls_flags', 'delete_segments+independent_segments',
|
||||||
'-hls_segment_type', 'mpegts',
|
'-hls_segment_type', 'mpegts',
|
||||||
'-hls_segment_filename', 'seg_%03d.ts',
|
'-hls_segment_filename', 'seg_%03d.ts',
|
||||||
'playlist.m3u8',
|
'playlist.m3u8',
|
||||||
|
|||||||
+48
-27
@@ -165,6 +165,17 @@
|
|||||||
|
|
||||||
function log(m) { console.log('[MobilePlayer] ' + m); }
|
function log(m) { console.log('[MobilePlayer] ' + m); }
|
||||||
|
|
||||||
|
let safariRetryCount = 0;
|
||||||
|
const safariMaxRetries = 3;
|
||||||
|
let isSafariMode = false;
|
||||||
|
|
||||||
|
function showError(msg) {
|
||||||
|
loading.style.display = 'none';
|
||||||
|
playOverlay.style.display = 'none';
|
||||||
|
error.style.display = 'block';
|
||||||
|
document.getElementById('err-msg').textContent = msg;
|
||||||
|
}
|
||||||
|
|
||||||
playOverlay.addEventListener('click', (e) => {
|
playOverlay.addEventListener('click', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -173,63 +184,73 @@
|
|||||||
playOverlay.style.display = 'none';
|
playOverlay.style.display = 'none';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Double tap for fullscreen
|
|
||||||
let lastTap = 0;
|
let lastTap = 0;
|
||||||
video.addEventListener('touchend', (e) => {
|
video.addEventListener('touchend', () => {
|
||||||
const now = Date.now();
|
lastTap = Date.now();
|
||||||
lastTap = now;
|
|
||||||
// Report activity to parent
|
|
||||||
window.parent.postMessage({ type: 'user_activity' }, '*');
|
window.parent.postMessage({ type: 'user_activity' }, '*');
|
||||||
});
|
});
|
||||||
|
|
||||||
video.addEventListener('playing', () => {
|
video.addEventListener('playing', () => {
|
||||||
loading.style.display = 'none';
|
loading.style.display = 'none';
|
||||||
playOverlay.style.display = 'none';
|
playOverlay.style.display = 'none';
|
||||||
|
safariRetryCount = 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
video.addEventListener('pause', () => {
|
video.addEventListener('pause', () => {
|
||||||
// If we're paused and not playing, and it looks like it was blocked or stopped
|
|
||||||
if (video.currentTime === 0 || video.paused) {
|
if (video.currentTime === 0 || video.paused) {
|
||||||
loading.style.display = 'none';
|
loading.style.display = 'none';
|
||||||
playOverlay.style.display = 'flex';
|
playOverlay.style.display = 'flex';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
video.addEventListener('error', () => {
|
||||||
|
const code = video.error?.code || 'unknown';
|
||||||
|
const msg = video.error?.message || '';
|
||||||
|
log('Video error - code: ' + code + ', message: ' + msg);
|
||||||
|
|
||||||
video.addEventListener('error', (e) => {
|
// Safari/iOS: auto-retry on transient errors
|
||||||
loading.style.display = 'none';
|
if (isSafariMode && safariRetryCount < safariMaxRetries) {
|
||||||
playOverlay.style.display = 'none';
|
safariRetryCount++;
|
||||||
error.style.display = 'block';
|
log('Safari retry ' + safariRetryCount + '/' + safariMaxRetries + ' in 2s');
|
||||||
const errorCode = video.error?.code || 'unknown';
|
loading.style.display = 'flex';
|
||||||
const errorMsg = video.error?.message || 'Unknown error';
|
error.style.display = 'none';
|
||||||
log('Video error - code: ' + errorCode + ', message: ' + errorMsg);
|
document.getElementById('l-text').textContent = 'Reconnexion ' + safariRetryCount + '/' + safariMaxRetries + '...';
|
||||||
document.getElementById('err-msg').textContent = 'Erreur ' + errorCode + ': ' + errorMsg;
|
setTimeout(loadSafariStream, 2000);
|
||||||
|
} else {
|
||||||
|
showError('Erreur ' + code + ': ' + (msg || 'Format non supporté ou lien expiré'));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function loadSafariStream() {
|
||||||
|
video.src = '';
|
||||||
|
video.load();
|
||||||
|
video.src = streamUrl;
|
||||||
|
video.load();
|
||||||
|
video.play().catch(() => {
|
||||||
|
log('Autoplay blocked by Safari, showing overlay');
|
||||||
|
loading.style.display = 'none';
|
||||||
|
playOverlay.style.display = 'flex';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
if (!streamUrl) {
|
if (!streamUrl) {
|
||||||
log('No stream URL provided');
|
log('No stream URL provided');
|
||||||
error.style.display = 'block';
|
showError('No stream URL provided');
|
||||||
document.getElementById('err-msg').textContent = 'No stream URL provided';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log('Initing mobile player for: ' + streamUrl);
|
log('Initing mobile player for: ' + streamUrl);
|
||||||
log('Stream type param: ' + streamType);
|
log('Stream type param: ' + streamType);
|
||||||
|
|
||||||
const isHLS = streamUrl.toLowerCase().includes('m3u8') || streamUrl.toLowerCase().includes('playlist.m3u8');
|
const isHLS = streamUrl.toLowerCase().includes('m3u8');
|
||||||
const isTS = streamUrl.toLowerCase().includes('/live/') || streamUrl.toLowerCase().endsWith('.ts');
|
const isTS = streamUrl.toLowerCase().endsWith('.ts');
|
||||||
log('Format detection - isHLS: ' + isHLS + ', isTS: ' + isTS);
|
log('Format detection - isHLS: ' + isHLS + ', isTS: ' + isTS);
|
||||||
|
|
||||||
// Safari / iOS Native HLS - MUST call load() and play()
|
// Safari / iOS Native HLS
|
||||||
if (isHLS && video.canPlayType('application/vnd.apple.mpegurl')) {
|
if (isHLS && video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||||
log('Using native HLS (Safari)');
|
log('Using native HLS (Safari/iOS)');
|
||||||
video.src = streamUrl;
|
isSafariMode = true;
|
||||||
video.load();
|
loadSafariStream();
|
||||||
video.play().catch(e => {
|
|
||||||
log('Autoplay blocked by Safari, showing overlay');
|
|
||||||
loading.style.display = 'none';
|
|
||||||
playOverlay.style.display = 'flex';
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user