feat: add mobile-optimized video player and streaming API handler

This commit is contained in:
Michael committed 2026-03-29 11:47:09 +02:00
1 parent db2eab0e66
commit 36a7658cf9
2 files changed
+58 -35

No files matched your search

+10 -8
View File
@@ -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
View File
@@ -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;
} }