
Rozeznat hovor od otevřené aplikace
GeekBye si umí všimnout, že ses připojil k videoschůzce, a nabídnout, že ji nahraje. Detekce se ukazuje být tou snadnou polovinou — Swift binary čtoucí titulky oken každých deset sekund. Tvrdá polovina je přesnost: nespustit se, když je Zoom jen otevřený, neptat se na schůzku, kterou už nahráváš, a neztlumit mikrofon v hovoru, ve kterém opravdu jsi. Tři vydání, a každé je pojistka, která se musela naučit neporazit sama sebe.
GeekBye má funkci, která zní triviálně, ale není: všimne si, když ses připojil k videoschůzce, a nabídne, jedním kliknutím, že ji začne nahrávat. Prezentace je jediná věta. Implementace jsou tři vydání učení se, že tím zajímavým problémem nikdy nebylo „detekuj schůzku" — bylo to „měj o ní pravdu". Pravdu o tom, jestli jsi opravdu v hovoru, nebo jen máš aplikaci otevřenou. Pravdu o tom neptat se dvakrát. Pravdu o tom neztlumit špatný mikrofon. Každé ze tří vydání je v jádru pojistka, kterou bylo třeba naučit, aby o sebe nezakopla.
Titulky oken, ne procesy
První a nejzávažnější rozhodnutí je zapsáno do commit zprávy jako prohlášení o návrhu, ne jako poznámka pod čarou:
Detection uses window title matching (not process detection) to distinguish "app is open" from "in an active call."
To rozlišení je celá funkce. Kdybys detekoval schůzky podle toho, které aplikace běží, spustil by ses ve chvíli, kdy se Zoom spustí — a Zoom je na spoustě desktopů otevřený celý den bez jediného hovoru. Takže GeekBye nesleduje procesy. Na macOS spouští malý nativní Swift binary, MeetingMonitor, který každých deset sekund volá CGWindowListCopyWindowInfo pro okna na obrazovce a čte titulek každého přes kCGWindowName. Okno s titulkem "Zoom Meeting" znamená, že jsi v hovoru; okno "Zoom Workplace" znamená, že aplikace je jen otevřená. Signál není „žije Zoom", je to „vypadá nějaké okno na tvé obrazovce jako živý hovor".
Interval pollingu je tvrdá konstanta — pollIntervalSeconds: Double = 10.0 — plus jedna okamžitá kontrola při startu, takže schůzka je zaznamenána do zhruba deseti sekund od objevení jejího okna. (To je hodnota z kódu, ne naměřená latence; ber „až ~10 s" jako aritmetiku, ne jako benchmark.)
Schůzky v prohlížeči potřebují ještě jeden krok, protože okno Chrome s titulkem "Meet - Weekly Sync" je slibné, ale jistota je v URL. Takže když je aplikace v popředí známý prohlížeč — Chrome, Safari, Edge, Brave a hrstka dalších identifikovaných podle bundle id — binary extrahuje URL aktivní záložky přes AppleScript a kontroluje ho proti vzorům jako meet.google.com/, zoom.us/j/ a teams.microsoft.com/. AppleScript do jiné aplikace se může zaseknout, takže to volání běží na pozadní DispatchQueue střežené DispatchSemaphore s 3sekundovým timeoutem; zaklíněný prohlížeč vyprší časem místo aby zmrazil celý detektor.
Tvrdá část je nespustit se
Tady je ta část, která mě překvapila, když jsem četl seznam vzorů. Detekovat hovor je pár shod řetězců. Nedetekovat ho — nespustit se, když nechceš — je místo, kam šla veškerá péče. Seznam vzorů titulků oken se čte jako sada záměrných vyloučení:
- Google Meet se shoduje s
"Meet - "a jeho en-dash variantou"Meet – ", ale ne s holým řetězcem"Google Meet"— protože to je úvodní stránka, kterou otevíráš před hovorem. - Zoom se shoduje s
"Zoom Meeting"a"Zoom Webinar", ale ne s"Zoom Workplace"nebo holým"Zoom"— aplikací, která sedí otevřená. - Teams se shoduje s
"Meeting with","Meeting in"," | Call"," | Meeting"— tvary titulků během hovoru, ne nečinným klientem. - Slack huddly, Webex místnosti a WhatsApp hovory dostávají každý své vlastní fragmenty platné jen během hovoru.
Každé z nich je rozhodnutí zůstat zticha v běžném případě, kdy je aplikace otevřená, ale ty nejsi na schůzce. Cílem návrhu není maximální recall; je to nebýt tím nástrojem, který vyskočí s bannerem „chceš nahrávat?", zatímco čteš Slack. Falešně pozitivní tady je nákladný způsobem, jakým falešně negativní není: zmeškej hovor a uživatel začne nahrávat ručně; spusť se chybně na otevřené aplikaci a uživatel se naučí tvůj banner ignorovat. Celá heuristika je vyladěná směrem k tomu, aby druhé selhání bylo vzácné.
Je poctivé pojmenovat meze tak jednoduché heuristiky. Vzory jsou jen v angličtině — lokalizovaný operační systém nebo neanglická schůzková aplikace se neshodnou, uznávaná limitace v1. A protože titulky oken pro jiné aplikace jsou viditelné jen s uděleným oprávněním Screen Recording, detekce se bez něj tiše degraduje na nic. Jednoduchá, čitelná a nedokonalá způsoby, které tým sepsal místo aby je skryl.
Nabídka, a mašinerie, která ji drží od otravování
Když se vzor shodne, GeekBye ukáže malý bezrámečkový always-on-top banner v rohu — vlastní okno, ne oznámení operačního systému — s tlačítkem Start Recording. Klikni na něj a IPC zpráva (meeting:start-recording) skryje banner, vyvolá hlavní okno a řekne rendereru, aby zahájil session. Zamítni ho a banner zmizí.
Ale naivní verze tohohle by byla k zbláznění. Titulky se během hovoru neustále mění — připojí se účastník, přejmenuješ schůzku, sdílení obrazovky vymění název okna — a každá změna by znovu spustila detektor, který se opíral o přesný řetězec. Takže detektor, v MeetingDetector.ts, nese malý stavový automat, jehož celou prací je zdrženlivost:
- Stabilní
meetingIdpro každou platformu. Swift strana vysílá id jakogoogle-meet-active, záměrně neodvozené z proměnlivého titulku okna, aby se připojení, odchody a přejmenování v rámci jednoho hovoru nečetly jako nová schůzka. - Jedno
dismissedMeetings: Set<string>. Zamítni banner a id té schůzky je ignorováno — dokud schůzka opravdu neskončí, v kterémžto okamžiku se id vyčistí, aby se příští skutečná schůzka mohla zeptat znovu. - Jeden boolean
notificationActive. Jen jeden banner naráz, aby Slack huddle vyskakující, zatímco už jsi v Meetu, nenaskládal druhý dotaz.
To je ta funkce, vlastně: trochu detekce a hodně „nebuď otravný". Což je přesně důvod, proč existují další dvě vydání — protože dvě z těch pojistek se ukázaly porážet samy sebe.
Ztlumení, které ztlumilo tvou skutečnou schůzku (v1.8.18)
Nahrávání má tlačítko ztlumení, a první implementace vzala „ztlumit" v nejdoslovnějším smyslu. Vydaná ve v1.8.15, ztlumovala mikrofon nastavením vstupní hlasitosti operačního systému na nulu:
osascript -e "set volume input volume 0"
s předchozí hlasitostí uloženou do databáze, aby mohla být obnovena, s Windows ekvivalentem přes waveInSetVolume a s before-quit záchrannou sítí, která zajistila odztlumení. Kód to dokonce popisoval jako funkci — ztlumení „na DVOU úrovních", systém a aplikace, takže mikrofon byl blokován pro všechny aplikace.
Ta poslední fráze je bug vyslovený jako chlouba. Blokovat mikrofon pro všechny aplikace znamená blokovat ho pro Zoom nebo Meet hovor, ve kterém vlastně sedíš. Zmáčkneš ztlumit v GeekBye, abys zastavil jeho v tom, aby tě slyšel, a ztichl jsi ve své skutečné schůzce. Oprava v1.8.18 je degradace: přestaň se úplně dotýkat vstupní hlasitosti OS a ztlum jen vlastní audio pipeline GeekBye. V režimu Listen RealtimeAudioCaptureService nahradí ztlumené mikrofonní framy nulovým bufferem, takže ticho teče do mixeru; v režimu video ScreenRecordingService nastaví Web Audio GainNode na nulu. Ztlumení na úrovni systému, obnova uložené hlasitosti, celý cross-platform aparát — vše smazáno. Ztlumit by mělo znamenat „tato aplikace přestane poslouchat", nikdy „tvůj mikrofon se vypne pro všechny".
Pojistka, která se sama resetovala (v1.8.19)
Druhá pojistka porážející sama sebe byla notificationActive. Na papíře už odpovídala na „neukazuj banner, pokud je jeden nahoře" — a vypadalo to, že by měla pokrýt i „nenabízej nahrání schůzky, kterou už nahrávám". Nedělala to, a commit, který to opravuje, vysvětluje proč přesně:
The notificationActive flag resets when the meeting detector cycles through ended/detected states, causing notifications to reappear mid-recording.
Detektor je živý polling; během dlouhého hovoru může na chvíli přečíst „ended" a pak „detected" znovu — probliknutí titulku, okno na okamžik mimo obrazovku — a každý cyklus resetoval flag. Takže pojistka se potichu dál znovu naváděla, zatímco jsi nahrával, a banner se znovu objevoval s nabídkou nahrát hovor, který jsi už nahrával. Oprava přestává důvěřovat lokálnímu flagu a místo toho se ptá zdroje pravdy: callbacku recordingActiveChecker, injektovaného z main.ts a napojeného na skutečný stav isRealtimeTranscriptionActive, kontrolovaného před tím, než se jakékoli oznámení ukáže.
// Don't show notification if recording/transcription is already active
if (this.recordingActiveChecker?.()) {
return
}
Poučení je malé a obecné: pojistka, jejíž stav vlastníš, může být resetována zpod tebe tvým vlastním řídicím tokem. Pojistka, která čte skutečný stav, který chrání, nemůže. notificationActive sledoval proxy pro „nahráváme?"; oprava se ptá, jestli nahráváme.
Tři věci, které nás tato vydání naučila
- Detekuj stav, který myslíš, ne ten, který je snadné vidět. „Aplikace běží" je triviální zkontrolovat a špatné. „Existuje okno, které vypadá jako živý hovor" je trochu těžší a správné. Mezera mezi těmi dvěma je celá funkce.
- U proaktivní funkce je přesnost produktem. Nikdo si nepamatuje schůzku, kterou jsi detekoval správně; pamatují si ty tři případy, kdy jsi je přerušil, když v žádné nebyli. Výjimky ve vzorech a stavový automat proti otravování nejsou lesk — jsou to rozdíl mezi užitečným bannerem a takovým, který se sám naučíš ignorovat.
- Pojistka, která se umí sama resetovat, není pojistka. Obě následná vydání mají stejný tvar: bezpečnostní mechanismus, který odkazoval na proxy, kterou ovládal (uložená hlasitost, lokální boolean), místo skutečného stavu (mikrofon OS, status přepisu). Namiř pojistky na pravdu, ne na její kopii.
Předchozí kapitolu příběhu v1 najdete v vyjmutí backendu z cesty nahrávání (v1.8.11–v1.8.13); a celý oblouk v anatomii dodávání softwaru k dokonalosti.