Revert to Twitch-style live mode (linear TV); non-DVR master; custom audio/fullscreen buttons
This commit is contained in:
parent
eaef77c4c0
commit
7c2f4eee89
@ -4,7 +4,7 @@ const express = require('express');
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
const STREAM_URL = process.env.HLS_URL || 'https://folxplay.b-cdn.net/live/stream1_master_dvr.m3u8';
|
||||
const STREAM_URL = process.env.HLS_URL || 'https://folxplay.b-cdn.net/live/stream1_master.m3u8';
|
||||
|
||||
app.set('view engine', 'ejs');
|
||||
app.set('views', path.join(__dirname, '..', 'views'));
|
||||
|
||||
215
views/index.ejs
215
views/index.ejs
@ -188,58 +188,48 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Overlay layer for the GO LIVE pill — only top of player, doesn't cover native controls */
|
||||
/* Overlay layer for the controls + GO LIVE pill */
|
||||
.player-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 80px;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 3;
|
||||
background: linear-gradient(180deg, rgba(0,0,0,0.4) 0%, rgba(0,0,0,0) 18%, rgba(0,0,0,0) 70%, rgba(0,0,0,0.55) 100%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s;
|
||||
}
|
||||
/* GO LIVE pill — appears top-right when user is behind live edge */
|
||||
.go-live-pill {
|
||||
.player-frame:hover .player-overlay,
|
||||
.player-frame:focus-within .player-overlay,
|
||||
.player-frame.touched .player-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
.player-controls {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
bottom: 14px;
|
||||
right: 14px;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
pointer-events: auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: rgba(220, 28, 76, 0.95);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 7px 12px 7px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-family: 'Archivo Black', sans-serif;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.pctl {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(8px);
|
||||
transition: background 0.15s, transform 0.15s;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
color: white;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
transition: all 0.15s;
|
||||
padding: 0;
|
||||
}
|
||||
.go-live-pill:hover {
|
||||
background: rgba(220, 28, 76, 1);
|
||||
transform: scale(1.04);
|
||||
}
|
||||
.go-live-dot {
|
||||
display: inline-block;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
animation: livePulse 1.6s infinite;
|
||||
}
|
||||
.go-live-time {
|
||||
font-family: 'Archivo', monospace;
|
||||
font-size: 10.5px;
|
||||
opacity: 0.85;
|
||||
letter-spacing: 0.05em;
|
||||
padding-left: 6px;
|
||||
border-left: 1px solid rgba(255,255,255,0.35);
|
||||
.pctl:hover {
|
||||
background: var(--folx-magenta);
|
||||
border-color: var(--folx-magenta);
|
||||
}
|
||||
.pctl {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
@ -516,7 +506,9 @@
|
||||
</div>
|
||||
|
||||
<div class="player-frame" id="playerFrame">
|
||||
<video id="v" autoplay muted playsinline controls></video>
|
||||
<video id="v" autoplay muted playsinline tabindex="-1"
|
||||
disablepictureinpicture
|
||||
controlsList="nodownload nofullscreen noremoteplayback noplaybackrate"></video>
|
||||
|
||||
<div class="player-msg visible" id="msg">
|
||||
<div class="spinner"></div>
|
||||
@ -524,11 +516,10 @@
|
||||
</div>
|
||||
|
||||
<div class="player-overlay">
|
||||
<button id="btnGoLive" class="go-live-pill" aria-label="Zum Live-Stream" title="Zum Live-Stream springen" style="display:none;">
|
||||
<span class="go-live-dot"></span>
|
||||
<span>LIVE</span>
|
||||
<span id="goLiveLabel" class="go-live-time">−0s</span>
|
||||
</button>
|
||||
<div class="player-controls">
|
||||
<button class="pctl" id="btnAudio" aria-label="Ton umschalten" title="Ton umschalten">🔇</button>
|
||||
<button class="pctl" id="btnFs" aria-label="Vollbild" title="Vollbild">⛶</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@ -647,7 +638,6 @@
|
||||
const video = document.getElementById('v');
|
||||
const msg = document.getElementById('msg');
|
||||
const playerFrame = document.getElementById('playerFrame');
|
||||
const btnGoLive = document.getElementById('btnGoLive');
|
||||
|
||||
let hls = null;
|
||||
function showMsg(html) { msg.innerHTML = html; msg.classList.add('visible'); }
|
||||
@ -666,13 +656,10 @@
|
||||
|
||||
if (window.Hls && window.Hls.isSupported()) {
|
||||
hls = new Hls({
|
||||
// DVR-friendly: don't force duration=Infinity, allow native scrubber to work
|
||||
liveDurationInfinity: false,
|
||||
liveDurationInfinity: true,
|
||||
liveSyncDurationCount: 4,
|
||||
// Larger back-buffer = more rewind window in browser memory
|
||||
backBufferLength: 3600, // up to 60 min back-buffer
|
||||
backBufferLength: 30,
|
||||
maxBufferLength: 60,
|
||||
maxMaxBufferLength: 600,
|
||||
manifestLoadingTimeOut: 10000,
|
||||
manifestLoadingMaxRetry: 6,
|
||||
fragLoadingTimeOut: 20000,
|
||||
@ -714,8 +701,8 @@
|
||||
lastT = video.currentTime;
|
||||
}, 5000);
|
||||
|
||||
// DVR mode — pause and seek are allowed within the 60-min DVR window.
|
||||
// We track "behind live" status and offer a GO LIVE button to skip to the edge.
|
||||
// Live-only mode (Twitch style) — pause is disabled.
|
||||
// If anything pauses the video, resume immediately AND seek to the live edge.
|
||||
function seekToLive() {
|
||||
try {
|
||||
if (hls && hls.liveSyncPosition) {
|
||||
@ -723,47 +710,93 @@
|
||||
} else if (video.seekable && video.seekable.length > 0) {
|
||||
video.currentTime = video.seekable.end(video.seekable.length - 1);
|
||||
}
|
||||
video.play().catch(() => {});
|
||||
} catch (e) {}
|
||||
}
|
||||
// Track distance from live edge → toggle "GO LIVE" pill visibility
|
||||
function updateLivePill() {
|
||||
if (!hls || !hls.liveSyncPosition) return;
|
||||
const behindSec = Math.max(0, hls.liveSyncPosition - video.currentTime);
|
||||
const pill = document.getElementById('btnGoLive');
|
||||
if (!pill) return;
|
||||
if (behindSec > 8) {
|
||||
pill.style.display = 'inline-flex';
|
||||
const min = Math.floor(behindSec / 60);
|
||||
const s = Math.floor(behindSec % 60);
|
||||
const lbl = document.getElementById('goLiveLabel');
|
||||
if (lbl) lbl.textContent = min > 0 ? `−${min}:${String(s).padStart(2,'0')}` : `−${s}s`;
|
||||
} else {
|
||||
pill.style.display = 'none';
|
||||
}
|
||||
}
|
||||
setInterval(updateLivePill, 1000);
|
||||
video.addEventListener('seeked', updateLivePill);
|
||||
|
||||
// Recover from unintended pauses (system-level stalls only — NOT user clicks)
|
||||
// We DON'T auto-resume on pause anymore because DVR mode allows pause.
|
||||
|
||||
// Visibility: when tab returns, do nothing (DVR mode allows continuing where paused)
|
||||
// (no auto-seek-to-live on visibility change in DVR mode)
|
||||
|
||||
// GO LIVE pill click → jump to live edge
|
||||
if (btnGoLive) {
|
||||
btnGoLive.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
video.addEventListener('pause', () => {
|
||||
setTimeout(() => {
|
||||
seekToLive();
|
||||
video.play().catch(() => {});
|
||||
}, 80);
|
||||
});
|
||||
// If user seeks back, force forward to live
|
||||
video.addEventListener('seeked', () => {
|
||||
if (!hls) return;
|
||||
if (hls.liveSyncPosition && video.currentTime < hls.liveSyncPosition - 10) {
|
||||
video.currentTime = hls.liveSyncPosition;
|
||||
}
|
||||
|
||||
// Unmute on first tap (browser autoplay policy: needs muted)
|
||||
// Native controls handle audio/fullscreen now
|
||||
video.addEventListener('volumechange', () => {
|
||||
// user interacted via native UI — leave alone
|
||||
});
|
||||
// Block right-click context menu
|
||||
video.addEventListener('contextmenu', (e) => e.preventDefault());
|
||||
video.disablePictureInPicture = true;
|
||||
video.setAttribute('disablePictureInPicture', 'true');
|
||||
try { video.disableRemotePlayback = true; } catch (e) {}
|
||||
|
||||
// Block keyboard shortcuts that could pause: spacebar, K, Media keys
|
||||
document.addEventListener('keydown', (e) => {
|
||||
const tag = (e.target && e.target.tagName) || '';
|
||||
if (tag === 'INPUT' || tag === 'TEXTAREA') return;
|
||||
if (e.key === ' ' || e.key === 'Spacebar' || e.code === 'Space' ||
|
||||
e.key === 'k' || e.key === 'K' ||
|
||||
e.key === 'MediaPlayPause' || e.key === 'MediaPause') {
|
||||
e.preventDefault();
|
||||
if (video.paused) {
|
||||
seekToLive();
|
||||
video.play().catch(() => {});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (!document.hidden) {
|
||||
seekToLive();
|
||||
video.play().catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
// Audio toggle (custom button)
|
||||
const btnAudio = document.getElementById('btnAudio');
|
||||
const btnFs = document.getElementById('btnFs');
|
||||
function updateAudio() {
|
||||
btnAudio.textContent = video.muted ? '🔇' : '🔊';
|
||||
btnAudio.title = video.muted ? 'Ton einschalten' : 'Stummschalten';
|
||||
}
|
||||
updateAudio();
|
||||
btnAudio.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
video.muted = !video.muted;
|
||||
video.volume = 1.0;
|
||||
updateAudio();
|
||||
});
|
||||
|
||||
// Fullscreen — handle iOS Safari separately
|
||||
function enterFullscreen() {
|
||||
if (typeof video.webkitEnterFullscreen === 'function' && !document.fullscreenEnabled) {
|
||||
try { video.webkitEnterFullscreen(); return; } catch (e) {}
|
||||
}
|
||||
if (video.requestFullscreen) { video.requestFullscreen().catch(() => {}); return; }
|
||||
if (video.webkitRequestFullscreen) { video.webkitRequestFullscreen(); return; }
|
||||
if (playerFrame.requestFullscreen) { playerFrame.requestFullscreen().catch(() => {}); return; }
|
||||
if (playerFrame.webkitRequestFullscreen) { playerFrame.webkitRequestFullscreen(); return; }
|
||||
if (typeof video.webkitEnterFullscreen === 'function') {
|
||||
try { video.webkitEnterFullscreen(); } catch (e) {}
|
||||
}
|
||||
}
|
||||
function exitFullscreen() {
|
||||
if (document.exitFullscreen) { document.exitFullscreen().catch(() => {}); return; }
|
||||
if (document.webkitExitFullscreen) { document.webkitExitFullscreen(); return; }
|
||||
if (typeof video.webkitExitFullscreen === 'function') { try { video.webkitExitFullscreen(); } catch (e) {} }
|
||||
}
|
||||
function isFullscreen() {
|
||||
return !!(document.fullscreenElement || document.webkitFullscreenElement || video.webkitDisplayingFullscreen);
|
||||
}
|
||||
btnFs.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
if (isFullscreen()) exitFullscreen();
|
||||
else enterFullscreen();
|
||||
});
|
||||
// Tap on video = audio toggle, double = fullscreen
|
||||
video.addEventListener('click', () => { video.muted = !video.muted; video.volume = 1.0; updateAudio(); });
|
||||
video.addEventListener('dblclick', (e) => { e.preventDefault(); btnFs.click(); });
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user