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,157 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>abdulive - Vorlesen</title>
|
||||
<link rel="stylesheet" href="/theme.css">
|
||||
<style>
|
||||
/* Die Seite zeigt fast nichts. Sie ist eine Tonquelle mit einer kleinen
|
||||
* Sichtkontrolle daneben - man soll sehen, was gerade gesprochen wird, und
|
||||
* zwar auch als Zuschauer. Wer nur hoert, was vorgelesen wird, kann nicht
|
||||
* nachlesen, wenn er es nicht verstanden hat. */
|
||||
#kasten {
|
||||
position: fixed;
|
||||
left: 16px; right: 16px; bottom: 16px;
|
||||
padding: 14px 18px;
|
||||
border-radius: 14px;
|
||||
background: var(--tinte-klar);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255, 255, 255, .1);
|
||||
box-shadow: var(--schatten), var(--glanz);
|
||||
opacity: 0;
|
||||
transform: translateY(14px);
|
||||
transition: opacity .3s, transform .3s;
|
||||
}
|
||||
#kasten.an { opacity: 1; transform: none; }
|
||||
|
||||
.wer {
|
||||
font-size: 13px;
|
||||
letter-spacing: .14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--tuerkis);
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.was { font-size: 22px; line-height: 1.35; }
|
||||
|
||||
/* Ein Hinweis nur fuer den Fall, dass die Browser-Quelle keine Sprachausgabe
|
||||
* kann. Im Stream soll das auffallen, nicht stillschweigend fehlen. */
|
||||
#fehlt {
|
||||
position: fixed;
|
||||
left: 16px; top: 16px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
background: rgba(239, 68, 68, .9);
|
||||
font: 600 15px/1.3 var(--font-zahl);
|
||||
max-width: 520px;
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="fehlt">
|
||||
Diese Browser-Quelle kann nicht sprechen (keine Sprachausgabe eingebaut).
|
||||
Vorgelesen wird nichts.
|
||||
</div>
|
||||
<div id="kasten"><div class="wer" id="wer"></div><div class="was" id="was"></div></div>
|
||||
|
||||
<script src="/client.js"></script>
|
||||
<script>
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var kasten = document.getElementById('kasten');
|
||||
var werEl = document.getElementById('wer');
|
||||
var wasEl = document.getElementById('was');
|
||||
|
||||
var sprache = Overlay.param('sprache', 'de-DE');
|
||||
var tempo = Overlay.param('tempo', 1);
|
||||
var lautstaerke = Overlay.param('laut', 1);
|
||||
var warteschlange = [];
|
||||
var laeuft = false;
|
||||
|
||||
/* Ob die Browser-Quelle ueberhaupt sprechen kann, weiss man erst zur
|
||||
* Laufzeit: OBS' eingebauter Browser wird teils ohne Sprachausgabe gebaut.
|
||||
* Ohne diese Pruefung bliebe es im Stream einfach still, und niemand wuesste
|
||||
* warum. */
|
||||
var kannSprechen = typeof window.speechSynthesis !== 'undefined' &&
|
||||
typeof window.SpeechSynthesisUtterance !== 'undefined';
|
||||
if (!kannSprechen) {
|
||||
document.getElementById('fehlt').style.display = 'block';
|
||||
console.error('[tts] Diese Browser-Quelle hat keine Sprachausgabe.');
|
||||
}
|
||||
|
||||
/* Stimmen werden asynchron geladen. Beim ersten Aufruf ist die Liste oft
|
||||
* noch leer - deshalb bei jedem Sprechen neu nachsehen statt einmal am
|
||||
* Anfang. */
|
||||
function stimme() {
|
||||
if (!kannSprechen) return null;
|
||||
var alle = window.speechSynthesis.getVoices() || [];
|
||||
var passend = alle.filter(function (v) { return v.lang && v.lang.indexOf(sprache.slice(0, 2)) === 0; });
|
||||
return passend[0] || alle[0] || null;
|
||||
}
|
||||
|
||||
function zeige(name, text) {
|
||||
werEl.textContent = name;
|
||||
wasEl.textContent = text;
|
||||
kasten.classList.add('an');
|
||||
}
|
||||
function verstecke() { kasten.classList.remove('an'); }
|
||||
|
||||
function naechste() {
|
||||
var n = warteschlange.shift();
|
||||
if (!n) { laeuft = false; verstecke(); return; }
|
||||
laeuft = true;
|
||||
zeige(n.name, n.text);
|
||||
|
||||
if (!kannSprechen) {
|
||||
// Wenigstens anzeigen, und nach der geschaetzten Lesedauer weiter.
|
||||
setTimeout(naechste, Math.min(15000, 2000 + n.text.length * 60));
|
||||
return;
|
||||
}
|
||||
|
||||
var rede = new SpeechSynthesisUtterance(n.text);
|
||||
rede.lang = sprache;
|
||||
rede.rate = tempo;
|
||||
rede.volume = lautstaerke;
|
||||
var v = stimme();
|
||||
if (v) rede.voice = v;
|
||||
|
||||
var weiter = false;
|
||||
var gehWeiter = function () {
|
||||
if (weiter) return;
|
||||
weiter = true;
|
||||
setTimeout(naechste, 400);
|
||||
};
|
||||
rede.onend = gehWeiter;
|
||||
rede.onerror = function (e) {
|
||||
console.error('[tts] Sprachausgabe fehlgeschlagen', e && e.error);
|
||||
gehWeiter();
|
||||
};
|
||||
// Notbremse: bleibt die Sprachausgabe haengen, steht die Warteschlange
|
||||
// sonst fuer den Rest des Streams. Grosszuegig bemessen.
|
||||
setTimeout(gehWeiter, 5000 + n.text.length * 250);
|
||||
|
||||
window.speechSynthesis.speak(rede);
|
||||
}
|
||||
|
||||
Overlay.on('tts', function (ev) {
|
||||
warteschlange.push({ name: ev.name, text: ev.text });
|
||||
if (warteschlange.length > 10) warteschlange.splice(0, warteschlange.length - 10);
|
||||
if (!laeuft) naechste();
|
||||
});
|
||||
|
||||
// Panikschalter: sofort still, Warteschlange weg.
|
||||
Overlay.on('still', function () {
|
||||
warteschlange.length = 0;
|
||||
if (kannSprechen) { try { window.speechSynthesis.cancel(); } catch (e) { /* egal */ } }
|
||||
laeuft = false;
|
||||
verstecke();
|
||||
});
|
||||
|
||||
// Nach dem Neuverbinden NICHT nachholen: eine Nachricht von vor zehn
|
||||
// Minuten vorzulesen ist schlimmer, als sie zu verpassen.
|
||||
Overlay.on('state', function () { });
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user