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>
72 lines
2.1 KiB
TypeScript
72 lines
2.1 KiB
TypeScript
// Einheitliches Ereignisformat. Alles, was aus IRC, EventSub, Helix oder OBS
|
|
// hereinkommt, wird darauf normalisiert und in `events` geschrieben.
|
|
// Siehe docs/03-data-model.md.
|
|
|
|
export type EventType =
|
|
| 'stream.online' | 'stream.offline' | 'stream.segment_changed'
|
|
| 'chat.message' | 'chat.first_message' | 'chat.spike'
|
|
| 'user.arrived' | 'user.returning'
|
|
| 'follow' | 'subscribe' | 'subgift' | 'cheer'
|
|
| 'raid.in' | 'raid.out'
|
|
| 'points.redeemed' | 'shop.redeemed'
|
|
| 'poll.begin' | 'poll.end'
|
|
| 'obs.scene_changed' | 'obs.replay_saved'
|
|
| 'clip.requested' | 'clip.created' | 'clip.rendered';
|
|
|
|
export type StreamEvent = {
|
|
id: string;
|
|
ts: string; // ISO-8601, UTC
|
|
type: EventType;
|
|
streamId: string | null;
|
|
userId: string | null;
|
|
payload: unknown;
|
|
};
|
|
|
|
export type ChatMessage = {
|
|
id: string; // msg-id aus IRC
|
|
ts: string;
|
|
userId: string;
|
|
login: string;
|
|
displayName: string;
|
|
text: string;
|
|
emotes: Array<{ id: string; name: string; count: number }>;
|
|
/**
|
|
* GIFs aus Twitchs Auswahlfenster. `from`/`to` zeigen auf die Zeichen im
|
|
* Text, an denen der Platzhalter steht - siehe `parseGifs` in #twitch/irc.ts.
|
|
*/
|
|
gifs: Array<{ id: string; url: string; from: number; to: number }>;
|
|
isFirst: boolean; // first-msg Tag
|
|
isReturning: boolean; // returning-chatter Tag
|
|
isCommand: boolean;
|
|
isMod: boolean;
|
|
isSub: boolean;
|
|
isBroadcaster: boolean;
|
|
bits: number;
|
|
color: string | null;
|
|
};
|
|
|
|
export type SpikeEvent = {
|
|
score: number;
|
|
rate: number;
|
|
baseline: number;
|
|
chatters: number;
|
|
emoteHits: number;
|
|
clipMentions: number;
|
|
windowFrom: string;
|
|
windowTo: string;
|
|
overThreshold: boolean;
|
|
};
|
|
|
|
/** ULID-artige, zeitlich sortierbare ID ohne Abhaengigkeit. */
|
|
const B32 = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
export function ulid(now = Date.now()): string {
|
|
let time = '';
|
|
let t = now;
|
|
for (let i = 0; i < 10; i++) { time = B32[t % 32]! + time; t = Math.floor(t / 32); }
|
|
const rnd = new Uint8Array(16);
|
|
crypto.getRandomValues(rnd);
|
|
let tail = '';
|
|
for (const b of rnd) tail += B32[b % 32]!;
|
|
return time + tail;
|
|
}
|