
Een call onderscheiden van een open app
GeekBye kan merken dat je aan een videovergadering bent gaan deelnemen en aanbieden om die op te nemen. De detectie blijkt de makkelijke helft — een Swift-binary die elke tien seconden vensternamen leest. De moeilijke helft is precisie: niet afgaan wanneer Zoom slechts openstaat, niet vragen om een vergadering die je al opneemt, en niet de microfoon dempen in het gesprek waar je daadwerkelijk in zit. Drie releases, en elk ervan is een waarborg die moest leren zichzelf niet te verslaan.
GeekBye heeft een feature die triviaal klinkt en dat niet is: het merkt op wanneer je aan een videovergadering bent gaan deelnemen en biedt aan om die met één klik op te nemen. De pitch is één zin. De implementatie is drie releases van het leren dat het interessante probleem nooit "detecteer een vergadering" was — het was "gelijk hebben erover." Gelijk over de vraag of je daadwerkelijk in een gesprek zit versus alleen de app open hebben. Gelijk over niet twee keer vragen. Gelijk over niet de verkeerde microfoon dempen. Elk van de drie releases is in de kern een waarborg die geleerd moest worden niet over zichzelf te struikelen.
Venstertitels, geen processen
De eerste en meest ingrijpende beslissing staat in een commit-bericht als ontwerpuitspraak, niet als voetnoot:
Detection uses window title matching (not process detection) to distinguish "app is open" from "in an active call."
Dat onderscheid is de hele feature. Als je vergaderingen detecteerde door te kijken welke apps draaien, zou je afgaan op het moment dat Zoom startte — en Zoom staat de hele dag open op een heleboel desktops zonder ook maar één gesprek. Dus GeekBye kijkt niet naar processen. Op macOS draait het een kleine native Swift-binary, MeetingMonitor, die elke tien seconden CGWindowListCopyWindowInfo aanroept voor de vensters op het scherm en de titel van elk venster leest via kCGWindowName. Een venster met de titel "Zoom Meeting" betekent dat je in een gesprek zit; het venster "Zoom Workplace" betekent dat de app slechts openstaat. Het signaal is niet "leeft Zoom," het is "ziet een venster op je scherm eruit als een live gesprek."
Het poll-interval is een harde constante — pollIntervalSeconds: Double = 10.0 — plus één onmiddellijke controle bij het opstarten, zodat een vergadering binnen ongeveer tien seconden na het verschijnen van het venster wordt opgemerkt. (Dat is een codewaarde, geen gemeten latency; behandel "tot ~10 s" als rekenwerk, niet als benchmark.)
Vergaderingen in de browser hebben één extra stap nodig, want een Chrome-venster met de titel "Meet - Weekly Sync" is veelbelovend maar de zekerheid zit in de URL. Dus wanneer de voorste app een bekende browser is — Chrome, Safari, Edge, Brave, en een handvol meer geïdentificeerd via bundle-id — haalt de binary de URL van het actieve tabblad op via AppleScript en toetst die aan patronen als meet.google.com/, zoom.us/j/, en teams.microsoft.com/. AppleScript naar een andere app kan vastlopen, dus die aanroep draait op een achtergrond-DispatchQueue bewaakt door een DispatchSemaphore met een time-out van 3 seconden; een vastgelopen browser loopt af via time-out in plaats van de hele detector te bevriezen.
Het moeilijke deel is niet afgaan
Dit is het deel dat me verraste toen ik de patroonlijst las. Een gesprek detecteren is een paar string-matches. Er géén detecteren — niet afgaan wanneer je dat niet wilt — is waar alle zorg naartoe ging. De lijst met venstertitel-patronen leest als een verzameling bewuste uitsluitingen:
- Google Meet matcht
"Meet - "en de en-dash-variant"Meet – ", maar niet de kale string"Google Meet"— want dat is de landingspagina die je vóór een gesprek opent. - Zoom matcht
"Zoom Meeting"en"Zoom Webinar", maar niet"Zoom Workplace"of een kale"Zoom"— de app die openstaat. - Teams matcht
"Meeting with","Meeting in"," | Call"," | Meeting"— de titelvormen tijdens een gesprek, niet de inactieve client. - Slack-huddles, Webex-rooms en WhatsApp-gesprekken krijgen elk hun eigen fragmenten die alleen tijdens een gesprek voorkomen.
Elk daarvan is een beslissing om stil te blijven in het veelvoorkomende geval waarin een app openstaat maar je niet in een vergadering zit. Het ontwerpdoel is niet maximale recall; het is niet de tool zijn die een "opnemen?"-banner laat opduiken terwijl je Slack aan het lezen bent. Een valse positieve is hier duur op een manier die een valse negatieve niet is: mis een gesprek en de gebruiker begint handmatig op te nemen; ga verkeerd af op een open app en de gebruiker leert je banner te negeren. De hele heuristiek is afgesteld op het zeldzaam maken van de tweede faalvorm.
Het is eerlijk om de grenzen van zo'n eenvoudige heuristiek te benoemen. De patronen zijn alleen Engels — een gelokaliseerd besturingssysteem of een niet-Engelse vergaderapp matcht niet, een erkende v1-beperking. En omdat venstertitels van andere apps alleen zichtbaar zijn met de toestemming Schermopname verleend, degradeert detectie stilletjes tot niets zonder die. Eenvoudig, leesbaar, en imperfect op manieren die het team opschreef in plaats van verborg.
Het aanbod, en de machinerie die het van zeuren weerhoudt
Wanneer een patroon matcht, toont GeekBye een kleine frameloze altijd-bovenop banner in de hoek — een eigen venster, geen OS-notificatie — met een Start Recording-knop. Klik erop en een IPC-bericht (meeting:start-recording) verbergt de banner, brengt het hoofdvenster naar voren, en vertelt de renderer een sessie te starten. Wijs hem af en de banner verdwijnt.
Maar een naïeve versie hiervan zou om gek van te worden zijn. Titels veranderen voortdurend tijdens een gesprek — een deelnemer komt erbij, je hernoemt de vergadering, een schermdeling verwisselt de vensternaam — en elke verandering zou een detector die op de exacte string afging opnieuw laten afgaan. Dus de detector, in MeetingDetector.ts, draagt een kleine toestandsmachine wiens hele taak terughoudendheid is:
- Een stabiele
meetingIdper platform. De Swift-kant zendt een id uit zoalsgoogle-meet-active, bewust niet afgeleid van de vluchtige venstertitel, zodat deelnemen, verlaten en hernoemen binnen één gesprek niet als een nieuwe vergadering worden gelezen. - Een
dismissedMeetings: Set<string>. Wijs de banner af en de id van die vergadering wordt genegeerd — totdat de vergadering daadwerkelijk eindigt, waarna de id wordt gewist zodat de volgende echte vergadering opnieuw kan vragen. - Een
notificationActive-boolean. Slechts één banner tegelijk, zodat een Slack-huddle die opduikt terwijl je al in een Meet zit geen tweede prompt stapelt.
Dat is de feature, eigenlijk: een beetje detectie en een hoop "wees niet vervelend." Wat precies de reden is dat de volgende twee releases bestaan — omdat twee van die waarborgen zichzelf bleken te verslaan.
Het dempen dat je echte vergadering dempte (v1.8.18)
Opnemen heeft een dempknop, en de eerste implementatie nam "dempen" in de meest letterlijke zin. Uitgebracht in v1.8.15, dempte het de microfoon door het input-volume van het besturingssysteem op nul te zetten:
osascript -e "set volume input volume 0"
met het vorige volume opgeslagen in de database zodat het hersteld kon worden, een Windows-equivalent via waveInSetVolume, en een before-quit-vangnet om te zorgen dat het weer aanging. De code beschreef het zelfs als een feature — dempen "op TWEE niveaus," systeem en app, zodat de mic werd geblokkeerd voor alle apps.
Die laatste zinsnede is de bug uitgesproken als een opschepperij. De mic blokkeren voor alle apps betekent hem blokkeren voor het Zoom- of Meet-gesprek waar je daadwerkelijk in zit. Je drukt op dempen in GeekBye om te voorkomen dat het je hoort, en je bent stil geworden in je echte vergadering. De fix in v1.8.18 is een degradatie: raak het OS-input-volume helemaal niet meer aan, en demp alleen GeekBye's eigen audiopijplijn. In Listen-modus vervangt RealtimeAudioCaptureService gedempte mic-frames door een nul-buffer zodat er stilte naar de mixer stroomt; in videomodus zet ScreenRecordingService een Web Audio GainNode op nul. Het systeembrede dempen, het herstel van het opgeslagen volume, het hele cross-platform-apparaat — allemaal verwijderd. Dempen zou moeten betekenen "deze app stopt met luisteren," nooit "je microfoon gaat voor iedereen uit."
De waarborg die zichzelf resette (v1.8.19)
De andere zichzelf-verslaande waarborg was notificationActive. Op papier beantwoordde het al "toon geen banner als er al een staat" — en het leek alsof het ook "bied niet aan om een vergadering op te nemen die ik al opneem" zou moeten dekken. Dat deed het niet, en de commit die het repareert legt precies uit waarom:
The notificationActive flag resets when the meeting detector cycles through ended/detected states, causing notifications to reappear mid-recording.
De detector is een live poll; over een lang gesprek kan het kort "ended" en dan weer "detected" lezen — een titel die flikkert, een venster dat even van het scherm is — en elke cyclus resette de flag. Dus de waarborg bleef zichzelf stilletjes opnieuw scherpstellen terwijl je aan het opnemen was, en de banner verscheen weer met het aanbod om het gesprek op te nemen dat je al aan het opnemen was. De fix stopt met vertrouwen op een lokale flag en vraagt in plaats daarvan de bron van waarheid: een recordingActiveChecker-callback, geïnjecteerd vanuit main.ts en gekoppeld aan de echte isRealtimeTranscriptionActive-status, gecontroleerd voordat er een notificatie verschijnt.
// Don't show notification if recording/transcription is already active
if (this.recordingActiveChecker?.()) {
return
}
De les is klein en algemeen: een waarborg wiens status je zelf beheert kan onder je vandaan worden gereset door je eigen control flow. Een waarborg die de daadwerkelijke status leest die het beschermt kan dat niet. notificationActive volgde een proxy voor "nemen we op"; de fix vraagt of we opnemen.
Drie dingen die deze releases ons leerden
- Detecteer de status die je bedoelt, niet degene die makkelijk te zien is. "De app draait" is triviaal te controleren en fout. "Er bestaat een venster dat eruitziet als een live gesprek" is iets moeilijker en juist. Het gat tussen die twee is de hele feature.
- Voor een proactieve feature is precisie het product. Niemand herinnert zich de vergadering die je correct detecteerde; ze herinneren zich de drie keer dat je ze onderbrak toen ze in geen enkele zaten. De patroonuitzonderingen en de anti-zeur-toestandsmachine zijn geen finishing touch — ze zijn het verschil tussen een behulpzame banner en een die je jezelf aanleert te negeren.
- Een waarborg die zichzelf kan resetten is geen waarborg. Beide vervolgreleases hebben dezelfde vorm: een veiligheidsmechanisme dat verwees naar een proxy die het beheerde (een opgeslagen volume, een lokale boolean) in plaats van naar de echte status (de OS-mic, de transcriptiestatus). Richt waarborgen op de waarheid, niet op een kopie ervan.
Voor het vorige hoofdstuk in het v1-verhaal, de backend uit het uploadpad halen (v1.8.11–v1.8.13); en voor de hele boog, de anatomie van software tot in de perfectie uitbrengen.