
A deosebi un apel de o aplicație deschisă
GeekBye poate observa că te-ai alăturat unei ședințe video și îți poate oferi s-o înregistreze. Detecția se dovedește a fi jumătatea ușoară — un binar Swift care citește titlurile ferestrelor la fiecare zece secunde. Jumătatea grea e precizia: să nu se declanșeze când Zoom e doar deschis, să nu întrebe pentru o ședință pe care deja o înregistrezi și să nu silențieze microfonul din apelul în care chiar te afli. Trei lansări, iar fiecare e o gardă care a trebuit să învețe să nu se învingă singură.
GeekBye are o funcție care sună banal, dar nu e: observă când te-ai alăturat unei ședințe video și oferă, cu un singur clic, să înceapă s-o înregistreze. Prezentarea încape într-o singură propoziție. Implementarea e formată din trei lansări în care s-a învățat că problema interesantă n-a fost niciodată „detectează o ședință" — a fost „ai dreptate în privința ei". Dreptate în privința faptului dacă chiar ești într-un apel sau doar ai aplicația deschisă. Dreptate în privința faptului de a nu întreba de două ori. Dreptate în privința faptului de a nu silenția microfonul greșit. Fiecare dintre cele trei lansări e, în esență, o gardă care a trebuit învățată să nu se împiedice de ea însăși.
Titluri de ferestre, nu procese
Prima și cea mai consecventă decizie e scrisă într-un mesaj de commit ca o declarație de design, nu ca o notă de subsol:
Detection uses window title matching (not process detection) to distinguish "app is open" from "in an active call."
Această distincție e toată funcția. Dacă ai detecta ședințele uitându-te la ce aplicații rulează, te-ai declanșa în clipa în care Zoom pornește — iar Zoom stă deschis pe multe desktopuri toată ziua fără un singur apel. Așa că GeekBye nu urmărește procese. Pe macOS rulează un mic binar Swift nativ, MeetingMonitor, care la fiecare zece secunde apelează CGWindowListCopyWindowInfo pentru ferestrele de pe ecran și citește titlul fiecăreia prin kCGWindowName. O fereastră cu titlul "Zoom Meeting" înseamnă că ești într-un apel; fereastra "Zoom Workplace" înseamnă că aplicația e doar deschisă. Semnalul nu e „e Zoom în viață", ci „arată vreo fereastră de pe ecranul tău ca un apel în desfășurare".
Intervalul de polling e o constantă fixă — pollIntervalSeconds: Double = 10.0 — plus o verificare imediată la pornire, astfel încât o ședință e observată în aproximativ zece secunde de la apariția ferestrei ei. (E o valoare din cod, nu o latență măsurată; tratează „până la ~10 s" ca aritmetică, nu ca benchmark.)
Ședințele din browser au nevoie de încă un pas, pentru că o fereastră Chrome cu titlul "Meet - Weekly Sync" e promițătoare, dar certitudinea e în URL. Așa că atunci când aplicația din prim-plan e un browser cunoscut — Chrome, Safari, Edge, Brave și încă o mână identificate prin bundle id — binarul extrage URL-ul tab-ului activ prin AppleScript și îl verifică față de tipare precum meet.google.com/, zoom.us/j/ și teams.microsoft.com/. AppleScript către altă aplicație se poate bloca, așa că acel apel rulează pe o DispatchQueue de fundal păzită de un DispatchSemaphore cu un timeout de 3 secunde; un browser înțepenit expiră în loc să înghețe tot detectorul.
Partea grea e să nu se declanșeze
Iată partea care m-a surprins când am citit lista de tipare. A detecta un apel înseamnă câteva potriviri de șiruri. A nu detecta unul — a nu te declanșa când nu vrei — e locul unde a mers toată grija. Lista de tipare de titluri de ferestre se citește ca un set de excluderi deliberate:
- Google Meet se potrivește cu
"Meet - "și varianta sa cu en-dash"Meet – ", dar nu cu șirul gol"Google Meet"— pentru că aceea e pagina pe care o deschizi înainte de un apel. - Zoom se potrivește cu
"Zoom Meeting"și"Zoom Webinar", dar nu cu"Zoom Workplace"sau un simplu"Zoom"— aplicația care stă deschisă. - Teams se potrivește cu
"Meeting with","Meeting in"," | Call"," | Meeting"— formele de titlu din timpul apelului, nu clientul inactiv. - Huddle-urile Slack, camerele Webex și apelurile WhatsApp primesc fiecare propriile fragmente valabile doar în timpul apelului.
Fiecare dintre acestea e o decizie de a rămâne tăcut în cazul comun în care o aplicație e deschisă, dar tu nu ești într-o ședință. Scopul designului nu e recall maxim; e să nu fii unealta care scoate un banner „vrei să înregistrezi?" cât timp citești Slack. Un fals pozitiv aici e costisitor într-un fel în care un fals negativ nu e: ratezi un apel și utilizatorul începe să înregistreze manual; te declanșezi greșit pe o aplicație deschisă și utilizatorul învață să ignore banner-ul tău. Toată euristica e reglată spre a face al doilea eșec rar.
E onest să numim limitele unei euristici atât de simple. Tiparele sunt doar în engleză — un sistem de operare localizat sau o aplicație de ședințe non-engleză nu se va potrivi, o limitare recunoscută a v1. Și pentru că titlurile ferestrelor altor aplicații sunt vizibile doar cu permisiunea Screen Recording acordată, detecția se degradează discret până la nimic fără ea. Simplă, lizibilă și imperfectă în feluri pe care echipa le-a scris pe hârtie în loc să le ascundă.
Oferta și mecanismul care o împiedică să sâcâie
Când un tipar se potrivește, GeekBye arată un mic banner fără ramă, mereu-deasupra, în colț — propria sa fereastră, nu o notificare a sistemului de operare — cu un buton Start Recording. Apasă-l și un mesaj IPC (meeting:start-recording) ascunde banner-ul, aduce fereastra principală în față și îi spune renderer-ului să înceapă o sesiune. Respinge-l și banner-ul dispare.
Dar o versiune naivă a acestui lucru ar fi exasperantă. Titlurile se schimbă constant în timpul unui apel — se alătură un participant, redenumești ședința, o partajare de ecran schimbă numele ferestrei — iar fiecare schimbare ar re-declanșa un detector care se baza pe șirul exact. Așa că detectorul, în MeetingDetector.ts, poartă o mică mașină de stări a cărei singură treabă e reținerea:
- Un
meetingIdstabil per platformă. Partea Swift emite un id precumgoogle-meet-active, în mod deliberat nederivat din titlul volatil al ferestrei, astfel încât intrările, ieșirile și redenumirile din cadrul unui apel să nu fie citite ca o ședință nouă. - Un
dismissedMeetings: Set<string>. Respinge banner-ul și id-ul acelei ședințe e ignorat — până când ședința chiar se termină, moment în care id-ul se golește ca următoarea ședință reală să poată întreba din nou. - Un boolean
notificationActive. Doar un banner o dată, astfel încât un huddle Slack care apare cât timp ești deja într-un Meet să nu stivuiască o a doua întrebare.
Asta e funcția, de fapt: puțină detecție și mult „nu fi enervant". Exact de aceea există următoarele două lansări — pentru că două dintre aceste gărzi s-au dovedit a se învinge singure.
Silențierea care ți-a silențiat ședința reală (v1.8.18)
Înregistrarea are un buton de silențiere, iar prima implementare a luat „mut" în sensul cel mai literal. Livrată în v1.8.15, silenția microfonul aducând la zero volumul de intrare al sistemului de operare:
osascript -e "set volume input volume 0"
cu volumul anterior salvat în baza de date ca să poată fi restaurat, un echivalent Windows prin waveInSetVolume și o plasă de siguranță before-quit care să se asigure că se de-silențiază. Codul chiar o descria ca pe o funcție — silențiere „pe DOUĂ niveluri", sistem și aplicație, astfel încât microfonul era blocat pentru toate aplicațiile.
Ultima frază e bug-ul enunțat ca o laudă. A bloca microfonul pentru toate aplicațiile înseamnă a-l bloca pentru apelul Zoom sau Meet în care chiar stai. Apeși mut în GeekBye ca să oprești acesta din a te auzi, și ai amuțit în ședința ta reală. Reparația din v1.8.18 e o retrogradare: nu mai atinge deloc volumul de intrare al sistemului de operare și silențiază doar propriul pipeline audio al GeekBye. În modul Listen, RealtimeAudioCaptureService înlocuiește cadrele de microfon silențiate cu un buffer de zerouri, astfel încât liniștea curge către mixer; în modul video, ScreenRecordingService aduce la zero un GainNode Web Audio. Silențierea la nivel de sistem, recuperarea volumului salvat, tot aparatul cross-platform — toate șterse. Mut ar trebui să însemne „această aplicație încetează să asculte", niciodată „microfonul tău se oprește pentru toată lumea".
Garda care se reseta singură (v1.8.19)
Cealaltă gardă care se învingea singură era notificationActive. Pe hârtie răspundea deja la „nu arăta un banner dacă unul e deja afișat" — și părea că ar trebui să acopere și „nu oferi să înregistrezi o ședință pe care deja o înregistrez". Nu o făcea, iar commit-ul care o repară explică de ce cu precizie:
The notificationActive flag resets when the meeting detector cycles through ended/detected states, causing notifications to reappear mid-recording.
Detectorul e un polling în timp real; de-a lungul unui apel lung poate citi pentru scurt timp „terminat" și apoi „detectat" din nou — o pâlpâire de titlu, o fereastră momentan ieșită de pe ecran — iar fiecare ciclu reseta flag-ul. Așa că garda continua să se re-armeze discret cât timp înregistrai, iar banner-ul reapărea oferind să înregistreze apelul pe care deja îl înregistrai. Reparația încetează să se încreadă într-un flag local și întreabă în schimb sursa adevărului: un callback recordingActiveChecker, injectat din main.ts și conectat la starea reală isRealtimeTranscriptionActive, verificat înainte ca vreo notificare să apară.
// Don't show notification if recording/transcription is already active
if (this.recordingActiveChecker?.()) {
return
}
Lecția e mică și generală: o gardă a cărei stare o deții poate fi resetată de sub tine de propriul tău flux de control. O gardă care citește starea reală pe care o protejează nu poate. notificationActive urmărea un proxy pentru „înregistrăm?"; reparația întreabă dacă înregistrăm.
Trei lucruri pe care ni le-au învățat aceste lansări
- Detectează starea pe care o vrei, nu pe cea ușor de văzut. „Aplicația rulează" e trivial de verificat și greșit. „Există o fereastră care arată ca un apel în desfășurare" e puțin mai greu și corect. Distanța dintre cele două e toată funcția.
- Pentru o funcție proactivă, precizia e produsul. Nimeni nu-și amintește ședința pe care ai detectat-o corect; își amintesc cele trei dăți când i-ai întrerupt când nu erau în vreuna. Excepțiile din tipare și mașina de stări anti-insistență nu sunt finisaj — sunt diferența dintre un banner util și unul pe care te înveți singur să-l ignori.
- O gardă care se poate reseta singură nu e o gardă. Ambele lansări ulterioare au aceeași formă: un mecanism de siguranță care făcea referire la un proxy pe care îl controla (un volum salvat, un boolean local) în loc de starea reală (microfonul sistemului de operare, statusul transcrierii). Îndreaptă gărzile spre adevăr, nu spre o copie a lui.
Pentru capitolul anterior din povestea v1, scoaterea backend-ului din calea de upload (v1.8.11–v1.8.13); iar pentru întregul arc, anatomia livrării de software până la perfecțiune.