Steven
Steven7 min čtení

Vyjmutí backendu z cesty nahrávání

GeekBye nahrává tvou obrazovku a ukládá video na tvůj Google Drive. První verze posílala každou nahrávku cestou tam přes vlastní servery GeekBye; o vydání později šel soubor přímo z tvého stroje na Drive a backend byl degradován na držení jediného ukazatele. Zajímavé je, jak málo kódu vlastně obsahuje „přímá, resumable" verze — protože resumabilita přišla ze smazání proxy, ne z jeho napsání.

Inženýrství
Architektura
Desktop
Vydání GeekBye
Vyjmutí backendu z cesty nahrávání

GeekBye umí nahrát tvou obrazovku — video, systémový zvuk a mikrofon — a hotovou nahrávku automaticky pustit na tvůj Google Drive. Mezi v1.8.11 a v1.8.13 se popis funkce viditelný pro uživatele sotva změnil, ale cesta, kterou video urazí, aby se dostalo na Drive, byla přesměrována úplně. V první verzi tvoje nahrávka cestovala přes servery GeekBye. Ve druhé šla přímo z tvého stroje na tvůj Drive a backend GeekBye z ní nikdy neviděl ani byte. To přesměrování je celý příběh a uspokojivá část je, že „lepší" verze obsahuje nápadně méně kódu než ta, kterou nahradila.

Pipeline, od začátku do konce

Nahrávání se děje v rendereru. ScreenRecordingService.ts vytvoří MediaRecorder nad capture streamem s { mimeType: 'video/webm;codecs=vp9,opus', videoBitsPerSecond: 1_000_000 } a spustí ho s jednosekundovým timeslice (CHUNK_INTERVAL_MS = 1000). Každý ondataavailable blob je poslán přes IPC do hlavního procesu, kde ho handler recording:save-chunk připojí k souboru na disku pomocí fs.promises.appendFile. Stojí za to to zdůraznit brzy, protože je to lákavá věc k mylnému výkladu: ta jednosekundová kadence je rytmus zápisu na disk, ne velikost chunku nahrávání. Než nahrávání začne, na disku sedí jediný hotový WebM soubor.

To, co se mezi dvěma vydáními liší, je jen to, co se stane s tím hotovým souborem.

Předtím: všechno přes backend (v1.8.11)

Cesta nahrávání z v1.8.11, v CloudUploader.processRecording(), je zřejmý návrh a naprosto rozumná první verze:

  1. POST /api/geekbye/recordings pro vytvoření backend záznamu s metadaty nahrávky.
  2. Přečti celé video do paměti a POST ho na backend jako multipart — const fileBuffer = fs.readFileSync(filePath) — udeřením na POST /api/geekbye/recordings/${id}/upload. Backend pak nahraje ten soubor na Drive a vrátí driveUrl.
  3. POST …/process pro odeslání transkriptu k AI analýze.

Nosný detail je krok 2: klient načte celou nahrávku do paměti a každý její byte prochází vlastními servery GeekBye na cestě na Drive. U krátkého klipu je to v pořádku. U dlouhé nahrávky obrazovky jsou to tři problémy najednou — tlak na paměť klienta z readFileSync, náklady na šířku pásma a přeposílání na backendu za soubor, který si ani nenechává, a jeden monolitický POST, který musí restartovat od nuly, pokud síť v půlce škytne.

Stojí za to být také upřímný, že tohle „předtím" nebyl nějaký bezvadný původní návrh. Funkce v1.8.11 zmáčkla dohromady pivot v rámci týdne: raná verze nahrávala na Cloudflare R2 přes pre-signed URL a konvertovala WebM na MP4 se zabaleným ffmpeg-static; to bylo nahrazeno Drive flow zprostředkovaným backendem; a pak byla ffmpeg konverze zcela vytržena ve prospěch nahrávání WebM tak, jak je (commit poznamenává, že snížil bitrate a produkoval zhruba čtyřikrát menší soubory — inline odhad, ne benchmark). Takže i „předtím" se už jednou naučilo smazat nativní závislost. Následující vydání mělo smazat i proxy.

Potom: přímo na Drive (v1.8.13)

v1.8.13 přepisuje CloudUploader.processRecording() kolem jedné věty z commitu, který ho vydal: nahrávej přímo na Drive, „keeping backend for metadata record + AI transcript analysis only." Nová cesta:

  1. POST /api/geekbye/recordingsjen metadata (titul, délka, velikost souboru, format: 'webm'). Komentář v kódu je bez obalu: // Create backend record (metadata only — no file upload).
  2. Nahraj video přímo na Google Drive, zcela přeskočíc backend prostředníka.
  3. PATCH /api/geekbye/recordings/${backendRecordingId} s výsledným { driveUrl, driveFolderId } — backend záznamu se řekne, kde soubor skončil.
  4. Transkript stále jde na …/process k analýze.

Backend přešel z toho, že byl v cestě souboru, k tomu, že se mu o souboru řekne dodatečně. Drží řádek a ukazatel; byty žijí jen na disku uživatele a v Drive uživatele.

Stát se skutečným Drive klientem

Aby klient mluvil s Drive přímo, musí být skutečný Google Drive API klient, a v1.8.13 přidává na to mašinerii: nový DriveService (Drive klient), nový DriveAuthRepository podepřený novou SQLite tabulkou drive_auth, a SDK googleapis jako závislost.

Autorizace je čisté předání schopnosti. app-config endpoint backendu předává Google OAuth klientské přihlašovací údajeclientId a clientSecret — dolů do aplikace, které CloudUploader přečte a předá do driveService.initialize(...). Odtud desktopová aplikace spouští celý OAuth flow sama: postaví dočasný http server na náhodném 127.0.0.1 portu, otevře obrazovku souhlasu v systémovém prohlížeči pomocí shell.openExternal, zachytí redirect na /callback a vymění kód za tokeny pomocí google-auth-library. Ty tokeny se ukládají lokálně — saveDriveAuth(access_token, refresh_token, expiry_date) do drive_auth — se scopem drive.file, který nechá aplikaci spravovat jen soubory, které vytvoří. Obnova je také lokální: listener oauth2Client.on('tokens', …) zapisuje obnovené tokeny rovnou zpátky do tabulky. Backend předá schopnost jednou a pak zůstane mimo smyčku.

Ta „resumable" část, upřímně

Zde je detail, ohledně kterého chci být nejvíc přímý, protože je to přesně ten druh věci, který changelog zaokrouhluje. Poznámka k vydání říká „resumable uploads" a to je pravda z místa, kde stojí uživatel. Ale GeekBye neimplementoval resumable protokol. Není tu žádná konstanta velikosti chunku, žádný tracker byte-offsetu a žádná obsluha Content-Range / 308 Resume Incomplete nikde ve zdrojovém kódu GeekBye. Udělej na ně grep a nedostaneš nic — žijí uvnitř SDK googleapis.

Co dělá vlastní kód GeekBye, je tohle:

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

Dvě volby dělají všechnu práci. Za prvé, media.body je stream (createReadStream), ne buffer — a předání streamu je přesně to, co přiměje googleapis, aby tvým jménem vyjednal resumable session místo toho, aby to udělal na jeden zátah. Za druhé, průběh se čte z callbacku onUploadProgress SDK proti fileStats.size. To je celá implementace „resumable upload" na úrovni aplikace: stream dovnitř, průběh ven. Těžký protokol — otevři session, nahraj v chuncích, pokračuj od offsetu po výpadku — je práce SDK, a správné inženýrské rozhodnutí bylo nechat ho tu práci dělat, místo abychom ji znovu vynalézali.

Což přerámuje celé vydání. Zlepšení spolehlivosti nepřišlo z napsání nahrávacího enginu. Přišlo ze smazání proxy, která seděla před jedním — a, dříve ve stejném týdnu, ze smazání ffmpeg kroku před tím. Méně kódu, více odolnosti, protože ta odolná věc byla dobře prošlapaná knihovna ve chvíli, kdy jsi ji přestal ručně krmit bufferem.

Kousky, které jsou opravdu GeekBye

Předání přenosu SDK neznamená, že nezbylo nic k postavení. Obklopují ho dva kusy skutečné aplikační logiky.

Rollback osiřelé složky. Každá nahrávka dostane svou vlastní Drive podsložku (vytvořenou s mimeType: 'application/vnd.google-apps.folder'), a transkript tam přistane jako druhý soubor vedle videa. Pokud nahrávání vyhodí výjimku, catch smaže tu složku — this.drive.files.delete({ fileId: folderId }), logováno jako Rolled back orphaned Drive folder — takže neúspěšné nahrání nenechá stopu prázdných složek ve tvém Drive. Vytvoření kontejneru předtím, než víš, že operace uspěje, je malý závazek; udělat cestu selhání tak, aby ho uklidila, je oprava.

Životní cyklus kolem singletonu. DriveService je singleton držící OAuth klienta v paměti, a ten klient v paměti nepřežije restart aplikace, i když tokeny přežijí (jsou v databázi). Takže CloudUploader reinicializuje DriveService z uložených tokenů před každým nahráním, což znamená, že nahrávka udělaná hned po opětovném spuštění se stále nahraje, aniž by tě žádala o opětovné připojení. A review průchod proměnil handler drive-connect na fire-and-forget, protože blokující connect volání zdržovalo status polling rendereru — UI nemohlo ukázat „connecting…", pokud connect volání drželo vlákno. Tyto dvě — reinicializace-z-úložiště a neblokující connect — jsou skutečné hraniční případy přímé cesty a, pozoruhodně, ani jeden z nich není o chunkingu nahrávání. Když deleguješ tu těžkou část, bugy, se kterými zůstaneš, jsou bugy životního cyklu.

Tři věci, které nás toto vydání naučilo

  1. Ptej se, kudy tečou byty, ne jen jestli to funguje. Proxy z v1.8.11 fungovala. Ale rutování každé nahrávky přes vlastní servery tě stojí šířku pásma, paměť a schopnost restartu za soubor, který si nenecháváš. Překreslení datové cesty tak, aby klient mluvil přímo s cílem, je často celá optimalizace.
  2. Nejlepší resumable nahrání je to, které jsi nenapsal. Předání streamu zralému SDK koupilo plnou resumable session za dva řádky kódu. Instinkt ručně napsat chunking a matematiku offsetu by produkoval více kódu a méně spolehlivosti.
  3. Když deleguješ jádro, dědíš životní cyklus, ne algoritmy. Skutečné bugy přímého klienta byly „reinicializuj singleton po restartu" a „neblokuj connect volání", ne byte offsety. To je dobrý obchod — bugy životního cyklu jsou viditelné a lokální; bugy protokolu nejsou ani jedno.

Předchozí kapitolu příběhu v1 najdete v zrozen a opraven za dvanáct minut (v1.8.10); a celý oblouk v anatomii dodávání softwaru k dokonalosti.

Související články

Tři slovesa, která udržují Web Audio naživu
Steven
Steven8 min čtení

Tři slovesa, která udržují Web Audio naživu

Dvě bodová vydání GeekBye, dva měsíce od sebe a ve dvou různých souborech, naučila náš audio kód stejné lekci z opačných konců: přestaň zacházet s AudioContext prohlížeče jako s jednorázovým. Jedno vydání se naučilo volat resume() na kontextu, který macOS tiše suspendoval uprostřed nahrávání; druhé se naučilo volat suspend() místo close(), aby po sobě jdoucí sesiony přestaly narážet do stropu Chromia zhruba šesti kontextů. Resume, suspend, close — to je celá zápletka.

Inženýrství
Audio
Desktop
Rozeznat hovor od otevřené aplikace
Steven
Steven8 min čtení

Rozeznat hovor od otevřené aplikace

GeekBye si umí všimnout, že ses připojil k videoschůzce, a nabídnout, že ji nahraje. Detekce se ukazuje být tou snadnou polovinou — Swift binary čtoucí titulky oken každých deset sekund. Tvrdá polovina je přesnost: nespustit se, když je Zoom jen otevřený, neptat se na schůzku, kterou už nahráváš, a neztlumit mikrofon v hovoru, ve kterém opravdu jsi. Tři vydání, a každé je pojistka, která se musela naučit neporazit sama sebe.

Inženýrství
macOS
Desktop
Zrozen a opraven za dvanáct minut
Steven
Steven7 min čtení

Zrozen a opraven za dvanáct minut

Changelog GeekBye v1.8.10 říká, že opravil pád při úpravě klávesových zkratek. Opravdu ho opravil — ale ten pád byl zaveden a opraven ve stejném pull requestu, s odstupem dvanácti minut, a nikdy nedosáhl ani jednoho uživatele. Skutečný příběh je kaskáda spolehlivosti, která ho vyprodukovala: malá, správná změna toho, kdy se zkratky pozastavují, a bug s React demontáží, který vypadl z jednoho řádku, jenž měl udělat editor bezpečnějším.

Inženýrství
React
Desktop