Ozadje = nakljucna FOLX slika na body (menja ob osvezitvi); player pol-prosojen; dodan Chromecast + AirPlay; fullscreen na strimu

This commit is contained in:
Sebastjan 2026-07-07 10:12:29 +00:00
parent 50c754718b
commit b3df6e9417
2 changed files with 47 additions and 29 deletions

View File

@ -38,7 +38,8 @@ app.use(express.static(path.join(__dirname, '..', 'public'), {
app.get('/', (_req, res) => {
// Use our proxy route for the player — it always returns fresh tokens
res.render('index', { hlsUrl: '/stream/1/master.m3u8' });
const bg = '/assets/bg/bg' + (1 + Math.floor(Math.random() * 4)) + '.jpg';
res.render('index', { hlsUrl: '/stream/1/master.m3u8', bg });
});
app.get('/test-embed', (_req, res) => {

View File

@ -21,24 +21,22 @@
:root { --akzent: #DC1D4D; --font-display: 'Geograph', system-ui, sans-serif; --font-mono: 'JetBrains Mono', ui-monospace, monospace; }
* { box-sizing: border-box; }
html, body { margin: 0; background: #000; }
body { color: #fff; font-family: var(--font-display); }
body { color: #fff; font-family: var(--font-display); background-size: cover; background-position: center; background-attachment: fixed; background-repeat: no-repeat; }
a { color: var(--akzent); text-decoration: none; }
a:hover { color: #FF4A73; }
@keyframes fx-pulse { 50% { opacity: .3; } }
@keyframes fx-vu { from { transform: scaleY(.15); } to { transform: scaleY(1); } }
input[type="range"].fxvol { accent-color: var(--akzent); }
::selection { background: var(--akzent); color: #fff; }
.wrap { min-height: 100vh; background: #000; display: flex; flex-direction: column; }
.wrap { min-height: 100vh; display: flex; flex-direction: column; background: rgba(0,0,0,0.35); }
header.top { padding: 14px 24px; border-bottom: 1px solid rgba(255,255,255,0.14); }
.top-inner { width: min(900px, 100%); margin: 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
header.top .logo { height: 64px; width: auto; display: block; margin: -14px 0; }
.uhr { font-family: var(--font-mono); font-size: 13px; color: #B8B8B8; letter-spacing: 0.06em; }
main.stage-wrap { width: min(1240px, 100%); margin: 0 auto; padding: clamp(16px,3vw,40px) clamp(12px,2.5vw,24px) 0; flex: 1; }
.player-col { width: min(900px, 100%); margin: 0 auto; }
.player-frame { position: relative; aspect-ratio: 16/9; overflow: hidden; isolation: isolate; background: #0A0A0A; }
.player-frame video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: 1; background:#000; }
.stage-poster { position: absolute; inset: 0; width:100%; height:100%; object-fit: cover; z-index: 0; opacity: 1; transition: opacity 1.2s ease; }
.stage-poster.b { opacity: 0; }
.player-frame { position: relative; aspect-ratio: 16/9; overflow: hidden; isolation: isolate; background: rgba(10,10,10,0.35); border: 1px solid rgba(255,255,255,0.14); }
.player-frame video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: 1; background:transparent; }
.scrim { position: absolute; inset: 0; z-index: 1; background: rgba(10,10,10,0.28); pointer-events:none; }
.msg { position:absolute; inset:0; z-index:20; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:14px; background:rgba(10,10,10,0.55); color:#fff; font-size:13px; letter-spacing:0.1em; text-transform:uppercase; }
.msg.hidden { display:none; }
@ -59,7 +57,7 @@
:fullscreen .player-frame, .player-frame:fullscreen { aspect-ratio: auto; width:100%; height:100%; }
</style>
</head>
<body>
<body style="background-image: url('<%= bg %>');">
<div class="wrap">
<header class="top">
<div class="top-inner">
@ -71,11 +69,9 @@
<main class="stage-wrap">
<div class="player-col">
<div class="player-frame" id="playerFrame">
<img class="stage-poster" id="posterA" src="/assets/bg/bg1.jpg" alt="" />
<img class="stage-poster b" id="posterB" src="/assets/bg/bg2.jpg" alt="" />
<div class="scrim"></div>
<video id="v" autoplay muted playsinline tabindex="-1" disablepictureinpicture
controlsList="nodownload noremoteplayback noplaybackrate"></video>
<video id="v" autoplay muted playsinline tabindex="-1"
x-webkit-airplay="allow"
controlsList="nodownload noplaybackrate"></video>
<div class="msg" id="msg"><div class="spinner"></div><div>Stream wird geladen…</div></div>
</div>
@ -89,6 +85,10 @@
<div style="display:flex; align-items:center; gap:12px; flex:none;">
<button class="ic" id="btnAudio" title="Ton umschalten" aria-label="Ton umschalten">🔇</button>
<span class="quality">AUTO · HD</span>
<google-cast-launcher id="castBtn" title="Auf TV streamen" style="--connected-color:var(--akzent); --disconnected-color:#fff; width:26px; height:26px; cursor:pointer; display:none;"></google-cast-launcher>
<button class="ic" id="btnAirplay" title="AirPlay" aria-label="AirPlay" style="display:none;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"><path d="M5 17H3a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h18a1 1 0 0 1 1 1v11a1 1 0 0 1-1 1h-2"/><path d="M12 15l5 6H7l5-6Z" fill="currentColor"/></svg>
</button>
<button class="ic" id="btnFs" title="Vollbild" aria-label="Vollbild">⛶</button>
</div>
</div>
@ -103,30 +103,17 @@
</footer>
</div>
<script src="https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1"></script>
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.17/dist/hls.min.js"></script>
<script>
(function(){
var HLS_URL = <%- JSON.stringify(hlsUrl) %>;
var video = document.getElementById('v');
var msg = document.getElementById('msg');
var posterA = document.getElementById('posterA');
var posterB = document.getElementById('posterB');
var hls = null;
function hideMsg(){ msg.classList.add('hidden'); posterA.style.display='none'; posterB.style.display='none'; }
function hideMsg(){ msg.classList.add('hidden'); }
function showMsg(){ msg.classList.remove('hidden'); }
// Rotirajoce ozadje (crossfade) dokler se video ne nalozi
var bgList = ['/assets/bg/bg1.jpg','/assets/bg/bg2.jpg','/assets/bg/bg3.jpg','/assets/bg/bg4.jpg'];
var bgIdx = 1, showingA = true;
bgList.forEach(function(s){ var im = new Image(); im.src = s; }); // predpomni
setInterval(function(){
if (posterA.style.display === 'none') return; // video tece, ne rotiraj
bgIdx = (bgIdx + 1) % bgList.length;
if (showingA) { posterB.src = bgList[bgIdx]; posterB.classList.remove('b'); posterA.classList.add('b'); }
else { posterA.src = bgList[bgIdx]; posterA.classList.remove('b'); posterB.classList.add('b'); }
showingA = !showingA;
}, 4000);
function start(){
try { hls && hls.destroy(); } catch(e){}
hls = null;
@ -176,7 +163,37 @@
}
});
// Now-Playing poll
// AirPlay (Safari/iOS): pokazi gumb ce je na voljo
var btnAP = document.getElementById('btnAirplay');
if (window.WebKitPlaybackTargetAvailabilityEvent) {
video.addEventListener('webkitplaybacktargetavailabilitychanged', function(e){
btnAP.style.display = (e.availability === 'available') ? 'flex' : 'none';
});
btnAP.addEventListener('click', function(){ try { video.webkitShowPlaybackTargetPicker(); } catch(err){} });
}
// Chromecast: SDK klic ob nalozitvi
window.__onGCastApiAvailable = function(isAvailable){
if (!isAvailable) return;
var ctx = cast.framework.CastContext.getInstance();
ctx.setOptions({ receiverApplicationId: chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID, autoJoinPolicy: chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED });
var castBtn = document.getElementById('castBtn');
castBtn.style.display = 'inline-block';
// absoluten URL do streama za cast napravo
var streamAbs = location.origin + HLS_URL;
ctx.addEventListener(cast.framework.CastContextEventType.SESSION_STATE_CHANGED, function(ev){
if (ev.sessionState === cast.framework.SessionState.SESSION_STARTED || ev.sessionState === cast.framework.SessionState.SESSION_RESUMED) {
var session = ctx.getCurrentSession();
if (!session) return;
var mediaInfo = new chrome.cast.media.MediaInfo(streamAbs, 'application/x-mpegURL');
mediaInfo.streamType = chrome.cast.media.StreamType.LIVE;
mediaInfo.metadata = new chrome.cast.media.GenericMediaMetadata();
mediaInfo.metadata.title = 'Folx Music Television — Live';
var request = new chrome.cast.media.LoadRequest(mediaInfo);
session.loadMedia(request).catch(function(){});
}
});
};
var nowT = document.getElementById('nowTitle');
var lastSong = null;
function applyNP(np){