
Rozoznať hovor od otvorenej aplikácie
GeekBye si vie všimnúť, že si sa pripojil k videohovoru, a ponúknuť jeho nahranie. Detekcia sa ukáže ako ľahšia polovica — natívny Swift binárnik čítajúci tituly okien každých desať sekúnd. Ťažká polovica je presnosť: nespustiť sa, keď je Zoom len otvorený, neponúkať nahrávanie stretnutia, ktoré už nahrávaš, a nestlmiť mikrofón v hovore, v ktorom naozaj sedíš. Tri vydania, a každé z nich je poistka, ktorá sa musela naučiť neporaziť samu seba.
GeekBye má funkciu, ktorá znie triviálne a nie je: všimne si, keď si sa pripojil k videohovoru, a ponúkne, jedným kliknutím, spustiť jeho nahrávanie. Pitch sa zmestí do jednej vety. Implementácia sú tri vydania učenia sa, že zaujímavým problémom nikdy nebolo „detekovať stretnutie" — bolo ním „mať v tom pravdu". Pravdu o tom, či si naozaj v hovore, alebo len máš aplikáciu otvorenú. Pravdu o tom, aby si sa nepýtal dvakrát. Pravdu o tom, aby si nestlmil nesprávny mikrofón. Každé z troch vydaní je v jadre poistka, ktorú bolo treba naučiť, aby nezakopla sama o seba.
Tituly okien, nie procesy
Prvé a najzávažnejšie rozhodnutie je zapísané v správe commitu ako projektové vyhlásenie, nie poznámka pod čiarou:
Detection uses window title matching (not process detection) to distinguish "app is open" from "in an active call."
Toto rozlíšenie je celá funkcia. Keby si detekoval stretnutia pohľadom na to, ktoré aplikácie bežia, spustil by si sa v okamihu, ako sa Zoom naštartoval — a Zoom býva otvorený na množstve plôch celý deň bez jediného hovoru. Takže GeekBye nesleduje procesy. Na macOS spúšťa malý natívny Swift binárnik, MeetingMonitor, ktorý každých desať sekúnd volá CGWindowListCopyWindowInfo pre viditeľné okná a číta titul každého cez kCGWindowName. Okno s titulom "Zoom Meeting" znamená, že si v hovore; okno "Zoom Workplace" znamená, že aplikácia je len otvorená. Signál nie je „žije Zoom", ale „vyzerá okno na tvojej obrazovke ako živý hovor".
Interval dopytovania je tvrdá konštanta — pollIntervalSeconds: Double = 10.0 — plus jedna okamžitá kontrola pri štarte, takže stretnutie sa spozoruje asi do desiatich sekúnd od objavenia sa jeho okna. (To je hodnota z kódu, nie nameraná latencia; ber „do ~10 s" ako aritmetiku, nie benchmark.)
Stretnutia v prehliadači potrebujú ešte jeden krok, lebo okno Chrome s titulom "Meet - Weekly Sync" je sľubné, ale istota je v URL. Takže keď je aplikácia navrchu známy prehliadač — Chrome, Safari, Edge, Brave a hŕstka ďalších rozpoznaných podľa bundle id — binárnik vytiahne URL aktívnej karty cez AppleScript a skontroluje ho voči vzorom ako meet.google.com/, zoom.us/j/ a teams.microsoft.com/. AppleScript do inej aplikácie môže zamrznúť, takže to volanie beží na pozadí na DispatchQueue, strážené DispatchSemaphore s 3-sekundovým timeoutom; zaseknutý prehliadač vyprší časovým limitom namiesto toho, aby zmrazil celý detektor.
Ťažká časť je nespustiť sa
Tu je časť, ktorá ma prekvapila, keď som čítal zoznam vzorov. Detekovať hovor je pár zhôd reťazcov. _Ne_detekovať ho — nespustiť sa, keď to nechceš — tam išla všetka starostlivosť. Zoznam vzorov titulov okien sa číta ako súbor zámerných vylúčení:
- Google Meet sa zhoduje s
"Meet - "a jeho variantom s dlhou pomlčkou"Meet – ", ale nie s holým reťazcom"Google Meet"— lebo to je úvodná stránka, ktorú otvoríš pred hovorom. - Zoom sa zhoduje so
"Zoom Meeting"a"Zoom Webinar", ale nie so"Zoom Workplace"či holým"Zoom"— aplikáciou, ktorá je len otvorená. - Teams sa zhoduje s
"Meeting with","Meeting in"," | Call"," | Meeting"— tvarmi titulu počas hovoru, nie s nečinným klientom. - Slack huddly, Webex miestnosti a WhatsApp hovory dostávajú každý svoje fragmenty vyskytujúce sa len počas hovoru.
Každé z nich je rozhodnutie mlčať v častom prípade, keď je aplikácia otvorená, ale nie si na stretnutí. Cieľom dizajnu nie je maximálna úplnosť; je ním nebyť nástrojom, ktorý vyskočí bannerom „chceš nahrávať?", kým čítaš Slack. Falošný poplach je tu drahý spôsobom, akým falošné zmeškanie nie je: zmeškaj hovor a používateľ začne nahrávať ručne; spusti sa na otvorenej aplikácii a používateľ sa naučí ignorovať tvoj banner. Celá heuristika je vyladená tak, aby druhý typ zlyhania bol zriedkavý.
Je čestné pomenovať limity takej jednoduchej heuristiky. Vzory sú len anglické — lokalizovaný OS či neanglická aplikácia na stretnutia sa nezhodne, uznaný limit v1. A keďže tituly okien iných aplikácií sú viditeľné len s udeleným povolením Nahrávania obrazovky, bez neho detekcia ticho degraduje na nulu. Jednoduché, čitateľné a nedokonalé spôsobmi, ktoré tím radšej zapísal, než skryl.
Ponuka a mašinéria, čo ju drží od otravovania
Keď sa vzor zhodne, GeekBye ukáže v rohu malý bezrámový banner vždy-navrchu — vlastné okno, nie OS notifikáciu — s tlačidlom Spustiť nahrávanie. Klikni naň a správa IPC (meeting:start-recording) skryje banner, vyvolá hlavné okno a povie rendereru, nech spustí reláciu. Zamietni ho a banner zmizne.
Ale naivná verzia tohto by bola na zbláznenie. Tituly sa počas hovoru neustále menia — pripojí sa účastník, premenuješ stretnutie, zdieľanie obrazovky vymení názov okna — a každá zmena by znovu spustila detektor, ktorý sa kľúčoval na presný reťazec. Takže detektor, v MeetingDetector.ts, nesie malý stavový automat, ktorého celou úlohou je zdržanlivosť:
- Stabilný
meetingIdna platformu. Swift strana emituje id akogoogle-meet-active, zámerne _ne_odvodené z prchavého titulu okna, takže pripojenia, odchody a premenovania v rámci jedného hovoru sa nečítajú ako nové stretnutie. dismissedMeetings: Set<string>. Zamietni banner a id toho stretnutia sa ignoruje — kým sa stretnutie naozaj neskončí, v ktorom bode sa id vyčistí, aby ďalšie skutočné stretnutie mohlo pripomenúť nanovo.- Boolean
notificationActive. Len jeden banner naraz, takže Slack huddle vyskočivší, kým už si v Meet, nenaskladá druhú ponuku.
To je tá funkcia, vlastne: trochu detekcie a veľa „nebuď otravný". Presne preto existujú ďalšie dve vydania — lebo dve z týchto poistiek sa ukázali poraziť samy seba.
Stlmenie, čo stlmilo tvoje skutočné stretnutie (v1.8.18)
Nahrávanie má tlačidlo stlmenia a prvá implementácia vzala „stlmiť" najdoslovnejšie. Vydaná v v1.8.15, stlmila mikrofón nastavením hlasitosti vstupu operačného systému na nulu:
osascript -e "set volume input volume 0"
s predošlou hlasitosťou uloženou do databázy, aby sa dala obnoviť, ekvivalentom pre Windows cez waveInSetVolume a záchrannou sieťou before-quit, aby sa uistilo, že sa odtlmilo. Kód to dokonca opisoval ako feature — stlmenie „na DVOCH úrovniach", systémovej a aplikačnej, takže mikrofón bol zablokovaný pre všetky aplikácie.
Tá posledná fráza je bug vyslovený ako chvastanie. Blokovať mikrofón pre všetky aplikácie znamená blokovať ho pre Zoom alebo Meet hovor, v ktorom naozaj sedíš. Stlačíš stlmenie v GeekBye, aby on ťa prestal počuť, a stíchol si vo svojom skutočnom stretnutí. Oprava v1.8.18 je degradácia: prestaň sa vôbec dotýkať hlasitosti vstupu OS a stlm len vlastný audio reťazec GeekBye. V režime Listen RealtimeAudioCaptureService nahradí stlmené snímky mikrofónu nulovým bufferom, takže do mixéra tečie ticho; vo video režime ScreenRecordingService nastaví webaudio GainNode na nulu. Celosystémové stlmenie, obnova uloženej hlasitosti, celý medziplatformový aparát — všetko zmazané. Stlmenie by malo znamenať „táto aplikácia prestáva počúvať", nikdy „tvoj mikrofón sa vypína pre všetkých".
Poistka, čo sa sama resetovala (v1.8.19)
Druhou poistkou porážajúcou samu seba bol notificationActive. Na papieri už odpovedal na „neukazuj banner, ak jeden visí" — a vyzeralo, že by mal pokrývať aj „neponúkaj nahrávanie stretnutia, ktoré už nahrávam". Nepokrýval, a commit, čo to opravuje, presne vysvetľuje prečo:
The notificationActive flag resets when the meeting detector cycles through ended/detected states, causing notifications to reappear mid-recording.
Detektor je živé dopytovanie; počas dlhého hovoru môže na chvíľu prečítať „ended" a potom znova „detected" — blikanie titulu, okno na moment mimo obrazovky — a každý cyklus resetoval príznak. Takže poistka sa ticho stále znova naťahovala, kým si nahrával, a banner sa vracal, ponúkajúc nahrať hovor, ktorý si už nahrával. Oprava prestane dôverovať lokálnemu príznaku a pýta sa namiesto toho zdroja pravdy: callback recordingActiveChecker, vstreknutý z main.ts a napojený na skutočný stav isRealtimeTranscriptionActive, kontrolovaný pred zobrazením akejkoľvek notifikácie.
// Don't show notification if recording/transcription is already active
if (this.recordingActiveChecker?.()) {
return
}
Poučenie je malé a všeobecné: poistku, ktorej stav vlastníš, ti môže spod rúk resetovať tvoj vlastný riadiaci tok. Poistka, ktorá číta skutočný stav, čo chráni, nemôže. notificationActive sledoval náhradu za „nahrávame"; oprava sa pýta, či nahrávame.
Tri veci, ktoré nás tieto vydania naučili
- Detekuj stav, ktorý myslíš, nie ten, ktorý je ľahké vidieť. „Aplikácia beží" je triviálne skontrolovať a nesprávne. „Existuje okno, ktoré vyzerá ako živý hovor" je trochu ťažšie a správne. Priepasť medzi tými dvoma je celá funkcia.
- Pre proaktívnu funkciu je presnosť produkt. Nikto si nepamätá stretnutie, ktoré si detekoval správne; pamätajú si tie tri razy, keď si ich vyrušil, hoci na žiadnom neboli. Výnimky vo vzoroch a stavový automat proti otravovaniu nie sú lesk — sú rozdielom medzi nápomocným bannerom a takým, ktorý sa sám naučíš ignorovať.
- Poistka, ktorá sa vie sama resetovať, nie je poistka. Obe nadväzujúce vydania majú ten istý tvar: bezpečnostný mechanizmus, ktorý odkazoval na náhradu, ktorú kontroloval (uložená hlasitosť, lokálny boolean), namiesto skutočného stavu (mikrofón OS, stav prepisu). Namieruj poistky na pravdu, nie na jej kópiu.
Pre predchádzajúcu kapitolu príbehu v1 — vyberáme backend z cesty nahrávania (v1.8.11–v1.8.13); a pre celý oblúk — anatómiu dodávania softvéru do dokonalosti.