diff --git a/src/server.js b/src/server.js index 037a097..4f6cbf4 100644 --- a/src/server.js +++ b/src/server.js @@ -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')); diff --git a/views/index.ejs b/views/index.ejs index 53cd716..78d4d67 100644 --- a/views/index.ejs +++ b/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 @@
- +
@@ -524,11 +516,10 @@
- +
+ + +
@@ -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'; + 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; + } + }); + // 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) {} } } - 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(); - seekToLive(); - }); + 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) {} } } - - // 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 + 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(); }); })();