Steven
Steven8 perc olvasás

Megkülönböztetni egy hívást egy nyitott alkalmazástól

A GeekBye észreveheti, hogy csatlakoztál egy videohíváshoz, és felajánlhatja, hogy rögzíti. Az észlelés a könnyebbik felének bizonyul — egy Swift bináris, amely tízmásodpercenként ablakcímeket olvas. A nehezebbik fél a pontosság: nem elsülni, amikor a Zoom csak nyitva van, nem rákérdezni egy megbeszélésre, amelyet már rögzítesz, és nem némítani a mikrofont abban a hívásban, amelyben valójában benne vagy. Három kiadás, és mindegyik egy védelem, amelynek meg kellett tanulnia, hogy ne győzze le önmagát.

Mérnöki munka
macOS
Desktop
GeekBye kiadások
Megkülönböztetni egy hívást egy nyitott alkalmazástól

A GeekBye-nak van egy funkciója, amely triviálisnak hangzik, de nem az: észreveszi, amikor csatlakoztál egy videohíváshoz, és felajánlja egyetlen kattintással, hogy elkezdi rögzíteni. A prezentálás egyetlen mondat. A megvalósítás három kiadásnyi tanulás arról, hogy az érdekes probléma sosem az volt, hogy „észlelj egy megbeszélést" — hanem hogy „legyél igazad felőle". Igazad afelől, hogy valóban hívásban vagy-e, vagy csak nyitva van az alkalmazás. Igazad afelől, hogy ne kérdezz kétszer. Igazad afelől, hogy ne a rossz mikrofont némítsd. A három kiadás mindegyike lényegében egy védelem, amelyet meg kellett tanítani, hogy ne botoljon meg önmagában.

Ablakcímek, nem folyamatok

Az első és legmeghatározóbb döntés egy commit üzenetbe van beírva tervezési kijelentésként, nem lábjegyzetként:

Detection uses window title matching (not process detection) to distinguish "app is open" from "in an active call."

Ez a megkülönböztetés az egész funkció. Ha úgy észlelnéd a megbeszéléseket, hogy azt nézed, mely alkalmazások futnak, abban a pillanatban elsülnél, ahogy a Zoom elindul — és a Zoom sok gépen egész nap nyitva van egyetlen hívás nélkül. Így a GeekBye nem figyeli a folyamatokat. macOS-en egy kis natív Swift binárist futtat, a MeetingMonitor-t, amely tízmásodpercenként meghívja a CGWindowListCopyWindowInfo-t a képernyőn lévő ablakokra, és beolvassa mindegyik címét a kCGWindowName-en át. Egy "Zoom Meeting" című ablak azt jelenti, hogy hívásban vagy; a "Zoom Workplace" ablak azt jelenti, hogy az alkalmazás pusztán nyitva van. A jelzés nem az, hogy „él-e a Zoom", hanem az, hogy „úgy néz-e ki egy ablak a képernyődön, mint egy élő hívás".

A poll intervalluma egy kemény konstans — pollIntervalSeconds: Double = 10.0 — plusz egy azonnali ellenőrzés indításkor, így egy megbeszélést az ablaka megjelenésétől számított körülbelül tíz másodpercen belül észrevesz. (Ez egy kódérték, nem egy mért késleltetés; kezeld a „legfeljebb ~10 s"-t aritmetikaként, ne benchmarkként.)

A böngészős megbeszéléseknek egy további lépésre van szükségük, mert egy "Meet - Weekly Sync" című Chrome ablak ígéretes, de a bizonyosság az URL-ben van. Így amikor az előtérben lévő alkalmazás egy ismert böngésző — Chrome, Safari, Edge, Brave, és még egy maroknyi, bundle id alapján azonosítva — a bináris kinyeri az aktív fül URL-jét AppleScript-en át, és olyan minták ellen ellenőrzi, mint a meet.google.com/, zoom.us/j/, és teams.microsoft.com/. Az AppleScript egy másik alkalmazásba lefagyhat, így az a hívás egy háttér DispatchQueue-n fut, amelyet egy DispatchSemaphore őriz 3 másodperces timeouttal; egy beakadt böngésző időtúllépéssel jár, ahelyett, hogy az egész detektort megfagyasztaná.

A nehéz rész az, hogy ne süljön el

Íme a rész, amely meglepett, amikor elolvastam a mintalistát. Egy hívás észlelése néhány sztringegyezés. Egy hívás nem észlelése — nem elsülni, amikor nem akarod — ide ment az összes gondosság. Az ablakcím-minták listája szándékos kizárások halmazaként olvasható:

  • A Google Meet a "Meet - "-re és annak en-dash változatára, a "Meet – "-re illeszkedik, de nem a puszta "Google Meet" sztringre — mert az az a kezdőoldal, amelyet hívás előtt nyitsz meg.
  • A Zoom a "Zoom Meeting"-re és "Zoom Webinar"-re illeszkedik, de nem a "Zoom Workplace"-re vagy egy puszta "Zoom"-ra — a nyitva ülő alkalmazásra.
  • A Teams a "Meeting with", "Meeting in", " | Call", " | Meeting"-re illeszkedik — a híváson belüli cím-alakokra, nem a tétlen kliensre.
  • A Slack huddle-ök, a Webex szobák és a WhatsApp hívások mind saját, csak-híváson-belüli töredékeket kapnak.

Mindegyikük egy döntés, hogy csendben maradj a gyakori esetben, amikor egy alkalmazás nyitva van, de te nem vagy megbeszélésen. A tervezési cél nem a maximális recall; hanem az, hogy ne az az eszköz legyél, amely egy „szeretnél rögzíteni?" bannert dob fel, miközben Slacket olvasol. Egy hamis pozitív itt olyan módon drága, ahogy egy hamis negatív nem: ha lemaradsz egy hívásról, a felhasználó kézzel kezd rögzíteni; ha tévesen elsülsz egy nyitott alkalmazáson, a felhasználó megtanulja figyelmen kívül hagyni a bannered. Az egész heurisztika arra van hangolva, hogy a második hiba legyen ritka.

Őszinte megnevezni egy ilyen egyszerű heurisztika korlátait. A minták csak angolul vannak — egy lokalizált OS vagy egy nem angol megbeszélés-alkalmazás nem fog illeszkedni, egy elismert v1-korlát. És mivel más alkalmazások ablakcímei csak megadott Screen Recording engedéllyel láthatók, az észlelés csendben lefokozódik a semmivé nélküle. Egyszerű, olvasható, és tökéletlen olyan módokon, amelyeket a csapat leírt, ahelyett, hogy elrejtett volna.

Az ajánlat, és a gépezet, amely visszatartja a zaklatástól

Amikor egy minta illeszkedik, a GeekBye egy kis keret nélküli mindig-felül bannert mutat a sarokban — a saját ablakát, nem egy OS értesítést — egy Start Recording gombbal. Kattints rá, és egy IPC üzenet (meeting:start-recording) elrejti a bannert, előhozza a főablakot, és megmondja a renderernek, hogy indítson egy munkamenetet. Utasítsd el, és a banner eltűnik.

De ennek egy naiv változata őrjítő lenne. A címek hívás közben folyamatosan változnak — csatlakozik egy résztvevő, átnevezed a megbeszélést, egy képernyőmegosztás lecseréli az ablak nevét — és minden változás újra elsütne egy detektort, amely a pontos sztringre kulcsolt. Így a detektor, a MeetingDetector.ts-ben, egy kis állapotgépet hordoz, amelynek teljes feladata az önmérséklet:

  • Egy platformonként stabil meetingId. A Swift oldal egy google-meet-active-hez hasonló id-t bocsát ki, szándékosan nem az illékony ablakcímből származtatva, hogy egy híváson belüli csatlakozások, kilépések és átnevezések ne olvasódjanak új megbeszélésként.
  • Egy dismissedMeetings: Set<string>. Utasítsd el a bannert, és annak a megbeszélésnek az id-ját figyelmen kívül hagyja — amíg a megbeszélés valóban véget nem ér, amikor is az id kiürül, hogy a következő valódi megbeszélés frissen kérdezhessen.
  • Egy notificationActive boolean. Egyszerre csak egy banner, hogy egy Slack huddle, amely felbukkan, miközben már egy Meetben vagy, ne halmozzon fel egy második kérdést.

Ez a funkció, valójában: egy kis észlelés és egy nagy adag „ne légy bosszantó". Pontosan ezért létezik a következő két kiadás — mert e védelmek közül kettő önmagát győzte le.

A némítás, amely a valódi megbeszélésedet némította el (v1.8.18)

A rögzítésnek van egy némítás gombja, és az első megvalósítás a „némítás"-t a legszó szerintibb értelmében vette. A v1.8.15-ben szállítva, a mikrofont úgy némította, hogy az operációs rendszer bemeneti hangerejét nullára állította:

osascript -e "set volume input volume 0"

a korábbi hangerővel az adatbázisba mentve, hogy visszaállítható legyen, egy Windows-megfelelővel a waveInSetVolume-on át, és egy before-quit biztonsági hálóval, hogy biztosan feloldja a némítást. A kód még funkcióként is leírta — némítás „KÉT szinten", rendszer és alkalmazás, így a mikrofon minden alkalmazás számára le volt tiltva.

Az az utolsó kifejezés a bug, dicsekvésként kimondva. A mikrofon letiltása minden alkalmazás számára azt jelenti, hogy letiltod a Zoom vagy Meet híváshoz, amelyben valójában ülsz. Megnyomod a némítást a GeekBye-ban, hogy azt állítsd le abban, hogy halljon, és elnémultál a valódi megbeszéléseden. A v1.8.18 javítása egy lefokozás: hagyd abba az OS bemeneti hangerő érintését teljesen, és csak a GeekBye saját audio pipeline-ját némítsd. Listen módban a RealtimeAudioCaptureService a némított mikrofon-frame-eket egy nulla bufferrel helyettesíti, hogy csend áramoljon a mixerhez; videó módban a ScreenRecordingService egy Web Audio GainNode-ot nullára állít. A rendszerszintű némítás, a mentett-hangerő visszaállítás, az egész cross-platform apparátus — mind törölve. A némításnak azt kellene jelentenie, hogy „ez az alkalmazás abbahagyja a hallgatást", sosem azt, hogy „a mikrofonod mindenki számára kikapcsol".

A védelem, amely önmagát nullázta (v1.8.19)

A másik önmagát legyőző védelem a notificationActive volt. Papíron már megválaszolta a „ne mutass bannert, ha van egy fenn"-t — és úgy tűnt, hogy le kellene fednie a „ne ajánld fel egy olyan megbeszélés rögzítését, amelyet már rögzítek"-et is. Nem tette, és a commit, amely javítja, pontosan elmagyarázza, miért:

The notificationActive flag resets when the meeting detector cycles through ended/detected states, causing notifications to reappear mid-recording.

A detektor egy élő poll; egy hosszú hívás során röviden olvashat „ended"-et, majd „detected"-et újra — egy címvillanás, egy pillanatra a képernyőn kívüli ablak — és minden ciklus nullázta a flaget. Így a védelem csendben tovább élesítette önmagát, miközben rögzítettél, és a banner újra megjelent, felajánlva, hogy rögzítse a hívást, amelyet már rögzítettél. A javítás abbahagyja egy lokális flagben való bízást, és helyette az igazság forrását kérdezi: egy recordingActiveChecker callbacket, amelyet a main.ts-ből injektálnak és a valós isRealtimeTranscriptionActive állapothoz kötnek, bármely értesítés megjelenése előtt ellenőrizve.

// Don't show notification if recording/transcription is already active
if (this.recordingActiveChecker?.()) {
  return
}

A tanulság kicsi és általános: egy védelem, amelynek állapotát birtoklod, kinullázható alólad a saját vezérlési folyamatod által. Egy védelem, amely a valós állapotot olvassa, amelyet véd, nem. A notificationActive egy proxyt követett a „rögzítünk-e" helyett; a javítás azt kérdezi, hogy rögzítünk-e.

Három dolog, amit ezek a kiadások megtanítottak nekünk

  1. Azt az állapotot észleld, amelyet gondolsz, ne azt, amelyet könnyű látni. Az „az alkalmazás fut" triviális ellenőrizni és rossz. A „létezik egy ablak, amely úgy néz ki, mint egy élő hívás" kicsit nehezebb és helyes. A kettő közötti rés az egész funkció.
  2. Egy proaktív funkciónál a pontosság a termék. Senki sem emlékszik a megbeszélésre, amelyet helyesen észleltél; arra a háromszorra emlékeznek, amikor félbeszakítottad őket, amikor nem voltak egyben. A minta-kivételek és a zaklatásellenes állapotgép nem csiszolás — ők a különbség egy hasznos banner és egy olyan között, amelyet megtanítod magadnak figyelmen kívül hagyni.
  3. Egy védelem, amely önmagát nullázhatja, nem védelem. Mindkét követő kiadás ugyanolyan alakú: egy biztonsági mechanizmus, amely egy általa vezérelt proxyra hivatkozott (egy mentett hangerő, egy lokális boolean) a valós állapot helyett (az OS mikrofonja, az átírás státusza). Irányítsd a védelmeket az igazságra, ne annak egy másolatára.

A v1-történet előző fejezetéért lásd a backend kivétele a feltöltési útvonalból (v1.8.11–v1.8.13); a teljes ívért pedig a szoftver tökéletesre szállításának anatómiája.

Kapcsolódó cikkek

A három ige, amely életben tartja a Web Audiót
Steven
Steven8 perc olvasás

A három ige, amely életben tartja a Web Audiót

Két GeekBye pontkiadás, két hónap eltéréssel és két különböző fájlban, ugyanarra a leckére tanította meg az audio kódunkat ellentétes végekről: ne kezeld a böngésző AudioContext-jét eldobhatóként. Az egyik kiadás megtanult resume()-t hívni egy contexten, amelyet a macOS csendben suspendelt a felvétel közepén; a másik megtanult suspend()-et hívni close() helyett, hogy az egymást követő munkamenetek ne csapódjanak bele a Chromium nagyjából hat contextes plafonjába. Resume, suspend, close — ez az egész cselekmény.

Mérnöki munka
Audio
Desktop
A backend kivétele a feltöltési útvonalból
Steven
Steven8 perc olvasás

A backend kivétele a feltöltési útvonalból

A GeekBye rögzíti a képernyődet, és elmenti a videót a Google Drive-odba. Az első verzió minden felvételt a GeekBye saját szerverein keresztül továbbított odafelé; egy kiadással később a fájl egyenesen a gépedről ment a Drive-ba, a backendet pedig lefokozták egyetlen mutató tartására. Az érdekes rész az, hogy milyen kevés kódot tartalmaz valójában a „közvetlen, folytatható" verzió — mert a folytathatóság egy proxy törléséből jött, nem a megírásából.

Mérnöki munka
Architektúra
Desktop
Megszületett és megjavult tizenkét perc alatt
Steven
Steven7 perc olvasás

Megszületett és megjavult tizenkét perc alatt

A GeekBye v1.8.10 changelogja azt mondja, hogy megjavított egy összeomlást a billentyűparancsok szerkesztése közben. Meg is javította — de az összeomlást ugyanabban a pull requestben vezették be és javították meg, tizenkét perc különbséggel, és soha nem jutott el egyetlen felhasználóhoz sem. Az igazi történet a megbízhatósági kaszkád, amely előállította: egy kicsi, helyes változtatás abban, hogy mikor függesztődnek fel a parancsok, és a React-lebontási bug, amely egyetlen sorból esett ki, amelynek a szerkesztőt biztonságosabbá kellett volna tennie.

Mérnöki munka
React
Desktop