
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.
GeekBye puede grabar tu pantalla — video, audio del sistema y micrófono — y dejar la grabación terminada en tu Google Drive automáticamente. Entre la v1.8.11 y la v1.8.13, la descripción de cara al usuario de la función apenas cambió, pero la ruta que toma el video para llegar a Drive fue reencaminada por completo. En la primera versión, tu grabación viajaba a través de los servidores de GeekBye. En la segunda, iba directo desde tu máquina a tu Drive, y el backend de GeekBye nunca vio un solo byte de ella. Ese reencaminamiento es toda la historia, y lo satisfactorio es que la versión «mejor» contiene notoriamente menos código que la que reemplazó.
El pipeline, de principio a fin
La grabación ocurre en el renderizador. ScreenRecordingService.ts crea un MediaRecorder sobre el stream de captura con { mimeType: 'video/webm;codecs=vp9,opus', videoBitsPerSecond: 1_000_000 } y lo arranca con un timeslice de un segundo (CHUNK_INTERVAL_MS = 1000). Cada blob de ondataavailable se envía por IPC al proceso principal, donde el handler recording:save-chunk lo añade a un archivo en disco con fs.promises.appendFile. Vale la pena señalarlo pronto, porque es algo tentador de malinterpretar: esa cadencia de un segundo es el ritmo de escritura en disco, no un tamaño de trozo de subida. Para cuando empieza la subida, hay un único archivo WebM terminado descansando en el disco.
Lo único que difiere entre las dos releases es qué le pasa a ese archivo terminado.
El antes: todo a través del backend (v1.8.11)
La ruta de subida de la v1.8.11, en CloudUploader.processRecording(), es el diseño obvio y una primera versión completamente razonable:
POST /api/geekbye/recordingspara crear un registro en el backend con los metadatos de la grabación.- Leer todo el video en memoria y enviarlo por POST al backend como multipart —
const fileBuffer = fs.readFileSync(filePath)— llegando aPOST /api/geekbye/recordings/${id}/upload. El backend entonces sube ese archivo a Drive y devuelve undriveUrl. POST …/processpara enviar la transcripción al análisis de IA.
El detalle que sostiene todo es el paso 2: el cliente carga toda la grabación en memoria y cada uno de sus bytes transita por los propios servidores de GeekBye camino a Drive. Para un clip corto está bien. Para una grabación de pantalla larga son tres problemas a la vez — presión de memoria en el cliente por readFileSync, coste de ancho de banda y de retransmisión en el backend por un archivo que ni siquiera conserva, y un único POST monolítico que tiene que reiniciarse desde cero si la red hipa a mitad de camino.
También conviene ser honesto: este «antes» no era un diseño original impoluto. La función de la v1.8.11 aplastó junto un giro de la misma semana: una versión temprana subía a Cloudflare R2 mediante URLs pre-firmadas y convertía WebM a MP4 con un ffmpeg-static empaquetado; eso fue reemplazado por el flujo de Drive mediado por el backend; y luego la conversión de ffmpeg se arrancó por completo a favor de subir el WebM tal cual (el commit anota que bajó el bitrate y produjo archivos aproximadamente cuatro veces más pequeños — una estimación al vuelo, no un benchmark). Así que hasta el «antes» ya había aprendido a borrar una dependencia nativa una vez. La siguiente release borraría también el proxy.
El después: directo a Drive (v1.8.13)
La v1.8.13 reescribe CloudUploader.processRecording() en torno a una sola frase del commit que la publicó: subir directamente a Drive, "keeping backend for metadata record + AI transcript analysis only." La nueva ruta:
POST /api/geekbye/recordings— solo metadatos (título, duración, tamaño de archivo,format: 'webm'). El comentario en el código es directo:// Create backend record (metadata only — no file upload).- Subir el video directamente a Google Drive, saltándose por completo al intermediario del backend.
PATCH /api/geekbye/recordings/${backendRecordingId}con el{ driveUrl, driveFolderId }resultante — al registro del backend se le informa de dónde terminó el archivo.- La transcripción sigue yendo a
…/processpara el análisis.
El backend pasó de estar en la ruta del archivo a que se le informe sobre el archivo a posteriori. Sostiene una fila y un puntero; los bytes viven solo en el disco del usuario y en el Drive del usuario.
Convertirse en un cliente de Drive de verdad
Para que el cliente hable con Drive directamente, tiene que ser un cliente de la Google Drive API de verdad, y la v1.8.13 añade la maquinaria para eso: un nuevo DriveService (el cliente de Drive), un nuevo DriveAuthRepository respaldado por una nueva tabla SQLite drive_auth, y el SDK googleapis como dependencia.
La autorización es un traspaso de capacidad limpio. El endpoint de app-config del backend retransmite las credenciales de cliente de Google OAuth — un clientId y un clientSecret — hasta la app, que CloudUploader lee y pasa a driveService.initialize(...). A partir de ahí la app de escritorio ejecuta todo el flujo OAuth ella misma: levanta un servidor http temporal en un puerto 127.0.0.1 al azar, abre la pantalla de consentimiento en el navegador del sistema con shell.openExternal, atrapa la redirección en un /callback, e intercambia el código por tokens con google-auth-library. Esos tokens se guardan localmente — saveDriveAuth(access_token, refresh_token, expiry_date) en drive_auth — con el scope drive.file, que deja a la app gestionar solo los archivos que crea. El refresco también es local: un listener oauth2Client.on('tokens', …) escribe los tokens refrescados directamente de vuelta a la tabla. El backend entrega la capacidad una vez y luego se queda fuera del bucle.
La parte «reanudable», honestamente
Aquí está el detalle sobre el que más quiero ser franco, porque es exactamente el tipo de cosa que un changelog redondea. La nota de la versión dice «subidas reanudables», y eso es cierto desde donde está el usuario. Pero GeekBye no implementó el protocolo resumable. No hay ninguna constante de tamaño de trozo, ningún rastreador de offset de byte, ni manejo de Content-Range / 308 Resume Incomplete en ningún lugar del código fuente de GeekBye. Búscalos con grep y no obtienes nada — viven dentro del SDK googleapis.
Lo que hace el propio código de GeekBye es esto:
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)
},
},
)
Dos decisiones hacen todo el trabajo. Primero, media.body es un stream (createReadStream), no un buffer — y pasar un stream es precisamente lo que hace que googleapis negocie una sesión reanudable en tu nombre en vez de hacerlo de un solo tiro. Segundo, el progreso se lee del callback onUploadProgress del SDK contra fileStats.size. Esa es toda la implementación de «subida reanudable» a nivel de aplicación: stream que entra, progreso que sale. El protocolo difícil — abrir una sesión, subir en trozos, reanudar desde un offset tras una caída — es trabajo del SDK, y la decisión de ingeniería correcta fue dejarle hacer ese trabajo en vez de reinventarlo.
Lo cual replantea toda la release. La mejora de fiabilidad no vino de escribir un motor de subida. Vino de borrar el proxy que se sentaba delante de uno — y, antes esa misma semana, de borrar el paso de ffmpeg que estaba delante de aquel. Menos código, más resiliencia, porque lo resiliente era una librería más que probada en el momento en que dejaste de alimentarla a mano con un buffer.
Las partes que son genuinamente de GeekBye
Entregarle la transferencia al SDK no significa que no quedara nada por construir. Dos piezas de lógica de aplicación real la rodean.
Un rollback de carpeta huérfana. Cada grabación recibe su propia subcarpeta de Drive (creada con mimeType: 'application/vnd.google-apps.folder'), y la transcripción aterriza allí como un segundo archivo junto al video. Si la subida lanza un error, el catch borra esa carpeta — this.drive.files.delete({ fileId: folderId }), registrado como Rolled back orphaned Drive folder — para que una subida fallida no deje un rastro de carpetas vacías en tu Drive. Crear un contenedor antes de saber que la operación tendrá éxito es un pequeño pasivo; hacer que la ruta de fallo lo limpie es el arreglo.
Ciclo de vida alrededor de un singleton. DriveService es un singleton que sostiene un cliente OAuth en memoria, y ese cliente en memoria no sobrevive a un reinicio de la app aunque los tokens sí (están en la base de datos). Así que CloudUploader reinicializa DriveService a partir de los tokens guardados antes de cada subida, lo que significa que una grabación hecha justo después de relanzar sigue subiendo sin pedirte que te reconectes. Y la pasada de revisión convirtió el handler drive-connect en fire-and-forget, porque una llamada de conexión bloqueante estaba atascando el sondeo de estado del renderizador — la UI no podía mostrar «conectando…» si la llamada de conexión retenía el hilo. Esas dos — reinicializar-desde-almacenamiento y conexión no bloqueante — son los verdaderos casos límite de la ruta directa, y notablemente ninguna de las dos trata sobre el troceado de la subida. Cuando delegas la parte difícil, los bugs que te quedan son bugs de ciclo de vida.
Tres cosas que nos enseñó esta release
- Pregunta por dónde fluyen los bytes, no solo si funciona. El proxy de la v1.8.11 funcionaba. Pero enrutar cada grabación a través de tus propios servidores te cuesta ancho de banda, memoria y capacidad de reinicio por un archivo que no conservas. Redibujar la ruta de datos para que el cliente hable directamente con el destino es a menudo toda la optimización.
- La mejor subida reanudable es la que no escribiste. Pasar un stream a un SDK maduro compró una sesión reanudable completa por dos líneas de código. El instinto de programar a mano el troceado y la aritmética de offsets habría producido más código y menos fiabilidad.
- Cuando delegas el núcleo, heredas ciclo de vida, no algoritmos. Los verdaderos bugs del cliente directo eran «reinicializar el singleton tras el reinicio» y «no bloquear la llamada de conexión», no offsets de bytes. Es un buen trato — los bugs de ciclo de vida son visibles y locales; los de protocolo no son ni lo uno ni lo otro.
Para el capítulo anterior de la historia v1, nacido y arreglado en doce minutos (v1.8.10); y para todo el arco, la anatomía de publicar software hasta la perfección.