Steven
Steven9 min de lectura

Treure el backend del camí de pujada

GeekBye enregistra la teva pantalla i desa el vídeo al teu Google Drive. La primera versió enviava cada enregistrament a través dels servidors del propi GeekBye pel camí; una versió després, el fitxer anava directament de la teva màquina a Drive, i el backend va quedar degradat a sostenir un únic punter. La part interessant és com poc codi conté realment la versió 'directa i resumible' — perquè la resumibilitat va venir d'esborrar un proxy, no d'escriure'n un.

Engineering
Architecture
Desktop
GeekBye Releases
Treure el backend del camí de pujada

GeekBye pot enregistrar la teva pantalla — vídeo, àudio del sistema i micròfon — i deixar l'enregistrament acabat al teu Google Drive automàticament. Entre la v1.8.11 i la v1.8.13, la descripció de cara a l'usuari de la funcionalitat gairebé no va canviar, però el camí que fa el vídeo per arribar a Drive es va redirigir completament. A la primera versió, el teu enregistrament viatjava a través dels servidors de GeekBye. A la segona, anava directament de la teva màquina al teu Drive, i el backend de GeekBye no en va veure ni un byte. Aquesta redirecció és tota la història, i la part satisfactòria és que la versió "millor" conté ostensiblement menys codi que la que va substituir.

El pipeline, de punta a punta

L'enregistrament passa al renderer. ScreenRecordingService.ts crea un MediaRecorder sobre el flux de captura amb { mimeType: 'video/webm;codecs=vp9,opus', videoBitsPerSecond: 1_000_000 } i l'engega amb un interval d'un segon (CHUNK_INTERVAL_MS = 1000). Cada blob ondataavailable s'envia per IPC al procés principal, on el handler recording:save-chunk l'afegeix a un fitxer al disc amb fs.promises.appendFile. Val la pena assenyalar-ho aviat, perquè és una cosa temptadora de malinterpretar: aquesta cadència d'un segon és el ritme d'escriptura-a-disc, no una mida de tros de pujada. Quan comença la pujada, hi ha un únic fitxer WebM acabat assegut al disc.

El que difereix entre les dues versions és només què li passa a aquell fitxer acabat.

L'abans: tot a través del backend (v1.8.11)

El camí de pujada de la v1.8.11, a CloudUploader.processRecording(), és el disseny obvi i una primera versió completament raonable:

  1. POST /api/geekbye/recordings per crear un registre al backend amb les metadades de l'enregistrament.
  2. Llegeix tot el vídeo a memòria i fes-ne un POST al backend com a multipart — const fileBuffer = fs.readFileSync(filePath) — colpejant POST /api/geekbye/recordings/${id}/upload. El backend després puja aquell fitxer a Drive i retorna un driveUrl.
  3. POST …/process per enviar el transcript per a l'anàlisi d'IA.

El detall que sosté tot és el pas 2: el client carrega tot l'enregistrament a memòria i cada byte seu transita pels propis servidors de GeekBye camí de Drive. Per a un clip curt està bé. Per a un enregistrament de pantalla llarg són tres problemes alhora — pressió de memòria al client per readFileSync, cost de banda ampla i de relé al backend per a un fitxer que ni tan sols conserva, i un únic POST monolític que ha de reiniciar des de zero si la xarxa fa un salt a mig camí.

També val la pena ser honest que aquest "abans" no era cap disseny original impol·lut. La funcionalitat de la v1.8.11 va aixafar junts un gir dins de la mateixa setmana: un tall primerenc pujava a Cloudflare R2 via URL pre-signades i convertia WebM a MP4 amb un ffmpeg-static empaquetat; això es va substituir pel flux de Drive mediat pel backend; i després la conversió d'ffmpeg es va arrencar del tot en favor de pujar la WebM tal qual (el commit assenyala que va abaixar el bitrate i va produir fitxers aproximadament quatre vegades més petits — una estimació en línia, no un benchmark). Així que fins i tot l'"abans" ja havia après a esborrar una dependència nativa una vegada. La següent versió també esborraria el proxy.

El després: directament a Drive (v1.8.13)

La v1.8.13 reescriu CloudUploader.processRecording() al voltant d'una frase del commit que la va publicar: puja directament a Drive, "keeping backend for metadata record + AI transcript analysis only." El nou camí:

  1. POST /api/geekbye/recordingsnomés metadades (títol, durada, mida del fitxer, format: 'webm'). El comentari al codi és directe: // Create backend record (metadata only — no file upload).
  2. Puja el vídeo directament a Google Drive, saltant-se l'intermediari del backend del tot.
  3. PATCH /api/geekbye/recordings/${backendRecordingId} amb el { driveUrl, driveFolderId } resultant — al registre del backend se li diu on va acabar el fitxer.
  4. El transcript encara va a …/process per a l'anàlisi.

El backend va passar d'estar dins del camí del fitxer a que se li digués sobre el fitxer després del fet. Sosté una fila i un punter; els bytes viuen només al disc de l'usuari i al Drive de l'usuari.

Esdevenir un client de Drive de veritat

Perquè el client parli amb Drive directament, ha de ser un client real de l'API de Google Drive, i la v1.8.13 afegeix la maquinària per a això: un nou DriveService (el client de Drive), un nou DriveAuthRepository recolzat per una nova taula SQLite drive_auth, i l'SDK googleapis com a dependència.

L'autorització és un traspàs de capacitat net. L'endpoint de configuració de l'app del backend transmet les credencials de client d'OAuth de Google — un clientId i un clientSecret — cap avall a l'app, que CloudUploader llegeix i passa a driveService.initialize(...). A partir d'aquí l'app d'escriptori executa tot el flux d'OAuth ella mateixa: aixeca un servidor http temporal en un port 127.0.0.1 aleatori, obre la pantalla de consentiment al navegador del sistema amb shell.openExternal, atrapa la redirecció en un /callback, i intercanvia el codi per tokens amb google-auth-library. Aquests tokens es desen localment — saveDriveAuth(access_token, refresh_token, expiry_date) a drive_auth — amb l'abast drive.file, que deixa que l'app gestioni només els fitxers que crea. El refresc també és local: un listener oauth2Client.on('tokens', …) escriu els tokens refrescats directament de tornada a la taula. El backend cedeix la capacitat una vegada i després es queda fora del bucle.

La part "resumible", honestament

Aquí tens el detall sobre el qual més vull ser franc, perquè és exactament el tipus de cosa que un changelog arrodoneix. La nota de la versió diu "resumable uploads," i això és cert des d'on està l'usuari. Però GeekBye no va implementar el protocol resumible. No hi ha cap constant de mida de tros, cap rastrejador d'offset de bytes, ni cap gestió de Content-Range / 308 Resume Incomplete enlloc de la font de GeekBye. Fes-ne un grep i no obtens res — viuen dins de l'SDK googleapis.

El que fa el codi propi de GeekBye és això:

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

Dues eleccions fan tota la feina. Primer, media.body és un flux (createReadStream), no un buffer — i passar un flux és precisament el que fa que googleapis negociï una sessió resumible en nom teu en comptes de fer-ho d'un sol tret. Segon, el progrés es llegeix del callback onUploadProgress de l'SDK contra fileStats.size. Aquesta és tota la implementació de "resumable upload" a nivell d'aplicació: flux endins, progrés enfora. El protocol difícil — obrir una sessió, pujar en trossos, reprendre des d'un offset després d'una caiguda — és la feina de l'SDK, i la decisió d'enginyeria correcta va ser deixar-lo fer aquesta feina en comptes de reinventar-la.

Cosa que replanteja tota la versió. La millora de fiabilitat no va venir d'escriure un motor de pujada. Va venir d'esborrar el proxy que hi havia al davant — i, abans la mateixa setmana, d'esborrar el pas d'ffmpeg que hi havia al davant d'això. Menys codi, més resiliència, perquè la cosa resilient era una biblioteca ben rodada en el moment que vas deixar d'alimentar-la a mà amb un buffer.

Les parts que són genuïnament de GeekBye

Lliurar la transferència a l'SDK no vol dir que no quedés res per construir. Dues peces de lògica d'aplicació real l'envolten.

Una reversió de carpeta òrfena. Cada enregistrament rep la seva pròpia subcarpeta de Drive (creada amb mimeType: 'application/vnd.google-apps.folder'), i el transcript hi aterra com a segon fitxer al costat del vídeo. Si la pujada llança una excepció, el catch esborra aquella carpeta — this.drive.files.delete({ fileId: folderId }), registrat com a Rolled back orphaned Drive folder — perquè una pujada fallida no deixi un rastre de carpetes buides al teu Drive. Crear un contenidor abans de saber que l'operació tindrà èxit és una petita responsabilitat; fer que el camí de fallida ho netegi és la solució.

Cicle de vida al voltant d'un singleton. DriveService és un singleton que sosté un client OAuth en memòria, i aquell client en memòria no sobreviu a un reinici de l'app tot i que els tokens sí (són a la base de dades). Així que CloudUploader reinicialitza DriveService des dels tokens desats abans de cada pujada, cosa que vol dir que un enregistrament fet just després de rellançar encara puja sense demanar-te que et reconnectis. I la passada de revisió va convertir el handler drive-connect en fes-i-oblida, perquè una crida de connexió bloquejant estava encallant el sondeig d'estat del renderer — la UI no podia mostrar "connecting…" si la crida de connexió retenia el fil. Aquestes dues — reinicialitzar-des-de-l'emmagatzematge i connexió no bloquejant — són els casos límit reals del camí directe, i notablement cap d'elles no va sobre el trossejat de pujada. Quan delegues la part difícil, els bugs que et queden són bugs de cicle de vida.

Tres coses que ens va ensenyar aquesta versió

  1. Pregunta per on flueixen els bytes, no només si funciona. El proxy de la v1.8.11 funcionava. Però encaminar cada enregistrament a través dels teus propis servidors et costa banda ampla, memòria i capacitat de reinici per a un fitxer que no conserves. Redibuixar el camí de dades perquè el client parli directament amb la destinació és sovint tota l'optimització.
  2. La millor pujada resumible és la que no vas escriure. Passar un flux a un SDK madur va comprar una sessió resumible completa per dues línies de codi. L'instint de fer a mà el trossejat i l'aritmètica d'offset hauria produït més codi i menys fiabilitat.
  3. Quan delegues el nucli, heretes cicle de vida, no algorismes. Els bugs reals del client directe eren "reinicialitza el singleton després del reinici" i "no bloquegis la crida de connexió," no offsets de bytes. Això és un bon intercanvi — els bugs de cicle de vida són visibles i locals; els bugs de protocol no són cap de les dues coses.

Per al capítol anterior de la història de la v1, nascut i arreglat en dotze minuts (v1.8.10); i per a tot l'arc, l'anatomia de publicar programari fins a la perfecció.

Articles Relacionats

Els tres verbs que mantenen viu el Web Audio
Steven
Steven10 min de lectura

Els tres verbs que mantenen viu el Web Audio

Dues point releases de GeekBye, amb dos mesos de diferència i en dos fitxers diferents, van ensenyar al nostre codi d'àudio la mateixa lliçó des d'extrems oposats: deixa de tractar l'AudioContext del navegador com a rebutjable. Una versió va aprendre a fer resume() d'un context que macOS havia suspès silenciosament enmig d'una gravació; l'altra va aprendre a fer suspend() en comptes de close() perquè les sessions consecutives deixin d'estavellar-se contra el sostre de prop de sis contextos de Chromium. Resume, suspend, close — aquest és tot l'argument.

Engineering
Audio
Desktop
Distingir una trucada d'una app oberta
Steven
Steven9 min de lectura

Distingir una trucada d'una app oberta

GeekBye pot notar que t'has unit a una videotrucada i oferir-se a gravar-la. La detecció resulta ser la meitat fàcil — un binari de Swift que llegeix títols de finestra cada deu segons. La meitat difícil és la precisió: no disparar-se quan Zoom només està obert, no preguntar per una reunió que ja estàs gravant, i no silenciar el micròfon de la trucada on realment ets. Tres versions, i cadascuna és una salvaguarda que va haver d'aprendre a no derrotar-se a si mateixa.

Engineering
macOS
Desktop
Nascut i arreglat en dotze minuts
Steven
Steven8 min de lectura

Nascut i arreglat en dotze minuts

El changelog de GeekBye v1.8.10 diu que va arreglar un crash mentre s'editaven les dreceres de teclat. Ho va fer — però el crash es va introduir i arreglar en la mateixa pull request, amb dotze minuts de diferència, i mai no va arribar a cap usuari. La història real és la cascada de fiabilitat que el va produir: un canvi petit i correcte sobre quan se suspenen les dreceres, i el bug de teardown de React que va caure d'una sola línia pensada per fer l'editor més segur.

Engineering
React
Desktop