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
GeekBye-Releases
Einen Anruf von einer offenen App unterscheiden

GeekBye hat ein Feature, das trivial klingt und es nicht ist: es bemerkt, wenn du einem Videomeeting beigetreten bist, und bietet mit einem Klick an, es aufzunehmen. Der Pitch ist ein einziger Satz. Die Umsetzung sind drei Releases des Lernens, dass das interessante Problem nie „ein Meeting erkennen“ war — es war „damit richtig zu liegen“. Richtig darüber, ob du tatsächlich in einem Anruf bist oder nur die App offen hast. Richtig darüber, nicht zweimal zu fragen. Richtig darüber, nicht das falsche Mikrofon stummzuschalten. Jedes der drei Releases ist im Kern ein Wächter, dem beigebracht werden musste, nicht über sich selbst zu stolpern.

Fenstertitel, keine Prozesse

Die erste und folgenreichste Entscheidung ist in einer Commit-Nachricht als Design-Aussage festgehalten, nicht als Fußnote:

Detection uses window title matching (not process detection) to distinguish "app is open" from "in an active call."

Diese Unterscheidung ist das ganze Feature. Würdest du Meetings erkennen, indem du schaust, welche Apps laufen, würdest du in dem Moment auslösen, in dem Zoom startet — und Zoom ist auf vielen Desktops den ganzen Tag offen ohne einen einzigen Anruf. Also überwacht GeekBye keine Prozesse. Auf macOS führt es ein kleines natives Swift-Binary aus, MeetingMonitor, das alle zehn Sekunden CGWindowListCopyWindowInfo für die Fenster auf dem Bildschirm aufruft und den Titel jedes einzelnen via kCGWindowName liest. Ein Fenster mit dem Titel "Zoom Meeting" bedeutet, dass du in einem Anruf bist; das "Zoom Workplace"-Fenster bedeutet, dass die App bloß offen ist. Das Signal ist nicht „lebt Zoom“, es ist „sieht ein Fenster auf deinem Bildschirm wie ein Live-Anruf aus“.

Das Abfrageintervall ist eine harte Konstante — pollIntervalSeconds: Double = 10.0 — plus eine sofortige Prüfung beim Start, sodass ein Meeting innerhalb von etwa zehn Sekunden nach Erscheinen seines Fensters bemerkt wird. (Das ist ein Code-Wert, keine gemessene Latenz; behandle „bis zu ~10 s“ als Arithmetik, nicht als Benchmark.)

Browser-Meetings brauchen einen Schritt mehr, weil ein Chrome-Fenster mit dem Titel "Meet - Weekly Sync" vielversprechend ist, aber die Gewissheit in der URL liegt. Wenn also die vorderste App ein bekannter Browser ist — Chrome, Safari, Edge, Brave und eine Handvoll mehr, identifiziert per bundle id — extrahiert das Binary die URL des aktiven Tabs via AppleScript und prüft sie gegen Muster wie meet.google.com/, zoom.us/j/ und teams.microsoft.com/. AppleScript in eine andere App kann hängen, also läuft dieser Aufruf auf einer Hintergrund-DispatchQueue, abgesichert durch ein DispatchSemaphore mit einem 3-Sekunden-Timeout; ein festgefahrener Browser läuft in ein Timeout, statt den ganzen Detektor einzufrieren.

Der harte Teil ist das Nicht-Auslösen

Hier ist der Teil, der mich überraschte, als ich die Musterliste las. Einen Anruf zu erkennen sind ein paar String-Abgleiche. Einen nicht zu erkennen — nicht auszulösen, wenn du es nicht willst — ist, wohin all die Sorgfalt ging. Die Liste der Fenstertitel-Muster liest sich wie eine Reihe bewusster Ausschlüsse:

  • Google Meet trifft "Meet - " und seine Halbgeviertstrich-Variante "Meet – ", aber nicht den bloßen String "Google Meet" — weil das die Landingpage ist, die du vor einem Anruf öffnest.
  • Zoom trifft "Zoom Meeting" und "Zoom Webinar", aber nicht "Zoom Workplace" oder ein bloßes "Zoom" — die offen dastehende App.
  • Teams trifft "Meeting with", "Meeting in", " | Call", " | Meeting" — die Im-Anruf-Titelformen, nicht den ruhenden Client.
  • Slack-Huddles, Webex-Räume und WhatsApp-Anrufe bekommen jeweils ihre eigenen Nur-im-Anruf-Fragmente.

Jeder davon ist eine Entscheidung, im häufigen Fall still zu bleiben, in dem eine App offen ist, du aber nicht in einem Meeting bist. Das Designziel ist nicht maximaler Recall; es ist, nicht das Tool zu sein, das ein „aufnehmen?“-Banner aufpoppt, während du Slack liest. Ein Fehlalarm ist hier teuer auf eine Weise, wie es ein verpasster Treffer nicht ist: verpass einen Anruf, und der Nutzer nimmt manuell auf; feuere fälschlich bei einer offenen App, und der Nutzer lernt, dein Banner zu ignorieren. Die ganze Heuristik ist darauf abgestimmt, dass der zweite Fehler selten ist.

Es ist ehrlich, die Grenzen einer so einfachen Heuristik zu benennen. Die Muster sind nur auf Englisch — ein lokalisiertes OS oder eine nicht-englische Meeting-App trifft nicht zu, eine eingestandene v1-Einschränkung. Und weil Fenstertitel anderer Apps nur mit erteilter Bildschirmaufnahme-Berechtigung sichtbar sind, degradiert die Erkennung ohne sie stillschweigend zu nichts. Einfach, lesbar und auf Weisen unvollkommen, die das Team aufschrieb, statt sie zu verbergen.

Das Angebot und die Mechanik, die es vom Nerven abhält

Wenn ein Muster trifft, zeigt GeekBye ein kleines rahmenloses immer-im-Vordergrund-Banner in der Ecke — sein eigenes Fenster, keine OS-Benachrichtigung — mit einem Aufnahme starten-Button. Klick darauf, und eine IPC-Nachricht (meeting:start-recording) verbirgt das Banner, holt das Hauptfenster hervor und weist den Renderer an, eine Sitzung zu starten. Verwirf es, und das Banner verschwindet.

Aber eine naive Version davon wäre zum Verrücktwerden. Titel ändern sich während eines Anrufs ständig — ein Teilnehmer tritt bei, du benennst das Meeting um, ein Screenshare tauscht den Fensternamen — und jede Änderung würde einen Detektor erneut auslösen, der auf den exakten String abstellte. Also trägt der Detektor in MeetingDetector.ts eine kleine Zustandsmaschine, deren ganze Aufgabe Zurückhaltung ist:

  • Eine stabile meetingId pro Plattform. Die Swift-Seite gibt eine id wie google-meet-active aus, absichtlich nicht aus dem flüchtigen Fenstertitel abgeleitet, sodass Beitreten, Verlassen und Umbenennen innerhalb eines Anrufs nicht als neues Meeting gelesen werden.
  • Ein dismissedMeetings: Set<string>. Verwirf das Banner, und die id dieses Meetings wird ignoriert — bis das Meeting tatsächlich endet, woraufhin die id sich leert, damit das nächste echte Meeting frisch auffordern kann.
  • Ein notificationActive-Boolean. Nur ein Banner zur Zeit, sodass ein Slack-Huddle, der auftaucht, während du bereits in einem Meet bist, keine zweite Aufforderung stapelt.

Das ist das Feature, wirklich: ein bisschen Erkennung und viel „nicht nervig sein“. Was genau der Grund ist, warum die nächsten zwei Releases existieren — weil zwei dieser Wächter sich als selbstbesiegend herausstellten.

Die Stummschaltung, die dein tatsächliches Meeting stummschaltete (v1.8.18)

Die Aufnahme hat einen Stummschalt-Button, und die erste Umsetzung nahm „stumm“ im wörtlichsten Sinn. In v1.8.15 ausgeliefert, schaltete sie das Mikrofon stumm, indem sie die Eingangslautstärke des Betriebssystems auf null setzte:

osascript -e "set volume input volume 0"

mit der vorherigen Lautstärke in der Datenbank gespeichert, damit sie wiederhergestellt werden konnte, einem Windows-Äquivalent über waveInSetVolume und einem before-quit-Sicherheitsnetz, um sicherzustellen, dass sie die Stummschaltung aufhob. Der Code beschrieb es sogar als Feature — stummschalten „auf ZWEI Ebenen“, System und App, sodass das Mikro für alle Apps blockiert war.

Dieser letzte Satz ist der Bug, formuliert als Prahlerei. Das Mikro für alle Apps zu blockieren heißt, es für den Zoom- oder Meet-Anruf zu blockieren, in dem du tatsächlich sitzt. Du drückst in GeekBye auf stumm, um es am Zuhören zu hindern, und bist in deinem echten Meeting verstummt. Der v1.8.18-Fix ist eine Herabstufung: die OS-Eingangslautstärke gar nicht mehr anzufassen und nur GeekByes eigene Audio-Pipeline stummzuschalten. Im Listen-Modus ersetzt RealtimeAudioCaptureService stummgeschaltete Mikro-Frames durch einen Null-Buffer, sodass Stille zum Mixer fließt; im Videomodus setzt ScreenRecordingService einen Web-Audio-GainNode auf null. Die systemweite Stummschaltung, die Wiederherstellung der gespeicherten Lautstärke, der ganze plattformübergreifende Apparat — alles gelöscht. Stumm sollte heißen „diese App hört auf zuzuhören“, niemals „dein Mikrofon geht für alle aus“.

Der Wächter, der sich selbst zurücksetzte (v1.8.19)

Der andere selbstbesiegende Wächter war notificationActive. Auf dem Papier beantwortete er bereits „zeige kein Banner, wenn eines oben ist“ — und es sah aus, als sollte er auch „biete nicht an, ein Meeting aufzunehmen, das ich bereits aufnehme“ abdecken. Tat er nicht, und der Commit, der es behebt, erklärt präzise, warum:

The notificationActive flag resets when the meeting detector cycles through ended/detected states, causing notifications to reappear mid-recording.

Der Detektor ist eine Live-Abfrage; über einen langen Anruf kann er kurz „beendet“ und dann wieder „erkannt“ lesen — ein Titel-Flackern, ein Fenster kurzzeitig außerhalb des Bildschirms — und jeder Zyklus setzte das Flag zurück. So bewaffnete sich der Wächter still immer wieder neu, während du aufnahmst, und das Banner tauchte wieder auf und bot an, den Anruf aufzunehmen, den du bereits aufnahmst. Der Fix hört auf, einem lokalen Flag zu vertrauen, und fragt stattdessen die Quelle der Wahrheit: einen recordingActiveChecker-Callback, injiziert aus main.ts und verdrahtet mit dem echten isRealtimeTranscriptionActive-Zustand, geprüft, bevor irgendeine Benachrichtigung erscheint.

// Don't show notification if recording/transcription is already active
if (this.recordingActiveChecker?.()) {
  return
}

Die Lehre ist klein und allgemein: ein Wächter, dessen Zustand du besitzt, kann dir von deinem eigenen Kontrollfluss unter den Füßen zurückgesetzt werden. Ein Wächter, der den tatsächlichen Zustand liest, den er schützt, kann das nicht. notificationActive verfolgte einen Proxy für „nehmen wir auf“; der Fix fragt, ob wir aufnehmen.

Drei Dinge, die uns diese Releases lehrten

  1. Erkenne den Zustand, den du meinst, nicht den, der leicht zu sehen ist. „Die App läuft“ ist trivial zu prüfen und falsch. „Ein Fenster, das wie ein Live-Anruf aussieht, existiert“ ist etwas schwerer und richtig. Die Lücke zwischen diesen beiden ist das ganze Feature.
  2. Für ein proaktives Feature ist Präzision das Produkt. Niemand erinnert sich an das Meeting, das du korrekt erkannt hast; sie erinnern sich an die drei Male, die du sie unterbrachst, als sie in keinem waren. Die Muster-Ausnahmen und die Anti-Nerv-Zustandsmaschine sind kein Feinschliff — sie sind der Unterschied zwischen einem hilfreichen Banner und einem, das du dir antrainierst zu ignorieren.
  3. Ein Wächter, der sich selbst zurücksetzen kann, ist kein Wächter. Beide Folge-Releases haben dieselbe Form: ein Sicherheitsmechanismus, der einen von ihm kontrollierten Proxy (eine gespeicherte Lautstärke, einen lokalen Boolean) referenzierte statt des echten Zustands (das OS-Mikro, den Transkriptionsstatus). Richte Wächter auf die Wahrheit, nicht auf eine Kopie davon.

Für das vorige Kapitel der v1-Geschichte, das Backend aus dem Upload-Pfad nehmen (v1.8.11–v1.8.13); und für den ganzen Bogen, die Anatomie, Software bis zur Perfektion auszuliefern.

Ähnliche Artikel

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
Das Backend aus dem Upload-Pfad nehmen
Steven
Steven8 Min. Lesezeit

Das Backend aus dem Upload-Pfad nehmen

GeekBye nimmt deinen Bildschirm auf und speichert das Video auf deinem Google Drive. Die erste Version schickte jede Aufnahme unterwegs durch GeekByes eigene Server; eine Version später ging die Datei direkt von deiner Maschine zu Drive, und das Backend wurde zum Halten eines einzigen Zeigers degradiert. Das Interessante ist, wie wenig Code die „direkte, fortsetzbare“ Version tatsächlich enthält — denn die Fortsetzbarkeit kam vom Löschen eines Proxys, nicht vom Schreiben eines.

Technik
Architektur
Desktop
Geboren und behoben in zwölf Minuten
Steven
Steven7 Min. Lesezeit

Geboren und behoben in zwölf Minuten

Das Changelog von GeekBye v1.8.10 sagt, es habe einen Crash beim Bearbeiten von Tastaturkürzeln behoben. Hat es — aber der Crash wurde im selben Pull Request eingeführt und behoben, zwölf Minuten auseinander, und erreichte keinen einzigen Nutzer. Die eigentliche Geschichte ist die Zuverlässigkeitskaskade, die ihn hervorbrachte: eine kleine, korrekte Änderung daran, wann Kürzel suspendiert werden, und der React-Teardown-Bug, der aus einer einzigen Zeile fiel, die den Editor sicherer machen sollte.

Technik
React
Desktop