Steven
Steven9 min de leitura

Tirando o backend do caminho de upload

A GeekBye grava sua tela e salva o vídeo no seu Google Drive. A primeira versão enviava cada gravação pelos próprios servidores da GeekBye no caminho; uma release depois, o arquivo ia direto da sua máquina para o Drive, e o backend foi rebaixado a segurar um único ponteiro. A parte interessante é o quão pouco código a versão «direta e retomável» realmente contém — porque a retomabilidade veio de apagar um proxy, não de escrever um.

Engenharia
Arquitetura
Desktop
Lançamentos da GeekBye
Tirando o backend do caminho de upload

A GeekBye pode gravar sua tela — vídeo, áudio do sistema e microfone — e soltar a gravação finalizada no seu Google Drive automaticamente. Entre a v1.8.11 e a v1.8.13, a descrição voltada ao usuário do recurso mal mudou, mas o caminho que o vídeo percorre para chegar ao Drive foi redirecionado por completo. Na primeira versão, sua gravação viajava pelos servidores da GeekBye. Na segunda, ia direto da sua máquina para o seu Drive, e o backend da GeekBye nunca viu um byte dela. Esse redirecionamento é a história toda, e a parte satisfatória é que a versão «melhor» contém visivelmente menos código do que a que substituiu.

O pipeline, de ponta a ponta

A gravação acontece no renderizador. ScreenRecordingService.ts cria um MediaRecorder sobre o stream de captura com { mimeType: 'video/webm;codecs=vp9,opus', videoBitsPerSecond: 1_000_000 } e o inicia com um timeslice de um segundo (CHUNK_INTERVAL_MS = 1000). Cada blob de ondataavailable é enviado via IPC para o processo principal, onde o handler recording:save-chunk o anexa a um arquivo no disco com fs.promises.appendFile. Vale sinalizar cedo, porque é algo tentador de interpretar errado: essa cadência de um segundo é o ritmo de escrita em disco, não um tamanho de pedaço de upload. Quando o upload começa, há um único arquivo WebM finalizado descansando no disco.

O que difere entre as duas releases é apenas o que acontece com esse arquivo finalizado.

O antes: tudo através do backend (v1.8.11)

O caminho de upload da v1.8.11, em CloudUploader.processRecording(), é o design óbvio e uma primeira versão completamente razoável:

  1. POST /api/geekbye/recordings para criar um registro no backend com os metadados da gravação.
  2. Ler o vídeo inteiro na memória e enviá-lo por POST ao backend como multipart — const fileBuffer = fs.readFileSync(filePath) — chegando a POST /api/geekbye/recordings/${id}/upload. O backend então faz upload desse arquivo para o Drive e devolve um driveUrl.
  3. POST …/process para enviar a transcrição para a análise de IA.

O detalhe que sustenta tudo é o passo 2: o cliente carrega a gravação inteira na memória e cada byte dela transita pelos próprios servidores da GeekBye a caminho do Drive. Para um clipe curto, tudo bem. Para uma gravação de tela longa são três problemas de uma vez — pressão de memória no cliente por causa do readFileSync, custo de banda e de retransmissão no backend por um arquivo que ele nem guarda, e um único POST monolítico que tem que reiniciar do zero se a rede soluçar no meio do caminho.

Também vale ser honesto: esse «antes» não era um design original impecável. O recurso da v1.8.11 espremeu junto um pivô dentro da mesma semana: uma versão inicial fazia upload para o Cloudflare R2 via URLs pré-assinadas e convertia WebM para MP4 com um ffmpeg-static empacotado; isso foi substituído pelo fluxo do Drive mediado pelo backend; e então a conversão de ffmpeg foi arrancada por completo em favor de fazer upload do WebM como está (o commit anota que baixou o bitrate e produziu arquivos aproximadamente quatro vezes menores — uma estimativa de improviso, não um benchmark). Então até o «antes» já havia aprendido a apagar uma dependência nativa uma vez. A próxima release apagaria o proxy também.

O depois: direto para o Drive (v1.8.13)

A v1.8.13 reescreve CloudUploader.processRecording() em torno de uma única frase do commit que a publicou: fazer upload diretamente para o Drive, "keeping backend for metadata record + AI transcript analysis only." O novo caminho:

  1. POST /api/geekbye/recordingsapenas metadados (título, duração, tamanho do arquivo, format: 'webm'). O comentário no código é direto: // Create backend record (metadata only — no file upload).
  2. Fazer upload do vídeo diretamente para o Google Drive, pulando o intermediário do backend por completo.
  3. PATCH /api/geekbye/recordings/${backendRecordingId} com o { driveUrl, driveFolderId } resultante — o registro do backend é informado de onde o arquivo foi parar.
  4. A transcrição ainda vai para …/process para análise.

O backend passou de estar no caminho do arquivo para ser informado sobre o arquivo depois do fato. Ele segura uma linha e um ponteiro; os bytes vivem só no disco do usuário e no Drive do usuário.

Virando um cliente de Drive de verdade

Para o cliente falar com o Drive diretamente, ele tem que ser um cliente de fato da Google Drive API, e a v1.8.13 adiciona a maquinaria para isso: um novo DriveService (o cliente do Drive), um novo DriveAuthRepository apoiado por uma nova tabela SQLite drive_auth, e o SDK googleapis como dependência.

A autorização é um repasse de capacidade limpo. O endpoint de app-config do backend retransmite as credenciais de cliente do Google OAuth — um clientId e um clientSecret — até o app, que CloudUploader lê e passa para driveService.initialize(...). A partir daí o app de desktop executa todo o fluxo OAuth ele mesmo: sobe um servidor http temporário numa porta 127.0.0.1 aleatória, abre a tela de consentimento no navegador do sistema com shell.openExternal, captura o redirecionamento num /callback, e troca o código por tokens com google-auth-library. Esses tokens são salvos localmente — saveDriveAuth(access_token, refresh_token, expiry_date) em drive_auth — com o escopo drive.file, que deixa o app gerenciar apenas os arquivos que cria. A renovação também é local: um listener oauth2Client.on('tokens', …) escreve os tokens renovados direto de volta na tabela. O backend entrega a capacidade uma vez e depois fica fora do laço.

A parte «retomável», honestamente

Aqui está o detalhe sobre o qual mais quero ser franco, porque é exatamente o tipo de coisa que um changelog arredonda. A nota de versão diz «uploads retomáveis», e isso é verdade do ponto de vista do usuário. Mas a GeekBye não implementou o protocolo resumable. Não há nenhuma constante de tamanho de pedaço, nenhum rastreador de offset de byte, nem tratamento de Content-Range / 308 Resume Incomplete em lugar nenhum do código-fonte da GeekBye. Procure por eles com grep e você não obtém nada — eles vivem dentro do SDK googleapis.

O que o próprio código da GeekBye faz é isto:

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

Duas escolhas fazem todo o trabalho. Primeiro, media.body é um stream (createReadStream), não um buffer — e passar um stream é precisamente o que faz googleapis negociar uma sessão retomável em seu nome em vez de fazer de uma vez só. Segundo, o progresso é lido do callback onUploadProgress do SDK contra fileStats.size. Essa é toda a implementação de «upload retomável» no nível da aplicação: o stream entra, o progresso sai. O protocolo difícil — abrir uma sessão, fazer upload em pedaços, retomar de um offset após uma queda — é trabalho do SDK, e a decisão de engenharia correta foi deixá-lo fazer esse trabalho em vez de reinventá-lo.

O que reenquadra a release inteira. A melhoria de confiabilidade não veio de escrever um motor de upload. Veio de apagar o proxy que ficava na frente de um — e, antes na mesma semana, de apagar o passo de ffmpeg que ficava na frente daquele. Menos código, mais resiliência, porque a coisa resiliente era uma biblioteca mais que testada no momento em que você parou de alimentá-la à mão com um buffer.

As partes que são genuinamente da GeekBye

Entregar a transferência ao SDK não significa que não sobrou nada para construir. Duas peças de lógica de aplicação real a cercam.

Um rollback de pasta órfã. Cada gravação recebe sua própria subpasta no Drive (criada com mimeType: 'application/vnd.google-apps.folder'), e a transcrição aterrissa ali como um segundo arquivo ao lado do vídeo. Se o upload lança um erro, o catch apaga essa pasta — this.drive.files.delete({ fileId: folderId }), registrado como Rolled back orphaned Drive folder — para que um upload que falhe não deixe um rastro de pastas vazias no seu Drive. Criar um contêiner antes de saber que a operação vai ter sucesso é um pequeno passivo; fazer o caminho de falha limpá-lo é a correção.

Ciclo de vida em torno de um singleton. DriveService é um singleton que segura um cliente OAuth em memória, e esse cliente em memória não sobrevive a um reinício do app mesmo que os tokens sobrevivam (eles estão no banco de dados). Então CloudUploader reinicializa DriveService a partir dos tokens guardados antes de cada upload, o que significa que uma gravação feita logo após relançar ainda faz upload sem pedir que você se reconecte. E a passada de revisão transformou o handler drive-connect em fire-and-forget, porque uma chamada de conexão bloqueante estava travando o polling de status do renderizador — a UI não conseguia mostrar «conectando…» se a chamada de conexão segurasse a thread. Essas duas — reinicializar-do-armazenamento e conexão não bloqueante — são os verdadeiros casos extremos do caminho direto, e notavelmente nenhuma das duas é sobre o fatiamento do upload. Quando você delega a parte difícil, os bugs que sobram para você são bugs de ciclo de vida.

Três coisas que esta release nos ensinou

  1. Pergunte por onde os bytes fluem, não só se funciona. O proxy da v1.8.11 funcionava. Mas rotear cada gravação pelos seus próprios servidores custa banda, memória e capacidade de reinício por um arquivo que você não guarda. Redesenhar o caminho de dados para que o cliente fale direto com o destino é muitas vezes a otimização inteira.
  2. O melhor upload retomável é o que você não escreveu. Passar um stream para um SDK maduro comprou uma sessão retomável completa por duas linhas de código. O instinto de programar à mão o fatiamento e a aritmética de offset teria produzido mais código e menos confiabilidade.
  3. Quando você delega o núcleo, você herda ciclo de vida, não algoritmos. Os verdadeiros bugs do cliente direto eram «reinicializar o singleton após o reinício» e «não bloquear a chamada de conexão», não offsets de bytes. É uma boa troca — bugs de ciclo de vida são visíveis e locais; bugs de protocolo não são nem uma coisa nem outra.

Para o capítulo anterior da história v1, nascido e corrigido em doze minutos (v1.8.10); e para todo o arco, a anatomia de entregar software até a perfeição.

Artigos Relacionados

Os três verbos que mantêm o Web Audio vivo
Steven
Steven10 min de leitura

Os três verbos que mantêm o Web Audio vivo

Duas releases pontuais da GeekBye, com dois meses de diferença e em dois arquivos distintos, ensinaram ao nosso código de áudio a mesma lição a partir de extremos opostos: parar de tratar o AudioContext do navegador como descartável. Uma release aprendeu a fazer resume() de um contexto que o macOS havia suspendido silenciosamente no meio da gravação; a outra aprendeu a fazer suspend() em vez de close() para que sessões consecutivas parem de se espatifar contra o teto de aproximadamente seis contextos do Chromium. Resume, suspend, close — é esse o enredo inteiro.

Engenharia
Audio
Desktop
Distinguir uma chamada de um app aberto
Steven
Steven9 min de leitura

Distinguir uma chamada de um app aberto

A GeekBye pode notar que você entrou em uma reunião por vídeo e oferecer gravá-la. A detecção acaba sendo a metade fácil — um binário Swift que lê títulos de janela a cada dez segundos. A metade difícil é a precisão: não disparar quando o Zoom está meramente aberto, não oferecer gravar uma reunião que você já está gravando, e não silenciar o microfone na chamada em que você realmente está. Três releases, e cada uma é uma salvaguarda que teve que aprender a não se sabotar.

Engenharia
macOS
Desktop
Nascido e corrigido em doze minutos
Steven
Steven8 min de leitura

Nascido e corrigido em doze minutos

O changelog da GeekBye v1.8.10 diz que corrigiu um crash ao editar os atalhos de teclado. Corrigiu — mas o crash foi introduzido e corrigido no mesmo pull request, com doze minutos de diferença, e nunca chegou a um único usuário. A história real é a cascata de confiabilidade que o produziu: uma mudança pequena e correta sobre quando os atalhos são suspensos, e o bug de teardown de React que caiu de uma linha pensada para tornar o editor mais seguro.

Engenharia
React
Desktop