diff --git a/frontend/app.js b/frontend/app.js index 2273899..f24997d 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -7,6 +7,10 @@ let isLoading = false; let hlsPlayer = null; let currentLoadController = null; let errorToastTimer = null; +let playerMode = 'modal'; +let playerHome = null; +let onFullscreenChange = null; +let onWebkitEndFullscreen = null; // 2. Observer Definition (Must be defined before initApp uses it) const observer = new IntersectionObserver((entries) => { @@ -178,6 +182,25 @@ function formatDuration(seconds) { return `${minutes}m`; } +function isMobilePlayback() { + if (navigator.userAgentData && typeof navigator.userAgentData.mobile === 'boolean') { + return navigator.userAgentData.mobile; + } + const ua = navigator.userAgent || ''; + if (/iPhone|iPad|iPod|Android/i.test(ua)) return true; + return window.matchMedia('(pointer: coarse)').matches && window.matchMedia('(max-width: 900px)').matches; +} + +function getMobileVideoHost() { + let host = document.getElementById('mobile-video-host'); + if (!host) { + host = document.createElement('div'); + host.id = 'mobile-video-host'; + document.body.appendChild(host); + } + return host; +} + // 4. Initialization (Run this last) async function initApp() { // Clear old data if you want a fresh start every refresh @@ -238,6 +261,12 @@ function showError(message) { async function openPlayer(url) { const modal = document.getElementById('video-modal'); const video = document.getElementById('player'); + const useMobileFullscreen = isMobilePlayback(); + let playbackStarted = false; + + if (!playerHome) { + playerHome = video.parentElement; + } // 1. Define isHls (the missing piece!) let refererParam = ''; @@ -275,14 +304,65 @@ async function openPlayer(url) { } } + if (useMobileFullscreen) { + const host = getMobileVideoHost(); + if (video.parentElement !== host) { + host.appendChild(video); + } + playerMode = 'mobile'; + video.removeAttribute('playsinline'); + video.removeAttribute('webkit-playsinline'); + video.playsInline = false; + } else { + if (playerHome && video.parentElement !== playerHome) { + playerHome.appendChild(video); + } + playerMode = 'modal'; + video.setAttribute('playsinline', ''); + video.setAttribute('webkit-playsinline', ''); + video.playsInline = true; + } + + const requestFullscreen = () => { + if (playerMode !== 'mobile') return; + if (typeof video.webkitEnterFullscreen === 'function') { + try { + video.webkitEnterFullscreen(); + } catch (err) { + // Ignore if fullscreen is not allowed. + } + return; + } + if (video.requestFullscreen) { + video.requestFullscreen().catch(() => {}); + } + }; + + const startPlayback = () => { + if (playbackStarted) return; + playbackStarted = true; + const playPromise = video.play(); + if (playPromise && typeof playPromise.catch === 'function') { + playPromise.catch(() => {}); + } + if (playerMode === 'mobile') { + if (video.readyState >= 1) { + requestFullscreen(); + } else { + video.addEventListener('loadedmetadata', requestFullscreen, { once: true }); + } + } + }; + if (isHls) { if (window.Hls && window.Hls.isSupported()) { hlsPlayer = new window.Hls(); hlsPlayer.loadSource(streamUrl); hlsPlayer.attachMedia(video); hlsPlayer.on(window.Hls.Events.MANIFEST_PARSED, function() { - video.play(); + startPlayback(); }); + startPlayback(); hlsPlayer.on(window.Hls.Events.ERROR, function(event, data) { if (data && data.fatal) { showError('Unable to play this stream.'); @@ -291,6 +371,7 @@ async function openPlayer(url) { }); } else if (video.canPlayType('application/vnd.apple.mpegurl')) { video.src = streamUrl; + startPlayback(); } else { console.error("HLS not supported in this browser."); showError('HLS is not supported in this browser.'); @@ -298,6 +379,7 @@ async function openPlayer(url) { } } else { video.src = streamUrl; + startPlayback(); } video.onerror = () => { @@ -305,8 +387,29 @@ async function openPlayer(url) { closePlayer(); }; - modal.style.display = 'flex'; - document.body.style.overflow = 'hidden'; + if (playerMode === 'modal') { + modal.style.display = 'flex'; + document.body.style.overflow = 'hidden'; + } else { + modal.style.display = 'none'; + document.body.style.overflow = 'auto'; + if (!onFullscreenChange) { + onFullscreenChange = () => { + if (playerMode === 'mobile' && !document.fullscreenElement) { + closePlayer(); + } + }; + } + document.addEventListener('fullscreenchange', onFullscreenChange); + if (!onWebkitEndFullscreen) { + onWebkitEndFullscreen = () => { + if (playerMode === 'mobile') { + closePlayer(); + } + }; + } + video.addEventListener('webkitendfullscreen', onWebkitEndFullscreen); + } } function closePlayer() { @@ -316,11 +419,24 @@ function closePlayer() { hlsPlayer.destroy(); hlsPlayer = null; } + if (document.fullscreenElement && document.exitFullscreen) { + document.exitFullscreen().catch(() => {}); + } + if (onFullscreenChange) { + document.removeEventListener('fullscreenchange', onFullscreenChange); + } + if (onWebkitEndFullscreen) { + video.removeEventListener('webkitendfullscreen', onWebkitEndFullscreen); + } video.onerror = null; video.pause(); video.src = ''; modal.style.display = 'none'; document.body.style.overflow = 'auto'; + if (playerHome && video.parentElement !== playerHome) { + playerHome.appendChild(video); + } + playerMode = 'modal'; } function handleSearch(value) { diff --git a/frontend/style.css b/frontend/style.css index 8914265..1d9a9ca 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -768,6 +768,15 @@ video { object-fit: contain; } +#mobile-video-host { + position: fixed; + left: -9999px; + top: 0; + width: 1px; + height: 1px; + overflow: hidden; +} + .error-toast { position: fixed; right: 20px;