Steven
Steven8 min läsning

Att ta backend ur uppladdningsvägen

GeekBye spelar in din skärm och sparar videon till din Google Drive. Den första versionen skickade varje inspelning genom GeekByes egna servrar på vägen dit; en release senare gick filen rakt från din maskin till Drive, och backend degraderades till att hålla en enda pekare. Den intressanta delen är hur lite kod den 'direkta, återupptagbara' versionen faktiskt innehåller — eftersom återupptagbarheten kom från att radera en proxy, inte från att skriva en.

Utveckling
Arkitektur
Desktop
GeekBye-releaser
Att ta backend ur uppladdningsvägen

GeekBye kan spela in din skärm — video, systemljud och mikrofon — och släppa den färdiga inspelningen i din Google Drive automatiskt. Mellan v1.8.11 och v1.8.13 ändrades funktionens användarvända beskrivning knappt, men vägen videon tar för att nå Drive dirigerades om helt. I den första versionen färdades din inspelning genom GeekByes servrar. I den andra gick den rakt från din maskin till din Drive, och GeekByes backend såg aldrig en byte av den. Den omdirigeringen är hela historien, och det tillfredsställande är att den "bättre" versionen innehåller påfallande mindre kod än den den ersatte.

Pipelinen, från ände till ände

Inspelning sker i rendereraren. ScreenRecordingService.ts skapar en MediaRecorder över infångningsströmmen med { mimeType: 'video/webm;codecs=vp9,opus', videoBitsPerSecond: 1_000_000 } och startar den med en tidsdel på en sekund (CHUNK_INTERVAL_MS = 1000). Varje ondataavailable-blob skickas över IPC till huvudprocessen, där recording:save-chunk-hanteraren lägger till den i en fil på disk med fs.promises.appendFile. Värt att flagga tidigt, eftersom det är frestande att missläsa: den där en-sekunds-kadensen är disk-skrivnings-rytmen, inte en uppladdnings-bitstorlek. När uppladdningen väl börjar ligger det en enda färdig WebM-fil på disken.

Det som skiljer mellan de två releaserna är bara vad som händer med den färdiga filen.

Före: allt genom backenden (v1.8.11)

v1.8.11-uppladdningsvägen, i CloudUploader.processRecording(), är den uppenbara designen och en helt rimlig första version:

  1. POST /api/geekbye/recordings för att skapa en backend-post med inspelningens metadata.
  2. Läs hela videon in i minnet och POST:a den till backenden som multipart — const fileBuffer = fs.readFileSync(filePath) — och träffar POST /api/geekbye/recordings/${id}/upload. Backenden laddar sedan upp den filen till Drive och returnerar en driveUrl.
  3. POST …/process för att skicka transkriptet för AI-analys.

Den bärande detaljen är steg 2: klienten laddar hela inspelningen in i minnet, och varje byte av den transiterar GeekByes egna servrar på vägen till Drive. För ett kort klipp är det okej. För en lång skärminspelning är det tre problem på en gång — minnestryck på klienten från readFileSync, bandbredds- och reläkostnad på backenden för en fil den inte ens behåller, och en enda monolitisk POST som måste starta om från noll om nätverket hackar halvvägs.

Det är också värt att vara ärlig om att detta "före" inte var någon orörd ursprungsdesign. v1.8.11-funktionen tryckte ihop en vändning inom samma vecka: en tidig version laddade upp till Cloudflare R2 via försignerade URL:er och konverterade WebM till MP4 med en medföljande ffmpeg-static; det ersattes av det backend-medlade Drive-flödet; och sedan revs ffmpeg-konverteringen ut helt till förmån för att ladda upp WebM:en som den är (commiten noterar att den sänkte bithastigheten och producerade ungefär fyra gånger mindre filer — en inbyggd uppskattning, inte ett benchmark). Så till och med "före" hade redan lärt sig att radera ett nativt beroende en gång. Nästa release skulle radera proxyn också.

Efter: rakt till Drive (v1.8.13)

v1.8.13 skriver om CloudUploader.processRecording() runt en mening från commiten som levererade det: ladda upp direkt till Drive, "keeping backend for metadata record + AI transcript analysis only." Den nya vägen:

  1. POST /api/geekbye/recordingsendast metadata (titel, längd, filstorlek, format: 'webm'). Kommentaren i koden är rakt på sak: // Create backend record (metadata only — no file upload).
  2. Ladda upp videon direkt till Google Drive, och hoppa över backend-mellanhanden helt.
  3. PATCH /api/geekbye/recordings/${backendRecordingId} med den resulterande { driveUrl, driveFolderId } — backend-posten får veta var filen hamnade.
  4. Transkriptet går fortfarande till …/process för analys.

Backenden gick från att vara i filens väg till att bli informerad om filen i efterhand. Den håller en rad och en pekare; byten bor bara på användarens disk och i användarens Drive.

Att bli en riktig Drive-klient

För att klienten ska tala direkt med Drive måste den vara en verklig Google Drive API-klient, och v1.8.13 lägger till maskineriet för det: en ny DriveService (Drive-klienten), en ny DriveAuthRepository med en ny drive_auth SQLite-tabell i botten, och googleapis-SDK:n som ett beroende.

Auktoriseringen är en ren överlämning av förmåga. Backendens app-config-endpoint reläar Google OAuth klientuppgifter — ett clientId och ett clientSecret — ner till appen, som CloudUploader läser och skickar till driveService.initialize(...). Därifrån kör skrivbordsappen hela OAuth-flödet själv: den reser en tillfällig http-server på en slumpmässig 127.0.0.1-port, öppnar samtyckesskärmen i systemwebbläsaren med shell.openExternal, fångar omdirigeringen på en /callback, och byter koden mot tokens med google-auth-library. De tokens sparas lokalt — saveDriveAuth(access_token, refresh_token, expiry_date) in i drive_auth — med drive.file-scopet, vilket låter appen hantera bara de filer den skapar. Uppdatering är också lokal: en oauth2Client.on('tokens', …)-lyssnare skriver uppdaterade tokens rakt tillbaka till tabellen. Backenden lämnar över förmågan en gång och håller sig sedan ur loopen.

Den "återupptagbara" delen, ärligt

Här är detaljen jag mest vill vara rak om, eftersom det är precis en sådan sak som en ändringslogg avrundar. Release-noten säger "resumable uploads", och det är sant där användaren står. Men GeekBye implementerade inte det återupptagbara protokollet. Det finns ingen bitstorleks-konstant, ingen byte-offset-spårare och ingen Content-Range / 308 Resume Incomplete-hantering någonstans i GeekByes källkod. Grep:a efter dem och du får ingenting — de bor inuti googleapis-SDK:n.

Det GeekByes egen kod gör är detta:

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

Två val gör hela jobbet. Först, media.body är en ström (createReadStream), inte en buffert — och att skicka en ström är precis vad som får googleapis att förhandla en återupptagbar session åt dig istället för att göra ett enda skott. För det andra läses förloppet av från SDK:ns onUploadProgress-callback mot fileStats.size. Det är hela "återupptagbara uppladdnings"-implementationen på applikationsnivå: ström in, förlopp ut. Det svåra protokollet — öppna en session, ladda upp i bitar, återuppta från en offset efter ett avbrott — är SDK:ns jobb, och det korrekta ingenjörsbeslutet var att låta den göra det jobbet istället för att uppfinna det på nytt.

Vilket ramar in hela releasen på nytt. Tillförlitlighetsförbättringen kom inte från att skriva en uppladdningsmotor. Den kom från att radera proxyn som satt framför en — och, tidigare samma vecka, att radera ffmpeg-steget framför den. Mindre kod, mer motståndskraft, eftersom den motståndskraftiga saken var ett välanvänt bibliotek i det ögonblick du slutade handmata det en buffert.

Bitarna som genuint är GeekByes

Att lämna över överföringen till SDK:n betyder inte att det inte fanns något kvar att bygga. Två delar av verklig applikationslogik omger den.

En föräldralös-mapp-återställning. Varje inspelning får sin egen Drive-undermapp (skapad med mimeType: 'application/vnd.google-apps.folder'), och transkriptet landar där som en andra fil bredvid videon. Om uppladdningen kastar raderar catch den mappen — this.drive.files.delete({ fileId: folderId }), loggad som Rolled back orphaned Drive folder — så att en misslyckad uppladdning inte lämnar ett spår av tomma mappar i din Drive. Att skapa en behållare innan du vet att operationen kommer lyckas är en liten skuld; att låta felvägen städa upp den är fixen.

Livscykel runt en singleton. DriveService är en singleton som håller en minnesbaserad OAuth-klient, och den minnesbaserade klienten överlever inte en appstart även om tokens gör det (de ligger i databasen). Så CloudUploader återinitierar DriveService från de lagrade tokens före varje uppladdning, vilket betyder att en inspelning gjord direkt efter omstart fortfarande laddas upp utan att be dig återansluta. Och granskningsvarvet gjorde drive-connect-hanteraren till fire-and-forget, eftersom ett blockerande connect-anrop stoppade upp rendererarens statuspollning — gränssnittet kunde inte visa "ansluter…" om connect-anropet höll tråden. De två — återinitiera-från-lagring och icke-blockerande connect — är den direkta vägens verkliga kantfall, och notabelt handlar ingen av dem om uppladdningsbitning. När du delegerar den svåra delen är buggarna du sitter kvar med livscykelbuggar.

Tre saker denna release lärde oss

  1. Fråga vart byten flödar, inte bara om det fungerar. v1.8.11-proxyn fungerade. Men att dirigera varje inspelning genom dina egna servrar kostar dig bandbredd, minne och återupptagbarhet för en fil du inte behåller. Att rita om datavägen så att klienten talar direkt med destinationen är ofta hela optimeringen.
  2. Den bästa återupptagbara uppladdningen är den du inte skrev. Att skicka en ström till ett moget SDK köpte en full återupptagbar session för två rader kod. Instinkten att handrulla bitning och offset-matte hade producerat mer kod och mindre tillförlitlighet.
  3. När du delegerar kärnan ärver du livscykel, inte algoritmer. Den direkta klientens verkliga buggar var "återinitiera singletonen efter omstart" och "blockera inte connect-anropet", inte byte-offsets. Det är en bra affär — livscykelbuggar är synliga och lokala; protokollbuggar är ingetdera.

För det föregående kapitlet i v1-historien, född och fixad på tolv minuter (v1.8.10); och för hela bågen, anatomin i att skeppa mjukvara till perfektion.

Relaterade artiklar

De tre verben som håller Web Audio vid liv
Steven
Steven8 min läsning

De tre verben som håller Web Audio vid liv

Två GeekBye-punktreleaser, två månader isär och i två olika filer, lärde vår ljudkod samma läxa från motsatta håll: sluta behandla webbläsarens AudioContext som slit-och-släng. Den ena releasen lärde sig att anropa resume() på en kontext som macOS tyst hade suspenderat mitt i inspelningen; den andra lärde sig att anropa suspend() i stället för close() så att inspelningar i följd slutar dundra in i Chromiums tak på ungefär sex kontexter. Resume, suspend, close — det är hela handlingen.

Utveckling
Audio
Desktop
Att skilja ett samtal från en öppen app
Steven
Steven8 min läsning

Att skilja ett samtal från en öppen app

GeekBye kan märka att du har gått med i ett videomöte och erbjuda sig att spela in det. Detekteringen visar sig vara den lätta halvan — en Swift-binär som läser fönstertitlar var tionde sekund. Den svåra halvan är precision: att inte lösa ut när Zoom bara är öppet, att inte fråga om ett möte du redan spelar in, och att inte tysta mikrofonen i samtalet du faktiskt sitter i. Tre releaser, och var och en är en skyddsspärr som måste lära sig att inte besegra sig själv.

Utveckling
macOS
Desktop
Född och fixad på tolv minuter
Steven
Steven7 min läsning

Född och fixad på tolv minuter

GeekBye v1.8.10:s ändringslogg säger att den fixade en krasch vid redigering av tangentbordsgenvägar. Det gjorde den — men kraschen introducerades och fixades i samma pull request, tolv minuter isär, och nådde aldrig en enda användare. Den verkliga historien är tillförlitlighetskaskaden som producerade den: en liten, korrekt ändring av när genvägar pausas, och React-nedmonteringsbuggen som föll ut ur en enda rad som var tänkt att göra editorn säkrare.

Utveckling
React
Desktop