Steven
Steven7 min lugemist

Backend-i eemaldamine üleslaadimise teelt

GeekBye salvestab su ekraani ja hoiab video su Google Drive-i. Esimene versioon saatis iga salvestuse teel sinna läbi GeekBye enda serverite; üks väljalase hiljem läks fail otse su masinast Drive-i, ja backend alandati ühe kursori hoidmiseni. Huvitav osa on see, kui vähe koodi 'otsene, jätkatav' versioon tegelikult sisaldab — sest jätkatavus tuli proksi kustutamisest, mitte selle kirjutamisest.

Arendus
Arhitektuur
Desktop
GeekBye väljalasked
Backend-i eemaldamine üleslaadimise teelt

GeekBye oskab su ekraani salvestada — video, süsteemiheli ja mikrofon — ja panna valmis salvestuse automaatselt su Google Drive-i. v1.8.11 ja v1.8.13 vahel funktsiooni kasutajale nähtav kirjeldus vaevu muutus, aga tee, mida video Drive-i jõudmiseks läbib, suunati täielikult ümber. Esimeses versioonis rändas su salvestus läbi GeekBye serverite. Teises läks see otse su masinast su Drive-i, ja GeekBye backend ei näinud sellest ühtki baiti. See ümbersuunamine on kogu lugu, ja rahuldust pakkuv osa on see, et "parem" versioon sisaldab silmatorkavalt vähem koodi kui see, mille ta asendas.

Torustik, otsast otsani

Salvestamine toimub renderdajas. ScreenRecordingService.ts loob MediaRecorder-i jäädvustusvoo peale { mimeType: 'video/webm;codecs=vp9,opus', videoBitsPerSecond: 1_000_000 }-ga ja käivitab selle ühesekundilise ajaviiluga (CHUNK_INTERVAL_MS = 1000). Iga ondataavailable blob saadetakse IPC kaudu peaprotsessi, kus recording:save-chunk käitleja lisab selle kettal olevasse faili fs.promises.appendFile-iga. Väärt varakult märkida, sest seda on ahvatlev valesti lugeda: see ühesekundiline kadents on kettalekirjutamise rütm, mitte üleslaadimise tükisuurus. Selleks ajaks, kui üleslaadimine algab, on kettal üksainus valmis WebM fail.

Kahe väljalaske vahel erineb ainult see, mis selle valmis failiga juhtub.

Enne: kõik läbi backendi (v1.8.11)

v1.8.11 üleslaadimise rada failis CloudUploader.processRecording() on ilmselge disain ja täiesti mõistlik esimene versioon:

  1. POST /api/geekbye/recordings, et luua backend kirje salvestuse metaandmetega.
  2. Loe kogu video mällu ja POST see backendi multipart-ina — const fileBuffer = fs.readFileSync(filePath) — tabades POST /api/geekbye/recordings/${id}/upload. Backend siis laeb selle faili Drive-i ja tagastab driveUrl.
  3. POST …/process, et saata transkript AI-analüüsiks.

Kandev detail on samm 2: klient laadib kogu salvestuse mällu, ja iga selle bait läbib teel Drive-i GeekBye enda serverid. Lühikese klipi puhul on see korras. Pika ekraanisalvestuse puhul on see kolm probleemi korraga — mälusurve kliendile readFileSync-ist, ribalaiuse- ja relee-kulu backendile faili eest, mida ta isegi ei hoia, ja üksainus monoliitne POST, mis peab nullist otsast alustama, kui võrk poole peal tõrgub.

Väärt ka ausalt öelda, et see "enne" polnud mingi puutumatu algne disain. v1.8.11 funktsioon surus kokku ühe nädala sees toimunud pöörde: varane versioon laadis Cloudflare R2-te eelallkirjastatud URL-ide kaudu ja teisendas WebM-i MP4-ks komplekteeritud ffmpeg-static-uga; see asendati backendi-vahendatud Drive-vooga; ja siis kisti ffmpeg teisendus täielikult välja WebM-i sellisena üleslaadimise kasuks (commit märgib, et see langetas bitikiirust ja tootis umbes neli korda väiksemaid faile — sisene hinnang, mitte võrdlustest). Nii et isegi "enne" oli juba korra õppinud natiivse sõltuvuse kustutama. Järgmine väljalase kustutaks ka proksi.

Pärast: otse Drive-i (v1.8.13)

v1.8.13 kirjutab CloudUploader.processRecording() ümber ühe lause ümber selle commitist, mis selle saatis: lae otse Drive-i, "keeping backend for metadata record + AI transcript analysis only." Uus rada:

  1. POST /api/geekbye/recordingsainult metaandmed (pealkiri, kestus, faili suurus, format: 'webm'). Kommentaar koodis on otsekohene: // Create backend record (metadata only — no file upload).
  2. Lae video otse Google Drive-i, jättes backend-vahemehe täielikult vahele.
  3. PATCH /api/geekbye/recordings/${backendRecordingId} saadud { driveUrl, driveFolderId }-ga — backend kirjele antakse teada, kuhu fail lõpuks sattus.
  4. Transkript läheb ikka …/process-ile analüüsiks.

Backend läks faili raja sees olemiselt selleks, kellele failist tagantjärele teatatakse. Ta hoiab rida ja kursorit; baidid elavad ainult kasutaja kettal ja kasutaja Drive-is.

Päris Drive kliendiks saamine

Selleks, et klient saaks Drive-iga otse rääkida, peab ta olema tõeline Google Drive API klient, ja v1.8.13 lisab selle mehhanismi: uue DriveService-i (Drive klient), uue DriveAuthRepository, mida toetab uus drive_auth SQLite tabel, ja googleapis SDK sõltuvusena.

Autoriseerimine on puhas võimekuse üleandmine. Backendi rakendus-konfiguratsiooni lõpp-punkt edastab Google OAuth kliendimandaadidclientId ja clientSecret — alla rakendusse, mille CloudUploader loeb ja annab driveService.initialize(...)-ile. Sealt jooksutab töölauarakendus kogu OAuth-voo ise: see püstitab ajutise http serveri juhuslikul 127.0.0.1 pordil, avab nõusolekuekraani süsteemibrauseris shell.openExternal-iga, püüab ümbersuunamise /callback-il ja vahetab koodi tokenite vastu google-auth-library-ga. Need tokenid salvestatakse lokaalselt — saveDriveAuth(access_token, refresh_token, expiry_date) drive_auth-i — drive.file skoobiga, mis laseb rakendusel hallata ainult neid faile, mis ta ise loob. Värskendus on samuti lokaalne: oauth2Client.on('tokens', …) kuulaja kirjutab värskendatud tokenid otse tabelisse tagasi. Backend annab võimekuse üle korra ja jääb siis silmusest välja.

"Jätkatav" osa, ausalt

Siin on detail, mille suhtes ma kõige rohkem tahan aus olla, sest see on täpselt selline asi, mille muudatuste logi ümardab. Väljalaskemärkus ütleb "resumable uploads", ja sealt, kus kasutaja seisab, on see tõsi. Aga GeekBye ei implementeerinud jätkatavat protokolli. Kuskil GeekBye lähtekoodis pole tükisuuruse konstanti, baidinihke jälgijat ega Content-Range / 308 Resume Incomplete käsitlemist. Grep neid ja sa ei saa midagi — need elavad googleapis SDK sees.

See, mida GeekBye enda kood teeb, on järgmine:

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

Kaks valikut teevad kogu töö. Esiteks, media.body on voog (createReadStream), mitte puhver — ja voo edastamine on täpselt see, mis paneb googleapis-i sinu eest jätkatavat seanssi läbi rääkima, selle asemel et teha üks lask. Teiseks, edenemist loetakse SDK onUploadProgress tagasikutse pealt fileStats.size vastu. See on kogu "jätkatava üleslaadimise" implementatsioon rakenduse tasandil: voog sisse, edenemine välja. Raske protokoll — ava seanss, lae tükkidena üles, jätka nihkelt pärast katkestust — on SDK töö, ja õige inseneriotsus oli lasta tal seda tööd teha, mitte seda ümber leiutada.

Mis raamib kogu väljalaske ümber. Töökindluse paranemine ei tulnud üleslaadimismootori kirjutamisest. See tuli proksi kustutamisest, mis istus selle ees — ja varem samal nädalal ffmpeg sammu kustutamisest selle ees. Vähem koodi, rohkem vastupidavust, sest vastupidav asi oli hästi äratallatud teek hetkest, mil sa lõpetasid selle käsitsi puhvriga söötmise.

Osad, mis on ehtsalt GeekBye omad

Ülekande SDK-le andmine ei tähenda, et polnud midagi ehitada. Kaks tükki tõelist rakendusloogikat ümbritsevad seda.

Orvuks-jäänud-kausta tagasikerimine. Iga salvestus saab oma Drive alamkausta (loodud mimeType: 'application/vnd.google-apps.folder'-iga), ja transkript maandub sinna teise failina video kõrvale. Kui üleslaadimine viskab, kustutab catch selle kausta — this.drive.files.delete({ fileId: folderId }), logitud kui Rolled back orphaned Drive folder — nii et ebaõnnestunud üleslaadimine ei jäta su Drive-i tühjade kaustade jälge. Konteineri loomine enne, kui tead, et operatsioon õnnestub, on väike vastutus; ebaõnnestumisraja panemine seda koristama on parandus.

Elutsükkel singletoni ümber. DriveService on singleton, mis hoiab mälusisest OAuth klienti, ja see mälusisene klient ei ela rakenduse taaskäivitust üle, isegi kui tokenid elavad (need on andmebaasis). Nii et CloudUploader initsialiseerib DriveService-i salvestatud tokenitest uuesti enne iga üleslaadimist, mis tähendab, et kohe pärast taaskäivitust tehtud salvestus laeb ikka üles, ilma et paluks sul uuesti ühenduda. Ja ülevaatuskäik muutis drive-connect käitleja käivita-ja-unusta tüüpi, sest blokeeriv ühenduskutse peatas renderdaja staatuse pollimise — UI ei suutnud näidata "ühendan…", kui ühenduskutse hoidis lõime. Need kaks — uuesti-initsialiseerimine talletusest ja mitteblokeeriv ühendus — on otsese raja tõelised piirjuhtumid, ja märkimisväärselt pole kumbki neist üleslaadimise tükeldamisest. Kui delegeerid raske osa, on vead, mis sulle jäävad, elutsükli vead.

Kolm asja, mida see väljalase meile õpetas

  1. Küsi, kus baidid voolavad, mitte ainult kas see töötab. v1.8.11 proksi töötas. Aga iga salvestuse marsruutimine läbi su enda serverite maksab sulle ribalaiust, mälu ja jätkatavust faili eest, mida sa ei hoia. Andmeraja ümberjoonistamine nii, et klient räägib sihtkohaga otse, on sageli kogu optimeerimine.
  2. Parim jätkatav üleslaadimine on see, mida sa ei kirjutanud. Voo andmine küpsele SDK-le ostis täis jätkatava seansi kahe koodirea eest. Instinkt käsitsi tükeldamist ja nihkematemaatikat kirjutada oleks tootnud rohkem koodi ja vähem töökindlust.
  3. Kui delegeerid tuuma, pärid elutsükli, mitte algoritmid. Otsese kliendi tõelised vead olid "initsialiseeri singleton pärast taaskäivitust uuesti" ja "ära blokeeri ühenduskutset", mitte baidinihked. See on hea vahetus — elutsükli vead on nähtavad ja lokaalsed; protokolli vead pole kumbki.

Eelmise peatüki jaoks v1 loos — sündinud ja parandatud kaheteistkümne minutiga (v1.8.10); ja kogu kaare jaoks tarkvara täiuslikkuseni tarnimise anatoomia.

Seotud artiklid

Kolm tegusõna, mis hoiavad Web Audio elus
Steven
Steven7 min lugemist

Kolm tegusõna, mis hoiavad Web Audio elus

Kaks GeekBye väljalaset, kahe kuu vahega ja kahes eri failis, õpetasid meie helikoodile sama õppetunni vastasotstest: lõpeta brauseri AudioContexti kohtlemine ühekordselt kasutatavana. Üks väljalase õppis kutsuma `resume()` kontekstile, mille macOS oli salaja peatanud keset salvestust; teine õppis kutsuma `suspend()` mitte `close()`, et järjestikused sessioonid lakkaksid põrkumast vastu Chromiumi umbes-kuue-konteksti lage. Resume, suspend, close — see ongi kogu süžee.

Arendus
Audio
Desktop
Kõne eristamine avatud rakendusest
Steven
Steven7 min lugemist

Kõne eristamine avatud rakendusest

GeekBye võib märgata, et oled videokoosolekuga liitunud, ja pakkuda selle salvestamist. Tuvastus osutub lihtsamaks pooleks — Swift-binaar, mis loeb aknapealkirju iga kümne sekundi tagant. Raske pool on täpsus: mitte vallanduda, kui Zoom on lihtsalt avatud, mitte pakkuda koosolekut, mida sa juba salvestad, ja mitte vaigistada mikrofoni kõnes, milles sa tegelikult oled. Kolm väljalaset, ja igaüks neist on valvur, kes pidi õppima ennast mitte lööma.

Arendus
macOS
Desktop
Sündinud ja parandatud kaheteistkümne minutiga
Steven
Steven6 min lugemist

Sündinud ja parandatud kaheteistkümne minutiga

GeekBye v1.8.10 muudatuste logi ütleb, et see parandas krahhi klaviatuuri otseteede redigeerimisel. Nii oligi — aga krahh tutvustati ja parandati samas pull requestis, kaheteistkümne minuti vahega, ega jõudnud ühegi kasutajani. Tegelik lugu on töökindluse kaskaad, mis selle tekitas: väike, õige muudatus selles, millal otseteed peatatakse, ja React-i lammutamise viga, mis kukkus välja ühest reast, mis pidi redaktorit turvalisemaks tegema.

Arendus
React
Desktop