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
Release di GeekBye
Togliere il backend dal percorso di upload

GeekBye può registrare il tuo schermo — video, audio di sistema e microfono — e depositare la registrazione finita nel tuo Google Drive automaticamente. Tra la v1.8.11 e la v1.8.13, la descrizione rivolta all'utente della funzione è cambiata a malapena, ma il percorso che il video prende per arrivare a Drive è stato reindirizzato del tutto. Nella prima versione, la tua registrazione viaggiava attraverso i server di GeekBye. Nella seconda, andava direttamente dalla tua macchina al tuo Drive, e il backend di GeekBye non ne ha mai visto un byte. Quel reindirizzamento è tutta la storia, e la parte soddisfacente è che la versione «migliore» contiene visibilmente meno codice di quella che ha sostituito.

La pipeline, da un capo all'altro

La registrazione avviene nel renderer. ScreenRecordingService.ts crea un MediaRecorder sullo stream di cattura con { mimeType: 'video/webm;codecs=vp9,opus', videoBitsPerSecond: 1_000_000 } e lo avvia con un timeslice di un secondo (CHUNK_INTERVAL_MS = 1000). Ogni blob di ondataavailable viene inviato via IPC al processo principale, dove il handler recording:save-chunk lo accoda a un file su disco con fs.promises.appendFile. Vale la pena segnalarlo presto, perché è una cosa tentante da fraintendere: quella cadenza di un secondo è il ritmo di scrittura su disco, non una dimensione di pezzo di upload. Quando l'upload inizia, c'è un singolo file WebM finito posato sul disco.

Ciò che differisce tra le due release è solo cosa succede a quel file finito.

Il prima: tutto attraverso il backend (v1.8.11)

Il percorso di upload della v1.8.11, in CloudUploader.processRecording(), è il design ovvio e una prima versione del tutto ragionevole:

  1. POST /api/geekbye/recordings per creare un record backend con i metadati della registrazione.
  2. Leggere tutto il video in memoria e inviarlo via POST al backend come multipart — const fileBuffer = fs.readFileSync(filePath) — colpendo POST /api/geekbye/recordings/${id}/upload. Il backend poi carica quel file su Drive e restituisce un driveUrl.
  3. POST …/process per inviare la trascrizione all'analisi IA.

Il dettaglio portante è il passo 2: il client carica tutta la registrazione in memoria e ogni suo byte transita per i server di GeekBye stessa in viaggio verso Drive. Per un clip breve va bene. Per una lunga registrazione dello schermo sono tre problemi in una volta — pressione di memoria sul client da readFileSync, costo di banda e di inoltro sul backend per un file che nemmeno conserva, e un singolo POST monolitico che deve ripartire da zero se la rete singhiozza a metà strada.

Vale anche la pena essere onesti: questo «prima» non era un design originale immacolato. La funzione della v1.8.11 ha schiacciato insieme un pivot nell'arco di una settimana: una prima versione caricava su Cloudflare R2 tramite URL pre-firmate e convertiva WebM in MP4 con un ffmpeg-static incluso; ciò è stato sostituito dal flusso Drive mediato dal backend; e poi la conversione ffmpeg è stata strappata via del tutto a favore del caricare il WebM così com'è (il commit annota che ha abbassato il bitrate e prodotto file circa quattro volte più piccoli — una stima al volo, non un benchmark). Quindi anche il «prima» aveva già imparato a cancellare una dipendenza nativa una volta. La release successiva avrebbe cancellato anche il proxy.

Il dopo: dritto a Drive (v1.8.13)

La v1.8.13 riscrive CloudUploader.processRecording() attorno a una singola frase del commit che l'ha pubblicata: caricare direttamente su Drive, "keeping backend for metadata record + AI transcript analysis only." Il nuovo percorso:

  1. POST /api/geekbye/recordingssolo metadati (titolo, durata, dimensione del file, format: 'webm'). Il commento nel codice è schietto: // Create backend record (metadata only — no file upload).
  2. Caricare il video direttamente su Google Drive, saltando del tutto l'intermediario del backend.
  3. PATCH /api/geekbye/recordings/${backendRecordingId} con il { driveUrl, driveFolderId } risultante — al record backend viene detto dove è finito il file.
  4. La trascrizione va ancora a …/process per l'analisi.

Il backend è passato dall'essere nel percorso del file all'essere informato del file a posteriori. Tiene una riga e un puntatore; i byte vivono solo sul disco dell'utente e nel Drive dell'utente.

Diventare un vero client Drive

Perché il client parli con Drive direttamente, deve essere un vero client della Google Drive API, e la v1.8.13 aggiunge il macchinario per questo: un nuovo DriveService (il client Drive), un nuovo DriveAuthRepository sostenuto da una nuova tabella SQLite drive_auth, e l'SDK googleapis come dipendenza.

L'autorizzazione è un passaggio di capacità pulito. L'endpoint app-config del backend inoltra le credenziali client di Google OAuth — un clientId e un clientSecret — fino all'app, che CloudUploader legge e passa a driveService.initialize(...). Da lì l'app desktop esegue tutto il flusso OAuth da sé: tira su un server http temporaneo su una porta 127.0.0.1 casuale, apre la schermata di consenso nel browser di sistema con shell.openExternal, cattura il reindirizzamento su un /callback, e scambia il codice per i token con google-auth-library. Quei token vengono salvati localmente — saveDriveAuth(access_token, refresh_token, expiry_date) in drive_auth — con lo scope drive.file, che lascia l'app gestire solo i file che crea. Anche il rinnovo è locale: un listener oauth2Client.on('tokens', …) riscrive i token rinnovati direttamente nella tabella. Il backend cede la capacità una volta e poi resta fuori dal giro.

La parte «riprendibile», onestamente

Ecco il dettaglio su cui più voglio essere schietto, perché è esattamente il tipo di cosa che un changelog arrotonda. La nota di rilascio dice «upload riprendibili», e questo è vero dal punto di vista dell'utente. Ma GeekBye non ha implementato il protocollo resumable. Non c'è alcuna costante di dimensione di pezzo, nessun tracker di offset di byte, né alcuna gestione di Content-Range / 308 Resume Incomplete da nessuna parte nel codice sorgente di GeekBye. Cercali con grep e non ottieni nulla — vivono dentro l'SDK googleapis.

Ciò che fa il codice di GeekBye stessa è questo:

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)
    },
  },
)

Due scelte fanno tutto il lavoro. Primo, media.body è uno stream (createReadStream), non un buffer — e passare uno stream è precisamente ciò che fa negoziare a googleapis una sessione riprendibile per tuo conto invece di farlo in un colpo solo. Secondo, il progresso viene letto dal callback onUploadProgress dell'SDK contro fileStats.size. Questa è tutta l'implementazione dell'«upload riprendibile» a livello applicativo: lo stream entra, il progresso esce. Il protocollo difficile — aprire una sessione, caricare a pezzi, riprendere da un offset dopo una caduta — è compito dell'SDK, e la decisione ingegneristica corretta è stata lasciargli fare quel compito invece di reinventarlo.

Il che riformula l'intera release. Il miglioramento di affidabilità non è venuto dallo scrivere un motore di upload. È venuto dal cancellare il proxy che stava davanti a uno — e, prima nella stessa settimana, dal cancellare il passo ffmpeg che stava davanti a quello. Meno codice, più resilienza, perché la cosa resiliente era una libreria ben collaudata nel momento in cui hai smesso di imboccarla a mano con un buffer.

Le parti che sono davvero di GeekBye

Consegnare il trasferimento all'SDK non significa che non restasse nulla da costruire. Due pezzi di vera logica applicativa lo circondano.

Un rollback di cartella orfana. Ogni registrazione ottiene la propria sottocartella Drive (creata con mimeType: 'application/vnd.google-apps.folder'), e la trascrizione atterra lì come secondo file accanto al video. Se l'upload solleva un errore, il catch cancella quella cartella — this.drive.files.delete({ fileId: folderId }), registrato come Rolled back orphaned Drive folder — così che un upload fallito non lasci una scia di cartelle vuote nel tuo Drive. Creare un contenitore prima di sapere che l'operazione riuscirà è una piccola passività; far pulire al percorso di fallimento è la correzione.

Ciclo di vita attorno a un singleton. DriveService è un singleton che tiene un client OAuth in memoria, e quel client in memoria non sopravvive a un riavvio dell'app anche se i token sì (sono nel database). Così CloudUploader reinizializza DriveService dai token memorizzati prima di ogni upload, il che significa che una registrazione fatta subito dopo il rilancio si carica comunque senza chiederti di riconnetterti. E la passata di revisione ha trasformato il handler drive-connect in fire-and-forget, perché una chiamata di connessione bloccante stava intoppando il polling di stato del renderer — la UI non poteva mostrare «connessione…» se la chiamata di connessione teneva il thread. Questi due — reinizializzare-dallo-storage e connessione non bloccante — sono i veri casi limite del percorso diretto, e in modo notevole nessuno dei due riguarda il chunking dell'upload. Quando deleghi la parte difficile, i bug che ti restano sono bug di ciclo di vita.

Tre cose che questa release ci ha insegnato

  1. Chiedi dove scorrono i byte, non solo se funziona. Il proxy della v1.8.11 funzionava. Ma instradare ogni registrazione attraverso i tuoi server ti costa banda, memoria e capacità di ripresa per un file che non conservi. Ridisegnare il percorso dei dati così che il client parli direttamente con la destinazione è spesso tutta l'ottimizzazione.
  2. Il miglior upload riprendibile è quello che non hai scritto. Passare uno stream a un SDK maturo ha comprato una sessione riprendibile completa per due righe di codice. L'istinto di programmare a mano il chunking e l'aritmetica di offset avrebbe prodotto più codice e meno affidabilità.
  3. Quando deleghi il cuore, erediti il ciclo di vita, non gli algoritmi. I veri bug del client diretto erano «reinizializzare il singleton dopo il riavvio» e «non bloccare la chiamata di connessione», non offset di byte. È un buon compromesso — i bug di ciclo di vita sono visibili e locali; i bug di protocollo non sono né l'uno né l'altro.

Per il capitolo precedente della storia v1, nato e corretto in dodici minuti (v1.8.10); 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
Distinguere una chiamata da un'app aperta
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
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