Steven
Steven7 min. skaitymo

Atskirti skambutį nuo atvertos programos

GeekBye gali pastebėti, kad prisijungei prie vaizdo susitikimo, ir pasiūlyti jį įrašyti. Aptikimas pasirodo esąs lengvoji pusė — Swift binaras, kas dešimt sekundžių skaitantis langų pavadinimus. Sunkioji pusė yra tikslumas: nesuveikti, kai Zoom tiesiog atvertas, nesiūlyti susitikimo, kurį jau įrašinėji, ir nenutildyti mikrofono skambutyje, kuriame iš tikrųjų esi. Trys leidimai, ir kiekvienas yra apsauga, kuriai teko išmokti nenugalėti pačios savęs.

Inžinerija
macOS
Desktop
GeekBye leidimai
Atskirti skambutį nuo atvertos programos

GeekBye turi funkciją, kuri skamba trivialiai, bet tokia nėra: ji pastebi, kai prisijungi prie vaizdo susitikimo, ir vienu paspaudimu pasiūlo pradėti jį įrašinėti. Pristatymas telpa į vieną sakinį. Įgyvendinimas — trys leidimai mokymosi, kad įdomioji problema niekada nebuvo „aptikti susitikimą" — ji buvo „būti dėl to teisiam". Teisiam dėl to, ar iš tikrųjų esi skambutyje, ar tiesiog turi atvertą programą. Teisiam dėl to, kad neklaustum du kartus. Teisiam dėl to, kad neužtildytum ne to mikrofono. Kiekvienas iš trijų leidimų iš esmės yra apsauga, kurią teko išmokyti neužklupti pačios savęs.

Langų pavadinimai, ne procesai

Pirmasis ir svarbiausias pasekmėmis sprendimas įrašytas commit žinutėje kaip dizaino pareiškimas, o ne kaip išnaša:

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

Tas skirtumas ir yra visa funkcija. Jei susitikimus aptiktum žiūrėdamas, kurios programos veikia, suveiktum tą pačią akimirką, kai paleidžiamas Zoom — o Zoom daugelyje kompiuterių būna atvertas ištisą dieną be nė vieno skambučio. Tad GeekBye nestebi procesų. macOS jis paleidžia mažą natyvų Swift binarą, MeetingMonitor, kuris kas dešimt sekundžių kviečia CGWindowListCopyWindowInfo ekrane esantiems langams ir per kCGWindowName perskaito kiekvieno pavadinimą. Langas pavadinimu "Zoom Meeting" reiškia, kad esi skambutyje; langas "Zoom Workplace" reiškia, kad programa tiesiog atverta. Signalas yra ne „ar Zoom gyvas", o „ar langas tavo ekrane atrodo kaip gyvas skambutis".

Apklausos intervalas yra kietoji konstanta — pollIntervalSeconds: Double = 10.0 — plius vienas nedelsiamas patikrinimas paleidžiant, tad susitikimas pastebimas maždaug per dešimt sekundžių nuo jo lango pasirodymo. (Tai kodo reikšmė, o ne išmatuotas vėlavimas; „iki ~10 s" laikyk aritmetika, ne benchmarku.)

Naršyklės susitikimams reikia dar vieno žingsnio, nes Chrome langas pavadinimu "Meet - Weekly Sync" yra daug žadantis, bet tikrumas slypi URL adrese. Tad kai priekinė programa yra žinoma naršyklė — Chrome, Safari, Edge, Brave ir dar keletas, atpažįstamų pagal bundle id — binaras per AppleScript ištraukia aktyvios kortelės URL ir tikrina jį pagal šablonus, tokius kaip meet.google.com/, zoom.us/j/ ir teams.microsoft.com/. AppleScript į kitą programą gali užstrigti, tad tas iškvietimas vyksta fone, DispatchQueue, saugomas DispatchSemaphore su 3 sekundžių laiko limitu; užstrigusi naršyklė pasiekia laiko limitą, užuot užšaldžiusi visą detektorių.

Sunkioji dalis — nesuveikti

Štai dalis, kuri mane nustebino, kai perskaičiau šablonų sąrašą. Aptikti skambutį — tai kelios eilučių atitiktys. _Ne_aptikti jo — nesuveikti tada, kai nenori, — štai kur nuėjo visas rūpestis. Langų pavadinimų šablonų sąrašas skaitosi kaip sąmoningų išimčių rinkinys:

  • Google Meet atitinka "Meet - " ir jo en-brūkšnio variantą "Meet – ", bet ne gryną eilutę "Google Meet" — nes tai pradinis puslapis, kurį atveri prieš skambutį.
  • Zoom atitinka "Zoom Meeting" ir "Zoom Webinar", bet ne "Zoom Workplace" ar gryną "Zoom" — tiesiog atvertą programą.
  • Teams atitinka "Meeting with", "Meeting in", " | Call", " | Meeting" — skambučio metu būdingus pavadinimus, ne neveiklų klientą.
  • Slack huddle'ai, Webex kambariai ir WhatsApp skambučiai kiekvienas gauna savo tik-skambučio-metu fragmentus.

Kiekviena iš jų yra sprendimas tylėti dažnu atveju, kai programa atverta, bet nesi susitikime. Dizaino tikslas yra ne maksimalus aptikimas; jis — nebūti įrankiu, kuris iššoka su „nori įrašyti?" juostele, kol skaitai Slack. Klaidingas teigiamas rezultatas čia brangus taip, kaip klaidingas neigiamas nėra: praleisk skambutį — ir naudotojas pradės įrašyti rankiniu būdu; suveik ne vietoje ties atverta programa — ir naudotojas išmoks ignoruoti tavo juostelę. Visa euristika suderinta taip, kad antroji nesėkmė būtų reta.

Sąžininga įvardyti tokios paprastos euristikos ribas. Šablonai yra tik anglų kalba — lokalizuota OS ar ne angliška susitikimų programa neatitiks, pripažinta v1 riba. Ir kadangi kitų programų langų pavadinimai matomi tik suteikus Ekrano įrašymo leidimą, be jo aptikimas tyliai nusileidžia iki nieko. Paprasta, aišku ir netobula būdais, kuriuos komanda užrašė, o ne paslėpė.

Pasiūlymas ir mašinerija, sauganti nuo įkyrumo

Kai šablonas atitinka, GeekBye kampe parodo mažą berėmę visada-viršuje juostelę — savo langą, o ne OS pranešimą — su mygtuku Start Recording. Paspaudus jį, IPC žinutė (meeting:start-recording) paslepia juostelę, iškelia pagrindinį langą ir liepia rendereriui pradėti sesiją. Atmesk ją — ir juostelė dingsta.

Bet naivi tokio dalyko versija varytų iš proto. Pavadinimai skambučio metu nuolat keičiasi — prisijungia dalyvis, pervadini susitikimą, ekrano bendrinimas pakeičia lango pavadinimą — ir kiekvienas pokytis iš naujo suaktyvintų detektorių, kuris rėmėsi tikslia eilute. Tad detektorius, faile MeetingDetector.ts, turi mažą būsenų mašiną, kurios visas darbas — susilaikymas:

  • Stabilus meetingId kiekvienai platformai. Swift pusė išspjauna id, tokį kaip google-meet-active, sąmoningai _ne_išvestą iš nepastovaus lango pavadinimo, tad prisijungimai, išėjimai ir pervadinimai per vieną skambutį neskaitomi kaip naujas susitikimas.
  • dismissedMeetings: Set<string>. Atmesk juostelę — ir to susitikimo id ignoruojamas, kol susitikimas iš tikrųjų pasibaigs, tada id išsivalo, kad kitas tikras susitikimas galėtų iškviesti pasiūlymą iš naujo.
  • notificationActive loginė reikšmė. Tik viena juostelė vienu metu, tad Slack huddle, iššokęs kol jau esi Meet, neužkrauna antro pasiūlymo.

Štai ir visa funkcija, tiesą sakant: truputis aptikimo ir daug „nebūk įkyrus". Kaip tik todėl ir egzistuoja kiti du leidimai — nes dvi iš tų apsaugų pasirodė nugalinčios pačios save.

Nutildymas, nutildęs tavo tikrąjį susitikimą (v1.8.18)

Įrašymas turi nutildymo mygtuką, ir pirmoji implementacija suprato „nutildyti" pačia tiesiogiškiausia prasme. Išleistas v1.8.15, jis nutildydavo mikrofoną nustatydamas operacinės sistemos įvesties garsumą į nulį:

osascript -e "set volume input volume 0"

su ankstesniu garsumu, išsaugotu duomenų bazėje, kad jį būtų galima atkurti, su Windows atitikmeniu per waveInSetVolume ir su before-quit apsauginiu tinklu, užtikrinančiu, kad nutildymas būtų atšauktas. Kodas net apibūdino tai kaip funkciją — nutildymą „DVIEM lygmenimis", sistemos ir programos, tad mikrofonas buvo blokuojamas visoms programoms.

Ta paskutinė frazė — tai bagas, išsakytas kaip pasigyrimas. Blokuoti mikrofoną visoms programoms reiškia blokuoti jį ir Zoom ar Meet skambučiui, kuriame iš tikrųjų sėdi. Paspaudi nutildymą GeekBye, kad jis tavęs nebegirdėtų, ir nutyli savo tikrame susitikime. v1.8.18 pataisymas yra pažeminimas: visai nustoti liesti OS įvesties garsumą ir nutildyti tik paties GeekBye garso konvejerį. Listen režime RealtimeAudioCaptureService pakeičia nutildytus mikrofono kadrus nuliniu buferiu, tad į maišytuvą teka tyla; vaizdo režime ScreenRecordingService nustato Web Audio GainNode į nulį. Sisteminis nutildymas, išsaugoto garsumo atkūrimas, visas kelių platformų aparatas — viskas ištrinta. Nutildymas turėtų reikšti „ši programa nustoja klausytis", niekada — „tavo mikrofonas išsijungia visiems".

Apsauga, kuri pati save atstatydavo (v1.8.19)

Kita save nugalinti apsauga buvo notificationActive. Popieriuje ji jau atsakė į „nerodyk juostelės, jei viena jau iškelta" — ir atrodė, kad ji taip pat turėtų padengti „nesiūlyk įrašyti susitikimo, kurį jau įrašinėju". Nepadengė, ir commit, kuris tai ištaiso, tiksliai paaiškina kodėl:

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

Detektorius yra gyva apklausa; per ilgą skambutį jis gali trumpam nuskaityti „ended", o paskui vėl „detected" — pavadinimo mirktelėjimas, akimirkai už ekrano atsidūręs langas — ir kiekvienas ciklas atstatydavo vėliavėlę. Tad apsauga tyliai iš naujo užsitaisydavo, kol tu įrašinėjai, ir juostelė vėl pasirodydavo, siūlydama įrašyti skambutį, kurį jau įrašinėjai. Pataisymas nustoja pasitikėti lokalia vėliavėle ir vietoj to klausia tiesos šaltinio: recordingActiveChecker atgalinio iškvietimo, įterpto iš main.ts ir sujungto su tikrąja isRealtimeTranscriptionActive būsena, patikrinamo prieš parodant bet kokį pranešimą.

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

Pamoka maža ir bendra: apsauga, kurios būseną valdai pats, gali būti atstatyta tau po kojomis tavo paties valdymo srauto. Apsauga, kuri skaito tikrąją savo saugomą būseną, — negali. notificationActive sekė tarpininką klausimui „ar mes įrašinėjame"; pataisymas klausia, ar mes įrašinėjame.

Trys dalykai, kurių šie leidimai mus išmokė

  1. Aptik būseną, kurią turi omenyje, o ne tą, kurią lengva pamatyti. „Programa veikia" — trivialu patikrinti ir klaidinga. „Egzistuoja langas, kuris atrodo kaip gyvas skambutis" — kiek sunkiau ir teisinga. Atotrūkis tarp tų dviejų ir yra visa funkcija.
  2. Proaktyviai funkcijai tikslumas yra produktas. Niekas neprisimena susitikimo, kurį aptikai teisingai; jie prisimena tris kartus, kai pertraukei juos, kai jie nebuvo jokiame. Šablonų išimtys ir anti-įkyrumo būsenų mašina yra ne šlifavimas — jos yra skirtumas tarp naudingos juostelės ir tokios, kurią išmoksti ignoruoti.
  3. Apsauga, galinti pati save atstatyti, nėra apsauga. Abu tęstiniai leidimai yra tokios pat formos: saugos mechanizmas, kuris rėmėsi jo paties valdomu tarpininku (išsaugotu garsumu, lokaliu loginiu kintamuoju) vietoj tikrosios būsenos (OS mikrofono, transkripcijos statuso). Nukreipk apsaugas į tiesą, o ne į jos kopiją.

Ankstesniam v1 istorijos skyriui — išimame backend iš įkėlimo kelio (v1.8.11–v1.8.13); o visai istorijos linijai — programinės įrangos tobulybės pristatymo anatomija.

Susiję straipsniai

Trys veiksmažodžiai, kurie palaiko Web Audio gyvą
Steven
Steven8 min. skaitymo

Trys veiksmažodžiai, kurie palaiko Web Audio gyvą

Du GeekBye taškiniai leidimai, per du mėnesius vienas nuo kito ir dviejuose skirtinguose failuose, išmokė mūsų garso kodą tos pačios pamokos iš priešingų galų: liaukis elgtis su naršyklės AudioContext kaip su vienkartiniu. Vienas leidimas išmoko resume() kontekstą, kurį macOS tyliai pristabdė įrašymo viduryje; kitas išmoko suspend() vietoj close(), kad vienas po kito einantys seansai nustotų atsitrenkti į Chromium primestą maždaug šešių kontekstų lubą. Resume, suspend, close — štai ir visas siužetas.

Inžinerija
Audio
Desktop
Išimame backend iš įkėlimo kelio
Steven
Steven7 min. skaitymo

Išimame backend iš įkėlimo kelio

GeekBye įrašo tavo ekraną ir išsaugo vaizdą į tavo Google Drive. Pirmoji versija siuntė kiekvieną įrašą per pačios GeekBye serverius pakeliui; leidimu vėliau failas ėjo tiesiai iš tavo mašinos į Drive, o backend buvo pažemintas iki vienintelės rodyklės laikymo. Įdomi dalis ta, kaip mažai kodo iš tikrųjų talpina „tiesioginė, atnaujinama" versija — nes atnaujinamumas atsirado iš proxy ištrynimo, o ne iš jo rašymo.

Inžinerija
Architektūra
Desktop
Gimęs ir ištaisytas per dvylika minučių
Steven
Steven6 min. skaitymo

Gimęs ir ištaisytas per dvylika minučių

GeekBye v1.8.10 changelog sako, kad ištaisė strigtį redaguojant klaviatūros sparčiuosius klavišus. Ištaisė — bet strigtis buvo įvesta ir ištaisyta tame pačiame pull request, dvylikos minučių skirtumu, ir nepasiekė nė vieno naudotojo. Tikroji istorija — patikimumo kaskada, kuri ją pagimdė: mažas, teisingas pakeitimas to, kada spartieji klavišai pristabdomi, ir React išardymo bagas, iškritęs iš vienos eilutės, skirtos padaryti redaktorių saugesnį.

Inžinerija
React
Desktop