
At skelne et opkald fra en åben app
GeekBye kan bemærke, at du er gået med i et videomøde, og tilbyde at optage det. Detekteringen viser sig at være den lette halvdel — en Swift-binær, der læser vinduestitler hvert tiende sekund. Den svære halvdel er præcision: ikke at udløse, når Zoom bare er åben, ikke at spørge om et møde, du allerede optager, og ikke at dæmpe mikrofonen i det opkald, du faktisk sidder i. Tre releases, og hver enkelt er et værn, der måtte lære ikke at besejre sig selv.
GeekBye har en funktion, der lyder triviel og ikke er det: den bemærker, når du er gået med i et videomøde, og tilbyder med ét klik at begynde at optage det. Pitchet er en enkelt sætning. Implementeringen er tre releases af at lære, at det interessante problem aldrig var "detektér et møde" — det var "at have ret om det." Ret om, hvorvidt du faktisk sidder i et opkald kontra bare at have appen åben. Ret om ikke at spørge to gange. Ret om ikke at dæmpe den forkerte mikrofon. Hver af de tre releases er, i sin kerne, et værn, der måtte læres ikke at snuble over sig selv.
Vinduestitler, ikke processer
Den første og mest følgesvangre beslutning er skrevet ind i en commit-besked som et designudsagn, ikke en fodnote:
Detection uses window title matching (not process detection) to distinguish "app is open" from "in an active call."
Den distinktion er hele funktionen. Hvis du detekterede møder ved at kigge på, hvilke apps der kører, ville du udløse i det øjeblik Zoom startede — og Zoom er åben på mange skriveborde hele dagen uden et eneste opkald. Så GeekBye overvåger ikke processer. På macOS kører den en lille nativ Swift-binær, MeetingMonitor, der hvert tiende sekund kalder CGWindowListCopyWindowInfo for vinduerne på skærmen og læser hvert vindues titel via kCGWindowName. Et vindue med titlen "Zoom Meeting" betyder, at du er i et opkald; vinduet "Zoom Workplace" betyder, at appen blot er åben. Signalet er ikke "lever Zoom," det er "ser et vindue på din skærm ud som et igangværende opkald."
Pollingintervallet er en hård konstant — pollIntervalSeconds: Double = 10.0 — plus ét øjeblikkeligt tjek ved opstart, så et møde bemærkes inden for cirka ti sekunder efter, at dets vindue dukker op. (Det er en kodeværdi, ikke en målt latens; behandl "op til ~10 s" som aritmetik, ikke en benchmark.)
Browsermøder kræver ét skridt mere, fordi et Chrome-vindue med titlen "Meet - Weekly Sync" er lovende, men sikkerheden ligger i URL'en. Så når den forreste app er en kendt browser — Chrome, Safari, Edge, Brave og en håndfuld flere identificeret via bundle-id — udtrækker binæren den aktive fanes URL gennem AppleScript og tjekker den mod mønstre som meet.google.com/, zoom.us/j/ og teams.microsoft.com/. AppleScript ind i en anden app kan hænge, så det kald kører på en baggrunds-DispatchQueue beskyttet af en DispatchSemaphore med en 3-sekunders timeout; en fastlåst browser rammer timeout i stedet for at fryse hele detektoren.
Det svære er ikke at udløse
Her er den del, der overraskede mig, da jeg læste mønsterlisten. At detektere et opkald er nogle få strengmatch. Ikke at detektere et — ikke at udløse, når du ikke vil — er der, al omhuen gik hen. Listen af vinduestitel-mønstre læses som et sæt bevidste undtagelser:
- Google Meet matcher
"Meet - "og dens en-dash-variant"Meet – ", men ikke den nøgne streng"Google Meet"— fordi det er landingssiden, du åbner før et opkald. - Zoom matcher
"Zoom Meeting"og"Zoom Webinar", men ikke"Zoom Workplace"eller et nøgent"Zoom"— appen, der står åben. - Teams matcher
"Meeting with","Meeting in"," | Call"," | Meeting"— titelformerne under opkald, ikke den inaktive klient. - Slack-huddles, Webex-rum og WhatsApp-opkald får hver deres egne fragmenter, der kun gælder under opkald.
Hver eneste af dem er en beslutning om at holde tyst i det almindelige tilfælde, hvor en app er åben, men du ikke er i et møde. Designmålet er ikke maksimal recall; det er ikke at være værktøjet, der popper en "vil du optage?"-banner op, mens du læser Slack. En falsk positiv her er dyr på en måde, en falsk negativ ikke er: gå glip af et opkald, og brugeren begynder at optage manuelt; udløs forkert på en åben app, og brugeren lærer at ignorere din banner. Hele heuristikken er trimmet mod, at den anden fejl er sjælden.
Det er ærligt at nævne grænserne for en heuristik så simpel som denne. Mønstrene er kun på engelsk — et lokaliseret OS eller en ikke-engelsk mødeapp matcher ikke, en anerkendt v1-begrænsning. Og fordi vinduestitler for andre apps kun er synlige med tilladelsen Skærmoptagelse givet, degraderer detekteringen stille til ingenting uden den. Simpel, læsbar og ufuldkommen på måder, som teamet skrev ned frem for at skjule.
Tilbuddet, og maskineriet, der holder det fra at plage
Når et mønster matcher, viser GeekBye en lille rammeløs altid-øverst-banner i hjørnet — sit eget vindue, ikke en OS-notifikation — med en Start Recording-knap. Klik på den, og en IPC-besked (meeting:start-recording) skjuler banneret, henter hovedvinduet frem og beder rendereren om at starte en session. Afvis den, og banneret forsvinder.
Men en naiv version af dette ville være til at blive vanvittig af. Titler ændrer sig konstant under et opkald — en deltager går med, du omdøber mødet, en skærmdeling skifter vinduets navn — og hver ændring ville genudløse en detektor, der nøglede på den nøjagtige streng. Så detektoren, i MeetingDetector.ts, bærer en lille tilstandsmaskine, hvis hele opgave er tilbageholdenhed:
- Et stabilt
meetingIdper platform. Swift-siden udsender et id somgoogle-meet-active, bevidst ikke afledt af den flygtige vinduestitel, så tilslutninger, frakoblinger og omdøbninger inden for ét opkald ikke læses som et nyt møde. - Et
dismissedMeetings: Set<string>. Afvis banneret, og det mødes id ignoreres — indtil mødet faktisk slutter, hvorpå id'et ryddes, så det næste virkelige møde kan spørge på ny. - En
notificationActive-boolean. Kun én banner ad gangen, så en Slack-huddle, der dukker op, mens du allerede er i en Meet, ikke stabler en anden prompt.
Det er funktionen, egentlig: lidt detektering og en masse "vær ikke irriterende." Hvilket er præcis derfor, de næste to releases eksisterer — fordi to af de værn viste sig at besejre sig selv.
Dæmpningen, der dæmpede dit faktiske møde (v1.8.18)
Optagelse har en dæmpningsknap, og den første implementering tog "dæmp" i dens mest bogstavelige forstand. Leveret i v1.8.15 dæmpede den mikrofonen ved at sætte operativsystemets inputvolumen til nul:
osascript -e "set volume input volume 0"
med den forrige volumen gemt i databasen, så den kunne gendannes, en Windows-ækvivalent gennem waveInSetVolume og et before-quit-sikkerhedsnet for at sikre, at den blev afdæmpet. Koden beskrev det endda som en funktion — at dæmpe "på TO niveauer," system og app, så mikrofonen blev blokeret for alle apps.
Den sidste sætning er buggen udtrykt som en pralen. At blokere mikrofonen for alle apps betyder at blokere den for det Zoom- eller Meet-opkald, du faktisk sidder i. Du trykker på dæmp i GeekBye for at forhindre den i at høre dig, og du er blevet tavs i dit rigtige møde. v1.8.18-fixet er en degradering: hold helt op med at røre OS'ets inputvolumen, og dæmp kun GeekByes egen lydpipeline. I Listen-tilstand erstatter RealtimeAudioCaptureService dæmpede mikrofon-frames med en nulbuffer, så stilhed flyder til mixeren; i videotilstand sætter ScreenRecordingService en Web Audio-GainNode til nul. Den systemomfattende dæmpning, gendannelsen af den gemte volumen, hele det tværplatforms-apparat — alt sammen slettet. Dæmp bør betyde "denne app holder op med at lytte," aldrig "din mikrofon slukker for alle."
Værnet, der nulstillede sig selv (v1.8.19)
Det andet selvbesejrende værn var notificationActive. På papiret svarede det allerede på "vis ikke en banner, hvis en er oppe" — og det så ud til, at det også burde dække "tilbyd ikke at optage et møde, jeg allerede optager." Det gjorde det ikke, og commiten, der fikser det, forklarer præcis hvorfor:
The notificationActive flag resets when the meeting detector cycles through ended/detected states, causing notifications to reappear mid-recording.
Detektoren er en levende polling; over et langt opkald kan den kortvarigt læse "ended" og så "detected" igen — et titel-flimmer, et vindue et øjeblik uden for skærmen — og hver cyklus nulstillede flaget. Så værnet blev ved med stille at genaktivere sig selv, mens du optog, og banneret dukkede op igen og tilbød at optage det opkald, du allerede optog. Fixet holder op med at stole på et lokalt flag og spørger sandhedskilden i stedet: et recordingActiveChecker-callback, indsprøjtet fra main.ts og koblet til den virkelige isRealtimeTranscriptionActive-tilstand, tjekket før nogen notifikation vises.
// Don't show notification if recording/transcription is already active
if (this.recordingActiveChecker?.()) {
return
}
Lektien er lille og generel: et værn, hvis tilstand du ejer, kan blive nulstillet under dig af dit eget kontrolflow. Et værn, der læser den faktiske tilstand, det beskytter, kan ikke. notificationActive sporede en proxy for "optager vi"; fixet spørger, om vi optager.
Tre ting, disse releases lærte os
- Detektér den tilstand, du mener, ikke den, der er let at se. "Appen kører" er trivielt at tjekke og forkert. "Der findes et vindue, der ligner et igangværende opkald" er lidt sværere og korrekt. Kløften mellem de to er hele funktionen.
- For en proaktiv funktion er præcision produktet. Ingen husker mødet, du detekterede korrekt; de husker de tre gange, du afbrød dem, da de ikke var i ét. Mønsterundtagelserne og anti-plage-tilstandsmaskinen er ikke finpudsning — de er forskellen mellem en hjælpsom banner og en, du træner dig selv til at ignorere.
- Et værn, der kan nulstille sig selv, er ikke et værn. Begge opfølgende releases har samme form: en sikkerhedsmekanisme, der refererede til en proxy, den kontrollerede (en gemt volumen, en lokal boolean), i stedet for den virkelige tilstand (OS-mikrofonen, transskriptionsstatussen). Ret værn mod sandheden, ikke mod en kopi af den.
For det forrige kapitel i v1-historien, at tage backend ud af upload-stien (v1.8.11–v1.8.13); og for hele buen, anatomien i at shippe software til perfektion.