Download live/index.html from jaynesmax/studio: direct link, hf CLI and curl.
- Browser
- Download file 10.5 kB
-
https://huggingface.co/spaces/jaynesmax/studio/resolve/main/live/index.html
- Command line
-
hf download hf://spaces/jaynesmax/studio/live/index.html
-
curl -L -o index.html https://huggingface.co/spaces/jaynesmax/studio/resolve/main/live/index.html
10.5 kB
| <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> | |