Overlay: fuenf Browser-Sources fuer OBS
Einblendungen, Chat, Statusleiste, Effekte, Pausenbild. Kein Build, kein Framework - OBS laedt die Dateien direkt, und jede Zwischenstufe waere ein Ding, das mitten im Stream kaputtgehen kann. Server-Sent Events statt WebSocket, entgegen der Skizze in chat-control.md: Node bringt einen WebSocket-*Client* mit, aber keinen Server, `EventSource` verbindet sich von allein neu, und die Richtung stimmt - das Overlay hoert nur zu. Gegen die tote Browser-Source nach einem Neustart stehen drei Dinge: der eingebaute Reconnect, ein Herzschlag als echtes Ereignis (ein SSE-Kommentar haelt die Verbindung offen, ist im Browser aber unsichtbar), und ein Zustands-Schnappschuss beim Verbinden. Abos, Raids, Bits und Loeschungen kommen ueber IRC-USERNOTICE und CLEARCHAT herein; das wurde bisher weggeworfen. Follows fehlen weiter, die gibt es nur ueber EventSub. GIFs im Chat: Twitch schickt dafuer einen Tag `gifs`, den wir nicht kannten. gifs=0-22|Pnd7qLFuGCOntZcNKB|https://media0.giphy.com/media/.../giphy.gif PRIVMSG #abdulive_ :[Idk GIF by Respective] Der Nachrichtentext ist nur ein Platzhalter, der Bereich markiert ihn. Ausgewertet wird er jetzt; ohne das stand woertlich "[Idk GIF by Respective]" im Bild. Gefunden wurde der Tag durch einen Rohmitschnitt - in 3094 Nachrichten aus acht grossen Kanaelen war kein einziges GIF, weil die Adresse eben nicht im Text steht. Zwei Fehler, die beim Durchklicken im Browser aufgefallen sind: - Die Aufraeumfunktion im Chat zaehlte Zeilen, die erst nach der Ausblend-Animation aus dem DOM verschwinden, und lief in eine Endlosschleife. In OBS waere die Quelle beim vierzehnten Chatbeitrag eingefroren. - Der Bereichsparser der GIFs war zu nachsichtig: `-1-4` zerfaellt beim Trennen in ['', '1', '4'], und `Number('')` ist 0 - der Bereich saehe gueltig aus und wuerde den falschen Textteil ersetzen. Auf der Uebersichtsseite steht fuer jede Einblendung, jede Chat-Variante und jeden Effekt ein Knopf. Ohne den liesse sich eine Raid-Einblendung nur pruefen, indem man auf einen echten Raid wartet. Was einen Zustand setzt, ist waehrend eines Streams gesperrt; "Stand zuruecksetzen" ausdruecklich nicht, sonst kaeme man aus einem erfundenen Zustand nicht mehr heraus. build-package.ts nimmt jetzt auch html/css/js mit - sonst liefert das Paket 404 statt Overlay. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
3b606e36fb
commit
bc2c8f6cbe
24 files changed
+3221
-8
No files matched your search
@@ -0,0 +1,141 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>abdulive - Statusleiste</title>
|
||||
<link rel="stylesheet" href="/theme.css">
|
||||
<style>
|
||||
#leiste {
|
||||
position: fixed;
|
||||
left: 28px; right: 28px; top: 22px;
|
||||
height: 92px;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
gap: 28px;
|
||||
padding: 0 30px;
|
||||
border-radius: 20px;
|
||||
background: var(--tinte-klar);
|
||||
backdrop-filter: blur(16px) saturate(150%);
|
||||
border: 1px solid rgba(255, 255, 255, .1);
|
||||
box-shadow: var(--schatten), var(--glanz);
|
||||
/* Ein Farbverlauf an der Oberkante statt eines Rahmens: er bindet die
|
||||
* Leiste an die Marke, ohne einen Kasten um alles zu ziehen. */
|
||||
overflow: hidden;
|
||||
}
|
||||
#leiste::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0; right: 0; top: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, var(--lila), var(--pink), var(--tuerkis), var(--lila));
|
||||
background-size: 300% 100%;
|
||||
animation: wandern 9s linear infinite;
|
||||
}
|
||||
@keyframes wandern { to { background-position: 300% 0; } }
|
||||
|
||||
.marke { display: flex; align-items: center; gap: 12px; }
|
||||
.punkt {
|
||||
width: 13px; height: 13px;
|
||||
border-radius: 50%;
|
||||
background: var(--rot);
|
||||
box-shadow: 0 0 0 0 rgba(239, 68, 68, .6);
|
||||
}
|
||||
body.live .punkt { background: var(--rot); animation: klopfen 2s ease-out infinite; }
|
||||
body:not(.live) .punkt { background: #555; }
|
||||
@keyframes klopfen {
|
||||
0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, .55); }
|
||||
70% { box-shadow: 0 0 0 16px rgba(239, 68, 68, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); }
|
||||
}
|
||||
.kanal { font-size: 26px; font-weight: 700; letter-spacing: -.01em; }
|
||||
|
||||
.mitte { min-width: 0; }
|
||||
#titel {
|
||||
font-size: 25px; font-weight: 600;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
#kategorie { font-size: 17px; color: var(--schrift-leise); margin-top: 2px; }
|
||||
|
||||
.werte { display: flex; gap: 30px; }
|
||||
.wert { text-align: right; min-width: 92px; }
|
||||
.wert .zahl {
|
||||
font-size: 30px; font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1.1;
|
||||
display: block;
|
||||
}
|
||||
.wert .beschriftung {
|
||||
font-size: 12px; letter-spacing: .14em; text-transform: uppercase;
|
||||
color: var(--schrift-leise);
|
||||
}
|
||||
/* Kurzes Aufleuchten, wenn sich eine Zahl aendert. Ohne das uebersieht man
|
||||
* jede Veraenderung, weil die Leiste sonst voellig still steht. */
|
||||
.zahl.neu { animation: aufleuchten .8s ease-out; }
|
||||
@keyframes aufleuchten {
|
||||
0% { color: var(--tuerkis); transform: translateY(-3px); }
|
||||
100% { color: inherit; transform: none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="leiste">
|
||||
<div class="marke">
|
||||
<span class="punkt"></span>
|
||||
<span class="kanal kontur" id="kanal">abdulive_</span>
|
||||
</div>
|
||||
<div class="mitte">
|
||||
<div id="titel" class="kontur">nicht live</div>
|
||||
<div id="kategorie"></div>
|
||||
</div>
|
||||
<div class="werte">
|
||||
<div class="wert"><span class="zahl" id="zuschauer">-</span>
|
||||
<span class="beschriftung">Zuschauer</span></div>
|
||||
<div class="wert"><span class="zahl" id="laufzeit">-</span>
|
||||
<span class="beschriftung">live seit</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/client.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var titel = document.getElementById('titel');
|
||||
var kategorie = document.getElementById('kategorie');
|
||||
var zuschauer = document.getElementById('zuschauer');
|
||||
var laufzeit = document.getElementById('laufzeit');
|
||||
var kanal = document.getElementById('kanal');
|
||||
var seit = null;
|
||||
|
||||
kanal.textContent = Overlay.param('kanal', 'abdulive_');
|
||||
|
||||
function setze(el, wert) {
|
||||
if (el.textContent === wert) return;
|
||||
el.textContent = wert;
|
||||
el.classList.remove('neu');
|
||||
// Neuzeichnen erzwingen, sonst startet die Animation beim zweiten Mal nicht.
|
||||
void el.offsetWidth;
|
||||
el.classList.add('neu');
|
||||
}
|
||||
|
||||
function stand(ev) {
|
||||
document.body.classList.toggle('live', !!ev.live);
|
||||
titel.textContent = ev.live ? (ev.title || 'ohne Titel') : 'nicht live';
|
||||
kategorie.textContent = ev.live ? (ev.category || '') : '';
|
||||
setze(zuschauer, ev.live ? Overlay.zahl(ev.viewers) : '-');
|
||||
seit = ev.live ? ev.startedAt : null;
|
||||
tick();
|
||||
}
|
||||
|
||||
function tick() {
|
||||
laufzeit.textContent = seit ? Overlay.laufzeit(seit) : '-';
|
||||
}
|
||||
setInterval(tick, 10000);
|
||||
|
||||
Overlay.on('stream', stand);
|
||||
Overlay.on('state', function (ev) { stand(ev.stream); });
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user