Steven
Steven8 perc olvasás

A backend kivétele a feltöltési útvonalból

A GeekBye rögzíti a képernyődet, és elmenti a videót a Google Drive-odba. Az első verzió minden felvételt a GeekBye saját szerverein keresztül továbbított odafelé; egy kiadással később a fájl egyenesen a gépedről ment a Drive-ba, a backendet pedig lefokozták egyetlen mutató tartására. Az érdekes rész az, hogy milyen kevés kódot tartalmaz valójában a „közvetlen, folytatható" verzió — mert a folytathatóság egy proxy törléséből jött, nem a megírásából.

Mérnöki munka
Architektúra
Desktop
GeekBye kiadások
A backend kivétele a feltöltési útvonalból

A GeekBye rögzíteni tudja a képernyődet — videó, rendszerhang és mikrofon — és a kész felvételt automatikusan beejti a Google Drive-odba. A v1.8.11 és a v1.8.13 között a funkció felhasználó felé mutatott leírása alig változott, de az útvonal, amelyet a videó bejár, hogy eljusson a Drive-ba, teljesen átirányítódott. Az első verzióban a felvételed a GeekBye szerverein utazott át. A másodikban egyenesen a gépedről ment a Drive-odba, és a GeekBye backendje soha nem látott belőle egyetlen bájtot sem. Ez az átirányítás az egész történet, és a kielégítő rész az, hogy a „jobb" verzió szembeötlően kevesebb kódot tartalmaz, mint az, amelyet felváltott.

A pipeline, elejétől a végéig

A rögzítés a rendererben történik. A ScreenRecordingService.ts létrehoz egy MediaRecorder-t a capture stream fölött { mimeType: 'video/webm;codecs=vp9,opus', videoBitsPerSecond: 1_000_000 }-mal, és egy egymásodperces timeslice-szal indítja (CHUNK_INTERVAL_MS = 1000). Minden ondataavailable blob IPC-n keresztül kerül a fő folyamathoz, ahol a recording:save-chunk handler hozzáfűzi egy lemezen lévő fájlhoz a fs.promises.appendFile-lal. Érdemes korán jelezni, mert csábító félreérteni: ez az egymásodperces ütem a lemezre-írás ritmusa, nem egy feltöltési chunkméret. Mire a feltöltés elindul, egyetlen kész WebM fájl ül a lemezen.

Ami különbözik a két kiadás között, az csak az, hogy mi történik azzal a kész fájllal.

Az előtte: minden a backenden át (v1.8.11)

A v1.8.11 feltöltési útvonala, a CloudUploader.processRecording()-ban, a kézenfekvő terv és egy teljesen ésszerű első verzió:

  1. POST /api/geekbye/recordings egy backend rekord létrehozásához a felvétel metaadataival.
  2. Olvasd be az egész videót a memóriába, és POST-old a backendre multipartként — const fileBuffer = fs.readFileSync(filePath) — a POST /api/geekbye/recordings/${id}/upload-ot elérve. A backend aztán feltölti azt a fájlt a Drive-ba, és visszaad egy driveUrl-t.
  3. POST …/process a transzkript AI elemzésre küldéséhez.

A teherhordó részlet a 2. lépés: a kliens az egész felvételt a memóriába tölti, és annak minden bájtja a GeekBye saját szerverein halad át a Drive felé vezető úton. Egy rövid klipnél ez rendben van. Egy hosszú képernyőfelvételnél ez három probléma egyszerre — memórianyomás a kliensen a readFileSync-től, sávszélesség- és továbbítási költség a backenden egy fájlért, amelyet meg sem tart, és egyetlen monolitikus POST, amelynek nulláról kell újraindulnia, ha a hálózat félúton csuklik egyet.

Azt is érdemes őszintén elmondani, hogy ez az „előtte" nem valami makulátlan eredeti terv volt. A v1.8.11 funkció egy héten belüli pivotot gyömöszölt össze: egy korai változat Cloudflare R2-be töltött fel előre aláírt URL-eken keresztül, és WebM-et konvertált MP4-re egy csomagolt ffmpeg-static-kel; azt felváltotta a backend-közvetített Drive flow; majd az ffmpeg konverziót teljesen kitépték a WebM változatlan feltöltése javára (a commit megjegyzi, hogy leszállította a bitrátát, és nagyjából négyszer kisebb fájlokat produkált — egy inline becslés, nem benchmark). Így már az „előtte" is megtanulta egyszer törölni egy natív függőséget. A következő kiadás a proxyt is törölte volna.

Az utána: egyenesen a Drive-ba (v1.8.13)

A v1.8.13 újraírja a CloudUploader.processRecording()-ot egyetlen mondat köré abból a commitból, amely kiszállította: tölts fel közvetlenül a Drive-ba, „keeping backend for metadata record + AI transcript analysis only." Az új útvonal:

  1. POST /api/geekbye/recordingscsak metaadat (cím, időtartam, fájlméret, format: 'webm'). A kommentár a kódban nyers: // Create backend record (metadata only — no file upload).
  2. Töltsd fel a videót közvetlenül a Google Drive-ba, teljesen kihagyva a backend közvetítőt.
  3. PATCH /api/geekbye/recordings/${backendRecordingId} a keletkező { driveUrl, driveFolderId }-vel — a backend rekordnak megmondják, hova került a fájl.
  4. A transzkript továbbra is a …/process-hez megy elemzésre.

A backend abból, hogy benne volt a fájl útvonalában, oda jutott, hogy tájékoztatják a fájlról utólag. Tart egy sort és egy mutatót; a bájtok csak a felhasználó lemezén és a felhasználó Drive-jában élnek.

Valódi Drive kliensé válni

Ahhoz, hogy a kliens közvetlenül beszéljen a Drive-val, egy tényleges Google Drive API kliensnek kell lennie, és a v1.8.13 hozzáadja ehhez a gépezetet: egy új DriveService (a Drive kliens), egy új DriveAuthRepository, amelyet egy új drive_auth SQLite tábla támogat, és a googleapis SDK mint függőség.

Az engedélyezés egy tiszta képesség-átadás. A backend app-config végpontja lehozza a Google OAuth kliens-hitelesítő adatait — egy clientId-t és egy clientSecret-et — az alkalmazáshoz, amelyet a CloudUploader beolvas és átad a driveService.initialize(...)-nek. Onnan a desktop alkalmazás maga futtatja az egész OAuth flow-t: felállít egy ideiglenes http szervert egy véletlenszerű 127.0.0.1 porton, megnyitja a hozzájárulási képernyőt a rendszer böngészőjében a shell.openExternal-lal, elkapja az átirányítást egy /callback-en, és a google-auth-library-vel tokenekre cseréli a kódot. Azokat a tokeneket helyben menti — saveDriveAuth(access_token, refresh_token, expiry_date) a drive_auth-ba — a drive.file scope-pal, amely engedi az alkalmazásnak, hogy csak az általa létrehozott fájlokat kezelje. A frissítés is helyi: egy oauth2Client.on('tokens', …) listener a frissített tokeneket egyenesen visszaírja a táblába. A backend egyszer átadja a képességet, majd kimarad a hurokból.

A „folytatható" rész, őszintén

Íme a részlet, amivel a legjobban egyenes akarok lenni, mert pontosan az a fajta dolog, amit egy changelog lekerekít. A kiadási jegyzet azt mondja: „resumable uploads", és ez igaz onnan, ahol a felhasználó áll. De a GeekBye nem implementálta a folytatható protokollt. Nincs chunkméret-konstans, nincs bájt-offset-követő, és nincs Content-Range / 308 Resume Incomplete kezelés sehol a GeekBye forráskódjában. Keress rájuk grep-pel, és semmit sem kapsz — a googleapis SDK-ban élnek.

Amit a GeekBye saját kódja csinál, az ez:

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

Két döntés végzi az egész munkát. Először, a media.body egy stream (createReadStream), nem egy buffer — és egy stream átadása pontosan az, ami ráveszi a googleapis-t, hogy a nevedben folytatható munkamenetet tárgyaljon le ahelyett, hogy egyetlen lövéssel csinálná. Másodszor, a haladás az SDK onUploadProgress callbackjéből olvasódik ki a fileStats.size-hoz mérve. Ez az egész „resumable upload" implementáció alkalmazásszinten: stream be, haladás ki. A nehéz protokoll — nyiss egy munkamenetet, tölts fel chunkokban, folytasd egy offsettől egy megszakadás után — az SDK dolga, és a helyes mérnöki döntés az volt, hogy hagyjuk elvégezni azt a munkát, ahelyett, hogy újra feltalálnánk.

Ami az egész kiadást újrakeretezi. A megbízhatósági javulás nem egy feltöltési motor megírásából jött. Abból jött, hogy töröltük a proxyt, amely egy ilyen előtt ült — és, korábban ugyanazon a héten, hogy töröltük az ffmpeg lépést az előtt. Kevesebb kód, több ellenállóképesség, mert az ellenálló dolog egy jól bejáratott könyvtár volt abban a pillanatban, amikor abbahagytad, hogy kézzel etesd egy bufferrel.

A darabok, amelyek valóban a GeekBye-é

Az átvitel átadása az SDK-nak nem jelenti azt, hogy nem maradt semmi megépíteni való. Két darab valódi alkalmazáslogika veszi körül.

Egy árva-mappa visszagörgetés. Minden felvétel megkapja a saját Drive almappáját (amelyet mimeType: 'application/vnd.google-apps.folder'-rel hoznak létre), és a transzkript oda érkezik második fájlként a videó mellé. Ha a feltöltés kivételt dob, a catch törli azt a mappát — this.drive.files.delete({ fileId: folderId }), Rolled back orphaned Drive folder néven naplózva — hogy egy sikertelen feltöltés ne hagyjon üres mappák nyomát a Drive-odban. Egy konténer létrehozása azelőtt, hogy tudnád, a művelet sikerülni fog, egy kis kötelezettség; ha a hibaútvonalat rábírod, hogy takarítsa el, az a javítás.

Életciklus egy singleton körül. A DriveService egy singleton, amely egy memóriabeli OAuth klienst tart, és ez a memóriabeli kliens nem éli túl az alkalmazás újraindítását, még ha a tokenek túl is élik (azok az adatbázisban vannak). Így a CloudUploader újrainicializálja a DriveService-t a tárolt tokenekből minden feltöltés előtt, ami azt jelenti, hogy egy közvetlenül újraindítás után készített felvétel még mindig feltöltődik anélkül, hogy megkérne, hogy csatlakozz újra. És a review menet a drive-connect handlert fire-and-forget-té alakította, mert egy blokkoló connect hívás megakasztotta a renderer állapot-pollingját — a UI nem tudta megjeleníteni, hogy „connecting…", ha a connect hívás fogta a szálat. Ez a kettő — újrainicializálás-tárolóból és nem-blokkoló connect — a közvetlen útvonal valódi peremesetei, és figyelemre méltóan egyik sem a feltöltési chunkolásról szól. Amikor delegálod a nehéz részt, a bugok, amelyek megmaradnak neked, életciklus-bugok.

Három dolog, amit ez a kiadás megtanított nekünk

  1. Kérdezd meg, merre folynak a bájtok, ne csak azt, hogy működik-e. A v1.8.11 proxy működött. De ha minden felvételt a saját szervereiden vezetsz át, az sávszélességbe, memóriába és újraindíthatóságba kerül egy fájlért, amelyet nem tartasz meg. Az adatútvonal újrarajzolása úgy, hogy a kliens közvetlenül a célponttal beszéljen, gyakran maga az egész optimalizálás.
  2. A legjobb folytatható feltöltés az, amelyet nem írtál meg. Egy stream átadása egy érett SDK-nak egy teljes folytatható munkamenetet vett meg két sor kódért. Az ösztön, hogy kézzel írd meg a chunkolást és az offset-matekot, több kódot és kevesebb megbízhatóságot eredményezett volna.
  3. Amikor delegálod a magot, életciklust örökölsz, nem algoritmusokat. A közvetlen kliens valódi bugjai „inicializáld újra a singletont újraindítás után" és „ne blokkold a connect hívást" voltak, nem bájt-offsetek. Ez jó csere — az életciklus-bugok láthatók és lokálisak; a protokoll-bugok egyik sem.

A v1-történet előző fejezetéért lásd megszületett és megjavult tizenkét perc alatt (v1.8.10); a teljes ívért pedig a szoftver tökéletesre szállításának anatómiája.

Kapcsolódó cikkek

A három ige, amely életben tartja a Web Audiót
Steven
Steven8 perc olvasás

A három ige, amely életben tartja a Web Audiót

Két GeekBye pontkiadás, két hónap eltéréssel és két különböző fájlban, ugyanarra a leckére tanította meg az audio kódunkat ellentétes végekről: ne kezeld a böngésző AudioContext-jét eldobhatóként. Az egyik kiadás megtanult resume()-t hívni egy contexten, amelyet a macOS csendben suspendelt a felvétel közepén; a másik megtanult suspend()-et hívni close() helyett, hogy az egymást követő munkamenetek ne csapódjanak bele a Chromium nagyjából hat contextes plafonjába. Resume, suspend, close — ez az egész cselekmény.

Mérnöki munka
Audio
Desktop
Megkülönböztetni egy hívást egy nyitott alkalmazástól
Steven
Steven8 perc olvasás

Megkülönböztetni egy hívást egy nyitott alkalmazástól

A GeekBye észreveheti, hogy csatlakoztál egy videohíváshoz, és felajánlhatja, hogy rögzíti. Az észlelés a könnyebbik felének bizonyul — egy Swift bináris, amely tízmásodpercenként ablakcímeket olvas. A nehezebbik fél a pontosság: nem elsülni, amikor a Zoom csak nyitva van, nem rákérdezni egy megbeszélésre, amelyet már rögzítesz, és nem némítani a mikrofont abban a hívásban, amelyben valójában benne vagy. Három kiadás, és mindegyik egy védelem, amelynek meg kellett tanulnia, hogy ne győzze le önmagát.

Mérnöki munka
macOS
Desktop
Megszületett és megjavult tizenkét perc alatt
Steven
Steven7 perc olvasás

Megszületett és megjavult tizenkét perc alatt

A GeekBye v1.8.10 changelogja azt mondja, hogy megjavított egy összeomlást a billentyűparancsok szerkesztése közben. Meg is javította — de az összeomlást ugyanabban a pull requestben vezették be és javították meg, tizenkét perc különbséggel, és soha nem jutott el egyetlen felhasználóhoz sem. Az igazi történet a megbízhatósági kaszkád, amely előállította: egy kicsi, helyes változtatás abban, hogy mikor függesztődnek fel a parancsok, és a React-lebontási bug, amely egyetlen sorból esett ki, amelynek a szerkesztőt biztonságosabbá kellett volna tennie.

Mérnöki munka
React
Desktop