
Distingir una trucada d'una app oberta
GeekBye pot notar que t'has unit a una videotrucada i oferir-se a gravar-la. La detecció resulta ser la meitat fàcil — un binari de Swift que llegeix títols de finestra cada deu segons. La meitat difícil és la precisió: no disparar-se quan Zoom només està obert, no preguntar per una reunió que ja estàs gravant, i no silenciar el micròfon de la trucada on realment ets. Tres versions, i cadascuna és una salvaguarda que va haver d'aprendre a no derrotar-se a si mateixa.
GeekBye té una funcionalitat que sona trivial i no ho és: nota quan t'has unit a una videotrucada i s'ofereix, amb un sol clic, a començar a gravar-la. L'argument de venda és una sola frase. La implementació són tres versions d'aprendre que el problema interessant mai va ser "detectar una reunió" — era "encertar-la." Encertar si realment ets en una trucada versus només tenir l'app oberta. Encertar a no preguntar dues vegades. Encertar a no silenciar el micròfon equivocat. Cadascuna de les tres versions és, en el fons, una salvaguarda a qui es va haver d'ensenyar a no ensopegar amb si mateixa.
Títols de finestra, no processos
La primera decisió i la de més conseqüències està escrita en un missatge de commit com una declaració de disseny, no com una nota al peu:
Detection uses window title matching (not process detection) to distinguish "app is open" from "in an active call."
Aquesta distinció és tota la funcionalitat. Si detectessis reunions mirant quines apps s'estan executant, dispararies l'instant que Zoom s'obrís — i Zoom està obert en molts escriptoris tot el dia sense ni una sola trucada. Així que GeekBye no vigila processos. A macOS executa un petit binari natiu de Swift, MeetingMonitor, que cada deu segons crida CGWindowListCopyWindowInfo per a les finestres a la pantalla i llegeix el títol de cadascuna via kCGWindowName. Una finestra titulada "Zoom Meeting" vol dir que ets en una trucada; la finestra "Zoom Workplace" vol dir que l'app només està oberta. El senyal no és "està viu Zoom," és "sembla una finestra de la teva pantalla una trucada en directe."
L'interval de sondeig és una constant fixa — pollIntervalSeconds: Double = 10.0 — més una comprovació immediata a l'arrencada, de manera que una reunió es nota dins d'uns deu segons de l'aparició de la seva finestra. (Això és un valor de codi, no una latència mesurada; tracta "fins a ~10 s" com a aritmètica, no com a benchmark.)
Les reunions al navegador necessiten un pas més, perquè una finestra de Chrome titulada "Meet - Weekly Sync" és prometedora però la certesa és a la URL. Així que quan l'app en primer pla és un navegador conegut — Chrome, Safari, Edge, Brave, i uns quants més identificats per bundle id — el binari extreu la URL de la pestanya activa a través d'AppleScript i la comprova contra patrons com meet.google.com/, zoom.us/j/, i teams.microsoft.com/. AppleScript cap a una altra app pot penjar-se, així que aquella crida s'executa en una DispatchQueue de fons protegida per un DispatchSemaphore amb un temps d'espera de 3 segons; un navegador encallat expira per temps en comptes de congelar tot el detector.
La part difícil és no disparar-se
Aquí tens la part que em va sorprendre quan vaig llegir la llista de patrons. Detectar una trucada són uns quants aparellaments de cadena. No detectar-ne una — no disparar-se quan no ho vols — és on va anar tota la cura. La llista de patrons de títol de finestra es llegeix com un conjunt d'exclusions deliberades:
- Google Meet coincideix amb
"Meet - "i la seva variant amb guionet llarg"Meet – ", però no amb la cadena nua"Google Meet"— perquè aquesta és la pàgina d'aterratge que obres abans d'una trucada. - Zoom coincideix amb
"Zoom Meeting"i"Zoom Webinar", però no amb"Zoom Workplace"ni un"Zoom"nu — l'app asseguda oberta. - Teams coincideix amb
"Meeting with","Meeting in"," | Call"," | Meeting"— les formes de títol durant la trucada, no el client inactiu. - Els huddles de Slack, les sales de Webex i les trucades de WhatsApp reben cadascun els seus propis fragments exclusius de dins-de-trucada.
Cadascun d'aquests és una decisió de mantenir-se en silenci en el cas comú on una app està oberta però no ets en una reunió. L'objectiu de disseny no és la recuperació màxima; és no ser l'eina que fa saltar un bàner de "vols gravar?" mentre estàs llegint Slack. Un fals positiu aquí és car d'una manera que un fals negatiu no ho és: perd una trucada i l'usuari comença a gravar manualment; falla en una app oberta i l'usuari aprèn a ignorar el teu bàner. Tota l'heurística està afinada cap al fet que la segona fallada sigui rara.
És honest anomenar els límits d'una heurística tan simple. Els patrons són només en anglès — un OS localitzat o una app de reunions no anglesa no coincidiran, una limitació reconeguda de la v1. I com que els títols de finestra d'altres apps només són visibles amb el permís de Gravació de Pantalla concedit, la detecció es degrada silenciosament fins a res sense ell. Simple, llegible, i imperfecta de maneres que l'equip va escriure en comptes d'amagar.
L'oferta, i la maquinària que la manté sense empipar
Quan un patró coincideix, GeekBye mostra un petit bàner sense marc sempre-a-dalt a la cantonada — una finestra pròpia, no una notificació de l'OS — amb un botó Start Recording. Fes-hi clic i un missatge d'IPC (meeting:start-recording) amaga el bàner, fa aparèixer la finestra principal, i diu al renderer que comenci una sessió. Descarta'l i el bàner desapareix.
Però una versió ingènua d'això seria per tornar-se boig. Els títols canvien constantment durant una trucada — s'hi uneix un participant, canvies el nom de la reunió, una compartició de pantalla intercanvia el nom de la finestra — i cada canvi tornaria a disparar un detector que s'aferrava a la cadena exacta. Així que el detector, a MeetingDetector.ts, porta una petita màquina d'estats la feina sencera de la qual és la contenció:
- Un
meetingIdestable per plataforma. La banda de Swift emet un id comgoogle-meet-active, deliberadament no derivat del volàtil títol de finestra, de manera que unir-se, marxar i canviar el nom dins d'una trucada no es llegeixin com una reunió nova. - Un
dismissedMeetings: Set<string>. Descarta el bàner i l'id d'aquella reunió s'ignora — fins que la reunió acaba de debò, moment en què l'id es neteja perquè la següent reunió real pugui preguntar de nou. - Un booleà
notificationActive. Només un bàner alhora, de manera que un huddle de Slack que apareix mentre ja ets en un Meet no apila una segona sol·licitud.
Aquesta és la funcionalitat, de veritat: una mica de detecció i molt de "no siguis empipador." Que és exactament per què existeixen les dues versions següents — perquè dues d'aquelles salvaguardes van resultar derrotar-se a si mateixes.
El silenciament que silenciava la teva reunió real (v1.8.18)
Gravar té un botó de silenci, i la primera implementació es va prendre "silenciar" en el seu sentit més literal. Publicat a la v1.8.15, silenciava el micròfon posant el volum d'entrada del sistema operatiu a zero:
osascript -e "set volume input volume 0"
amb el volum previ desat a la base de dades perquè es pogués restaurar, un equivalent de Windows a través de waveInSetVolume, i una xarxa de seguretat before-quit per assegurar que es tornava a activar. El codi fins i tot ho descrivia com una funcionalitat — silenciar "a DOS nivells," sistema i app, de manera que el micròfon quedava bloquejat per a totes les apps.
Aquesta darrera frase és el bug enunciat com una fanfarronada. Bloquejar el micròfon per a totes les apps vol dir bloquejar-lo per a la trucada de Zoom o Meet on realment estàs assegut. Prems silenciar a GeekBye per impedir que ell et senti, i t'has quedat mut a la teva reunió real. La solució de la v1.8.18 és una degradació: deixa de tocar el volum d'entrada de l'OS del tot, i silencia només el propi pipeline d'àudio de GeekBye. En mode Listen, RealtimeAudioCaptureService substitueix els fotogrames de micròfon silenciats per un buffer a zero perquè flueixi silenci cap al mesclador; en mode vídeo, ScreenRecordingService posa un GainNode de Web Audio a zero. El silenciament a tot el sistema, la recuperació del volum desat, tot l'aparell multiplataforma — tot esborrat. Silenciar hauria de voler dir "aquesta app deixa d'escoltar," mai "el teu micròfon s'apaga per a tothom."
La salvaguarda que es reiniciava a si mateixa (v1.8.19)
L'altra salvaguarda que es derrotava a si mateixa era notificationActive. Sobre el paper ja responia "no mostris un bàner si n'hi ha un" — i semblava que també hauria de cobrir "no t'ofereixis a gravar una reunió que ja estic gravant." No ho feia, i el commit que ho arregla explica per què amb precisió:
The notificationActive flag resets when the meeting detector cycles through ended/detected states, causing notifications to reappear mid-recording.
El detector és un sondeig en directe; al llarg d'una trucada llarga pot llegir breument "ended" i després "detected" de nou — un parpelleig de títol, una finestra momentàniament fora de pantalla — i cada cicle reiniciava l'indicador. Així que la salvaguarda es tornava a armar silenciosament mentre gravaves, i el bàner reapareixia oferint gravar la trucada que ja estaves gravant. La solució deixa de confiar en un indicador local i pregunta a la font de veritat: una devolució de trucada recordingActiveChecker, injectada des de main.ts i connectada a l'estat real isRealtimeTranscriptionActive, comprovada abans que es mostri cap notificació.
// Don't show notification if recording/transcription is already active
if (this.recordingActiveChecker?.()) {
return
}
La lliçó és petita i general: una salvaguarda l'estat de la qual controles pot ser reiniciada per sota teu pel teu propi flux de control. Una salvaguarda que llegeix l'estat real que protegeix no pot. notificationActive seguia un proxy de "estem gravant"; la solució pregunta si estem gravant.
Tres coses que ens van ensenyar aquestes versions
- Detecta l'estat que vols dir, no el que és fàcil de veure. "L'app s'està executant" és trivial de comprovar i incorrecte. "Existeix una finestra que sembla una trucada en directe" és una mica més difícil i correcte. L'escletxa entre aquests dos és tota la funcionalitat.
- Per a una funcionalitat proactiva, la precisió és el producte. Ningú recorda la reunió que vas detectar correctament; recorden les tres vegades que els vas interrompre quan no eren en cap. Les excepcions de patró i la màquina d'estats anti-empipament no són poliment — són la diferència entre un bàner útil i un que t'entrenes a ignorar.
- Una salvaguarda que es pot reiniciar a si mateixa no és una salvaguarda. Totes dues versions posteriors tenen la mateixa forma: un mecanisme de seguretat que referenciava un proxy que controlava (un volum desat, un booleà local) en comptes de l'estat real (el micròfon de l'OS, l'estat de transcripció). Apunta les salvaguardes cap a la veritat, no cap a una còpia.
Per al capítol anterior de la història de la v1, treure el backend del camí de pujada (v1.8.11–v1.8.13); i per a tot l'arc, l'anatomia de publicar programari fins a la perfecció.