Steven
Steven9 min di lettura

Distinguere una chiamata da un'app aperta

GeekBye può accorgersi che ti sei unito a una riunione video e offrirti di registrarla. La rilevazione si rivela la metà facile — un binario Swift che legge i titoli delle finestre ogni dieci secondi. La metà difficile è la precisione: non scattare quando Zoom è semplicemente aperto, non proporti di registrare una riunione che stai già registrando, e non silenziare il microfono nella chiamata in cui sei davvero. Tre release, e ognuna è una salvaguardia che ha dovuto imparare a non sabotare se stessa.

Ingegneria
macOS
Desktop
Release di GeekBye
Distinguere una chiamata da un'app aperta

GeekBye ha una funzionalità che suona banale e non lo è: si accorge di quando ti sei unito a una riunione video e offre, con un clic, di iniziare a registrarla. Il pitch è una singola frase. L'implementazione sono tre release di imparare che il problema interessante non è mai stato «rilevare una riunione» — era «azzeccarci». Azzeccarci sul fatto che sei davvero in una chiamata rispetto ad avere solo l'app aperta. Azzeccarci sul non chiedere due volte. Azzeccarci sul non silenziare il microfono sbagliato. Ognuna delle tre release è, in fondo, una salvaguardia a cui si è dovuto insegnare a non inciampare su se stessa.

Titoli di finestra, non processi

La prima e più gravida di conseguenze delle decisioni è scritta in un messaggio di commit come una dichiarazione di design, non come una nota a piè di pagina:

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

Quella distinzione è tutta la funzionalità. Se rilevassi le riunioni guardando quali app sono in esecuzione, scatteresti nell'istante in cui Zoom si avvia — e Zoom sta aperto su un mucchio di desktop tutto il giorno senza una singola chiamata. Quindi GeekBye non sorveglia i processi. Su macOS esegue un piccolo binario Swift nativo, MeetingMonitor, che ogni dieci secondi chiama CGWindowListCopyWindowInfo per le finestre sullo schermo e legge il titolo di ciascuna via kCGWindowName. Una finestra intitolata "Zoom Meeting" significa che sei in una chiamata; la finestra "Zoom Workplace" significa che l'app è semplicemente aperta. Il segnale non è «Zoom è vivo», è «una finestra sul tuo schermo sembra una chiamata dal vivo».

L'intervallo di interrogazione è una costante fissa — pollIntervalSeconds: Double = 10.0 — più un controllo immediato all'avvio, così che una riunione viene notata entro circa dieci secondi dalla comparsa della sua finestra. (Quello è un valore di codice, non una latenza misurata; tratta «fino a ~10 s» come aritmetica, non come un benchmark.)

Le riunioni nel browser richiedono un passo in più, perché una finestra di Chrome intitolata "Meet - Weekly Sync" è promettente ma la certezza è nell'URL. Così quando l'app in primo piano è un browser noto — Chrome, Safari, Edge, Brave, e una manciata in più identificati per bundle id — il binario estrae l'URL della scheda attiva attraverso AppleScript e la confronta con pattern come meet.google.com/, zoom.us/j/, e teams.microsoft.com/. AppleScript verso un'altra app può bloccarsi, quindi quella chiamata gira su una DispatchQueue in background protetta da un DispatchSemaphore con un timeout di 3 secondi; un browser incastrato va in timeout invece di congelare l'intero rilevatore.

La parte difficile è non scattare

Ecco la parte che mi ha sorpreso quando ho letto la lista di pattern. Rilevare una chiamata sono poche corrispondenze di stringhe. Non rilevarne una — non scattare quando non vuoi — è dove è andata tutta la cura. La lista dei pattern di titolo di finestra si legge come un insieme di esclusioni deliberate:

  • Google Meet corrisponde a "Meet - " e alla sua variante con lineetta "Meet – ", ma non alla nuda stringa "Google Meet" — perché quella è la landing page che apri prima di una chiamata.
  • Zoom corrisponde a "Zoom Meeting" e "Zoom Webinar", ma non a "Zoom Workplace" né a un nudo "Zoom" — l'app che sta lì aperta.
  • Teams corrisponde a "Meeting with", "Meeting in", " | Call", " | Meeting" — le forme di titolo in chiamata, non il client inattivo.
  • Gli huddle di Slack, le stanze di Webex e le chiamate di WhatsApp ricevono ciascuno i propri frammenti esclusivi dello stato in chiamata.

Ognuna di quelle è una decisione di restare in silenzio nel caso comune in cui un'app è aperta ma non sei in una riunione. L'obiettivo di design non è il recall massimo; è non essere lo strumento che fa spuntare un banner «vuoi registrare?» mentre leggi Slack. Un falso positivo qui costa caro in un modo in cui un falso negativo non costa: perdi una chiamata e l'utente inizia a registrare manualmente; scatti a vuoto su un'app aperta e l'utente impara a ignorare il tuo banner. Tutta l'euristica è tarata verso il rendere raro il secondo fallimento.

È onesto nominare i limiti di un'euristica così semplice. I pattern sono solo in inglese — un OS localizzato o un'app di riunione non anglofona non corrisponderà, una limitazione riconosciuta della v1. E poiché i titoli di finestra di altre app sono visibili solo con il permesso di Registrazione dello schermo concesso, la rilevazione si degrada silenziosamente fino al nulla senza di esso. Semplice, leggibile, e imperfetta in modi che il team ha annotato invece di nascondere.

L'offerta, e il macchinario che le impedisce di assillare

Quando un pattern corrisponde, GeekBye mostra un piccolo banner senza cornice sempre-in-primo-piano nell'angolo — la sua propria finestra, non una notifica dell'OS — con un pulsante Inizia a registrare. Cliccalo e un messaggio IPC (meeting:start-recording) nasconde il banner, porta su la finestra principale, e dice al renderer di avviare una sessione. Scartalo e il banner sparisce.

Ma una versione ingenua di questo sarebbe esasperante. I titoli cambiano di continuo durante una chiamata — un partecipante si unisce, rinomini la riunione, una condivisione schermo scambia il nome della finestra — e ogni cambiamento riscatenerebbe un rilevatore ancorato alla stringa esatta. Così il rilevatore, in MeetingDetector.ts, porta una piccola macchina a stati il cui unico compito è la moderazione:

  • Un meetingId stabile per piattaforma. Il lato Swift emette un id come google-meet-active, deliberatamente non derivato dal volatile titolo di finestra, così che unirsi, uscire e rinominare all'interno di una stessa chiamata non si leggano come una riunione nuova.
  • Un dismissedMeetings: Set<string>. Scarta il banner e l'id di quella riunione viene ignorato — finché la riunione non finisce davvero, momento in cui l'id si svuota così che la prossima riunione vera possa proporre da zero.
  • Un booleano notificationActive. Solo un banner alla volta, così che un huddle di Slack che spunta mentre sei già in un Meet non impili una seconda proposta.

È questa la funzionalità, davvero: un po' di rilevazione e molto di «non essere fastidioso». Che è esattamente il motivo per cui le due release successive esistono — perché due di quelle salvaguardie si sono rivelate sabotare se stesse.

Il silenziamento che silenziava la tua riunione vera (v1.8.18)

La registrazione ha un pulsante di silenziamento, e la prima implementazione ha preso «silenziare» nel suo senso più letterale. Rilasciato nella v1.8.15, silenziava il microfono impostando a zero il volume d'ingresso del sistema operativo:

osascript -e "set volume input volume 0"

con il volume precedente salvato nel database per poter essere ripristinato, un equivalente Windows attraverso waveInSetVolume, e una rete di sicurezza before-quit per assicurarsi che togliesse il silenziamento. Il codice lo descriveva perfino come una funzionalità — silenziare «a DUE livelli», sistema e app, così che il microfono fosse bloccato per tutte le app.

Quest'ultima frase è il bug enunciato come un vanto. Bloccare il microfono per tutte le app significa bloccarlo per la chiamata Zoom o Meet in cui sei davvero seduto. Premi silenzia in GeekBye per impedire a lui di sentirti, e sei rimasto muto nella tua riunione vera. Il fix della v1.8.18 è una retrocessione: smettere del tutto di toccare il volume d'ingresso dell'OS, e silenziare solo il pipeline audio proprio di GeekBye. In modalità Ascolto, RealtimeAudioCaptureService sostituisce i frame di microfono silenziati con un buffer a zero così che il silenzio scorra al mixer; in modalità video, ScreenRecordingService imposta un GainNode di Web Audio a zero. Il silenziamento a livello di sistema, il recupero del volume salvato, tutto l'apparato multipiattaforma — tutto cancellato. Silenziare dovrebbe significare «questa app smette di ascoltare», mai «il tuo microfono si spegne per tutti».

La salvaguardia che si reimpostava da sola (v1.8.19)

L'altra salvaguardia che sabotava se stessa era notificationActive. Sulla carta rispondeva già a «non mostrare un banner se ce n'è uno su» — e sembrava che dovesse anche coprire «non proporre di registrare una riunione che sto già registrando». Non lo faceva, e il commit che lo corregge spiega perché con precisione:

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

Il rilevatore è un'interrogazione dal vivo; nell'arco di una lunga chiamata può leggere brevemente «terminato» e poi «rilevato» di nuovo — uno sfarfallio di titolo, una finestra momentaneamente fuori schermo — e ogni ciclo reimpostava il flag. Così la salvaguardia continuava a riarmarsi silenziosamente mentre registravi, e il banner riappariva proponendo di registrare la chiamata che stavi già registrando. Il fix smette di fidarsi di un flag locale e interroga invece la fonte della verità: un callback recordingActiveChecker, iniettato da main.ts e cablato allo stato reale isRealtimeTranscriptionActive, controllato prima che qualsiasi notifica venga mostrata.

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

La lezione è piccola e generale: una salvaguardia il cui stato possiedi può esserti reimpostata sotto i piedi dal tuo stesso flusso di controllo. Una salvaguardia che legge lo stato reale che protegge non può. notificationActive stava tracciando un proxy di «stiamo registrando»; il fix chiede se stiamo registrando.

Tre cose che queste release ci hanno insegnato

  1. Rileva lo stato che intendi, non quello facile da vedere. «L'app è in esecuzione» è banale da controllare ed è sbagliato. «Esiste una finestra che sembra una chiamata dal vivo» è un po' più difficile ed è corretto. Il divario tra questi due è l'intera funzionalità.
  2. Per una funzionalità proattiva, la precisione è il prodotto. Nessuno ricorda la riunione che hai rilevato correttamente; ricordano le tre volte in cui li hai interrotti quando non erano in una. Le esclusioni di pattern e la macchina a stati anti-assillo non sono rifinitura — sono la differenza tra un banner utile e uno che ti alleni a ignorare.
  3. Una salvaguardia che può reimpostarsi da sola non è una salvaguardia. Entrambe le release di seguito hanno la stessa forma: un meccanismo di sicurezza che referenziava un proxy che controllava (un volume salvato, un booleano locale) invece dello stato reale (il microfono dell'OS, lo stato di trascrizione). Punta le salvaguardie verso la verità, non verso una copia di essa.

Per il capitolo precedente della storia v1, togliere il backend dal percorso di upload (v1.8.11–v1.8.13); e per tutto l'arco, l'anatomia del pubblicare software fino alla perfezione.

Articoli Correlati

I tre verbi che tengono vivo il Web Audio
Steven
Steven9 min di lettura

I tre verbi che tengono vivo il Web Audio

Due release puntuali di GeekBye, a due mesi di distanza e in due file diversi, hanno insegnato al nostro codice audio la stessa lezione da estremità opposte: smettere di trattare l'AudioContext del browser come usa e getta. Una release ha imparato a fare resume() di un contesto che macOS aveva silenziosamente sospeso a metà registrazione; l'altra ha imparato a fare suspend() invece di close() così che sessioni consecutive smettano di schiantarsi contro il tetto di circa sei contesti di Chromium. Resume, suspend, close — è tutta qui la trama.

Ingegneria
Audio
Desktop
Togliere il backend dal percorso di upload
Steven
Steven9 min di lettura

Togliere il backend dal percorso di upload

GeekBye registra il tuo schermo e salva il video sul tuo Google Drive. La prima versione faceva passare ogni registrazione attraverso i server di GeekBye lungo il tragitto; una release più tardi, il file andava direttamente dalla tua macchina a Drive, e il backend veniva retrocesso a tenere un singolo puntatore. La parte interessante è quanto poco codice contenga davvero la versione «diretta e riprendibile» — perché la riprendibilità è venuta dal cancellare un proxy, non dallo scriverne uno.

Ingegneria
Architettura
Desktop
Nato e corretto in dodici minuti
Steven
Steven8 min di lettura

Nato e corretto in dodici minuti

Il changelog di GeekBye v1.8.10 dice che ha corretto un crash durante la modifica delle scorciatoie da tastiera. L'ha fatto — ma il crash è stato introdotto e corretto nella stessa pull request, a dodici minuti di distanza, e non ha mai raggiunto un solo utente. La vera storia è la cascata di affidabilità che l'ha prodotto: un piccolo cambiamento corretto su quando le scorciatoie vengono sospese, e il bug di teardown di React caduto da una singola riga pensata per rendere l'editor più sicuro.

Ingegneria
React
Desktop