Adiciona env vars de defaults de UI e melhora compatibilidade mobile dos players
--- - Adicionado endpoint GET /api/config que expõe as env vars de defaults para o frontend; - Adicionadas env vars DEFAULT_PURE_MODE, DEFAULT_OPEN_NEW_TAB, DEFAULT_RELOAD_CLIENT e DEFAULT_RELOAD_CLIENT_TIME ao docker-compose.yml e README.md; - Na primeira sessão sem global-prefs em localStorage, a UI busca os defaults do servidor via /api/config; - Adicionado fallback de HLS nativo para iOS Safari em todos os players HTML (Hls.isSupported() → v.src + v.play()); - Substituído AbortSignal.timeout() por AbortController + setTimeout para compatibilidade com iOS ≤ 15; - Corrigido stall detection falso-positivo: verificação só dispara após currentTime > 0 (vídeo realmente iniciado); - Adicionado height: 100dvh nos players para corrigir clipping do iOS causado pelo 100vh incluir a chrome bar do browser; - Adicionado atributo muted no <video> do player /static/[id] para permitir autoplay mobile; - Adicionado listener de touchstart nos botões flutuantes de Back e Mute para exibição em touch; - Botão Back do player React ampliado no mobile (px-5 py-3 text-[1.1rem]) para paridade visual com o player HTML; - Comentários do docker-compose.yml traduzidos para inglês e reorganizados inline; ---
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
import { NextResponse } from "next/server"
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({
|
||||
pureMode: process.env.DEFAULT_PURE_MODE === "true",
|
||||
newTab: process.env.DEFAULT_OPEN_NEW_TAB === "true",
|
||||
autoReload: process.env.DEFAULT_RELOAD_CLIENT === "true",
|
||||
reloadInterval: Math.max(1, Number(process.env.DEFAULT_RELOAD_CLIENT_TIME) || 2),
|
||||
})
|
||||
}
|
||||
@@ -61,9 +61,11 @@ export async function GET(_req: NextRequest, { params }: Ctx) {
|
||||
last=v.currentTime;
|
||||
},10000);
|
||||
|
||||
fetch(directUrl,{method:'HEAD',signal:AbortSignal.timeout(2000)})
|
||||
.then(function(){load(directUrl);})
|
||||
.catch(function(){load(proxyUrl);});
|
||||
var ctrl=new AbortController();
|
||||
var fetchTimer=setTimeout(function(){ctrl.abort();},2000);
|
||||
fetch(directUrl,{method:'HEAD',signal:ctrl.signal})
|
||||
.then(function(){clearTimeout(fetchTimer);load(directUrl);})
|
||||
.catch(function(){clearTimeout(fetchTimer);load(proxyUrl);});
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
+7
-1
@@ -183,7 +183,13 @@ export default function GalleryPage() {
|
||||
const savedSize = localStorage.getItem("cardSize") as CardSize | null
|
||||
if (savedSize) setCardSize(savedSize)
|
||||
const savedPrefs = localStorage.getItem("global-prefs")
|
||||
if (savedPrefs) setGlobalPrefs({ ...DEFAULT_GLOBAL_PREFS, ...JSON.parse(savedPrefs) })
|
||||
if (savedPrefs) {
|
||||
setGlobalPrefs({ ...DEFAULT_GLOBAL_PREFS, ...JSON.parse(savedPrefs) })
|
||||
} else {
|
||||
fetch("/api/config").then(r => r.json()).then((d: Partial<GlobalPrefs>) => {
|
||||
setGlobalPrefs(prev => ({ ...prev, ...d }))
|
||||
}).catch(() => {})
|
||||
}
|
||||
} catch {}
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -24,8 +24,10 @@ function BackButton({ onClick }: { onClick: () => void }) {
|
||||
useEffect(() => {
|
||||
show()
|
||||
window.addEventListener("mousemove", show)
|
||||
window.addEventListener("touchstart", show)
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", show)
|
||||
window.removeEventListener("touchstart", show)
|
||||
if (timerRef.current) clearTimeout(timerRef.current)
|
||||
}
|
||||
}, [show])
|
||||
@@ -34,7 +36,7 @@ function BackButton({ onClick }: { onClick: () => void }) {
|
||||
<button
|
||||
onClick={onClick}
|
||||
style={{ opacity: visible ? 1 : 0, transition: "opacity 0.4s" }}
|
||||
className="absolute top-4 left-4 z-20 flex items-center gap-1.5 text-sm text-white bg-black/40 px-3 py-1.5 rounded-lg cursor-pointer"
|
||||
className="absolute top-4 left-4 z-20 flex items-center gap-2 sm:gap-1.5 text-[1.1rem] sm:text-sm text-white bg-black/40 px-5 py-3 sm:px-3 sm:py-1.5 rounded-[10px] sm:rounded-lg cursor-pointer"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" /> Back
|
||||
</button>
|
||||
@@ -86,18 +88,24 @@ function VideoPlayer({ src, controls }: { src: string; controls?: boolean }) {
|
||||
if (Hls.isSupported()) {
|
||||
load(Hls)
|
||||
} else if (v.canPlayType("application/vnd.apple.mpegurl")) {
|
||||
// Safari nativo
|
||||
// iOS Safari native HLS
|
||||
v.src = src
|
||||
v.play()
|
||||
v.play().catch(() => { v.muted = true; v.play().catch(() => {}) })
|
||||
}
|
||||
}
|
||||
document.head.appendChild(script)
|
||||
|
||||
// stall detection
|
||||
// stall detection — only fires after video has actually played once
|
||||
let last = 0
|
||||
let started = false
|
||||
const interval = setInterval(() => {
|
||||
if (!v) return
|
||||
if (v.currentTime === last && !v.paused) { showMsg("Stream stalled — reloading..."); hlsRef.current && load(window.Hls) }
|
||||
if (!started && v.currentTime > 0) started = true
|
||||
if (started && v.currentTime === last && !v.paused) {
|
||||
showMsg("Stream stalled — reloading...")
|
||||
if (hlsRef.current) load(window.Hls)
|
||||
else { const s = v.src; v.src = ""; v.src = s; v.play().catch(() => {}) }
|
||||
}
|
||||
last = v.currentTime
|
||||
}, 10000)
|
||||
|
||||
@@ -110,7 +118,7 @@ function VideoPlayer({ src, controls }: { src: string; controls?: boolean }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<video ref={videoRef} autoPlay muted playsInline controls={controls} className="w-screen h-screen object-contain bg-black" />
|
||||
<video ref={videoRef} autoPlay muted playsInline controls={controls} className="w-screen h-screen object-contain bg-black" style={{ height: "100dvh" }} />
|
||||
{msg && (
|
||||
<div className="fixed top-4 left-1/2 -translate-x-1/2 bg-black/75 text-white px-5 py-2 rounded-lg text-sm z-10">{msg}</div>
|
||||
)}
|
||||
@@ -137,10 +145,10 @@ function PlayerInner() {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="relative bg-black w-screen h-screen overflow-hidden">
|
||||
<div className="relative bg-black w-screen h-screen overflow-hidden" style={{ height: "100dvh" }}>
|
||||
<BackButton onClick={() => router.push("/")} />
|
||||
{mode === "hls" && <VideoPlayer src={streamSrc} controls />}
|
||||
{mode === "html" && <iframe src={`/static/${id}`} className="w-screen h-screen border-0" allowFullScreen />}
|
||||
{mode === "html" && <iframe src={`/static/${id}`} className="w-screen border-0" style={{ height: "100dvh" }} allowFullScreen />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,10 +9,11 @@ export async function GET(_req: NextRequest, { params }: Ctx) {
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
html,body{background:#000;overflow:hidden;width:100%;height:100%}
|
||||
video{width:100vw;height:100vh;display:block;object-fit:contain}
|
||||
video{width:100vw;height:100vh;height:100dvh;display:block;object-fit:contain}
|
||||
#msg{
|
||||
position:fixed;top:16px;left:50%;transform:translateX(-50%);
|
||||
background:rgba(0,0,0,0.75);color:#fff;padding:8px 20px;
|
||||
@@ -31,7 +32,7 @@ export async function GET(_req: NextRequest, { params }: Ctx) {
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<video id="v" autoplay playsinline></video>
|
||||
<video id="v" autoplay muted playsinline></video>
|
||||
<div id="msg"></div>
|
||||
<button id="back" onclick="history.back()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg>
|
||||
@@ -76,20 +77,29 @@ export async function GET(_req: NextRequest, { params }: Ctx) {
|
||||
uiTimer=setTimeout(function(){backBtn.style.opacity='0';muteBtn.style.opacity='0';},5000);
|
||||
}
|
||||
document.addEventListener('mousemove',showUI);
|
||||
document.addEventListener('touchstart',showUI);
|
||||
showUI();
|
||||
|
||||
function startHls(src){
|
||||
activeSrc=src;
|
||||
var v=document.getElementById('v');
|
||||
if(hls)hls.destroy();
|
||||
if(!Hls.isSupported()){
|
||||
if(v.canPlayType('application/vnd.apple.mpegurl')){
|
||||
v.src=src;
|
||||
var p=v.play();
|
||||
if(p)p.catch(function(){v.muted=true;updateMuteIcon(true);v.play();});
|
||||
}
|
||||
return;
|
||||
}
|
||||
hls=new Hls({
|
||||
liveSyncDurationCount:2,liveMaxLatencyDurationCount:4,
|
||||
manifestLoadingTimeOut:10000,manifestLoadingMaxRetry:10,
|
||||
fragLoadingTimeOut:10000,fragLoadingMaxRetry:10
|
||||
});
|
||||
hls.loadSource(src);
|
||||
hls.attachMedia(document.getElementById('v'));
|
||||
hls.attachMedia(v);
|
||||
hls.on(Hls.Events.MANIFEST_PARSED,function(){
|
||||
var v=document.getElementById('v');
|
||||
var p=v.play();
|
||||
if(p)p.catch(function(){
|
||||
v.muted=true;
|
||||
@@ -106,14 +116,21 @@ export async function GET(_req: NextRequest, { params }: Ctx) {
|
||||
|
||||
// Try direct MediaMTX first (lower latency, avoids proxy buffering).
|
||||
// Falls back to proxy if port 8888 is not reachable from this client.
|
||||
fetch(directUrl,{method:'HEAD',signal:AbortSignal.timeout(2000)})
|
||||
.then(function(){startHls(directUrl);})
|
||||
.catch(function(){startHls(proxyUrl);});
|
||||
var ctrl=new AbortController();
|
||||
var fetchTimer=setTimeout(function(){ctrl.abort();},2000);
|
||||
fetch(directUrl,{method:'HEAD',signal:ctrl.signal})
|
||||
.then(function(){clearTimeout(fetchTimer);startHls(directUrl);})
|
||||
.catch(function(){clearTimeout(fetchTimer);startHls(proxyUrl);});
|
||||
|
||||
var last=0;
|
||||
var last=0,started=false;
|
||||
setInterval(function(){
|
||||
var v=document.getElementById('v');
|
||||
if(v.currentTime===last&&!v.paused){showMsg('Stream stalled — reloading...');startHls(activeSrc);}
|
||||
if(!started&&v.currentTime>0)started=true;
|
||||
if(started&&v.currentTime===last&&!v.paused){
|
||||
showMsg('Stream stalled — reloading...');
|
||||
if(hls)startHls(activeSrc);
|
||||
else{var s=v.src;v.src='';v.src=s;v.play().catch(function(){});}
|
||||
}
|
||||
last=v.currentTime;
|
||||
},10000);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user