studio / live /index.html
jaynesmax's picture
Rename index.html to live/index.html
7e8b7db verified
Raw History Blame Contribute Delete
10.5 kB
<!doctype html>
<html lang="sw"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<title>JaynesTV Live</title>
<link rel="stylesheet" href="../shared.css">
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.13/dist/hls.min.js"></script>
<style>
body{overflow-x:hidden}
.player{position:relative;width:100%;aspect-ratio:16/9;background:#000;overflow:hidden;max-height:100vh}
.player:fullscreen{width:100vw;height:100vh;aspect-ratio:auto}
.player video{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;background:#000}
.overlay{position:absolute;inset:0;background:rgba(0,0,0,.88);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1rem;z-index:5;text-align:center}
.logo-center{width:min(40%,220px);opacity:.85}
.logo-tr{position:absolute;top:10px;right:12px;width:clamp(56px,10vw,96px);opacity:.75;z-index:4;pointer-events:none}
.live{position:absolute;top:12px;left:12px;background:var(--brand);color:#fff;font-weight:800;padding:.2rem .6rem;border-radius:6px;z-index:4;font-size:.85rem;letter-spacing:.5px}
.spin{width:42px;height:42px;border:4px solid #333;border-top-color:var(--brand);border-radius:50%;animation:sp 1s linear infinite}
@keyframes sp{to{transform:rotate(360deg)}}
.banner{position:absolute;left:50%;top:12%;transform:translateX(-50%);width:min(90%,560px);z-index:6;background:#111;border:1px solid var(--line);border-radius:12px;overflow:hidden}
.banner img{width:100%;display:block}
.banner button{position:absolute;top:6px;right:6px;background:rgba(0,0,0,.7);width:30px;height:30px;padding:0;border-radius:50%}
.pre-skip{position:absolute;right:14px;bottom:64px;z-index:7}
.pre-skip button{background:rgba(0,0,0,.75);border:1px solid #555}
.controls{position:absolute;left:0;right:0;bottom:0;display:flex;gap:.5rem;align-items:center;padding:.5rem .7rem;background:linear-gradient(transparent,rgba(0,0,0,.85));z-index:4}
.controls button{background:rgba(255,255,255,.14);padding:.4rem .6rem;border-radius:8px}
.controls input{width:110px;accent-color:var(--brand);padding:0;background:none;border:0}
.info{padding:1rem}
.ticker-wrap{overflow:hidden;background:var(--brand-dark);white-space:nowrap;border-top:1px solid var(--line)}
.ticker{display:inline-flex;animation:tk 28s linear infinite;padding:.5rem 0}
.ticker span{padding-right:4rem;font-weight:600}
@keyframes tk{from{transform:translateX(0)}to{transform:translateX(-50%)}}
</style>
</head><body>
<div id="player" class="player">
<video id="video" playsinline></video>
<video id="adv" playsinline hidden></video>
<img id="logoTR" class="logo-tr" alt="" hidden>
<div class="live" id="liveBadge" hidden>● LIVE</div>
<div id="playScreen" class="overlay" hidden>
<img class="logo-center logo" alt="" hidden>
<button id="playBtn">▶ Cheza</button>
</div>
<div id="loading" class="overlay" hidden>
<img class="logo-center logo" alt="" hidden>
<div class="spin"></div><p class="muted">Inapakia…</p>
</div>
<div id="reconnect" class="overlay" hidden>
<div class="spin"></div><p>Inaunganisha upya…</p>
</div>
<div id="banner" class="banner" hidden>
<a id="bannerLink" target="_blank" rel="noopener"><img id="bannerImg" alt=""></a>
<button id="bannerClose" aria-label="Funga">✕</button>
</div>
<div id="preSkip" class="pre-skip" hidden><button id="skipBtn" disabled>Ruka baada ya 5s</button></div>
<div class="controls">
<button id="ppBtn" aria-label="Cheza/Simamisha">❚❚</button>
<button id="muteBtn" aria-label="Sauti">🔊</button>
<input id="vol" type="range" min="0" max="1" step="0.05" value="1" aria-label="Sauti">
<span style="flex:1"></span>
<button id="rotBtn" aria-label="Zungusha">⟳</button>
<button id="fsBtn" aria-label="Skrini nzima">⛶</button>
</div>
</div>
<div id="tickerWrap" class="ticker-wrap" hidden><div id="ticker" class="ticker"></div></div>
<section class="info">
<h2 id="now" style="margin:.2rem 0">—</h2>
<p id="dj" class="muted" style="margin:.2rem 0"></p>
<p style="margin:.6rem 0">Inayofuata: <b id="next">—</b></p>
</section>
<script type="module">
import { CONFIG } from "../config.js";
import { sb, getSettings, h, nowPlaying } from "../common.js";
const $ = id => document.getElementById(id);
let S = {}, ads = [], SCH = null, hls = null, retryT = null;
// ---------- Mipangilio, matangazo ----------
S = await getSettings();
const logoUrl = S.LOGO_URL || "";
document.querySelectorAll(".logo").forEach(i => { if (logoUrl) { i.src = logoUrl; i.hidden = false; } });
if (logoUrl) { $("logoTR").src = logoUrl; $("logoTR").hidden = false; }
ads = (await sb.from("ads").select("*").eq("active", true)).data || [];
// ---------- Ratiba: kipindi kinachocheza sasa ----------
async function loadSchedule() {
if (!S.SCHEDULE_URL) return;
try { SCH = await (await fetch(S.SCHEDULE_URL, { cache: "no-store" })).json(); } catch {}
}
function renderNow() {
const np = nowPlaying(SCH);
if (!np) return;
$("now").textContent = np.cur.title;
$("dj").textContent = np.cur.dj ? "DJ: " + np.cur.dj : "";
$("next").textContent = np.next.title;
}
await loadSchedule();
renderNow();
setInterval(renderNow, 1000);
setInterval(loadSchedule, 60000);
// ---------- Ticker ----------
const tickers = ads.filter(a => a.kind === "ticker");
if (tickers.length) {
const txt = tickers.map(t => t.body || t.title).join(" • ");
$("ticker").innerHTML = `<span>${h(txt)}</span><span>${h(txt)}</span>`;
$("tickerWrap").hidden = false;
}
// ---------- Bango la tangazo kila dakika 15 (kulingana na saa ya ukuta, watazamaji wote sawa) ----------
const banners = ads.filter(a => a.kind === "banner" && a.image_url);
let lastSlot = -1, shown = null;
function showBanner(ad) {
shown = ad;
$("bannerImg").src = ad.image_url;
$("bannerLink").href = ad.link_url || "#";
$("bannerLink").onclick = ad.link_url ? null : e => e.preventDefault();
$("banner").hidden = false;
}
function hideBanner() { shown = null; $("banner").hidden = true; }
$("bannerClose").onclick = hideBanner;
function bannerTick() {
if (!banners.length) return;
const sec = Date.now() / 1000;
const slot = Math.floor(sec / CONFIG.AD_INTERVAL_SEC);
const pos = sec % CONFIG.AD_INTERVAL_SEC;
if (shown && pos >= (shown.duration_sec || 10)) hideBanner();
if (!shown && slot !== lastSlot) {
lastSlot = slot;
showBanner(banners[slot % banners.length]);
}
}
setInterval(bannerTick, 1000);
// ---------- Pre-roll: video fupi kabla ya live, na "Ruka baada ya 5s" ----------
function playPreroll(ad) {
return new Promise(resolve => {
const adv = $("adv"), skip = $("skipBtn");
$("video").hidden = true;
adv.src = ad.video_url;
adv.hidden = false;
$("preSkip").hidden = false;
skip.disabled = true;
skip.textContent = `Ruka baada ya ${CONFIG.PREROLL_SKIP_SEC}s`;
let done = false;
const finish = () => {
if (done) return;
done = true;
adv.pause(); adv.removeAttribute("src"); adv.load();
adv.hidden = true; $("preSkip").hidden = true;
resolve();
};
adv.ontimeupdate = () => {
const left = Math.ceil(CONFIG.PREROLL_SKIP_SEC - adv.currentTime);
if (left <= 0) { skip.disabled = false; skip.textContent = "Ruka ▶"; }
else skip.textContent = `Ruka baada ya ${left}s`;
};
adv.onended = finish;
adv.onerror = finish;
skip.onclick = finish;
adv.play().catch(finish);
});
}
// ---------- Kicheza HLS (hls.js), jaribu tena kila sekunde 5 ----------
function destroyHls() { if (hls) { hls.destroy(); hls = null; } }
function retryLater() {
destroyHls();
$("reconnect").hidden = false;
$("loading").hidden = true;
clearTimeout(retryT);
retryT = setTimeout(playLive, CONFIG.RETRY_SEC * 1000);
}
function showPlayScreen() { $("loading").hidden = true; $("playScreen").hidden = false; }
function playLive() {
clearTimeout(retryT);
const url = S.LIVE_HLS_URL;
const v = $("video");
if (!url) { $("loading").hidden = true; $("reconnect").hidden = true; return ($("now").textContent = "Kiungo cha Live hakijawekwa"); }
$("playScreen").hidden = true;
$("loading").hidden = false;
v.hidden = false;
destroyHls();
const ready = () => { $("loading").hidden = true; $("reconnect").hidden = true; $("liveBadge").hidden = false; v.play().catch(showPlayScreen); };
if (window.Hls && Hls.isSupported()) {
hls = new Hls({ lowLatencyMode: true, enableWorker: true });
hls.loadSource(url);
hls.attachMedia(v);
hls.on(Hls.Events.MANIFEST_PARSED, ready);
hls.on(Hls.Events.ERROR, (_, d) => { if (d.fatal) retryLater(); });
} else if (v.canPlayType("application/vnd.apple.mpegurl")) {
v.src = url;
v.onloadedmetadata = ready;
v.onerror = retryLater;
} else {
$("loading").hidden = true;
$("now").textContent = "Kivinjari hiki hakiwezi kucheza HLS";
}
}
async function start() {
const pre = ads.filter(a => a.kind === "preroll" && a.video_url);
if (pre.length && !sessionStorage.getItem("jtv_preroll_seen")) {
sessionStorage.setItem("jtv_preroll_seen", "1");
$("playScreen").hidden = true;
await playPreroll(pre[Math.floor(Math.random() * pre.length)]);
}
$("loading").hidden = false;
playLive();
}
// Kitufe "Cheza" kinatoa ruhusa ya kucheza (sauti) kwenye vivinjari vyote
$("playBtn").onclick = start;
$("playScreen").hidden = false;
// ---------- Vidhibiti ----------
const v = $("video");
$("ppBtn").onclick = () => (v.paused ? v.play() : v.pause());
v.onplay = () => ($("ppBtn").textContent = "❚❚");
v.onpause = () => ($("ppBtn").textContent = "▶");
$("vol").oninput = e => { v.volume = Number(e.target.value); v.muted = v.volume === 0; $("muteBtn").textContent = v.muted ? "🔇" : "🔊"; };
$("muteBtn").onclick = () => { v.muted = !v.muted; $("muteBtn").textContent = v.muted ? "🔇" : "🔊"; };
$("fsBtn").onclick = () => {
if (document.fullscreenElement) return document.exitFullscreen();
const p = $("player");
if (p.requestFullscreen) p.requestFullscreen().catch(() => {});
else if (v.webkitEnterFullscreen) v.webkitEnterFullscreen(); // iPhone
};
$("rotBtn").onclick = async () => {
try {
if (screen.orientation?.type?.startsWith("landscape")) screen.orientation.unlock();
else await screen.orientation.lock("landscape");
} catch { alert("Kivinjari hiki hakiruhusu kuzungusha skrini. Tumia skrini nzima."); }
};
</script>
</body></html>