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 @@
- +
@@ -495,10 +521,11 @@
-
- - -
+
@@ -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(); }); })();