Steven
Steven7 min čítania

Vyberáme backend z cesty nahrávania

GeekBye nahráva tvoju obrazovku a ukladá video na tvoj Google Drive. Prvá verzia posielala každú nahrávku cez vlastné servery GeekBye cestou tam; o vydanie neskôr išiel súbor priamo z tvojho stroja na Drive a backend bol degradovaný na držanie jediného ukazovateľa. Zaujímavá časť je, ako málo kódu naozaj obsahuje „priama, obnoviteľná" verzia — lebo obnoviteľnosť prišla zo zmazania proxy, nie z jeho napísania.

Inžinierstvo
Architektúra
Desktop
Vydania GeekBye
Vyberáme backend z cesty nahrávania

GeekBye vie nahrať tvoju obrazovku — video, systémový zvuk aj mikrofón — a automaticky vložiť hotový záznam na tvoj Google Drive. Medzi v1.8.11 a v1.8.13 sa používateľovi viditeľný opis funkcie takmer nezmenil, ale cesta, ktorou video putuje na Drive, bola prepojená úplne inak. V prvej verzii tvoj záznam cestoval cez servery GeekBye. V druhej išiel priamo z tvojho stroja na tvoj Drive a backend GeekBye nikdy nevidel ani jeden jeho bajt. To presmerovanie je celý príbeh a uspokojivá časť je, že „lepšia" verzia obsahuje nápadne menej kódu než tá, ktorú nahradila.

Reťazec, od začiatku do konca

Nahrávanie sa deje v rendereri. ScreenRecordingService.ts vytvorí MediaRecorder nad zachytávacím prúdom s { mimeType: 'video/webm;codecs=vp9,opus', videoBitsPerSecond: 1_000_000 } a spustí ho s jednosekundovým časovým rezom (CHUNK_INTERVAL_MS = 1000). Každý blob ondataavailable sa posiela cez IPC do hlavného procesu, kde ho handler recording:save-chunk pripojí k súboru na disku pomocou fs.promises.appendFile. Oplatí sa to zdôrazniť hneď, lebo je to lákavé prečítať zle: ten jednosekundový rytmus je rytmus zápisu na disk, nie veľkosť fragmentu nahrávania. Kým sa nahrávanie začne, na disku leží jediný hotový súbor WebM.

Medzi dvoma vydaniami sa líši len to, čo sa stane s tým hotovým súborom.

Predtým: všetko cez backend (v1.8.11)

Cesta nahrávania vo v1.8.11, v CloudUploader.processRecording(), je zjavný dizajn a úplne rozumná prvá verzia:

  1. POST /api/geekbye/recordings, aby vytvoril záznam backendu s metadátami nahrávky.
  2. Prečítať celé video do pamäte a poslať ho POST-om na backend ako multipart — const fileBuffer = fs.readFileSync(filePath) — trafiac POST /api/geekbye/recordings/${id}/upload. Backend potom nahrá ten súbor na Drive a vráti driveUrl.
  3. POST …/process, aby poslal prepis na AI analýzu.

Nosný detail je krok 2: klient načíta celú nahrávku do pamäte a každý jej bajt tranzituje cez vlastné servery GeekBye cestou na Drive. Pre krátky klip je to v poriadku. Pre dlhú nahrávku obrazovky sú to tri problémy naraz — tlak na pamäť klienta zo strany readFileSync, náklady na prenos a preposielanie na backende za súbor, ktorý si ani neuchováva, a jediný monolitický POST, ktorý musí začať od nuly, ak sieť čkne v polovici.

Oplatí sa byť aj úprimný, že toto „predtým" nebolo nejakým nedotknutým pôvodným dizajnom. Funkcia vo v1.8.11 stlačila do seba obrat, ktorý sa udial v rámci jedného týždňa: skorá verzia nahrávala na Cloudflare R2 cez vopred podpísané URL a konvertovala WebM na MP4 priloženým ffmpeg-static; to nahradil tok Drive sprostredkovaný backendom; a potom ffmpeg konverziu vytrhli úplne v prospech nahrávania WebM tak, ako je (commit poznamenáva, že to znížilo bitrate a vyprodukovalo zhruba štvornásobne menšie súbory — odhad od oka, nie benchmark). Takže aj „predtým" sa už raz naučilo zmazať natívnu závislosť. Ďalšie vydanie by zmazalo aj proxy.

Potom: priamo na Drive (v1.8.13)

v1.8.13 prepisuje CloudUploader.processRecording() okolo jednej vety z commitu, ktorý ho vydal: nahraj priamo na Drive, „ponechajúc backend len pre záznam metadát + AI analýzu prepisu." Nová cesta:

  1. POST /api/geekbye/recordingslen metadáta (názov, trvanie, veľkosť súboru, format: 'webm'). Komentár v kóde je bez okolkov: // Create backend record (metadata only — no file upload).
  2. Nahraj video priamo na Google Drive, úplne obíduc backendového prostredníka.
  3. PATCH /api/geekbye/recordings/${backendRecordingId} s výsledným { driveUrl, driveFolderId } — záznamu backendu sa povie, kde súbor skončil.
  4. Prepis stále ide do …/process na analýzu.

Backend prešiel od toho, že bol v ceste súboru, k tomu, že sa mu o súbore povie dodatočne. Drží riadok a ukazovateľ; bajty žijú len na disku používateľa a v jeho Drive.

Stávajúc sa skutočným klientom Drive

Aby klient hovoril s Drive priamo, musí byť skutočným klientom Google Drive API, a v1.8.13 na to pridáva mašinériu: nový DriveService (klient Drive), nové DriveAuthRepository opreté o novú SQLite tabuľku drive_auth a SDK googleapis ako závislosť.

Autorizácia je čisté odovzdanie oprávnenia. Endpoint konfigurácie aplikácie na backende odovzdáva klientske poverenia Google OAuth — clientId a clientSecret — dole do aplikácie, ktoré CloudUploader prečíta a odovzdá do driveService.initialize(...). Odtiaľ desktopová aplikácia sama prebehne celý OAuth tok: postaví dočasný http server na náhodnom porte 127.0.0.1, otvorí obrazovku súhlasu v systémovom prehliadači cez shell.openExternal, chytí presmerovanie na /callback a vymení kód za tokeny pomocou google-auth-library. Tie tokeny sa uložia lokálne — saveDriveAuth(access_token, refresh_token, expiry_date) do drive_auth — s rozsahom drive.file, ktorý aplikácii dovoľuje spravovať len súbory, ktoré sama vytvorí. Obnovovanie je tiež lokálne: poslucháč oauth2Client.on('tokens', …) píše obnovené tokeny rovno späť do tabuľky. Backend odovzdá oprávnenie raz a potom zostáva bokom.

Časť o „obnoviteľnosti", úprimne

Tu je detail, ohľadom ktorého chcem byť najviac priamy, lebo je to presne ten druh veci, ktorý changelog zaokrúhli. Poznámka k vydaniu hovorí „obnoviteľné nahrávania" a to je pravda z pohľadu používateľa. Ale GeekBye protokol obnoviteľnosti neimplementoval. Nikde v zdrojoch GeekBye nie je ani konštanta veľkosti fragmentu, ani sledovač bajtového posunu, ani spracovanie Content-Range / 308 Resume Incomplete. Pohľadaj ich grepom — a nedostaneš nič: žijú vnútri SDK googleapis.

Toto robí vlastný kód GeekBye:

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

Celú prácu robia dve rozhodnutia. Po prvé, media.body je prúd (createReadStream), nie buffer — a odovzdanie prúdu je presne to, čo prinúti googleapis dojednať obnoviteľnú reláciu za teba, namiesto toho, aby to spravil na jeden šup. Po druhé, priebeh sa číta z callbacku onUploadProgress SDK voči fileStats.size. To je celá implementácia „obnoviteľného nahrávania" na úrovni aplikácie: prúd dnu, priebeh von. Ťažký protokol — otvoriť reláciu, nahrávať vo fragmentoch, obnoviť od posunu po výpadku — je úloha SDK, a správnym inžinierskym rozhodnutím bolo nechať ho tú úlohu spraviť, a nie ju vynachádzať nanovo.

Čo prerámcuje celé vydanie. Zlepšenie spoľahlivosti neprišlo z napísania nahrávacieho engine. Prišlo zo zmazania proxy, ktoré stálo pred jedným — a skôr, v ten istý týždeň, zo zmazania kroku ffmpeg, ktorý stál pred tým. Menej kódu, viac odolnosti, lebo tou odolnou vecou bola ošúchaná knižnica v okamihu, keď si ju prestal ručne kŕmiť bufferom.

Kúsky, ktoré sú naozaj GeekBye

Odovzdanie prenosu SDK neznamená, že nezostalo čo stavať. Obklopujú ho dva kusy skutočnej aplikačnej logiky.

Vrátenie osirelého priečinka. Každá nahrávka dostane vlastný podpriečinok Drive (vytvorený s mimeType: 'application/vnd.google-apps.folder') a prepis pristane tam ako druhý súbor vedľa videa. Ak nahrávanie vyhodí výnimku, catch ten priečinok zmaže — this.drive.files.delete({ fileId: folderId }), logované ako Rolled back orphaned Drive folder — takže neúspešné nahrávanie nenechá stopu prázdnych priečinkov v tvojom Drive. Vytvoriť kontajner skôr, než vieš, že operácia uspeje, je malá záťaž; spraviť, aby ho chybová cesta upratala, je oprava.

Životný cyklus okolo singletona. DriveService je singleton držiaci v pamäti OAuth klienta, a ten klient v pamäti neprežije reštart aplikácie, hoci tokeny áno (sú v databáze). Takže CloudUploader znova inicializuje DriveService z uložených tokenov pred každým nahrávaním, čo znamená, že nahrávka spravená hneď po opätovnom spustení sa stále nahrá bez toho, aby ťa žiadala o opätovné pripojenie. A revízny prechod premenil handler drive-connect na fire-and-forget, lebo blokujúce volanie connect zdržiavalo dopytovanie stavu rendererom — UI nemohlo ukázať „pripája sa…", ak volanie connect držalo vlákno. Tie dva — reinicializácia z úložiska a neblokujúci connect — sú skutočné hraničné prípady priamej cesty, a je príznačné, že ani jeden z nich nie je o fragmentovaní nahrávania. Keď deleguješ ťažkú časť, bugy, ktoré ti zostanú, sú bugy životného cyklu.

Tri veci, ktoré nás toto vydanie naučilo

  1. Pýtaj sa, kde tečú bajty, nie len či to funguje. Proxy z v1.8.11 fungovalo. Ale trasovať každú nahrávku cez vlastné servery ťa stojí prenos, pamäť a obnoviteľnosť — za súbor, ktorý si neuchovávaš. Prekresliť cestu dát tak, aby klient hovoril s cieľom priamo, je často celá optimalizácia.
  2. Najlepšie obnoviteľné nahrávanie je to, ktoré si nenapísal. Odovzdanie prúdu zrelému SDK kúpilo plnú obnoviteľnú reláciu za dva riadky kódu. Inštinkt ručne zbúchať fragmentovanie a matematiku posunov by vyprodukoval viac kódu a menej spoľahlivosti.
  3. Keď deleguješ jadro, zdedíš životný cyklus, nie algoritmy. Skutočné bugy priameho klienta boli „reinicializuj singleton po reštarte" a „neblokuj volanie connect", nie bajtové posuny. To je dobrý obchod — bugy životného cyklu sú viditeľné a lokálne; bugy protokolu ani jedno.

Pre predchádzajúcu kapitolu príbehu v1 — zrodený a opravený za dvanásť minút (v1.8.10); a pre celý oblúk — anatómiu dodávania softvéru do dokonalosti.

Súvisiace články

Tri slovesá, ktoré udržujú Web Audio nažive
Steven
Steven8 min čítania

Tri slovesá, ktoré udržujú Web Audio nažive

Dve bodové vydania GeekBye, dva mesiace od seba a v dvoch rôznych súboroch, naučili náš audio kód tú istú lekciu z opačných koncov: prestaň zaobchádzať s prehliadačovým AudioContext ako s jednorazovým. Jedno vydanie sa naučilo resume() kontextu, ktorý macOS potichu pozastavil uprostred nahrávania; druhé sa naučilo suspend() namiesto close(), aby na seba nadväzujúce relácie prestali narážať do Chromiom nastaveného stropu zhruba šiestich kontextov. Resume, suspend, close — to je celá zápletka.

Inžinierstvo
Audio
Desktop
Rozoznať hovor od otvorenej aplikácie
Steven
Steven8 min čítania

Rozoznať hovor od otvorenej aplikácie

GeekBye si vie všimnúť, že si sa pripojil k videohovoru, a ponúknuť jeho nahranie. Detekcia sa ukáže ako ľahšia polovica — natívny Swift binárnik čítajúci tituly okien každých desať sekúnd. Ťažká polovica je presnosť: nespustiť sa, keď je Zoom len otvorený, neponúkať nahrávanie stretnutia, ktoré už nahrávaš, a nestlmiť mikrofón v hovore, v ktorom naozaj sedíš. Tri vydania, a každé z nich je poistka, ktorá sa musela naučiť neporaziť samu seba.

Inžinierstvo
macOS
Desktop
Zrodený a opravený za dvanásť minút
Steven
Steven7 min čítania

Zrodený a opravený za dvanásť minút

Changelog GeekBye v1.8.10 hovorí, že opravil pád počas úpravy klávesových skratiek. Opravil — ale pád bol zavedený a opravený v tom istom pull requeste, s odstupom dvanástich minút, a nedostal sa k jedinému používateľovi. Skutočný príbeh je kaskáda spoľahlivosti, ktorá ho vyprodukovala: malá, správna zmena toho, kedy sa skratky pozastavujú, a bug rozoberania Reactu, ktorý vypadol z jedného riadka, čo mal spraviť editor bezpečnejším.

Inžinierstvo
React
Desktop