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 @@
-
+
-
+
+
+
+
@@ -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(); });
})();