Upload 3 files
Browse files- studio/ads.html +108 -0
- studio/index.html +199 -18
- studio/settings.html +64 -0
studio/ads.html
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="sw"><head>
|
| 3 |
+
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
| 4 |
+
<title>Matangazo β JaynesTV Studio</title>
|
| 5 |
+
<link rel="stylesheet" href="../shared.css">
|
| 6 |
+
</head><body>
|
| 7 |
+
<nav class="top">
|
| 8 |
+
<b style="color:var(--brand);margin-right:auto">JaynesTV Studio</b>
|
| 9 |
+
<a href="/studio">Studio</a><a class="on" href="/studio/ads">Matangazo</a><a href="/studio/settings">Mipangilio</a><a href="/live">Angalia Live</a>
|
| 10 |
+
</nav>
|
| 11 |
+
<div id="gate"></div>
|
| 12 |
+
<main id="app" hidden style="max-width:1000px;margin:auto;padding:1rem">
|
| 13 |
+
<div class="card">
|
| 14 |
+
<h2 class="banner-title">Ongeza Tangazo</h2>
|
| 15 |
+
<form id="f">
|
| 16 |
+
<div class="grid2">
|
| 17 |
+
<div><label>Aina</label>
|
| 18 |
+
<select id="kind">
|
| 19 |
+
<option value="banner">Bango (picha + kiungo) β kila dakika 15</option>
|
| 20 |
+
<option value="ticker">Maandishi yanayopita chini ya video</option>
|
| 21 |
+
<option value="preroll">Video fupi kabla ya Live</option>
|
| 22 |
+
</select></div>
|
| 23 |
+
<div><label>Jina la tangazo</label><input id="title"></div>
|
| 24 |
+
</div>
|
| 25 |
+
<label>Maandishi (kwa ticker)</label><input id="body">
|
| 26 |
+
<label>Kiungo (link)</label><input id="link" type="url" placeholder="https://...">
|
| 27 |
+
<label>Faili: picha (bango) au video MP4 (pre-roll)</label>
|
| 28 |
+
<input id="file" type="file" accept="image/*,video/mp4">
|
| 29 |
+
<label>Muda wa kuonyeshwa (sekunde)</label>
|
| 30 |
+
<input id="dur" type="number" min="3" max="120" value="10">
|
| 31 |
+
<p id="fe" class="err"></p>
|
| 32 |
+
<button type="submit" style="margin-top:.8rem">Hifadhi tangazo</button>
|
| 33 |
+
</form>
|
| 34 |
+
</div>
|
| 35 |
+
<div class="card" style="overflow-x:auto">
|
| 36 |
+
<table>
|
| 37 |
+
<thead><tr><th>Aina</th><th>Jina / Maandishi</th><th>Muda</th><th>Hali</th><th></th></tr></thead>
|
| 38 |
+
<tbody id="rows"></tbody>
|
| 39 |
+
</table>
|
| 40 |
+
</div>
|
| 41 |
+
</main>
|
| 42 |
+
|
| 43 |
+
<script type="module">
|
| 44 |
+
import { sb, requireAdmin, h } from "../common.js";
|
| 45 |
+
const $ = id => document.getElementById(id);
|
| 46 |
+
const KIND = { banner: "Bango", ticker: "Ticker", preroll: "Pre-roll" };
|
| 47 |
+
|
| 48 |
+
async function load() {
|
| 49 |
+
const { data, error } = await sb.from("ads").select("*").order("created_at", { ascending: false });
|
| 50 |
+
if (error) return ($("rows").innerHTML = `<tr><td colspan="5" class="err">${h(error.message)}</td></tr>`);
|
| 51 |
+
$("rows").innerHTML = data.length ? data.map(a => `
|
| 52 |
+
<tr>
|
| 53 |
+
<td>${KIND[a.kind]}</td>
|
| 54 |
+
<td>${h(a.title || a.body || "β")}</td>
|
| 55 |
+
<td>${a.kind === "ticker" ? "β" : a.duration_sec + "s"}</td>
|
| 56 |
+
<td><button class="ghost" data-t="${a.id}" data-v="${!a.active}">${a.active ? "Zima" : "Washa"}</button></td>
|
| 57 |
+
<td><button class="danger" data-d="${a.id}">Futa</button></td>
|
| 58 |
+
</tr>`).join("")
|
| 59 |
+
: `<tr><td colspan="5" class="muted">Hakuna matangazo bado.</td></tr>`;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
$("rows").onclick = async e => {
|
| 63 |
+
const t = e.target;
|
| 64 |
+
if (t.dataset.t) await sb.from("ads").update({ active: t.dataset.v === "true" }).eq("id", t.dataset.t);
|
| 65 |
+
else if (t.dataset.d) {
|
| 66 |
+
if (!confirm("Futa tangazo hili?")) return;
|
| 67 |
+
await sb.from("ads").delete().eq("id", t.dataset.d);
|
| 68 |
+
} else return;
|
| 69 |
+
load();
|
| 70 |
+
};
|
| 71 |
+
|
| 72 |
+
async function upload(file) {
|
| 73 |
+
const path = `${Date.now()}-${file.name.replace(/[^\w.-]/g, "_")}`;
|
| 74 |
+
const { error } = await sb.storage.from("ads").upload(path, file);
|
| 75 |
+
if (error) throw error;
|
| 76 |
+
return sb.storage.from("ads").getPublicUrl(path).data.publicUrl;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
$("f").onsubmit = async e => {
|
| 80 |
+
e.preventDefault();
|
| 81 |
+
$("fe").textContent = "";
|
| 82 |
+
const kind = $("kind").value, file = $("file").files[0];
|
| 83 |
+
const body = $("body").value.trim();
|
| 84 |
+
let image_url = null, video_url = null;
|
| 85 |
+
try {
|
| 86 |
+
if (file) {
|
| 87 |
+
const url = await upload(file);
|
| 88 |
+
if (file.type.startsWith("video")) video_url = url; else image_url = url;
|
| 89 |
+
}
|
| 90 |
+
} catch (err) { return ($("fe").textContent = "Upload imeshindikana: " + err.message); }
|
| 91 |
+
|
| 92 |
+
if (kind === "banner" && !image_url) return ($("fe").textContent = "Bango linahitaji picha.");
|
| 93 |
+
if (kind === "preroll" && !video_url) return ($("fe").textContent = "Pre-roll inahitaji video ya MP4.");
|
| 94 |
+
if (kind === "ticker" && !body) return ($("fe").textContent = "Ticker inahitaji maandishi.");
|
| 95 |
+
|
| 96 |
+
const { error } = await sb.from("ads").insert({
|
| 97 |
+
kind, title: $("title").value || null, body: body || null, link_url: $("link").value || null,
|
| 98 |
+
image_url, video_url, duration_sec: Number($("dur").value) || 10, active: true
|
| 99 |
+
});
|
| 100 |
+
if (error) return ($("fe").textContent = error.message);
|
| 101 |
+
e.target.reset();
|
| 102 |
+
load();
|
| 103 |
+
};
|
| 104 |
+
|
| 105 |
+
await requireAdmin($("gate"), $("app"));
|
| 106 |
+
load();
|
| 107 |
+
</script>
|
| 108 |
+
</body></html>
|
studio/index.html
CHANGED
|
@@ -1,19 +1,200 @@
|
|
| 1 |
<!doctype html>
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
<!doctype html>
|
| 2 |
+
<html lang="sw"><head>
|
| 3 |
+
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
| 4 |
+
<title>JaynesTV Studio</title>
|
| 5 |
+
<link rel="stylesheet" href="../shared.css">
|
| 6 |
+
<style>
|
| 7 |
+
.stage{position:relative;background:#000;border-radius:14px;overflow:hidden;aspect-ratio:16/9}
|
| 8 |
+
#preview{width:100%;height:100%;object-fit:cover;display:block}
|
| 9 |
+
.plogo{position:absolute;top:10px;right:10px;width:72px;opacity:.9;pointer-events:none}
|
| 10 |
+
.stats{display:grid;grid-template-columns:repeat(3,1fr);gap:.6rem}
|
| 11 |
+
.stat{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:.7rem;text-align:center}
|
| 12 |
+
.stat b{display:block;font-size:1.2rem;font-variant-numeric:tabular-nums;margin-top:.3rem}
|
| 13 |
+
@media(max-width:600px){.stats{grid-template-columns:1fr}}
|
| 14 |
+
</style></head>
|
| 15 |
+
<body>
|
| 16 |
+
<nav class="top">
|
| 17 |
+
<b style="color:var(--brand);margin-right:auto">JaynesTV Studio</b>
|
| 18 |
+
<a class="on" href="/studio">Studio</a><a href="/studio/ads">Matangazo</a><a href="/studio/settings">Mipangilio</a><a href="/live">Angalia Live</a>
|
| 19 |
+
</nav>
|
| 20 |
+
<div id="gate"></div>
|
| 21 |
+
<main id="app" hidden style="max-width:1000px;margin:auto;padding:1rem">
|
| 22 |
+
<div class="stage">
|
| 23 |
+
<video id="preview" autoplay playsinline muted></video>
|
| 24 |
+
<img id="plogo" class="plogo" alt="" hidden>
|
| 25 |
+
</div>
|
| 26 |
+
<div class="row" style="margin:1rem 0">
|
| 27 |
+
<button id="btnCam">Washa Kamera</button>
|
| 28 |
+
<button id="btnLive" disabled>Anza Live</button>
|
| 29 |
+
<button id="btnStop" class="danger" disabled>Simamisha Live</button>
|
| 30 |
+
</div>
|
| 31 |
+
<div class="card">
|
| 32 |
+
<div class="grid2">
|
| 33 |
+
<div><label>Kamera</label>
|
| 34 |
+
<select id="facing"><option value="environment">Nyuma</option><option value="user">Mbele</option></select></div>
|
| 35 |
+
<div><label>Ubora</label>
|
| 36 |
+
<select id="quality"><option value="720p">720p</option><option value="480p">480p</option></select></div>
|
| 37 |
+
</div>
|
| 38 |
+
<div class="row" style="margin-top:.8rem">
|
| 39 |
+
<button id="btnMute" class="ghost" disabled>Zima sauti</button>
|
| 40 |
+
<span class="muted">Sauti: <b id="audioState">Imewashwa</b></span>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
<div class="stats">
|
| 44 |
+
<div class="stat"><span class="muted">Hali</span><b id="status"></b></div>
|
| 45 |
+
<div class="stat"><span class="muted">Muda wa live</span><b id="timer">00:00:00</b></div>
|
| 46 |
+
<div class="stat"><span class="muted">Watazamaji</span><b id="viewers">β</b></div>
|
| 47 |
+
</div>
|
| 48 |
+
<p id="msg" class="err"></p>
|
| 49 |
+
</main>
|
| 50 |
+
<div id="toast" class="toast" hidden></div>
|
| 51 |
+
|
| 52 |
+
<script type="module">
|
| 53 |
+
import { requireAdmin, getSettings } from "../common.js";
|
| 54 |
+
const $ = id => document.getElementById(id);
|
| 55 |
+
const QUAL = { "480p": { width: 854, height: 480 }, "720p": { width: 1280, height: 720 } };
|
| 56 |
+
const ST = {
|
| 57 |
+
idle: ["b-idle", "Imesimama"], conn: ["b-warn", "Inaunganishaβ¦"],
|
| 58 |
+
live: ["b-live", "Unarusha"], off: ["b-bad", "Imekatika"], rec: ["b-warn", "Inaunganisha upya"]
|
| 59 |
+
};
|
| 60 |
+
let S = {}, stream = null, pc = null, whipRes = null, wantLive = false, startedAt = 0, retryT = null, muted = false;
|
| 61 |
+
|
| 62 |
+
const setStatus = k => ($("status").innerHTML = `<span class="badge ${ST[k][0]}">${ST[k][1]}</span>`);
|
| 63 |
+
const msg = t => ($("msg").textContent = t || "");
|
| 64 |
+
function toast(t) {
|
| 65 |
+
const el = $("toast"); el.textContent = t; el.hidden = false;
|
| 66 |
+
clearTimeout(toast.t); toast.t = setTimeout(() => (el.hidden = true), 2500);
|
| 67 |
+
}
|
| 68 |
+
const fmt = s => [Math.floor(s / 3600), Math.floor((s % 3600) / 60), s % 60].map(n => String(n).padStart(2, "0")).join(":");
|
| 69 |
+
const updateTimer = () => ($("timer").textContent = startedAt ? fmt(Math.floor((Date.now() - startedAt) / 1000)) : "00:00:00");
|
| 70 |
+
|
| 71 |
+
function applyMute() {
|
| 72 |
+
stream?.getAudioTracks().forEach(t => (t.enabled = !muted));
|
| 73 |
+
$("audioState").textContent = muted ? "Imezimwa" : "Imewashwa";
|
| 74 |
+
$("btnMute").textContent = muted ? "Washa sauti" : "Zima sauti";
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
async function startCamera() {
|
| 78 |
+
if (!navigator.mediaDevices?.getUserMedia) return msg("Kivinjari hiki hakiruhusu kamera. Tumia HTTPS.");
|
| 79 |
+
try {
|
| 80 |
+
const q = QUAL[$("quality").value];
|
| 81 |
+
const next = await navigator.mediaDevices.getUserMedia({
|
| 82 |
+
video: { facingMode: { ideal: $("facing").value }, width: { ideal: q.width }, height: { ideal: q.height } },
|
| 83 |
+
audio: true
|
| 84 |
+
});
|
| 85 |
+
const old = stream;
|
| 86 |
+
stream = next;
|
| 87 |
+
$("preview").srcObject = stream;
|
| 88 |
+
applyMute();
|
| 89 |
+
if (pc) for (const t of stream.getTracks()) {
|
| 90 |
+
const s = pc.getSenders().find(x => x.track?.kind === t.kind || (!x.track && x.kind === t.kind));
|
| 91 |
+
if (s) await s.replaceTrack(t);
|
| 92 |
+
}
|
| 93 |
+
old?.getTracks().forEach(t => t.stop());
|
| 94 |
+
$("btnLive").disabled = wantLive;
|
| 95 |
+
$("btnMute").disabled = false;
|
| 96 |
+
$("btnCam").textContent = "Badilisha Kamera";
|
| 97 |
+
msg("");
|
| 98 |
+
} catch (e) {
|
| 99 |
+
msg("Imeshindikana kufungua kamera/maikrofoni: " + e.message);
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
const iceDone = pc => new Promise(res => {
|
| 104 |
+
if (pc.iceGatheringState === "complete") return res();
|
| 105 |
+
const check = () => {
|
| 106 |
+
if (pc.iceGatheringState === "complete") { pc.removeEventListener("icegatheringstatechange", check); res(); }
|
| 107 |
+
};
|
| 108 |
+
pc.addEventListener("icegatheringstatechange", check);
|
| 109 |
+
setTimeout(res, 3000);
|
| 110 |
+
});
|
| 111 |
+
|
| 112 |
+
function teardown() {
|
| 113 |
+
if (pc) { pc.onconnectionstatechange = null; pc.close(); pc = null; }
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
function retry() {
|
| 117 |
+
if (!wantLive) return;
|
| 118 |
+
setStatus("rec");
|
| 119 |
+
clearTimeout(retryT);
|
| 120 |
+
retryT = setTimeout(connect, 3000);
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
async function connect() {
|
| 124 |
+
clearTimeout(retryT);
|
| 125 |
+
teardown();
|
| 126 |
+
setStatus(startedAt ? "rec" : "conn");
|
| 127 |
+
try {
|
| 128 |
+
pc = new RTCPeerConnection({ iceServers: [{ urls: "stun:stun.l.google.com:19302" }] });
|
| 129 |
+
stream.getTracks().forEach(t => pc.addTrack(t, stream));
|
| 130 |
+
pc.onconnectionstatechange = () => {
|
| 131 |
+
const s = pc?.connectionState;
|
| 132 |
+
if (s === "connected") { if (!startedAt) startedAt = Date.now(); setStatus("live"); msg(""); }
|
| 133 |
+
else if (s === "failed" || s === "disconnected" || s === "closed") { setStatus("off"); retry(); }
|
| 134 |
+
};
|
| 135 |
+
const offer = await pc.createOffer();
|
| 136 |
+
await pc.setLocalDescription(offer);
|
| 137 |
+
await iceDone(pc);
|
| 138 |
+
// WHIP: POST SDP kwenda WHIP_URL, jibu ni SDP ya answer; Location ni kiungo cha kufuta (DELETE) kikao
|
| 139 |
+
const res = await fetch(S.WHIP_URL, {
|
| 140 |
+
method: "POST", headers: { "Content-Type": "application/sdp" }, body: pc.localDescription.sdp
|
| 141 |
+
});
|
| 142 |
+
if (!res.ok) throw new Error("seva ilijibu " + res.status);
|
| 143 |
+
const loc = res.headers.get("Location");
|
| 144 |
+
whipRes = loc ? new URL(loc, S.WHIP_URL).href : null;
|
| 145 |
+
await pc.setRemoteDescription({ type: "answer", sdp: await res.text() });
|
| 146 |
+
} catch (e) {
|
| 147 |
+
msg("Muunganisho umeshindikana: " + e.message);
|
| 148 |
+
setStatus("off");
|
| 149 |
+
retry();
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
async function goLive() {
|
| 154 |
+
if (!stream) return toast("Washa kamera kwanza");
|
| 155 |
+
if (!S.WHIP_URL) return msg("WHIP_URL haijawekwa. Nenda kwenye Mipangilio.");
|
| 156 |
+
wantLive = true;
|
| 157 |
+
$("btnLive").disabled = true;
|
| 158 |
+
$("btnStop").disabled = false;
|
| 159 |
+
await connect();
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
async function stopLive() {
|
| 163 |
+
wantLive = false;
|
| 164 |
+
clearTimeout(retryT);
|
| 165 |
+
if (whipRes) fetch(whipRes, { method: "DELETE" }).catch(() => {});
|
| 166 |
+
whipRes = null;
|
| 167 |
+
teardown();
|
| 168 |
+
startedAt = 0;
|
| 169 |
+
updateTimer();
|
| 170 |
+
setStatus("idle");
|
| 171 |
+
$("btnLive").disabled = !stream;
|
| 172 |
+
$("btnStop").disabled = true;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
async function pollViewers() {
|
| 176 |
+
if (!S.VIEWERS_URL) return;
|
| 177 |
+
try {
|
| 178 |
+
const j = await (await fetch(S.VIEWERS_URL, { cache: "no-store" })).json();
|
| 179 |
+
$("viewers").textContent = j.viewers ?? "β";
|
| 180 |
+
} catch {}
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
// Mwanzo
|
| 184 |
+
await requireAdmin($("gate"), $("app"));
|
| 185 |
+
S = await getSettings();
|
| 186 |
+
if (S.LOGO_URL) { $("plogo").src = S.LOGO_URL; $("plogo").hidden = false; }
|
| 187 |
+
setStatus("idle");
|
| 188 |
+
updateTimer();
|
| 189 |
+
setInterval(updateTimer, 1000);
|
| 190 |
+
pollViewers();
|
| 191 |
+
setInterval(pollViewers, 15000);
|
| 192 |
+
|
| 193 |
+
$("btnCam").onclick = startCamera;
|
| 194 |
+
$("btnLive").onclick = goLive;
|
| 195 |
+
$("btnStop").onclick = stopLive;
|
| 196 |
+
$("btnMute").onclick = () => { muted = !muted; applyMute(); };
|
| 197 |
+
$("facing").onchange = $("quality").onchange = () => stream && startCamera();
|
| 198 |
+
window.addEventListener("beforeunload", () => stopLive());
|
| 199 |
+
</script>
|
| 200 |
+
</body></html>
|
studio/settings.html
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="sw"><head>
|
| 3 |
+
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
| 4 |
+
<title>Mipangilio β JaynesTV Studio</title>
|
| 5 |
+
<link rel="stylesheet" href="../shared.css">
|
| 6 |
+
</head><body>
|
| 7 |
+
<nav class="top">
|
| 8 |
+
<b style="color:var(--brand);margin-right:auto">JaynesTV Studio</b>
|
| 9 |
+
<a href="/studio">Studio</a><a href="/studio/ads">Matangazo</a><a class="on" href="/studio/settings">Mipangilio</a><a href="/live">Angalia Live</a>
|
| 10 |
+
</nav>
|
| 11 |
+
<div id="gate"></div>
|
| 12 |
+
<main id="app" hidden style="max-width:700px;margin:auto;padding:1rem">
|
| 13 |
+
<div class="card">
|
| 14 |
+
<h2 class="banner-title">Mipangilio ya Live</h2>
|
| 15 |
+
<label>WHIP_URL (kiungo cha kutuma video)</label><input id="WHIP_URL" placeholder="https://...">
|
| 16 |
+
<label>LIVE_HLS_URL (kiungo cha HLS, .m3u8)</label><input id="LIVE_HLS_URL" placeholder="https://.../playlist.m3u8">
|
| 17 |
+
<label>SCHEDULE_URL (ratiba JSON)</label>
|
| 18 |
+
<input id="SCHEDULE_URL" placeholder="https://huggingface.co/datasets/jaynesmaxtv/movie-file/resolve/main/jaynestv/schedule.json">
|
| 19 |
+
<label>VIEWERS_URL (hiari β inarudisha {"viewers": n})</label><input id="VIEWERS_URL" placeholder="https://...">
|
| 20 |
+
<button id="save" style="margin-top:1rem">Hifadhi mipangilio</button>
|
| 21 |
+
<p id="msg" class="muted"></p>
|
| 22 |
+
</div>
|
| 23 |
+
<div class="card">
|
| 24 |
+
<h2 class="banner-title">Nembo</h2>
|
| 25 |
+
<p class="muted">Inaonekana kona ya juu kulia, skrini ya kupakia na skrini ya kuanza kucheza.</p>
|
| 26 |
+
<input id="logo" type="file" accept="image/png,image/jpeg,image/webp,image/svg+xml">
|
| 27 |
+
<img id="logoPrev" alt="" hidden style="max-width:160px;margin-top:1rem;background:#000;padding:.5rem;border-radius:10px">
|
| 28 |
+
</div>
|
| 29 |
+
</main>
|
| 30 |
+
|
| 31 |
+
<script type="module">
|
| 32 |
+
import { sb, requireAdmin, getSettings, saveSetting } from "../common.js";
|
| 33 |
+
const $ = id => document.getElementById(id);
|
| 34 |
+
const KEYS = ["WHIP_URL", "LIVE_HLS_URL", "SCHEDULE_URL", "VIEWERS_URL"];
|
| 35 |
+
const say = (t, ok = true) => { $("msg").className = ok ? "muted" : "err"; $("msg").textContent = t; };
|
| 36 |
+
|
| 37 |
+
await requireAdmin($("gate"), $("app"));
|
| 38 |
+
const S = await getSettings();
|
| 39 |
+
KEYS.forEach(k => ($(k).value = S[k] || ""));
|
| 40 |
+
if (S.LOGO_URL) { $("logoPrev").src = S.LOGO_URL; $("logoPrev").hidden = false; }
|
| 41 |
+
|
| 42 |
+
$("save").onclick = async () => {
|
| 43 |
+
for (const k of KEYS) {
|
| 44 |
+
const { error } = await saveSetting(k, $(k).value.trim());
|
| 45 |
+
if (error) return say("Imeshindikana kuhifadhi " + k + ": " + error.message, false);
|
| 46 |
+
}
|
| 47 |
+
say("Imehifadhiwa β");
|
| 48 |
+
};
|
| 49 |
+
|
| 50 |
+
$("logo").onchange = async () => {
|
| 51 |
+
const f = $("logo").files[0];
|
| 52 |
+
if (!f) return;
|
| 53 |
+
const path = `logo-${Date.now()}.${f.name.split(".").pop()}`;
|
| 54 |
+
const { error } = await sb.storage.from("branding").upload(path, f);
|
| 55 |
+
if (error) return say("Upload ya nembo imeshindikana: " + error.message, false);
|
| 56 |
+
const url = sb.storage.from("branding").getPublicUrl(path).data.publicUrl;
|
| 57 |
+
const { error: e2 } = await saveSetting("LOGO_URL", url);
|
| 58 |
+
if (e2) return say("Imeshindikana kuhifadhi nembo: " + e2.message, false);
|
| 59 |
+
$("logoPrev").src = url;
|
| 60 |
+
$("logoPrev").hidden = false;
|
| 61 |
+
say("Nembo imehifadhiwa β");
|
| 62 |
+
};
|
| 63 |
+
</script>
|
| 64 |
+
</body></html>
|