Steven
Steven7 min lugemist

Kõne eristamine avatud rakendusest

GeekBye võib märgata, et oled videokoosolekuga liitunud, ja pakkuda selle salvestamist. Tuvastus osutub lihtsamaks pooleks — Swift-binaar, mis loeb aknapealkirju iga kümne sekundi tagant. Raske pool on täpsus: mitte vallanduda, kui Zoom on lihtsalt avatud, mitte pakkuda koosolekut, mida sa juba salvestad, ja mitte vaigistada mikrofoni kõnes, milles sa tegelikult oled. Kolm väljalaset, ja igaüks neist on valvur, kes pidi õppima ennast mitte lööma.

Arendus
macOS
Desktop
GeekBye väljalasked
Kõne eristamine avatud rakendusest

GeekBye-l on funktsioon, mis kõlab triviaalselt, aga ei ole: see märkab, kui oled videokoosolekuga liitunud, ja pakub ühe klõpsuga selle salvestamist alustada. Müügijutt on üksainus lause. Teostus on kolm väljalaset õppimist, et huvitav probleem polnud kunagi "tuvasta koosolek" — see oli "ole selle suhtes õige". Õige selles, kas sa tegelikult oled kõnes või sul on lihtsalt rakendus avatud. Õige selles, et mitte kaks korda küsida. Õige selles, et mitte vaigistada valet mikrofoni. Igaüks kolmest väljalaskest on südames valvur, keda tuli õpetada mitte enda otsa komistama.

Aknapealkirjad, mitte protsessid

Esimene ja kõige tagajärjekam otsus on kirjutatud commit-sõnumisse disainiväitena, mitte joonealuse märkusena:

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

See eristus on kogu funktsioon. Kui tuvastaksid koosolekuid vaadates, millised rakendused jooksevad, vallanduksid sel hetkel, kui Zoom käivitus — ja Zoom on paljudel töölaudadel avatud terve päeva ilma ühegi kõneta. Nii et GeekBye ei jälgi protsesse. macOS-il jooksutab see väikest natiivset Swift-binaari, MeetingMonitor, mis iga kümne sekundi tagant kutsub CGWindowListCopyWindowInfo ekraanil olevate akende jaoks ja loeb igaühe pealkirja kCGWindowName kaudu. Aken pealkirjaga "Zoom Meeting" tähendab, et oled kõnes; "Zoom Workplace" aken tähendab, et rakendus on lihtsalt avatud. Signaal ei ole "kas Zoom on elus", vaid "kas aken su ekraanil näeb välja nagu elav kõne".

Polli intervall on kõva konstant — pollIntervalSeconds: Double = 10.0 — pluss üks kohene kontroll käivitusel, nii et koosolek märgatakse umbes kümne sekundi jooksul selle akna ilmumisest. (See on koodiväärtus, mitte mõõdetud latentsus; kohtle "kuni ~10 s" aritmeetikana, mitte võrdlustestina.)

Brauserikoosolekud vajavad veel üht sammu, sest Chrome'i aken pealkirjaga "Meet - Weekly Sync" on paljutõotav, aga kindlus on URL-is. Nii et kui esiplaanil olev rakendus on tuntud brauser — Chrome, Safari, Edge, Brave ja käputäis teisi, tuvastatud bundle id järgi — tõmbab binaar AppleScripti kaudu aktiivse vahekaardi URL-i ja kontrollib seda mustrite vastu nagu meet.google.com/, zoom.us/j/ ja teams.microsoft.com/. AppleScript teise rakendusse võib hanguda, nii et see kutse jookseb tausta-DispatchQueue-l, valvatud DispatchSemaphore-iga 3-sekundilise ajapiiranguga; kiilunud brauser aegub, selle asemel et kogu detektorit külmutada.

Raske osa on mitte vallanduda

Siin on osa, mis mind mustriloendit lugedes üllatas. Kõne tuvastamine on paar stringisobitust. Seda mitte tuvastada — mitte vallanduda, kui sa seda ei taha — sinna läks kogu hool. Aknapealkirjade mustrite loend loeb nagu tahtlike väljajätete kogum:

  • Google Meet sobitub "Meet - " ja selle en-kriipsu variandiga "Meet – ", aga mitte paljas string "Google Meet" — sest see on maandumisleht, mille avad enne kõnet.
  • Zoom sobitub "Zoom Meeting" ja "Zoom Webinar", aga mitte "Zoom Workplace" ega paljas "Zoom" — avatud istuv rakendus.
  • Teams sobitub "Meeting with", "Meeting in", " | Call", " | Meeting" — kõne-aegsed pealkirjakujud, mitte jõude klient.
  • Slacki huddle'id, Webexi ruumid ja WhatsAppi kõned saavad igaüks oma ainult-kõne-aegsed fragmendid.

Igaüks neist on otsus vaikida tavalisel juhul, kus rakendus on avatud, aga sa ei ole koosolekul. Disainieesmärk ei ole maksimaalne saagis; see on mitte olla tööriist, mis hüppab "tahad salvestada?" bänneriga ette, kui sa Slacki loed. Valepositiivne on siin kallis viisil, mille valenegatiivne ei ole: jäta kõne vahele ja kasutaja hakkab käsitsi salvestama; vallandu valesti avatud rakenduse peal ja kasutaja õpib su bännerit ignoreerima. Kogu heuristika on häälestatud teise ebaõnnestumise haruldaseks tegemisele.

On aus nimetada nii lihtsa heuristika piirid. Mustrid on ainult ingliskeelsed — lokaliseeritud OS või mitte-ingliskeelne koosolekurakendus ei sobitu, tunnistatud v1 piirang. Ja kuna teiste rakenduste aknapealkirjad on nähtavad ainult antud Ekraanisalvestuse loaga, taandub tuvastus ilma selleta vaikselt eimillekski. Lihtne, loetav ja ebatäiuslik viisidel, mille meeskond kirja pani, mitte ei peitnud.

Pakkumine ja masinavärk, mis hoiab seda tüütamast

Kui muster sobitub, näitab GeekBye nurgas väikest raamideta alati-pealpool bännerit — omaenda akent, mitte OS-i teavitust — Start Recording nupuga. Klõpsa seda ja IPC-sõnum (meeting:start-recording) peidab bänneri, toob esile põhiakna ja käsib rendereril sessiooni alustada. Hülga see ja bänner kaob.

Aga naiivne versioon sellest oleks hulluksajav. Pealkirjad muutuvad kõne ajal pidevalt — osaleja liitub, sa nimetad koosoleku ümber, ekraanijagamine vahetab akna nime — ja iga muutus vallandaks uuesti detektori, mis lähtus täpsest stringist. Nii et detektor failis MeetingDetector.ts kannab väikest olekumasinat, mille kogu töö on vaoshoitus:

  • Stabiilne meetingId platvormi kohta. Swifti pool väljastab id nagu google-meet-active, tahtlikult mitte tuletatud lenduvast aknapealkirjast, nii et liitumised, lahkumised ja ümbernimetamised ühe kõne sees ei loeta uueks koosolekuks.
  • dismissedMeetings: Set<string>. Hülga bänner ja selle koosoleku id-d ignoreeritakse — kuni koosolek tegelikult lõpeb, mispeale id tühjeneb, nii et järgmine tõeline koosolek saab värskelt pakkuda.
  • notificationActive tõeväärtus. Ainult üks bänner korraga, nii et Slacki huddle, mis hüppab ette kui oled juba Meetis, ei kuhja teist pakkumist peale.

See ongi tegelikult funktsioon: pisut tuvastust ja palju "ära ole tüütu". Just seepärast eksisteerivadki järgmised kaks väljalaset — sest kaks neist valvuritest osutusid end ise võitvateks.

Vaigistus, mis vaigistas su tegeliku koosoleku (v1.8.18)

Salvestamisel on vaigistusnupp, ja esimene teostus võttis "vaigista" selle kõige otsesemas tähenduses. Väljastatud v1.8.15-s, vaigistas see mikrofoni, seades operatsioonisüsteemi sisendhelitugevuse nulli:

osascript -e "set volume input volume 0"

eelmise helitugevusega andmebaasi salvestatud, et seda saaks taastada, Windowsi vaste waveInSetVolume kaudu ja before-quit turvavõrguga, et kindlustada vaigistuse tühistamine. Kood isegi kirjeldas seda kui funktsiooni — vaigistamist "KAHEL tasandil", süsteem ja rakendus, nii et mikrofon oli blokeeritud kõigi rakenduste jaoks.

See viimane fraas on viga, väljendatud kiitlemisena. Mikrofoni blokeerimine kõigi rakenduste jaoks tähendab selle blokeerimist Zoomi või Meeti kõne jaoks, milles sa tegelikult istud. Vajutad GeekBye-s vaigistust, et peatada seda sind kuulmast, ja oled oma tõelisel koosolekul vaikseks jäänud. v1.8.18 parandus on alandus: lõpeta OS-i sisendhelitugevuse puudutamine täielikult ja vaigista ainult GeekBye enda helikonveier. Listen-režiimis asendab RealtimeAudioCaptureService vaigistatud mikrofonikaadrid nullpuhvriga, nii et mikserisse voolab vaikus; videorežiimis seab ScreenRecordingService Web Audio GainNode-i nulli. Süsteemiülene vaigistus, salvestatud-helitugevuse taastamine, kogu platvormiülene aparaat — kõik kustutatud. Vaigistus peaks tähendama "see rakendus lõpetab kuulamise", mitte kunagi "su mikrofon lülitub kõigi jaoks välja".

Valvur, kes end ise lähtestas (v1.8.19)

Teine end ise võitev valvur oli notificationActive. Paberil vastas see juba "ära näita bännerit, kui üks on üleval" — ja tundus, et see peaks katma ka "ära paku salvestada koosolekut, mida ma juba salvestan". Ei katnud, ja commit, mis selle parandab, selgitab täpselt miks:

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

Detektor on elav poll; pika kõne jooksul võib see korraks lugeda "ended" ja siis taas "detected" — pealkirja värelus, aken hetkeks ekraanilt väljas — ja iga tsükkel lähtestas lipu. Nii et valvur kohendas end vaikselt uuesti valmis, kui sa salvestasid, ja bänner ilmus taas, pakkudes salvestada kõnet, mida sa juba salvestasid. Parandus lõpetab lokaalse lipu usaldamise ja küsib selle asemel tõeallikalt: recordingActiveChecker tagasikutse, süstitud main.ts-ist ja ühendatud tõelise isRealtimeTranscriptionActive olekuga, kontrollitud enne mis tahes teavituse näitamist.

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

Õppetund on väike ja üldine: valvur, mille olekut sa omad, saab su enda juhtvoo poolt su alt lähtestatud. Valvur, mis loeb tegelikku olekut, mida ta kaitseb, ei saa. notificationActive jälgis proksit küsimusele "kas me salvestame"; parandus küsib, kas me salvestame.

Kolm asja, mida need väljalasked meile õpetasid

  1. Tuvasta olek, mida sa mõtled, mitte see, mida on lihtne näha. "Rakendus jookseb" on triviaalne kontrollida ja vale. "Eksisteerib aken, mis näeb välja nagu elav kõne" on veidi raskem ja õige. Lõhe nende kahe vahel on kogu funktsioon.
  2. Proaktiivse funktsiooni jaoks on täpsus toode. Keegi ei mäleta koosolekut, mille sa õigesti tuvastasid; nad mäletavad kolme korda, mil sa neid katkestasid, kui nad üheski ei olnud. Mustri erandid ja tüütuse-vastane olekumasin ei ole viimistlus — need on vahe abistava bänneri ja sellise vahel, mida sa ise õpid ignoreerima.
  3. Valvur, kes saab end ise lähtestada, ei ole valvur. Mõlemad järelväljalasked on sama kujuga: turvamehhanism, mis viitas proksile, mida ta kontrollis (salvestatud helitugevus, lokaalne tõeväärtus), tegeliku oleku (OS-i mikrofon, transkriptsiooni staatus) asemel. Suuna valvurid tõe poole, mitte selle koopia poole.

Eelmise peatüki jaoks v1 loos — backend-i eemaldamine üleslaadimise teelt (v1.8.11–v1.8.13); ja kogu kaare jaoks tarkvara täiuslikkuseni tarnimise anatoomia.

Seotud artiklid

Kolm tegusõna, mis hoiavad Web Audio elus
Steven
Steven7 min lugemist

Kolm tegusõna, mis hoiavad Web Audio elus

Kaks GeekBye väljalaset, kahe kuu vahega ja kahes eri failis, õpetasid meie helikoodile sama õppetunni vastasotstest: lõpeta brauseri AudioContexti kohtlemine ühekordselt kasutatavana. Üks väljalase õppis kutsuma `resume()` kontekstile, mille macOS oli salaja peatanud keset salvestust; teine õppis kutsuma `suspend()` mitte `close()`, et järjestikused sessioonid lakkaksid põrkumast vastu Chromiumi umbes-kuue-konteksti lage. Resume, suspend, close — see ongi kogu süžee.

Arendus
Audio
Desktop
Backend-i eemaldamine üleslaadimise teelt
Steven
Steven7 min lugemist

Backend-i eemaldamine üleslaadimise teelt

GeekBye salvestab su ekraani ja hoiab video su Google Drive-i. Esimene versioon saatis iga salvestuse teel sinna läbi GeekBye enda serverite; üks väljalase hiljem läks fail otse su masinast Drive-i, ja backend alandati ühe kursori hoidmiseni. Huvitav osa on see, kui vähe koodi 'otsene, jätkatav' versioon tegelikult sisaldab — sest jätkatavus tuli proksi kustutamisest, mitte selle kirjutamisest.

Arendus
Arhitektuur
Desktop
Sündinud ja parandatud kaheteistkümne minutiga
Steven
Steven6 min lugemist

Sündinud ja parandatud kaheteistkümne minutiga

GeekBye v1.8.10 muudatuste logi ütleb, et see parandas krahhi klaviatuuri otseteede redigeerimisel. Nii oligi — aga krahh tutvustati ja parandati samas pull requestis, kaheteistkümne minuti vahega, ega jõudnud ühegi kasutajani. Tegelik lugu on töökindluse kaskaad, mis selle tekitas: väike, õige muudatus selles, millal otseteed peatatakse, ja React-i lammutamise viga, mis kukkus välja ühest reast, mis pidi redaktorit turvalisemaks tegema.

Arendus
React
Desktop