
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.
GeekBye peut enregistrer votre écran — vidéo, audio système et micro — et déposer l'enregistrement terminé sur votre Google Drive automatiquement. Entre la v1.8.11 et la v1.8.13, la description côté utilisateur de la fonctionnalité a à peine changé, mais le chemin que prend la vidéo pour atteindre Drive a été complètement réacheminé. Dans la première version, votre enregistrement voyageait à travers les serveurs de GeekBye. Dans la seconde, il allait directement de votre machine à votre Drive, et le backend de GeekBye n'en a jamais vu un octet. Ce réacheminement est toute l'histoire, et le côté satisfaisant est que la version « meilleure » contient nettement moins de code que celle qu'elle a remplacée.
Le pipeline, de bout en bout
L'enregistrement se passe dans le renderer. ScreenRecordingService.ts crée un MediaRecorder sur le flux de capture avec { mimeType: 'video/webm;codecs=vp9,opus', videoBitsPerSecond: 1_000_000 } et le démarre avec un timeslice d'une seconde (CHUNK_INTERVAL_MS = 1000). Chaque blob ondataavailable est envoyé via IPC au processus principal, où le handler recording:save-chunk l'ajoute à un fichier sur le disque avec fs.promises.appendFile. À signaler tôt, parce que c'est une chose tentante à mal lire : cette cadence d'une seconde est le rythme d'écriture disque, pas une taille de chunk d'upload. Au moment où l'upload démarre, il y a un seul fichier WebM terminé posé sur le disque.
Ce qui diffère entre les deux versions est seulement ce qui arrive à ce fichier terminé.
L'avant : tout à travers le backend (v1.8.11)
Le chemin d'upload de la v1.8.11, dans CloudUploader.processRecording(), est le design évident et une première version tout à fait raisonnable :
POST /api/geekbye/recordingspour créer un enregistrement backend avec les métadonnées de l'enregistrement.- Lire toute la vidéo en mémoire et l'envoyer par POST au backend en multipart —
const fileBuffer = fs.readFileSync(filePath)— en atteignantPOST /api/geekbye/recordings/${id}/upload. Le backend téléverse alors ce fichier vers Drive et renvoie undriveUrl. POST …/processpour envoyer la transcription à l'analyse IA.
Le détail porteur est l'étape 2 : le client charge tout l'enregistrement en mémoire et chacun de ses octets transite par les propres serveurs de GeekBye en route vers Drive. Pour un court extrait, c'est bon. Pour un long enregistrement d'écran, ce sont trois problèmes à la fois — pression mémoire sur le client à cause de readFileSync, coût de bande passante et de relais sur le backend pour un fichier qu'il ne garde même pas, et un seul POST monolithique qui doit redémarrer de zéro si le réseau hoquette à mi-chemin.
Il faut aussi être honnête : cet « avant » n'était pas un design d'origine immaculé. La fonctionnalité de la v1.8.11 a écrasé ensemble un pivot en cours de semaine : une première mouture téléversait vers Cloudflare R2 via des URLs pré-signées et convertissait le WebM en MP4 avec un ffmpeg-static embarqué ; cela a été remplacé par le flux Drive médiatisé par le backend ; puis la conversion ffmpeg a été entièrement arrachée au profit de téléverser le WebM tel quel (le commit note qu'elle a abaissé le bitrate et produit des fichiers environ quatre fois plus petits — une estimation à la volée, pas un benchmark). Donc même l'« avant » avait déjà appris à supprimer une dépendance native une fois. La version suivante supprimerait aussi le proxy.
L'après : directement vers Drive (v1.8.13)
La v1.8.13 réécrit CloudUploader.processRecording() autour d'une seule phrase du commit qui l'a livrée : téléverser directement vers Drive, "keeping backend for metadata record + AI transcript analysis only." Le nouveau chemin :
POST /api/geekbye/recordings— métadonnées seulement (titre, durée, taille de fichier,format: 'webm'). Le commentaire dans le code est direct :// Create backend record (metadata only — no file upload).- Téléverser la vidéo directement vers Google Drive, en sautant entièrement l'intermédiaire du backend.
PATCH /api/geekbye/recordings/${backendRecordingId}avec le{ driveUrl, driveFolderId }résultant — on dit à l'enregistrement backend où le fichier a atterri.- La transcription va toujours à
…/processpour l'analyse.
Le backend est passé d'être dans le chemin du fichier à être informé du fichier après coup. Il tient une ligne et un pointeur ; les octets ne vivent que sur le disque de l'utilisateur et dans le Drive de l'utilisateur.
Devenir un vrai client Drive
Pour que le client parle à Drive directement, il doit être un véritable client de la Google Drive API, et la v1.8.13 ajoute la machinerie pour ça : un nouveau DriveService (le client Drive), un nouveau DriveAuthRepository adossé à une nouvelle table SQLite drive_auth, et le SDK googleapis comme dépendance.
L'autorisation est un transfert de capacité propre. L'endpoint app-config du backend relaie les identifiants client Google OAuth — un clientId et un clientSecret — jusqu'à l'app, que CloudUploader lit et passe à driveService.initialize(...). À partir de là, l'app de bureau exécute tout le flux OAuth elle-même : elle monte un serveur http temporaire sur un port 127.0.0.1 aléatoire, ouvre l'écran de consentement dans le navigateur système avec shell.openExternal, attrape la redirection sur un /callback, et échange le code contre des tokens avec google-auth-library. Ces tokens sont sauvegardés localement — saveDriveAuth(access_token, refresh_token, expiry_date) dans drive_auth — avec le scope drive.file, qui laisse l'app gérer uniquement les fichiers qu'elle crée. Le rafraîchissement est local aussi : un listener oauth2Client.on('tokens', …) réécrit les tokens rafraîchis directement dans la table. Le backend cède la capacité une fois puis reste hors de la boucle.
La partie « reprenable », honnêtement
Voici le détail sur lequel je veux le plus être franc, parce que c'est exactement le genre de chose qu'un changelog arrondit. La note de version dit « uploads reprenables », et c'est vrai du point de vue de l'utilisateur. Mais GeekBye n'a pas implémenté le protocole resumable. Il n'y a aucune constante de taille de chunk, aucun traqueur d'offset d'octet, ni aucun traitement de Content-Range / 308 Resume Incomplete où que ce soit dans le code source de GeekBye. Cherchez-les avec grep et vous n'obtenez rien — ils vivent à l'intérieur du SDK googleapis.
Ce que le propre code de GeekBye fait, c'est ceci :
const fileStats = statSync(videoFilePath)
const videoStream = createReadStream(videoFilePath)
const videoRes = await this.drive.files.create(
{
requestBody: { name: `${title}.webm`, parents: [folderId] },
media: { mimeType: 'video/webm', body: videoStream },
fields: 'id',
},
{
onUploadProgress: (evt) => {
const percent = Math.round((evt.bytesRead / fileStats.size) * 100)
onProgress(percent)
},
},
)
Deux choix font tout le travail. D'abord, media.body est un flux (createReadStream), pas un buffer — et passer un flux est précisément ce qui fait que googleapis négocie une session reprenable en votre nom au lieu de tout envoyer d'un coup. Ensuite, la progression est lue depuis le callback onUploadProgress du SDK contre fileStats.size. C'est là toute l'implémentation de l'« upload reprenable » au niveau applicatif : le flux entre, la progression sort. Le protocole difficile — ouvrir une session, téléverser en chunks, reprendre depuis un offset après une coupure — est le travail du SDK, et la bonne décision d'ingénierie était de le laisser faire ce travail plutôt que de le réinventer.
Ce qui recadre toute la version. L'amélioration de fiabilité n'est pas venue de l'écriture d'un moteur d'upload. Elle est venue de la suppression du proxy posé devant un — et, plus tôt la même semaine, de la suppression de l'étape ffmpeg posée devant celui-là. Moins de code, plus de résilience, parce que la chose résiliente était une bibliothèque bien rodée dès l'instant où vous avez cessé de la nourrir à la main avec un buffer.
Les morceaux qui sont vraiment ceux de GeekBye
Confier le transfert au SDK ne veut pas dire qu'il ne restait rien à construire. Deux morceaux de vraie logique applicative l'entourent.
Un rollback de dossier orphelin. Chaque enregistrement obtient son propre sous-dossier Drive (créé avec mimeType: 'application/vnd.google-apps.folder'), et la transcription y atterrit comme un second fichier à côté de la vidéo. Si l'upload lève une erreur, le catch supprime ce dossier — this.drive.files.delete({ fileId: folderId }), journalisé comme Rolled back orphaned Drive folder — pour qu'un upload échoué ne laisse pas une traînée de dossiers vides dans votre Drive. Créer un conteneur avant de savoir que l'opération réussira est un petit passif ; faire nettoyer par le chemin d'échec est le correctif.
Cycle de vie autour d'un singleton. DriveService est un singleton tenant un client OAuth en mémoire, et ce client en mémoire ne survit pas à un redémarrage de l'app même si les tokens le font (ils sont dans la base de données). Alors CloudUploader réinitialise DriveService à partir des tokens stockés avant chaque upload, ce qui veut dire qu'un enregistrement fait juste après le relancement se téléverse quand même sans vous demander de vous reconnecter. Et la passe de revue a transformé le handler drive-connect en fire-and-forget, parce qu'un appel de connexion bloquant calait le polling de statut du renderer — l'UI ne pouvait pas afficher « connexion… » si l'appel de connexion retenait le thread. Ces deux-là — réinit-depuis-le-stockage et connexion non bloquante — sont les vrais cas limites du chemin direct, et notablement aucun des deux ne concerne le découpage de l'upload. Quand vous déléguez la partie difficile, les bugs qui vous restent sont des bugs de cycle de vie.
Trois choses que cette version nous a apprises
- Demandez où les octets circulent, pas seulement si ça marche. Le proxy de la v1.8.11 marchait. Mais router chaque enregistrement à travers vos propres serveurs vous coûte de la bande passante, de la mémoire et de la reprenabilité pour un fichier que vous ne gardez pas. Redessiner le chemin de données pour que le client parle directement à la destination est souvent toute l'optimisation.
- Le meilleur upload reprenable est celui que vous n'avez pas écrit. Passer un flux à un SDK mature a acheté une session reprenable complète pour deux lignes de code. L'instinct de coder à la main le découpage et l'arithmétique d'offset aurait produit plus de code et moins de fiabilité.
- Quand vous déléguez le cœur, vous héritez du cycle de vie, pas des algorithmes. Les vrais bugs du client direct étaient « réinitialiser le singleton après redémarrage » et « ne pas bloquer l'appel de connexion », pas des offsets d'octets. C'est un bon compromis — les bugs de cycle de vie sont visibles et locaux ; les bugs de protocole ne sont ni l'un ni l'autre.
Pour le chapitre précédent de l'histoire v1, né et corrigé en douze minutes (v1.8.10) ; et pour tout l'arc, l'anatomie de livrer un logiciel jusqu'à la perfection.