Steven
Steven5 Min. Lesezeit

Wie man einen Live-Bericht ohne das Flackern streamt

Wenn dein Meeting endet, füllt sich die Zusammenfassung von GeekBye jetzt live auf, statt dich auf einen Spinner starren zu lassen. Eine Streaming-Oberfläche ruhig statt zappelig wirken zu lassen, erforderte, das Flackern zweimal zu lösen — einmal für strukturierte Felder, einmal für Markdown — und die zweite Lösung war ein Renderer, den wir bereits besaßen.

Technik
Frontend
Streaming
GeekBye-Releases
Wie man einen Live-Bericht ohne das Flackern streamt

Vor diesem Release bedeutete das Beenden eines GeekBye-Meetings, auf einen Spinner zu starren. Die App schickte dein gesamtes Transkript zur Analyse, wartete auf die komplette Zusammenfassung — Bewertung, Kernpunkte, Aufgaben, alles — und ließ sie dann auf einen Schlag auf den Bildschirm fallen. Funktional, aber es fühlte sich langsam an, weil du ins Leere starrtest, während sie arbeitete.

GeekBye v1.6.13–v1.6.15 verwandelte das in einen Bericht, der live hereinstreamt, Feld für Feld, während er generiert wird. Der interessante Teil ist nicht das Streaming — es ist alles, was wir tun mussten, um das Streaming davon abzuhalten, zappelig auszusehen. Ruhiges Streaming und ruckeliges Streaming sind dieselbe Funktion mit sehr unterschiedlicher Umsetzung.

Flackern, Teil eins: lass nicht jeden Chunk React durchschütteln

Die Zusammenfassung ist kein einzelner Klumpen — es sind strukturierte Felder (eine Gesamtbewertung, Kernpunkte, Aufgaben und so weiter), die das Backend eines nach dem anderen über server-sent events ausgibt. Der naive Weg, das zu rendern, ist, den React-State bei jedem Ereignis zu aktualisieren, sobald es eintrifft.

Tu das, und du bekommst Flackern. Jedes eintreffende Feld löst sein eigenes Neu-Rendern aus; Komponenten, die „leer" zeigten, werden montiert, dann ausgehängt, dann als „geladen" neu montiert; das Layout zuckt bei jedem Paket. Der Bericht setzt sich vor deinen Augen selbst zusammen — auf die denkbar schlechteste Art, sichtbar, nervös.

Die Lösung ist eine zweiteilige Disziplin. Erstens: sammle die eintreffenden Felder in einem ref — einem veränderlichen Halter, der keine Renders auslöst — und veröffentliche einmal pro Chunk mit einer frischen Kopie in den React-State, damit sich der Baum bewusst aktualisiert statt bei jedem Mikro-Ereignis. Zweitens: rendere die ganze Zeit einen einzigen stabilen Komponentenbaum, ob im Streaming oder fertig, mit Inline-Platzhaltern für Felder, die noch nicht eingetroffen sind:

// accumulate in a ref, publish once per chunk
partialRef.current = { ...partialRef.current, [field]: value }
setPartial({ ...partialRef.current })

// one tree, never swapped; missing fields show a placeholder in place
const report = isStreaming ? partial : saved
<Score>{report.overallScore ?? '…'}</Score>

Die Komponente, die die Bewertung anzeigt, wird nie ausgehängt — sie zeigt einfach , bis die Zahl landet, dann die Zahl. Nichts zuckt. Und das vollständig zusammengesetzte Objekt wird am Ende trotzdem in der lokalen Datenbank zwischengespeichert, sodass Streaming-Oberfläche und dauerhafte Speicherung koexistieren, ohne sich zu bekämpfen.

Flackern, Teil zwei: der Renderer, den wir bereits besaßen

Das zweite Flackern ist subtiler und lebt im Markdown selbst. Der Bericht rendert formatiertes Markdown — Überschriften, Fettdruck, Listen, Code-Blöcke. Aber Markdown, das mitten im Stream eintrifft, ist in jedem gegebenen Moment halbfertig. Eine Liste mit bisher einem Aufzählungspunkt. Ein Code-Zaun, der geöffnet, aber nicht geschlossen wurde. Ein Fett-Marker noch ohne Partner.

Ein herkömmlicher Markdown-Renderer parst bei jedem Token die gesamte Zeichenkette neu, und dieser halbfertige Zustand parst jedes Mal zu etwas anderem — also flackert die Liste, der Code-Block blitzt auf und zu, das Layout springt, während die Tokens fertig werden. Es ist dieselbe nervöse Montage wie zuvor, eine Ebene tiefer.

Die Lösung war fast peinlich verfügbar: wir nutzten bereits einen streaming-bewussten Markdown-Renderer für den KI-Live-Chat. Ein Renderer, gebaut, um unvollständige Tokens zu vertragen — halbfertiges Markdown stabil zu rendern und es erst festzulegen, wenn die Tokens fertig sind — statt jedes Mal von Grund auf neu zu parsen. Der Bericht musste nur denselben verwenden. Wir hatten genau dieses Problem Monate zuvor für den Chat gelöst; die „Lösung" für den Bericht war zu erkennen, dass wir das Werkzeug bereits hatten, und es auf eine zweite Oberfläche zu richten. Wiederverwenden schlug Neubauen.

Der Bonus: formatiertes Kopieren ist ein Zwischenablage-Format-Problem

Sobald der Bericht gut aussah, wollten die Leute ihn einfügen — in ein Dokument, eine E-Mail — und die Formatierung und die Screenshots behalten. Der Instinkt ist, den Bericht in eine Markdown-Zeichenkette zu serialisieren und zu hoffen, dass das Ziel sie rendert. Meistens tut es das nicht.

Die wahre Antwort ist, dass die Zwischenablage mehrere Darstellungen gleichzeitig hält. Also schreiben wir zwei: eine HTML-Version mit intakter Formatierung und den als Inline-Bilder eingebetteten Screenshots und einen Nur-Text-Fallback für Ziele, die kein HTML annehmen. Füge in einen Rich-Editor ein und du bekommst den formatierten Bericht mit Bildern; füge in ein Nur-Text-Feld ein und du bekommst sauberen Text. „Formatiertes Kopieren" war nie ein Serialisierungsproblem — es war ein Problem des Das-richtige-Zwischenablage-Format-Wählens.

Dasselbe Release gab dem Bericht außerdem Chat-artige Ich / Sie-Beschriftungen mit den beiden Sprechern an gegenüberliegenden Seiten ausgerichtet, sodass sich ein Transkript wie das Gespräch liest, das es war, und verschob deine eigenen Screenshots passend auf ihre eigene Seite. (Ein Release in diesem Fenster, v1.6.14, war ein reiner Neubau — keine Geschichte darin, und es ist ehrlich, das zu sagen.)

Drei Dinge, die uns die Streaming-Oberfläche lehrte

  1. Puffere gestreamte Felder, veröffentliche einmal pro Chunk. Jedes server-sent event seine eigene State-Aktualisierung antreiben zu lassen, ist das Flackern. Ein ref-Akkumulator plus eine einzige Veröffentlichung pro Chunk, in einen Baum, der nie ausgehängt wird, verwandelt nervöse Montage in ruhiges Auffüllen.
  2. Alles, was streamt, braucht einen streaming-bewussten Renderer. Ein Renderer, der pro Token die gesamte Zeichenkette neu parst, flackert bei halbfertigem Markdown. Nutze einen, der für unvollständige Eingaben gebaut ist — und wenn du schon einen für eine andere Oberfläche hast, verwende ihn wieder, bevor du einen zweiten baust.
  3. Formatiertes Kopieren dreht sich um Zwischenablage-Formate, nicht um Serialisierung. Schreibe text/html mit eingebetteten Bildern und einen text/plain-Fallback. Die Zwischenablage wurde entworfen, um beides zu tragen; nutze sie.

Dies ist das fünfte Kapitel der Zuverlässigkeits- und Feinschliff-Geschichte, die zu GeekBye v2 wird. Für das vorherige Kapitel siehe den Ohrhörer-Bug (v1.6.12); für die Stelle, an der dieselbe server-sent-events-Idee später zu einem ganzen Fallback-Transport wurde, Live-Transkription, wenn die Firewall WebSockets blockiert (v2.0.8); und für den gesamten Bogen die Anatomie, Software bis zur Perfektion auszuliefern.

Ähnliche Artikel

Die Stille war tragend
Steven
Steven7 Min. Lesezeit

Die Stille war tragend

Die letzten beiden Releases von GeekBye v1 drehen sich um dieselbe unbequeme Wahrheit: Echtzeit-Transkription über ein echtes Netzwerk ist nicht verlustfrei, und der ehrliche Schritt ist, aufzuhören, so zu tun, als wäre sie es. v1.8.20 behielt eine Kopie jedes Audio-Chunks auf der Festplatte, bevor es ihn während eines Reconnects verwarf, und begann, die Lücken im Transkript laut zu markieren. v1.9.0 hörte auf, Stille zu senden, um Bandbreite zu sparen — und entdeckte, dass die Stille genau das Signal war, mit dem der Transkriber erkannte, dass ein Satz zu Ende war. Zwei Releases über die Kosten des Wegwerfens.

Technik
Audio
Zuverlässigkeit
Die drei Verben, die Web Audio am Leben halten
Steven
Steven9 Min. Lesezeit

Die drei Verben, die Web Audio am Leben halten

Zwei punktuelle GeekBye-Releases, zwei Monate auseinander und in zwei verschiedenen Dateien, brachten unserem Audio-Code dieselbe Lektion von entgegengesetzten Enden bei: den AudioContext des Browsers nicht länger als Wegwerfobjekt zu behandeln. Ein Release lernte, einen Kontext per resume() wiederaufzunehmen, den macOS mitten in der Aufnahme still suspendiert hatte; das andere lernte, suspend() statt close() aufzurufen, damit aufeinanderfolgende Sitzungen nicht länger gegen Chromiums Decke von etwa sechs Kontexten krachen. Resume, suspend, close — das ist die ganze Handlung.

Technik
Audio
Desktop
Einen Anruf von einer offenen App unterscheiden
Steven
Steven8 Min. Lesezeit

Einen Anruf von einer offenen App unterscheiden

GeekBye kann bemerken, dass du einem Videomeeting beigetreten bist, und anbieten, es aufzunehmen. Die Erkennung erweist sich als die einfache Hälfte — ein Swift-Binary, das alle zehn Sekunden Fenstertitel liest. Die harte Hälfte ist Präzision: nicht auszulösen, wenn Zoom bloß offen ist, nicht für ein Meeting zu fragen, das du bereits aufnimmst, und nicht das Mikrofon in dem Anruf stummzuschalten, in dem du tatsächlich bist. Drei Releases, und jedes ist ein Wächter, der lernen musste, sich nicht selbst zu besiegen.

Technik
macOS
Desktop