Steven
Steven8 min lesing

Å skille en samtale fra en åpen app

GeekBye kan merke at du har blitt med i et videomøte og tilby å ta det opp. Deteksjonen viser seg å være den lette halvdelen — en Swift-binær som leser vindustitler hvert tiende sekund. Den vanskelige halvdelen er presisjon: å ikke utløse når Zoom bare er åpent, å ikke spørre om et møte du allerede tar opp, og å ikke dempe mikrofonen i samtalen du faktisk sitter i. Tre releaser, og hver enkelt er et vern som måtte lære å ikke beseire seg selv.

Utvikling
macOS
Desktop
GeekBye-releaser
Å skille en samtale fra en åpen app

GeekBye har en funksjon som høres triviell ut og ikke er det: den merker når du har blitt med i et videomøte og tilbyr, med ett klikk, å begynne å ta det opp. Pitchen er én enkelt setning. Implementasjonen er tre releaser med å lære at det interessante problemet aldri var "detekter et møte" — det var "å ha rett om det." Rett om hvorvidt du faktisk sitter i en samtale kontra bare å ha appen åpen. Rett om å ikke spørre to ganger. Rett om å ikke dempe feil mikrofon. Hver av de tre releasene er, i sin kjerne, et vern som måtte læres å ikke snuble over seg selv.

Vindustitler, ikke prosesser

Den første og mest følgetunge beslutningen er skrevet inn i en commit-melding som et designutsagn, ikke en fotnote:

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

Den distinksjonen er hele funksjonen. Hvis du detekterte møter ved å se på hvilke apper som kjører, ville du utløse i det øyeblikket Zoom startet — og Zoom er åpent på mange skrivebord hele dagen uten en eneste samtale. Så GeekBye overvåker ikke prosesser. På macOS kjører den en liten nativ Swift-binær, MeetingMonitor, som hvert tiende sekund kaller CGWindowListCopyWindowInfo for vinduene på skjermen og leser hvert vindus tittel via kCGWindowName. Et vindu med tittelen "Zoom Meeting" betyr at du er i en samtale; vinduet "Zoom Workplace" betyr at appen bare er åpen. Signalet er ikke "lever Zoom," det er "ser et vindu på skjermen din ut som en pågående samtale."

Pollingintervallet er en hard konstant — pollIntervalSeconds: Double = 10.0 — pluss én umiddelbar sjekk ved oppstart, så et møte oppdages innen omtrent ti sekunder etter at vinduet dukker opp. (Det er en kodeverdi, ikke en målt latens; behandle "opp til ~10 s" som aritmetikk, ikke en benchmark.)

Nettlesermøter trenger ett steg til, fordi et Chrome-vindu med tittelen "Meet - Weekly Sync" er lovende, men sikkerheten ligger i URL-en. Så når den fremste appen er en kjent nettleser — Chrome, Safari, Edge, Brave, og en håndfull til identifisert via bundle-id — trekker binæren ut den aktive fanens URL gjennom AppleScript og sjekker den mot mønstre som meet.google.com/, zoom.us/j/, og teams.microsoft.com/. AppleScript inn i en annen app kan henge, så det kallet kjører på en bakgrunns-DispatchQueue beskyttet av en DispatchSemaphore med en 3-sekunders timeout; en fastkilt nettleser når timeout i stedet for å fryse hele detektoren.

Det vanskelige er å ikke utløse

Her er delen som overrasket meg da jeg leste mønsterlisten. Å detektere en samtale er noen få strengmatcher. Å ikke detektere en — å ikke utløse når du ikke vil — er dit all omsorgen gikk. Listen av vindustittel-mønstre leses som et sett bevisste unntak:

  • Google Meet matcher "Meet - " og dens en-dash-variant "Meet – ", men ikke den nakne strengen "Google Meet" — fordi det er landingssiden du åpner før en samtale.
  • Zoom matcher "Zoom Meeting" og "Zoom Webinar", men ikke "Zoom Workplace" eller et nakent "Zoom" — appen som står åpen.
  • Teams matcher "Meeting with", "Meeting in", " | Call", " | Meeting" — titelformene under samtale, ikke den inaktive klienten.
  • Slack-huddles, Webex-rom og WhatsApp-samtaler får hver sine egne fragmenter som bare gjelder under samtale.

Hver eneste av dem er en beslutning om å holde tyst i det vanlige tilfellet der en app er åpen, men du ikke er i et møte. Designmålet er ikke maksimal recall; det er å ikke være verktøyet som spretter opp en "vil du ta opp?"-banner mens du leser Slack. En falsk positiv her er dyr på en måte en falsk negativ ikke er: gå glipp av en samtale, og brukeren begynner å ta opp manuelt; utløs feil på en åpen app, og brukeren lærer å ignorere banneret ditt. Hele heuristikken er trimmet mot at den andre feilen er sjelden.

Det er ærlig å nevne grensene for en heuristikk så enkel som denne. Mønstrene er kun på engelsk — et lokalisert OS eller en ikke-engelsk møteapp matcher ikke, en anerkjent v1-begrensning. Og fordi vindustitler for andre apper bare er synlige med tillatelsen Skjermopptak gitt, degraderer deteksjonen stille til ingenting uten den. Enkel, lesbar og ufullkommen på måter teamet skrev ned i stedet for å skjule.

Tilbudet, og maskineriet som holder det fra å mase

Når et mønster matcher, viser GeekBye en liten rammeløs alltid-øverst-banner i hjørnet — sitt eget vindu, ikke et OS-varsel — med en Start Recording-knapp. Klikk på den, og en IPC-melding (meeting:start-recording) skjuler banneret, henter frem hovedvinduet og ber rendereren om å starte en økt. Avvis den, og banneret forsvinner.

Men en naiv versjon av dette ville vært til å bli gal av. Titler endrer seg hele tiden under en samtale — en deltaker blir med, du gir møtet nytt navn, en skjermdeling bytter vinduets navn — og hver endring ville re-utløse en detektor som nøklet på den nøyaktige strengen. Så detektoren, i MeetingDetector.ts, bærer en liten tilstandsmaskin hvis hele oppgave er tilbakeholdenhet:

  • En stabil meetingId per plattform. Swift-siden sender ut en id som google-meet-active, bevisst ikke avledet fra den flyktige vindustittelen, så det å bli med, forlate og gi nytt navn innen én samtale ikke leses som et nytt møte.
  • En dismissedMeetings: Set<string>. Avvis banneret, og det møtets id ignoreres — til møtet faktisk slutter, hvorpå id-en tømmes så det neste virkelige møtet kan spørre på nytt.
  • En notificationActive-boolean. Bare ett banner om gangen, så en Slack-huddle som dukker opp mens du allerede er i en Meet ikke stabler en ny forespørsel.

Det er funksjonen, egentlig: litt deteksjon og mye "ikke vær irriterende." Som er nettopp derfor de neste to releasene finnes — fordi to av de vernene viste seg å beseire seg selv.

Dempingen som dempet ditt faktiske møte (v1.8.18)

Opptak har en dempeknapp, og den første implementasjonen tok "demp" i sin mest bokstavelige forstand. Levert i v1.8.15 dempet den mikrofonen ved å sette operativsystemets inngangsvolum til null:

osascript -e "set volume input volume 0"

med det forrige volumet lagret i databasen så det kunne gjenopprettes, en Windows-ekvivalent gjennom waveInSetVolume, og et before-quit-sikkerhetsnett for å sørge for at den ble avdempet. Koden beskrev det til og med som en funksjon — å dempe "på TO nivåer," system og app, så mikrofonen ble blokkert for alle apper.

Den siste frasen er buggen uttrykt som en skryt. Å blokkere mikrofonen for alle apper betyr å blokkere den for Zoom- eller Meet-samtalen du faktisk sitter i. Du trykker på demp i GeekBye for å hindre den i å høre deg, og du har blitt stille i ditt virkelige møte. v1.8.18-fiksen er en degradering: slutt å røre OS-ets inngangsvolum helt, og demp bare GeekByes egen lydpipeline. I Listen-modus erstatter RealtimeAudioCaptureService dempede mikrofon-frames med en nullbuffer så stillhet flyter til mikseren; i videomodus setter ScreenRecordingService en Web Audio-GainNode til null. Den systemomfattende dempingen, gjenopprettingen av lagret volum, hele det plattformovergripende apparatet — alt slettet. Demp bør bety "denne appen slutter å lytte," aldri "mikrofonen din skrus av for alle."

Vernet som nullstilte seg selv (v1.8.19)

Det andre selvbeseirende vernet var notificationActive. På papiret svarte det allerede på "ikke vis en banner hvis en er oppe" — og det så ut som at det også burde dekke "ikke tilby å ta opp et møte jeg allerede tar opp." Det gjorde det ikke, og commiten som fikser det forklarer nøyaktig hvorfor:

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

Detektoren er en levende polling; over en lang samtale kan den kort lese "ended" og så "detected" igjen — et titel-flimmer, et vindu et øyeblikk utenfor skjermen — og hver syklus nullstilte flagget. Så vernet fortsatte å stille re-aktivere seg selv mens du tok opp, og banneret dukket opp igjen og tilbød å ta opp samtalen du allerede tok opp. Fiksen slutter å stole på et lokalt flagg og spør sannhetskilden i stedet: et recordingActiveChecker-callback, injisert fra main.ts og koblet til den virkelige isRealtimeTranscriptionActive-tilstanden, sjekket før noe varsel vises.

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

Lærdommen er liten og generell: et vern hvis tilstand du eier kan bli nullstilt under deg av din egen kontrollflyt. Et vern som leser den faktiske tilstanden det beskytter kan ikke det. notificationActive sporet en proxy for "tar vi opp"; fiksen spør om vi tar opp.

Tre ting disse releasene lærte oss

  1. Detekter tilstanden du mener, ikke den som er lett å se. "Appen kjører" er trivielt å sjekke og feil. "Det finnes et vindu som ser ut som en pågående samtale" er litt vanskeligere og riktig. Gapet mellom de to er hele funksjonen.
  2. For en proaktiv funksjon er presisjon produktet. Ingen husker møtet du detekterte riktig; de husker de tre gangene du avbrøt dem når de ikke var i ett. Mønsterunntakene og anti-mas-tilstandsmaskinen er ikke pynt — de er forskjellen mellom en hjelpsom banner og en du trener deg selv til å ignorere.
  3. Et vern som kan nullstille seg selv er ikke et vern. Begge oppfølgingsreleasene har samme form: en sikkerhetsmekanisme som refererte til en proxy den kontrollerte (et lagret volum, en lokal boolean) i stedet for den virkelige tilstanden (OS-mikrofonen, transkripsjonsstatusen). Rett vern mot sannheten, ikke mot en kopi av den.

For det forrige kapittelet i v1-historien, å ta backend ut av opplastingsstien (v1.8.11–v1.8.13); og for hele buen, anatomien i å shippe programvare til perfeksjon.

Relaterte artikler

De tre verbene som holder Web Audio i live
Steven
Steven8 min lesing

De tre verbene som holder Web Audio i live

To GeekBye-punktreleaser, to måneder fra hverandre og i to ulike filer, lærte lydkoden vår den samme lærdommen fra motsatte ender: slutt å behandle nettleserens AudioContext som engangsbruk. Den ene releasen lærte å kalle resume() på en kontekst macOS stille hadde suspendert midt i opptaket; den andre lærte å kalle suspend() i stedet for close(), så opptak på rad slutter å dundre inn i Chromiums tak på omtrent seks kontekster. Resume, suspend, close — det er hele handlingen.

Utvikling
Audio
Desktop
Å ta backend ut av opplastingsstien
Steven
Steven8 min lesing

Å ta backend ut av opplastingsstien

GeekBye tar opp skjermen din og lagrer videoen til din Google Drive. Den første versjonen sendte hvert opptak gjennom GeekByes egne servere på veien dit; én release senere gikk filen rett fra maskinen din til Drive, og backend ble degradert til å holde én enkelt peker. Den interessante delen er hvor lite kode den 'direkte, gjenopptakbare' versjonen faktisk inneholder — fordi gjenopptakbarheten kom fra å slette en proxy, ikke fra å skrive en.

Utvikling
Arkitektur
Desktop
Født og fikset på tolv minutter
Steven
Steven7 min lesing

Født og fikset på tolv minutter

GeekBye v1.8.10s endringslogg sier at den fikset et krasj under redigering av tastatursnarveier. Det gjorde den — men krasjen ble introdusert og fikset i den samme pull requesten, tolv minutter fra hverandre, og nådde aldri en eneste bruker. Den virkelige historien er pålitelighetskaskaden som produserte den: en liten, korrekt endring av når snarveier settes på pause, og React-nedrivingsbuggen som falt ut av én linje som skulle gjøre editoren tryggere.

Utvikling
React
Desktop