Steven
Steven8 min lesing

Å ta backend ut av opplastingsstien

GeekBye tar opp skjermen din og lagrer videoen til din Google Drive. Den første versjonen sendte hvert opptak gjennom GeekByes egne servere på veien dit; én release senere gikk filen rett fra maskinen din til Drive, og backend ble degradert til å holde én enkelt peker. Den interessante delen er hvor lite kode den 'direkte, gjenopptakbare' versjonen faktisk inneholder — fordi gjenopptakbarheten kom fra å slette en proxy, ikke fra å skrive en.

Utvikling
Arkitektur
Desktop
GeekBye-releaser
Å ta backend ut av opplastingsstien

GeekBye kan ta opp skjermen din — video, systemlyd og mikrofon — og slippe det ferdige opptaket i din Google Drive automatisk. Mellom v1.8.11 og v1.8.13 endret funksjonens brukervendte beskrivelse seg knapt, men veien videoen tar for å nå Drive ble rutet fullstendig om. I den første versjonen reiste opptaket ditt gjennom GeekByes servere. I den andre gikk det rett fra maskinen din til din Drive, og GeekByes backend så aldri en byte av det. Den omrutingen er hele historien, og det tilfredsstillende er at den "bedre" versjonen inneholder påfallende mindre kode enn den den erstattet.

Pipelinen, fra ende til ende

Opptak skjer i rendereren. ScreenRecordingService.ts oppretter en MediaRecorder over opptaksstrømmen med { mimeType: 'video/webm;codecs=vp9,opus', videoBitsPerSecond: 1_000_000 } og starter den med en ett-sekunds tidsskive (CHUNK_INTERVAL_MS = 1000). Hver ondataavailable-blob sendes over IPC til hovedprosessen, der recording:save-chunk-handleren føyer den til en fil på disk med fs.promises.appendFile. Verdt å flagge tidlig, fordi det er fristende å felllese: den ett-sekunds kadensen er disk-skrivings-rytmen, ikke en opplastings-stumpstørrelse. Når opplastingen begynner, ligger det én enkelt ferdig WebM-fil på disken.

Det som skiller de to releasene er bare hva som skjer med den ferdige filen.

Før: alt gjennom backend (v1.8.11)

v1.8.11-opplastingsveien, i CloudUploader.processRecording(), er den åpenbare designen og en fullstendig fornuftig første versjon:

  1. POST /api/geekbye/recordings for å opprette en backend-post med opptakets metadata.
  2. Les hele videoen inn i minnet og POST den til backend som multipart — const fileBuffer = fs.readFileSync(filePath) — og treff POST /api/geekbye/recordings/${id}/upload. Backend laster så opp den filen til Drive og returnerer en driveUrl.
  3. POST …/process for å sende transkriptet til AI-analyse.

Den bærende detaljen er steg 2: klienten laster hele opptaket inn i minnet, og hver byte av det transiterer GeekByes egne servere på veien til Drive. For et kort klipp er det greit. For et langt skjermopptak er det tre problemer på én gang — minnetrykk på klienten fra readFileSync, båndbredde- og relékostnad på backend for en fil den ikke engang beholder, og én monolittisk POST som må starte på nytt fra null hvis nettverket hakker halvveis.

Det er også verdt å være ærlig om at dette "før" ikke var noe uberørt opprinnelig design. v1.8.11-funksjonen klemte sammen et pivot innen samme uke: en tidlig utgave lastet opp til Cloudflare R2 via forhåndssignerte URL-er og konverterte WebM til MP4 med en medfølgende ffmpeg-static; det ble erstattet av den backend-medierte Drive-flyten; og så ble ffmpeg-konverteringen revet helt ut til fordel for å laste opp WebM som den er (commiten bemerker at den senket bitraten og produserte omtrent fire ganger mindre filer — et innebygd anslag, ikke en benchmark). Så selv "før" hadde allerede lært å slette en nativ avhengighet én gang. Den neste releasen ville slette proxyen også.

Etter: rett til Drive (v1.8.13)

v1.8.13 skriver om CloudUploader.processRecording() rundt én setning fra commiten som shippet det: last opp direkte til Drive, "keeping backend for metadata record + AI transcript analysis only." Den nye veien:

  1. POST /api/geekbye/recordingsbare metadata (tittel, varighet, filstørrelse, format: 'webm'). Kommentaren i koden er kontant: // Create backend record (metadata only — no file upload).
  2. Last opp videoen direkte til Google Drive, og hopp over backend-mellommannen helt.
  3. PATCH /api/geekbye/recordings/${backendRecordingId} med den resulterende { driveUrl, driveFolderId } — backend-posten får vite hvor filen endte.
  4. Transkriptet går fortsatt til …/process for analyse.

Backend gikk fra å være i filens vei til å bli fortalt om filen i etterkant. Den holder en rad og en peker; bytene bor bare på brukerens disk og i brukerens Drive.

Å bli en ekte Drive-klient

For at klienten skal snakke direkte med Drive, må den være en faktisk Google Drive API-klient, og v1.8.13 legger til maskineriet for det: en ny DriveService (Drive-klienten), en ny DriveAuthRepository støttet av en ny drive_auth SQLite-tabell, og googleapis-SDK-en som en avhengighet.

Autorisasjonen er en ren overlevering av kapabilitet. Backendens app-config-endepunkt reléer Google OAuth klientlegitimasjoner — en clientId og en clientSecret — ned til appen, som CloudUploader leser og sender til driveService.initialize(...). Derfra kjører skrivebordsappen hele OAuth-flyten selv: den reiser en midlertidig http-server på en tilfeldig 127.0.0.1-port, åpner samtykkeskjermen i systemnettleseren med shell.openExternal, fanger omdirigeringen på et /callback, og bytter koden mot tokens med google-auth-library. De tokenene lagres lokalt — saveDriveAuth(access_token, refresh_token, expiry_date) inn i drive_auth — med drive.file-scopet, som lar appen håndtere bare de filene den oppretter. Fornyelse er også lokal: en oauth2Client.on('tokens', …)-lytter skriver fornyede tokens rett tilbake til tabellen. Backend overleverer kapabiliteten én gang og holder seg så utenfor loopen.

Den "gjenopptakbare" delen, ærlig

Her er detaljen jeg mest vil være rett fram om, fordi det er nettopp den slags ting en endringslogg runder av. Release-notatet sier "resumable uploads", og det er sant der brukeren står. Men GeekBye implementerte ikke den gjenopptakbare protokollen. Det finnes ingen stumpstørrelse-konstant, ingen byte-offset-sporer og ingen Content-Range / 308 Resume Incomplete-håndtering noe sted i GeekByes kildekode. Grep etter dem, og du får ingenting — de bor inne i googleapis-SDK-en.

Det GeekByes egen kode gjør er dette:

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

To valg gjør hele jobben. Først er media.body en strøm (createReadStream), ikke en buffer — og å sende en strøm er nettopp det som får googleapis til å forhandle en gjenopptakbar økt på dine vegne i stedet for å gjøre det i ett skudd. For det andre leses fremdriften av fra SDK-ens onUploadProgress-callback mot fileStats.size. Det er hele "gjenopptakbar opplasting"-implementeringen på applikasjonsnivå: strøm inn, fremdrift ut. Den vanskelige protokollen — åpne en økt, last opp i stumper, gjenoppta fra en offset etter et frafall — er SDK-ens jobb, og den riktige ingeniørbeslutningen var å la den gjøre den jobben i stedet for å finne den opp på nytt.

Noe som rammer inn hele releasen på nytt. Pålitelighetsforbedringen kom ikke fra å skrive en opplastingsmotor. Den kom fra å slette proxyen som satt foran en — og, tidligere samme uke, å slette ffmpeg-steget foran den. Mindre kode, mer robusthet, fordi den robuste tingen var et velbrukt bibliotek i det øyeblikket du sluttet å håndmate det en buffer.

Stumpene som ekte er GeekByes

Å overlevere overføringen til SDK-en betyr ikke at det ikke var noe igjen å bygge. To stykker ekte applikasjonslogikk omgir den.

En foreldreløs-mappe-tilbakerulling. Hvert opptak får sin egen Drive-undermappe (opprettet med mimeType: 'application/vnd.google-apps.folder'), og transkriptet lander der som en andre fil ved siden av videoen. Hvis opplastingen kaster, sletter catch den mappen — this.drive.files.delete({ fileId: folderId }), logget som Rolled back orphaned Drive folder — slik at en mislykket opplasting ikke etterlater et spor av tomme mapper i din Drive. Å opprette en beholder før du vet at operasjonen vil lykkes, er et lite ansvar; å få feilveien til å rydde opp etter den er fiksen.

Livssyklus rundt en singleton. DriveService er en singleton som holder en in-memory OAuth-klient, og den in-memory klienten overlever ikke en app-omstart selv om tokenene gjør det (de er i databasen). Så CloudUploader reinitialiserer DriveService fra de lagrede tokenene før hver opplasting, som betyr at et opptak gjort rett etter omstart fortsatt lastes opp uten å be deg om å koble til på nytt. Og gjennomgangsrunden gjorde drive-connect-handleren til fire-and-forget, fordi et blokkerende connect-kall stanset rendererens statuspolling — brukergrensesnittet kunne ikke vise "kobler til…" hvis connect-kallet holdt tråden. De to — reinitialisér-fra-lager og ikke-blokkerende connect — er den direkte veiens virkelige kanttilfeller, og bemerkelsesverdig handler ingen av dem om opplastings-stumping. Når du delegerer den vanskelige delen, er buggene du sitter igjen med livssyklusbugger.

Tre ting denne releasen lærte oss

  1. Spør hvor bytene flyter, ikke bare om det virker. v1.8.11-proxyen virket. Men å rute hvert opptak gjennom dine egne servere koster deg båndbredde, minne og gjenopptakbarhet for en fil du ikke beholder. Å tegne dataveien på nytt slik at klienten snakker direkte med destinasjonen er ofte hele optimaliseringen.
  2. Den beste gjenopptakbare opplastingen er den du ikke skrev. Å sende en strøm til et modent SDK kjøpte en full gjenopptakbar økt for to linjer kode. Instinktet til å håndrulle stumping og offset-matte ville ha produsert mer kode og mindre pålitelighet.
  3. Når du delegerer kjernen, arver du livssyklus, ikke algoritmer. Den direkte klientens virkelige bugger var "reinitialisér singletonen etter omstart" og "ikke blokkér connect-kallet", ikke byte-offsets. Det er en god handel — livssyklusbugger er synlige og lokale; protokollbugger er ingen av delene.

For det forrige kapittelet i v1-historien, født og fikset på tolv minutter (v1.8.10); og for hele buen, anatomien i å shippe programvare til perfeksjon.

Relaterte artikler

De tre verbene som holder Web Audio i live
Steven
Steven8 min lesing

De tre verbene som holder Web Audio i live

To GeekBye-punktreleaser, to måneder fra hverandre og i to ulike filer, lærte lydkoden vår den samme lærdommen fra motsatte ender: slutt å behandle nettleserens AudioContext som engangsbruk. Den ene releasen lærte å kalle resume() på en kontekst macOS stille hadde suspendert midt i opptaket; den andre lærte å kalle suspend() i stedet for close(), så opptak på rad slutter å dundre inn i Chromiums tak på omtrent seks kontekster. Resume, suspend, close — det er hele handlingen.

Utvikling
Audio
Desktop
Å skille en samtale fra en åpen app
Steven
Steven8 min lesing

Å skille en samtale fra en åpen app

GeekBye kan merke at du har blitt med i et videomøte og tilby å ta det opp. Deteksjonen viser seg å være den lette halvdelen — en Swift-binær som leser vindustitler hvert tiende sekund. Den vanskelige halvdelen er presisjon: å ikke utløse når Zoom bare er åpent, å ikke spørre om et møte du allerede tar opp, og å ikke dempe mikrofonen i samtalen du faktisk sitter i. Tre releaser, og hver enkelt er et vern som måtte lære å ikke beseire seg selv.

Utvikling
macOS
Desktop
Født og fikset på tolv minutter
Steven
Steven7 min lesing

Født og fikset på tolv minutter

GeekBye v1.8.10s endringslogg sier at den fikset et krasj under redigering av tastatursnarveier. Det gjorde den — men krasjen ble introdusert og fikset i den samme pull requesten, tolv minutter fra hverandre, og nådde aldri en eneste bruker. Den virkelige historien er pålitelighetskaskaden som produserte den: en liten, korrekt endring av når snarveier settes på pause, og React-nedrivingsbuggen som falt ut av én linje som skulle gjøre editoren tryggere.

Utvikling
React
Desktop