Steven
Steven10 min de lectura

Distinguir una llamada de una app abierta

GeekBye puede notar que te has unido a una reunión por video y ofrecerte grabarla. La detección resulta ser la mitad fácil — un binario Swift que lee títulos de ventana cada diez segundos. La mitad difícil es la precisión: no dispararse cuando Zoom está meramente abierto, no ofrecerte grabar una reunión que ya estás grabando, y no silenciar el micrófono en la llamada en la que realmente estás. Tres releases, y cada una es una salvaguarda que tuvo que aprender a no derrotarse a sí misma.

Ingeniería
macOS
Desktop
Lanzamientos de GeekBye
Distinguir una llamada de una app abierta

GeekBye tiene una función que suena trivial y no lo es: nota cuándo te has unido a una reunión por video y ofrece, con un clic, empezar a grabarla. El argumento de venta es una sola frase. La implementación son tres releases de aprender que el problema interesante nunca fue «detectar una reunión» — fue «acertar con ella». Acertar sobre si de verdad estás en una llamada frente a solo tener la app abierta. Acertar sobre no preguntar dos veces. Acertar sobre no silenciar el micrófono equivocado. Cada una de las tres releases es, en el fondo, una salvaguarda que hubo que enseñar a no tropezar consigo misma.

Títulos de ventana, no procesos

La primera y más trascendental decisión está escrita en un mensaje de commit como una declaración de diseño, no como una nota al pie:

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

Esa distinción es toda la función. Si detectaras reuniones mirando qué apps están en ejecución, te dispararías en el instante en que Zoom se lanzara — y Zoom está abierto en un montón de escritorios todo el día sin una sola llamada. Así que GeekBye no vigila procesos. En macOS ejecuta un pequeño binario Swift nativo, MeetingMonitor, que cada diez segundos llama a CGWindowListCopyWindowInfo para las ventanas en pantalla y lee el título de cada una vía kCGWindowName. Una ventana titulada "Zoom Meeting" significa que estás en una llamada; la ventana "Zoom Workplace" significa que la app está meramente abierta. La señal no es «¿está Zoom vivo?», es «¿alguna ventana de tu pantalla parece una llamada en vivo?».

El intervalo de sondeo es una constante fija — pollIntervalSeconds: Double = 10.0 — más una comprobación inmediata al arrancar, así que una reunión se nota dentro de unos diez segundos desde que su ventana aparece. (Ese es un valor de código, no una latencia medida; trata «hasta ~10 s» como aritmética, no como un benchmark.)

Las reuniones en el navegador necesitan un paso más, porque una ventana de Chrome titulada "Meet - Weekly Sync" es prometedora pero la certeza está en la URL. Así que cuando la app en primer plano es un navegador conocido — Chrome, Safari, Edge, Brave, y un puñado más identificados por bundle id — el binario extrae la URL de la pestaña activa a través de AppleScript y la compara contra patrones como meet.google.com/, zoom.us/j/, y teams.microsoft.com/. AppleScript hacia otra app puede colgarse, así que esa llamada corre en una DispatchQueue en segundo plano protegida por un DispatchSemaphore con un timeout de 3 segundos; un navegador atascado agota el tiempo en vez de congelar todo el detector.

La parte difícil es no dispararse

Aquí está la parte que me sorprendió cuando leí la lista de patrones. Detectar una llamada son unas cuantas coincidencias de cadenas. No detectar una — no dispararse cuando no quieres que lo haga — es donde fue a parar todo el cuidado. La lista de patrones de título de ventana se lee como un conjunto de exclusiones deliberadas:

  • Google Meet coincide con "Meet - " y su variante con raya "Meet – ", pero no con la cadena pelada "Google Meet" — porque esa es la página de inicio que abres antes de una llamada.
  • Zoom coincide con "Zoom Meeting" y "Zoom Webinar", pero no con "Zoom Workplace" ni con un "Zoom" pelado — la app abierta sin más.
  • Teams coincide con "Meeting with", "Meeting in", " | Call", " | Meeting" — las formas de título en llamada, no el cliente inactivo.
  • Los huddles de Slack, las salas de Webex y las llamadas de WhatsApp reciben cada uno sus propios fragmentos exclusivos de estar en llamada.

Cada una de esas es una decisión de quedarse callado en el caso común en que una app está abierta pero no estás en una reunión. El objetivo de diseño no es el recall máximo; es no ser la herramienta que suelta un banner de «¿quieres grabar?» mientras lees Slack. Un falso positivo aquí es caro de un modo en que un falso negativo no lo es: pierde una llamada y el usuario empieza a grabar manualmente; dispárate en falso sobre una app abierta y el usuario aprende a ignorar tu banner. Toda la heurística está afinada hacia que el segundo fallo sea raro.

Es honesto nombrar los límites de una heurística tan simple. Los patrones son solo en inglés — un sistema operativo localizado o una app de reuniones en otro idioma no coincidirá, una limitación reconocida de la v1. Y como los títulos de ventana de otras apps solo son visibles con el permiso de Grabación de Pantalla concedido, la detección se degrada silenciosamente a nada sin él. Simple, legible, e imperfecta de maneras que el equipo anotó en vez de esconder.

La oferta, y la maquinaria que evita que fastidie

Cuando un patrón coincide, GeekBye muestra un pequeño banner sin marco siempre-encima en la esquina — su propia ventana, no una notificación del sistema operativo — con un botón Empezar a grabar. Haz clic y un mensaje IPC (meeting:start-recording) oculta el banner, trae la ventana principal, y le dice al renderizador que empiece una sesión. Descártalo y el banner desaparece.

Pero una versión ingenua de esto sería enloquecedora. Los títulos cambian constantemente durante una llamada — un participante se une, renombras la reunión, un compartir pantalla intercambia el nombre de la ventana — y cada cambio volvería a disparar un detector que se basaba en la cadena exacta. Así que el detector, en MeetingDetector.ts, lleva una pequeña máquina de estados cuyo único trabajo es la contención:

  • Un meetingId estable por plataforma. El lado Swift emite un id como google-meet-active, deliberadamente no derivado del volátil título de ventana, así que unirse, salir y renombrar dentro de una misma llamada no se leen como una reunión nueva.
  • Un dismissedMeetings: Set<string>. Descarta el banner y el id de esa reunión se ignora — hasta que la reunión de verdad termina, momento en el que el id se limpia para que la siguiente reunión real pueda ofrecerse de nuevo.
  • Un booleano notificationActive. Solo un banner a la vez, así que un huddle de Slack que aparece mientras ya estás en un Meet no apila una segunda oferta.

Esa es la función, en realidad: un poco de detección y mucho de «no ser molesto». Que es exactamente por lo que existen las dos releases siguientes — porque dos de esas salvaguardas resultaron derrotarse a sí mismas.

El silenciado que silenciaba tu reunión de verdad (v1.8.18)

La grabación tiene un botón de silencio, y la primera implementación tomó «silenciar» en su sentido más literal. Publicado en la v1.8.15, silenciaba el micrófono poniendo a cero el volumen de entrada del sistema operativo:

osascript -e "set volume input volume 0"

con el volumen previo guardado en la base de datos para poder restaurarlo, un equivalente en Windows a través de waveInSetVolume, y una red de seguridad before-quit para asegurarse de que quitaba el silencio. El código incluso lo describía como una función — silenciar «a DOS niveles», sistema y app, para que el micrófono quedara bloqueado para todas las apps.

Esa última frase es el bug enunciado como un alarde. Bloquear el micrófono para todas las apps significa bloquearlo para la llamada de Zoom o Meet en la que realmente estás sentado. Pulsas silenciar en GeekBye para que él deje de oírte, y te has quedado mudo en tu reunión de verdad. El arreglo de la v1.8.18 es una degradación: dejar de tocar por completo el volumen de entrada del sistema operativo, y silenciar solo el propio pipeline de audio de GeekBye. En modo Escucha, RealtimeAudioCaptureService reemplaza los frames de micrófono silenciados por un búfer a cero para que fluya silencio hacia el mezclador; en modo video, ScreenRecordingService pone a cero un GainNode de Web Audio. El silenciado a nivel de sistema, la recuperación del volumen guardado, todo el aparato multiplataforma — todo borrado. Silenciar debería significar «esta app deja de escuchar», nunca «tu micrófono se apaga para todos».

La salvaguarda que se reiniciaba a sí misma (v1.8.19)

La otra salvaguarda que se derrotaba a sí misma era notificationActive. Sobre el papel ya respondía a «no muestres un banner si hay uno arriba» — y parecía que también debería cubrir «no ofrezcas grabar una reunión que ya estoy grabando». No lo hacía, y el commit que lo arregla explica por qué con precisión:

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

El detector es un sondeo en vivo; a lo largo de una llamada larga puede leer brevemente «terminado» y luego «detectado» otra vez — un parpadeo de título, una ventana momentáneamente fuera de pantalla — y cada ciclo reiniciaba el flag. Así que la salvaguarda se seguía rearmando silenciosamente mientras grababas, y el banner reaparecía ofreciendo grabar la llamada que ya estabas grabando. El arreglo deja de confiar en un flag local y en su lugar consulta la fuente de la verdad: un callback recordingActiveChecker, inyectado desde main.ts y conectado al estado real isRealtimeTranscriptionActive, comprobado antes de que se muestre cualquier notificación.

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

La lección es pequeña y general: una salvaguarda cuyo estado posees puede ser reiniciada por debajo de ti por tu propio flujo de control. Una salvaguarda que lee el estado real que protege no puede. notificationActive estaba rastreando un proxy de «¿estamos grabando?»; el arreglo pregunta si estamos grabando.

Tres cosas que estas releases nos enseñaron

  1. Detecta el estado que quieres decir, no el que es fácil de ver. «La app está corriendo» es trivial de comprobar y está mal. «Existe una ventana que parece una llamada en vivo» es un poco más difícil y es correcto. La brecha entre esos dos es toda la función.
  2. Para una función proactiva, la precisión es el producto. Nadie recuerda la reunión que detectaste correctamente; recuerdan las tres veces que los interrumpiste cuando no estaban en una. Las exclusiones de patrones y la máquina de estados anti-fastidio no son pulido — son la diferencia entre un banner útil y uno que te entrenas para ignorar.
  3. Una salvaguarda que puede reiniciarse a sí misma no es una salvaguarda. Las dos releases de seguimiento tienen la misma forma: un mecanismo de seguridad que referenciaba un proxy que controlaba (un volumen guardado, un booleano local) en vez del estado real (el micrófono del sistema operativo, el estado de transcripción). Apunta las salvaguardas a la verdad, no a una copia de ella.

Para el capítulo anterior de la historia v1, sacar el backend de la ruta de subida (v1.8.11–v1.8.13); y para todo el arco, la anatomía de publicar software hasta la perfección.

Artículos Relacionados

Los tres verbos que mantienen vivo el Web Audio
Steven
Steven10 min de lectura

Los tres verbos que mantienen vivo el Web Audio

Dos releases puntuales de GeekBye, con dos meses de diferencia y en dos archivos distintos, le enseñaron a nuestro código de audio la misma lección desde extremos opuestos: dejar de tratar el AudioContext del navegador como desechable. Una release aprendió a hacer resume() de un contexto que macOS había suspendido silenciosamente a mitad de grabación; la otra aprendió a hacer suspend() en vez de close() para que las sesiones consecutivas dejen de estrellarse contra el techo de aproximadamente seis contextos de Chromium. Resume, suspend, close — ese es todo el argumento.

Ingeniería
Audio
Desktop
Sacar el backend de la ruta de subida
Steven
Steven9 min de lectura

Sacar el backend de la ruta de subida

GeekBye graba tu pantalla y guarda el video en tu Google Drive. La primera versión enviaba cada grabación a través de los propios servidores de GeekBye en el camino; una release después, el archivo iba directo desde tu máquina a Drive, y el backend quedó degradado a sostener un único puntero. Lo interesante es lo poco código que en realidad contiene la versión «directa y reanudable» — porque la reanudabilidad vino de borrar un proxy, no de escribir uno.

Ingeniería
Arquitectura
Desktop
Nacido y arreglado en doce minutos
Steven
Steven8 min de lectura

Nacido y arreglado en doce minutos

El changelog de GeekBye v1.8.10 dice que arregló un crash al editar los atajos de teclado. Lo hizo — pero el crash se introdujo y se arregló en el mismo pull request, con doce minutos de diferencia, y nunca llegó a un solo usuario. La historia real es la cascada de fiabilidad que lo produjo: un cambio pequeño y correcto sobre cuándo se suspenden los atajos, y el bug de teardown de React que se desprendió de una línea pensada para hacer el editor más seguro.

Ingeniería
React
Desktop