Steven
Steven10 min de lecture

Distinguer un appel d'une app ouverte

GeekBye peut remarquer que vous avez rejoint une réunion vidéo et vous proposer de l'enregistrer. La détection s'avère être la moitié facile — un binaire Swift qui lit les titres de fenêtre toutes les dix secondes. La moitié difficile, c'est la précision : ne pas se déclencher quand Zoom est simplement ouvert, ne pas proposer d'enregistrer une réunion que vous enregistrez déjà, et ne pas couper le microphone dans l'appel où vous êtes réellement. Trois versions, et chacune est un garde-fou qui a dû apprendre à ne pas se saboter lui-même.

Ingénierie
macOS
Desktop
Versions de GeekBye
Distinguer un appel d'une app ouverte

GeekBye a une fonctionnalité qui semble triviale et ne l'est pas : elle remarque quand vous avez rejoint une réunion vidéo et propose, en un clic, de commencer à l'enregistrer. L'argument tient en une seule phrase. L'implémentation, c'est trois versions à apprendre que le problème intéressant n'a jamais été « détecter une réunion » — c'était « avoir raison à son sujet ». Avoir raison sur le fait que vous êtes réellement dans un appel plutôt que d'avoir juste l'app ouverte. Avoir raison sur le fait de ne pas demander deux fois. Avoir raison sur le fait de ne pas couper le mauvais microphone. Chacune des trois versions est, au fond, un garde-fou qu'il a fallu apprendre à ne pas trébucher sur lui-même.

Titres de fenêtre, pas processus

La première et plus lourde de conséquences des décisions est écrite dans un message de commit comme une déclaration de conception, pas comme une note de bas de page :

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

Cette distinction est toute la fonctionnalité. Si vous détectiez les réunions en regardant quelles apps sont en cours d'exécution, vous vous déclencheriez à l'instant où Zoom se lance — et Zoom est ouvert sur beaucoup de bureaux toute la journée sans un seul appel. Donc GeekBye ne surveille pas les processus. Sur macOS, il exécute un petit binaire Swift natif, MeetingMonitor, qui toutes les dix secondes appelle CGWindowListCopyWindowInfo pour les fenêtres à l'écran et lit le titre de chacune via kCGWindowName. Une fenêtre intitulée "Zoom Meeting" signifie que vous êtes dans un appel ; la fenêtre "Zoom Workplace" signifie que l'app est simplement ouverte. Le signal n'est pas « Zoom est-il vivant », c'est « une fenêtre de votre écran ressemble-t-elle à un appel en direct ».

L'intervalle d'interrogation est une constante fixe — pollIntervalSeconds: Double = 10.0 — plus une vérification immédiate au démarrage, si bien qu'une réunion est remarquée dans les dix secondes environ suivant l'apparition de sa fenêtre. (C'est une valeur de code, pas une latence mesurée ; traitez « jusqu'à ~10 s » comme de l'arithmétique, pas comme un benchmark.)

Les réunions dans le navigateur nécessitent une étape de plus, parce qu'une fenêtre Chrome intitulée "Meet - Weekly Sync" est prometteuse mais la certitude est dans l'URL. Alors quand l'app au premier plan est un navigateur connu — Chrome, Safari, Edge, Brave, et une poignée d'autres identifiés par bundle id — le binaire extrait l'URL de l'onglet actif via AppleScript et la compare à des motifs comme meet.google.com/, zoom.us/j/, et teams.microsoft.com/. AppleScript vers une autre app peut se bloquer, donc cet appel s'exécute sur une DispatchQueue d'arrière-plan gardée par un DispatchSemaphore avec un timeout de 3 secondes ; un navigateur coincé expire au lieu de figer tout le détecteur.

La partie difficile est de ne pas se déclencher

Voici la partie qui m'a surpris quand j'ai lu la liste de motifs. Détecter un appel, ce sont quelques correspondances de chaînes. Ne pas en détecter un — ne pas se déclencher quand vous ne le voulez pas — c'est là qu'est allé tout le soin. La liste des motifs de titre de fenêtre se lit comme un ensemble d'exclusions délibérées :

  • Google Meet correspond à "Meet - " et sa variante avec tiret demi-cadratin "Meet – ", mais pas à la chaîne nue "Google Meet" — parce que c'est la page d'accueil que vous ouvrez avant un appel.
  • Zoom correspond à "Zoom Meeting" et "Zoom Webinar", mais pas à "Zoom Workplace" ni à un "Zoom" nu — l'app posée ouverte.
  • Teams correspond à "Meeting with", "Meeting in", " | Call", " | Meeting" — les formes de titre en appel, pas le client au repos.
  • Les huddles Slack, les salles Webex et les appels WhatsApp reçoivent chacun leurs propres fragments réservés à l'état en appel.

Chacune de celles-ci est une décision de rester silencieux dans le cas courant où une app est ouverte mais vous n'êtes pas en réunion. L'objectif de conception n'est pas le rappel maximal ; c'est de ne pas être l'outil qui fait surgir une bannière « envie d'enregistrer ? » pendant que vous lisez Slack. Un faux positif ici coûte cher d'une manière qu'un faux négatif ne coûte pas : ratez un appel et l'utilisateur commence à enregistrer manuellement ; déclenchez-vous à tort sur une app ouverte et l'utilisateur apprend à ignorer votre bannière. Toute l'heuristique est réglée pour que le second échec soit rare.

Il est honnête de nommer les limites d'une heuristique aussi simple. Les motifs sont uniquement en anglais — un OS localisé ou une app de réunion non anglophone ne correspondra pas, une limitation reconnue de la v1. Et parce que les titres de fenêtre des autres apps ne sont visibles qu'avec la permission d'Enregistrement de l'écran accordée, la détection se dégrade silencieusement jusqu'à rien sans elle. Simple, lisible, et imparfaite de façons que l'équipe a notées plutôt que cachées.

L'offre, et la machinerie qui l'empêche de harceler

Quand un motif correspond, GeekBye affiche une petite bannière sans cadre toujours-au-premier-plan dans le coin — sa propre fenêtre, pas une notification de l'OS — avec un bouton Démarrer l'enregistrement. Cliquez et un message IPC (meeting:start-recording) masque la bannière, fait apparaître la fenêtre principale, et dit au renderer de démarrer une session. Rejetez-la et la bannière disparaît.

Mais une version naïve de ceci serait exaspérante. Les titres changent constamment pendant un appel — un participant rejoint, vous renommez la réunion, un partage d'écran échange le nom de la fenêtre — et chaque changement redéclencherait un détecteur calé sur la chaîne exacte. Alors le détecteur, dans MeetingDetector.ts, porte une petite machine à états dont le seul travail est la retenue :

  • Un meetingId stable par plateforme. Le côté Swift émet un id comme google-meet-active, délibérément pas dérivé du titre de fenêtre volatil, si bien que rejoindre, quitter et renommer au sein d'un même appel ne se lisent pas comme une nouvelle réunion.
  • Un dismissedMeetings: Set<string>. Rejetez la bannière et l'id de cette réunion est ignoré — jusqu'à ce que la réunion se termine vraiment, moment où l'id se vide pour que la prochaine vraie réunion puisse proposer à neuf.
  • Un booléen notificationActive. Une seule bannière à la fois, si bien qu'un huddle Slack surgissant alors que vous êtes déjà dans un Meet n'empile pas une seconde proposition.

C'est ça la fonctionnalité, en fait : un peu de détection et beaucoup de « ne pas être agaçant ». Ce qui est exactement pourquoi les deux versions suivantes existent — parce que deux de ces garde-fous se sont avérés se saboter eux-mêmes.

La coupure qui coupait votre vraie réunion (v1.8.18)

L'enregistrement a un bouton de coupure micro, et la première implémentation a pris « couper » au sens le plus littéral. Livrée dans la v1.8.15, elle coupait le microphone en mettant le volume d'entrée du système d'exploitation à zéro :

osascript -e "set volume input volume 0"

avec le volume précédent sauvegardé en base de données pour pouvoir être restauré, un équivalent Windows via waveInSetVolume, et un filet de sécurité before-quit pour s'assurer qu'elle rétablissait le son. Le code le décrivait même comme une fonctionnalité — couper « à DEUX niveaux », système et app, si bien que le micro était bloqué pour toutes les apps.

Cette dernière phrase, c'est le bug énoncé comme une vantardise. Bloquer le micro pour toutes les apps signifie le bloquer pour l'appel Zoom ou Meet dans lequel vous êtes réellement assis. Vous appuyez sur couper dans GeekBye pour l'empêcher, lui, de vous entendre, et vous voilà muet dans votre vraie réunion. Le correctif de la v1.8.18 est une rétrogradation : cesser complètement de toucher au volume d'entrée de l'OS, et ne couper que le propre pipeline audio de GeekBye. En mode Écoute, RealtimeAudioCaptureService remplace les trames de micro coupées par un buffer à zéro pour que du silence circule vers le mixeur ; en mode vidéo, ScreenRecordingService met un GainNode Web Audio à zéro. La coupure à l'échelle du système, la récupération du volume sauvegardé, tout l'appareillage multiplateforme — tout supprimé. Couper devrait vouloir dire « cette app cesse d'écouter », jamais « votre microphone s'éteint pour tout le monde ».

Le garde-fou qui se réinitialisait lui-même (v1.8.19)

L'autre garde-fou qui se sabotait était notificationActive. Sur le papier, il répondait déjà à « ne pas montrer de bannière s'il y en a une » — et il semblait qu'il devait aussi couvrir « ne pas proposer d'enregistrer une réunion que j'enregistre déjà ». Ce n'était pas le cas, et le commit qui le corrige explique pourquoi avec précision :

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

Le détecteur est une interrogation en direct ; au fil d'un long appel, il peut lire brièvement « terminé » puis « détecté » à nouveau — un scintillement de titre, une fenêtre momentanément hors écran — et chaque cycle réinitialisait le flag. Alors le garde-fou continuait de se réarmer silencieusement pendant que vous enregistriez, et la bannière réapparaissait en proposant d'enregistrer l'appel que vous enregistriez déjà. Le correctif cesse de faire confiance à un flag local et interroge plutôt la source de vérité : un callback recordingActiveChecker, injecté depuis main.ts et câblé à l'état réel isRealtimeTranscriptionActive, vérifié avant qu'aucune notification ne s'affiche.

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

La leçon est petite et générale : un garde-fou dont vous possédez l'état peut être réinitialisé sous vos pieds par votre propre flux de contrôle. Un garde-fou qui lit l'état réel qu'il protège ne le peut pas. notificationActive suivait un proxy de « sommes-nous en train d'enregistrer » ; le correctif demande si nous enregistrons.

Trois choses que ces versions nous ont apprises

  1. Détectez l'état que vous visez, pas celui qui est facile à voir. « L'app tourne » est trivial à vérifier et faux. « Une fenêtre qui ressemble à un appel en direct existe » est un peu plus difficile et juste. L'écart entre ces deux-là est la fonctionnalité tout entière.
  2. Pour une fonctionnalité proactive, la précision est le produit. Personne ne se souvient de la réunion que vous avez détectée correctement ; ils se souviennent des trois fois où vous les avez interrompus alors qu'ils n'en étaient pas dans une. Les exclusions de motifs et la machine à états anti-harcèlement ne sont pas du vernis — elles sont la différence entre une bannière utile et une que vous vous entraînez à ignorer.
  3. Un garde-fou qui peut se réinitialiser lui-même n'est pas un garde-fou. Les deux versions de suivi ont la même forme : un mécanisme de sécurité qui référençait un proxy qu'il contrôlait (un volume sauvegardé, un booléen local) au lieu de l'état réel (le micro de l'OS, l'état de transcription). Pointez les garde-fous vers la vérité, pas vers une copie de celle-ci.

Pour le chapitre précédent de l'histoire v1, sortir le backend du chemin d'upload (v1.8.11–v1.8.13) ; et pour tout l'arc, l'anatomie de livrer un logiciel jusqu'à la perfection.

Articles Connexes

Les trois verbes qui gardent le Web Audio en vie
Steven
Steven10 min de lecture

Les trois verbes qui gardent le Web Audio en vie

Deux versions ponctuelles de GeekBye, à deux mois d'écart et dans deux fichiers différents, ont enseigné à notre code audio la même leçon depuis des extrémités opposées : cesser de traiter l'AudioContext du navigateur comme jetable. Une version a appris à faire resume() d'un contexte que macOS avait silencieusement suspendu en pleine session d'enregistrement ; l'autre a appris à faire suspend() au lieu de close() pour que des sessions consécutives cessent de percuter le plafond d'environ six contextes de Chromium. Resume, suspend, close — voilà toute l'intrigue.

Ingénierie
Audio
Desktop
Sortir le backend du chemin d'upload
Steven
Steven9 min de lecture

Sortir le backend du chemin d'upload

GeekBye enregistre votre écran et sauvegarde la vidéo sur votre Google Drive. La première version faisait passer chaque enregistrement par les propres serveurs de GeekBye en chemin ; une version plus tard, le fichier allait directement de votre machine à Drive, et le backend était rétrogradé à ne tenir qu'un simple pointeur. Le plus intéressant est le peu de code que contient réellement la version « directe et reprenable » — parce que la reprise venait de la suppression d'un proxy, pas de l'écriture d'un.

Ingénierie
Architecture
Desktop
Né et corrigé en douze minutes
Steven
Steven8 min de lecture

Né et corrigé en douze minutes

Le changelog de GeekBye v1.8.10 dit qu'il a corrigé un crash pendant l'édition des raccourcis clavier. C'est vrai — mais le crash a été introduit et corrigé dans le même pull request, à douze minutes d'intervalle, et n'a jamais atteint un seul utilisateur. La vraie histoire est la cascade de fiabilité qui l'a produit : un petit changement correct sur le moment où les raccourcis sont suspendus, et le bug de teardown de React tombé d'une seule ligne censée rendre l'éditeur plus sûr.

Ingénierie
React
Desktop