
Puhelun erottaminen avoimesta sovelluksesta
GeekBye voi huomata, että olet liittynyt videokokoukseen, ja tarjoutua tallentamaan sen. Tunnistus osoittautuu helpommaksi puoliskoksi — Swift-binääri, joka lukee ikkunoiden otsikoita joka kymmenes sekunti. Vaikea puolisko on tarkkuus: olla laukeamatta kun Zoom on vain auki, olla tarjoamatta kokousta jota jo tallennat, ja olla mykistämättä mikrofonia puhelussa jossa oikeasti olet. Kolme julkaisua, ja jokainen niistä on vartija jonka oli opittava olemaan kukistamatta itseään.
GeekByellä on ominaisuus joka kuulostaa triviaalilta mutta ei ole: se huomaa kun olet liittynyt videokokoukseen ja tarjoutuu yhdellä klikkauksella aloittamaan sen tallentamisen. Myyntipuhe on yksi lause. Toteutus on kolme julkaisua sen oppimista, että kiinnostava ongelma ei koskaan ollut "tunnista kokous" — se oli "ole siitä oikeassa". Oikeassa siitä oletko oikeasti puhelussa vai onko sinulla vain sovellus auki. Oikeassa siitä ettei kysytä kahdesti. Oikeassa siitä ettei mykistetä väärää mikrofonia. Jokainen kolmesta julkaisusta on pohjimmiltaan vartija joka piti opettaa olemaan kompastumatta itseensä.
Ikkunoiden otsikot, ei prosessit
Ensimmäinen ja seurauksellisin päätös on kirjoitettu commit-viestiin suunnitteluväitteenä, ei alaviitteenä:
Detection uses window title matching (not process detection) to distinguish "app is open" from "in an active call."
Tuo erottelu on koko ominaisuus. Jos tunnistaisit kokoukset katsomalla mitkä sovellukset ovat käynnissä, laukeaisit sillä hetkellä kun Zoom käynnistyi — ja Zoom on auki monilla työpöydillä koko päivän ilman yhtäkään puhelua. Joten GeekBye ei tarkkaile prosesseja. macOS:llä se ajaa pientä natiivia Swift-binääriä, MeetingMonitor, joka joka kymmenes sekunti kutsuu CGWindowListCopyWindowInfo:n näytöllä oleville ikkunoille ja lukee jokaisen otsikon kCGWindowName:n kautta. Ikkuna otsikolla "Zoom Meeting" tarkoittaa että olet puhelussa; "Zoom Workplace"-ikkuna tarkoittaa että sovellus on vain auki. Signaali ei ole "onko Zoom elossa", vaan "näyttääkö ikkuna näytölläsi elävältä puhelulta".
Pollausväli on kova vakio — pollIntervalSeconds: Double = 10.0 — plus yksi välitön tarkistus käynnistyksessä, joten kokous huomataan noin kymmenen sekunnin sisällä sen ikkunan ilmestymisestä. (Se on koodiarvo, ei mitattu viive; kohtele "korkeintaan ~10 s" aritmetiikkana, ei vertailutestinä.)
Selainkokoukset tarvitsevat yhden askeleen lisää, koska Chrome-ikkuna otsikolla "Meet - Weekly Sync" on lupaava mutta varmuus on URL:ssä. Joten kun etummaisin sovellus on tunnettu selain — Chrome, Safari, Edge, Brave ja kourallinen muita bundle id:llä tunnistettuja — binääri poimii aktiivisen välilehden URL:n AppleScriptin kautta ja tarkistaa sen kuvioita vasten kuten meet.google.com/, zoom.us/j/ ja teams.microsoft.com/. AppleScript toiseen sovellukseen voi jumittua, joten tuo kutsu ajetaan tausta-DispatchQueue:lla vartioituna DispatchSemaphore:lla 3 sekunnin aikakatkaisulla; kiilautunut selain aikakatkeaa sen sijaan että jäädyttäisi koko tunnistimen.
Vaikea osa on olla laukeamatta
Tässä on osa joka yllätti minut kun luin kuviolistan. Puhelun tunnistaminen on muutama merkkijonotäsmäys. Sen tunnistamatta jättäminen — olla laukeamatta kun et halua sen laukeavan — on se mihin kaikki huolellisuus meni. Ikkunoiden otsikkokuvioiden lista lukeutuu kuin tahallisten poissulkemisten joukko:
- Google Meet täsmää
"Meet - "ja sen en-viiva-variantin"Meet – ", mutta ei paljasta merkkijonoa"Google Meet"— koska se on aloitussivu jonka avaat ennen puhelua. - Zoom täsmää
"Zoom Meeting"ja"Zoom Webinar", mutta ei"Zoom Workplace"tai paljasta"Zoom"— auki istuvaa sovellusta. - Teams täsmää
"Meeting with","Meeting in"," | Call"," | Meeting"— puhelun-aikaiset otsikkomuodot, ei jouten olevaa asiakasta. - Slack-huddlet, Webex-huoneet ja WhatsApp-puhelut saavat kukin omat vain-puhelun-aikaiset fragmenttinsa.
Jokainen niistä on päätös pysyä hiljaa tavallisessa tapauksessa jossa sovellus on auki mutta et ole kokouksessa. Suunnittelutavoite ei ole maksimaalinen kattavuus; se on olla olematta työkalu joka pomppaa "haluatko tallentaa?" -banneriin kun luet Slackia. Väärä positiivinen on tässä kallis tavalla jolla väärä negatiivinen ei ole: missaa puhelu ja käyttäjä alkaa tallentaa käsin; laukea väärin auki olevan sovelluksen päällä ja käyttäjä oppii sivuuttamaan bannerisi. Koko heuristiikka on viritetty siihen että toinen epäonnistuminen on harvinainen.
On rehellistä nimetä näin yksinkertaisen heuristiikan rajat. Kuviot ovat vain englanninkielisiä — lokalisoitu käyttöjärjestelmä tai ei-englanninkielinen kokoussovellus ei täsmää, myönnetty v1-rajoitus. Ja koska muiden sovellusten ikkunoiden otsikot ovat näkyvissä vain Näytön tallennus -luvalla myönnettynä, tunnistus vaimenee hiljaa ei-miksikään ilman sitä. Yksinkertainen, luettava ja epätäydellinen tavoilla jotka tiimi kirjoitti ylös eikä piilottanut.
Tarjous, ja koneisto joka pitää sen kiusaamasta
Kun kuvio täsmää, GeekBye näyttää nurkassa pientä kehyksetöntä aina-päällimmäistä banneria — omaa ikkunaansa, ei käyttöjärjestelmän ilmoitusta — Start Recording -napilla. Klikkaa sitä ja IPC-viesti (meeting:start-recording) piilottaa bannerin, tuo esiin pääikkunan ja käskee renderöijää aloittamaan istunnon. Hylkää se ja banneri katoaa.
Mutta naiivi versio tästä olisi raivostuttava. Otsikot muuttuvat jatkuvasti puhelun aikana — osallistuja liittyy, nimeät kokouksen uudelleen, näytönjako vaihtaa ikkunan nimen — ja jokainen muutos laukaisisi uudelleen tunnistimen joka nojasi tarkkaan merkkijonoon. Joten tunnistin tiedostossa MeetingDetector.ts kantaa pientä tilakonetta jonka koko työ on pidättyväisyys:
- Vakaa
meetingIdper alusta. Swift-puoli lähettää id:n kutengoogle-meet-active, tarkoituksella ei johdettuna häilyvästä ikkunan otsikosta, joten liittymiset, poistumiset ja uudelleennimeämiset yhden puhelun sisällä eivät lukeudu uudeksi kokoukseksi. dismissedMeetings: Set<string>. Hylkää banneri ja sen kokouksen id:tä sivuutetaan — kunnes kokous oikeasti päättyy, jolloin id tyhjenee niin että seuraava oikea kokous voi tarjota tuoreena.notificationActive-totuusarvo. Vain yksi banneri kerrallaan, joten Slack-huddle joka pomppaa esiin kun olet jo Meetissä ei pinoa toista tarjousta.
Se on ominaisuus, oikeasti: hieman tunnistusta ja paljon "älä ole ärsyttävä". Juuri siksi seuraavat kaksi julkaisua ovatkin olemassa — koska kaksi noista vartijoista osoittautui kukistavan itsensä.
Mykistys joka mykisti oikean kokouksesi (v1.8.18)
Tallennuksessa on mykistysnappi, ja ensimmäinen toteutus otti "mykistyksen" sen kirjaimellisimmassa merkityksessä. Julkaistu v1.8.15:ssä, se mykisti mikrofonin asettamalla käyttöjärjestelmän tulon äänenvoimakkuuden nollaan:
osascript -e "set volume input volume 0"
edellinen äänenvoimakkuus tallennettuna tietokantaan jotta se voitiin palauttaa, Windows-vastine waveInSetVolume:n kautta, ja before-quit-turvaverkko varmistamaan että se poisti mykistyksen. Koodi jopa kuvasi sitä ominaisuutena — mykistys "KAHDELLA tasolla", järjestelmä ja sovellus, niin että mikrofoni oli estetty kaikilta sovelluksilta.
Tuo viimeinen lause on bugi ilmaistuna kehuskeluna. Mikrofonin estäminen kaikilta sovelluksilta tarkoittaa sen estämistä Zoom- tai Meet-puhelulta jossa oikeasti istut. Painat mykistystä GeekByessä pysäyttääksesi sen kuulemasta sinua, ja olet mennyt hiljaiseksi oikeassa kokouksessasi. v1.8.18:n korjaus on alennus: lopeta käyttöjärjestelmän tulon äänenvoimakkuuden koskeminen kokonaan, ja mykistä vain GeekByen oma ääniputki. Listen-tilassa RealtimeAudioCaptureService korvaa mykistetyt mikrofonikehykset nollapuskurilla niin että hiljaisuus virtaa mikseriin; videotilassa ScreenRecordingService asettaa Web Audio GainNode:n nollaan. Koko järjestelmän mykistys, tallennetun-äänenvoimakkuuden palautus, koko alustojenvälinen laitteisto — kaikki poistettu. Mykistyksen pitäisi tarkoittaa "tämä sovellus lopettaa kuuntelun", ei koskaan "mikrofonisi sammuu kaikilta".
Vartija joka nollasi itsensä (v1.8.19)
Toinen itsensä-kukistava vartija oli notificationActive. Paperilla se jo vastasi "älä näytä banneria jos yksi on ylhäällä" — ja näytti että sen pitäisi kattaa myös "älä tarjoa tallentaa kokousta jota jo tallennan". Se ei kattanut, ja commit joka korjaa sen selittää tarkalleen miksi:
The notificationActive flag resets when the meeting detector cycles through ended/detected states, causing notifications to reappear mid-recording.
Tunnistin on elävä pollaus; pitkän puhelun aikana se voi hetkeksi lukea "ended" ja sitten taas "detected" — otsikon välähdys, ikkuna hetkeksi näytön ulkopuolella — ja jokainen kierto nollasi lipun. Joten vartija viritti itsensä hiljaa uudelleen kun tallensit, ja banneri ilmestyi taas tarjoten tallentaa puhelun jota jo tallensit. Korjaus lopettaa paikalliseen lippuun luottamisen ja kysyy sen sijaan totuuden lähteeltä: recordingActiveChecker-takaisinkutsu, injektoituna main.ts:stä ja kytkettynä todelliseen isRealtimeTranscriptionActive-tilaan, tarkistettuna ennen minkään ilmoituksen näyttämistä.
// Don't show notification if recording/transcription is already active
if (this.recordingActiveChecker?.()) {
return
}
Oppi on pieni ja yleinen: vartija jonka tilan omistat voidaan nollata altasi oman ohjausvirtasi toimesta. Vartija joka lukee todellisen tilan jota se suojaa ei voida. notificationActive seurasi välikettä kysymykselle "tallennammeko"; korjaus kysyy, tallennammeko.
Kolme asiaa jotka nämä julkaisut opettivat meille
- Tunnista tila jonka tarkoitat, ei se jota on helppo nähdä. "Sovellus on käynnissä" on triviaalia tarkistaa ja väärin. "On olemassa ikkuna joka näyttää elävältä puhelulta" on hieman vaikeampaa ja oikein. Kuilu näiden kahden välillä on koko ominaisuus.
- Proaktiiviselle ominaisuudelle tarkkuus on tuote. Kukaan ei muista kokousta jonka tunnistit oikein; he muistavat ne kolme kertaa jolloin keskeytit heidät kun he eivät olleet missään. Kuviopoikkeukset ja kiusanteon-vastainen tilakone eivät ole viimeistelyä — ne ovat ero hyödyllisen bannerin ja sellaisen välillä jonka opetat itsesi sivuuttamaan.
- Vartija joka voi nollata itsensä ei ole vartija. Molemmat jatkojulkaisut ovat samaa muotoa: turvamekanismi joka viittasi hallitsemaansa välikkeeseen (tallennettu äänenvoimakkuus, paikallinen totuusarvo) todellisen tilan (käyttöjärjestelmän mikrofoni, transkription tila) sijaan. Suuntaa vartijat totuuteen, ei sen kopioon.
Edellinen luku v1-tarinassa: backendin poistaminen latauspolusta (v1.8.11–v1.8.13); ja koko kaarelle, ohjelmiston toimittamisen anatomia täydellisyyteen asti.