Steven
Steven8 min čtení

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.

Inženýrství
macOS
Desktop
Vydání GeekBye
Rozeznat hovor od otevřené aplikace

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í meetingId pro každou platformu. Swift strana vysílá id jako google-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

  1. 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.
  2. 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.
  3. 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.

Související články

Tři slovesa, která udržují Web Audio naživu
Steven
Steven8 min čtení

Tři slovesa, která udržují Web Audio naživu

Dvě bodová vydání GeekBye, dva měsíce od sebe a ve dvou různých souborech, naučila náš audio kód stejné lekci z opačných konců: přestaň zacházet s AudioContext prohlížeče jako s jednorázovým. Jedno vydání se naučilo volat resume() na kontextu, který macOS tiše suspendoval uprostřed nahrávání; druhé se naučilo volat suspend() místo close(), aby po sobě jdoucí sesiony přestaly narážet do stropu Chromia zhruba šesti kontextů. Resume, suspend, close — to je celá zápletka.

Inženýrství
Audio
Desktop
Vyjmutí backendu z cesty nahrávání
Steven
Steven7 min čtení

Vyjmutí backendu z cesty nahrávání

GeekBye nahrává tvou obrazovku a ukládá video na tvůj Google Drive. První verze posílala každou nahrávku cestou tam přes vlastní servery GeekBye; o vydání později šel soubor přímo z tvého stroje na Drive a backend byl degradován na držení jediného ukazatele. Zajímavé je, jak málo kódu vlastně obsahuje „přímá, resumable" verze — protože resumabilita přišla ze smazání proxy, ne z jeho napsání.

Inženýrství
Architektura
Desktop
Zrozen a opraven za dvanáct minut
Steven
Steven7 min čtení

Zrozen a opraven za dvanáct minut

Changelog GeekBye v1.8.10 říká, že opravil pád při úpravě klávesových zkratek. Opravdu ho opravil — ale ten pád byl zaveden a opraven ve stejném pull requestu, s odstupem dvanácti minut, a nikdy nedosáhl ani jednoho uživatele. Skutečný příběh je kaskáda spolehlivosti, která ho vyprodukovala: malá, správná změna toho, kdy se zkratky pozastavují, a bug s React demontáží, který vypadl z jednoho řádku, jenž měl udělat editor bezpečnějším.

Inženýrství
React
Desktop