jaynesmax commited on
Commit
f33b3bc
Β·
verified Β·
1 Parent(s): 41feacc

Upload 3 files

Browse files
Files changed (3) hide show
  1. studio/ads.html +108 -0
  2. studio/index.html +199 -18
  3. 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
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>