diff --git a/src/server.js b/src/server.js
index 4f6cbf4..037a097 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.m3u8';
+const STREAM_URL = process.env.HLS_URL || 'https://folxplay.b-cdn.net/live/stream1_master_dvr.m3u8';
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, '..', 'views'));
diff --git a/views/index.ejs b/views/index.ejs
index eb4b345..3dc57ab 100644
--- a/views/index.ejs
+++ b/views/index.ejs
@@ -194,21 +194,49 @@
inset: 0;
pointer-events: none;
z-index: 3;
- background: linear-gradient(180deg, rgba(0,0,0,0.5) 0%, rgba(0,0,0,0) 18%, rgba(0,0,0,0) 70%, rgba(0,0,0,0.65) 100%);
- opacity: 0;
- transition: opacity 0.25s;
}
- .player-frame:hover .player-overlay,
- .player-frame:focus-within .player-overlay {
- opacity: 1;
- }
- .player-controls {
+ /* GO LIVE pill — appears top-right when user is behind live edge */
+ .go-live-pill {
position: absolute;
- bottom: 14px;
+ top: 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;
+ backdrop-filter: blur(8px);
+ transition: background 0.15s, transform 0.15s;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.4);
+ }
+ .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 {
background: rgba(0, 0, 0, 0.7);
@@ -485,9 +513,7 @@
-
+
-
-
-
-
+
@@ -616,9 +643,8 @@
const HLS_URL = <%- JSON.stringify(hlsUrl) %>;
const video = document.getElementById('v');
const msg = document.getElementById('msg');
- const btnAudio = document.getElementById('btnAudio');
- const btnFs = document.getElementById('btnFs');
const playerFrame = document.getElementById('playerFrame');
+ const btnGoLive = document.getElementById('btnGoLive');
let hls = null;
function showMsg(html) { msg.innerHTML = html; msg.classList.add('visible'); }
@@ -682,8 +708,8 @@
lastT = video.currentTime;
}, 5000);
- // Live-only mode (Twitch style) — pause is disabled.
- // If anything pauses the video, resume immediately AND seek to the live edge.
+ // 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.
function seekToLive() {
try {
if (hls && hls.liveSyncPosition) {
@@ -691,99 +717,47 @@
} else if (video.seekable && video.seekable.length > 0) {
video.currentTime = video.seekable.end(video.seekable.length - 1);
}
+ video.play().catch(() => {});
} catch (e) {}
}
- video.addEventListener('pause', () => {
- setTimeout(() => {
+ // 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();
seekToLive();
- video.play().catch(() => {});
- }, 80);
- });
- // If user seeks back (e.g. via keyboard or scrubber), 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 (hides "Pause", "Save video as", etc.)
- video.addEventListener('contextmenu', (e) => e.preventDefault());
- // Block Picture-in-Picture (its UI exposes pause)
- video.disablePictureInPicture = true;
- video.setAttribute('disablePictureInPicture', 'true');
- // Block remote playback (AirPlay/Cast — can pause unexpectedly)
- try { video.disableRemotePlayback = true; } catch (e) {}
-
- // Block keyboard shortcuts that could pause: spacebar, K, Media keys
- document.addEventListener('keydown', (e) => {
- // Only intercept if focus is on body or video (not in input fields)
- 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();
- // Make sure we keep playing
- if (video.paused) {
- seekToLive();
- video.play().catch(() => {});
- }
- }
- });
-
- document.addEventListener('visibilitychange', () => {
- if (!document.hidden) {
- seekToLive();
- video.play().catch(() => {});
- }
- });
-
- // Audio toggle
- 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 — must handle iOS Safari separately (no fullscreen API on container,
- // only on the video element via webkitEnterFullscreen)
- function enterFullscreen() {
- // iOS Safari path: only video.webkitEnterFullscreen works
- if (typeof video.webkitEnterFullscreen === 'function' && !document.fullscreenEnabled) {
- try { video.webkitEnterFullscreen(); return; } catch (e) {}
- }
- // Standard Fullscreen API on the video element (most reliable cross-browser)
- if (video.requestFullscreen) { video.requestFullscreen().catch(() => {}); return; }
- if (video.webkitRequestFullscreen) { video.webkitRequestFullscreen(); return; }
- // Fallback: try the container
- if (playerFrame.requestFullscreen) { playerFrame.requestFullscreen().catch(() => {}); return; }
- if (playerFrame.webkitRequestFullscreen) { playerFrame.webkitRequestFullscreen(); return; }
- // Last-resort iOS path
- 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();
+ // 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
});
- video.addEventListener('click', () => { video.muted = !video.muted; video.volume = 1.0; updateAudio(); });
- video.addEventListener('dblclick', (e) => { e.preventDefault(); btnFs.click(); });
})();