Steven
Steven7 min. skaitymo

Išimame backend iš įkėlimo kelio

GeekBye įrašo tavo ekraną ir išsaugo vaizdą į tavo Google Drive. Pirmoji versija siuntė kiekvieną įrašą per pačios GeekBye serverius pakeliui; leidimu vėliau failas ėjo tiesiai iš tavo mašinos į Drive, o backend buvo pažemintas iki vienintelės rodyklės laikymo. Įdomi dalis ta, kaip mažai kodo iš tikrųjų talpina „tiesioginė, atnaujinama" versija — nes atnaujinamumas atsirado iš proxy ištrynimo, o ne iš jo rašymo.

Inžinerija
Architektūra
Desktop
GeekBye leidimai
Išimame backend iš įkėlimo kelio

GeekBye moka įrašyti tavo ekraną — vaizdą, sistemos garsą ir mikrofoną — ir automatiškai įmesti gatavą įrašą į tavo Google Drive. Tarp v1.8.11 ir v1.8.13 naudotojui matomas funkcijos aprašymas vos pasikeitė, bet kelias, kuriuo vaizdas keliauja į Drive, buvo perkreiptas visiškai. Pirmoje versijoje tavo įrašas keliavo per GeekBye serverius. Antroje jis ėjo tiesiai iš tavo mašinos į tavo Drive, ir GeekBye backend niekada nematė nė vieno jo baito. Tas peradresavimas — visa istorija, o maloni dalis ta, kad „geresnė" versija talpina pastebimai mažiau kodo nei ta, kurią pakeitė.

Konvejeris, nuo pradžios iki galo

Įrašymas vyksta rendereryje. ScreenRecordingService.ts sukuria MediaRecorder virš fiksavimo srauto su { mimeType: 'video/webm;codecs=vp9,opus', videoBitsPerSecond: 1_000_000 } ir paleidžia jį su vienos sekundės laiko pjūviu (CHUNK_INTERVAL_MS = 1000). Kiekvienas ondataavailable blob'as siunčiamas per IPC į pagrindinį procesą, kur recording:save-chunk tvarkyklė prideda jį prie failo diske su fs.promises.appendFile. Verta pažymėti iškart, nes tai vilioja perskaityti klaidingai: tas vienos sekundės ritmas yra rašymo į diską ritmas, o ne įkėlimo fragmento dydis. Kol įkėlimas prasideda, diske guli vienintelis gatavas WebM failas.

Tarp dviejų leidimų skiriasi tik tai, kas nutinka tam gatavam failui.

Kaip buvo: viskas per backend (v1.8.11)

Įkėlimo kelias v1.8.11, faile CloudUploader.processRecording(), yra akivaizdus dizainas ir visiškai protinga pirmoji versija:

  1. POST /api/geekbye/recordings, kad sukurtų backend įrašą su įrašo metaduomenimis.
  2. Perskaityti visą vaizdą į atmintį ir POST'inti jį į backend kaip multipart — const fileBuffer = fs.readFileSync(filePath) — pataikant į POST /api/geekbye/recordings/${id}/upload. Backend tada įkelia tą failą į Drive ir grąžina driveUrl.
  3. POST …/process, kad išsiųstų transkriptą AI analizei.

Laikančioji detalė yra 2 žingsnis: klientas įkelia visą įrašą į atmintį, ir kiekvienas jo baitas tranzitu eina per pačios GeekBye serverius pakeliui į Drive. Trumpam klipui tai gerai. Ilgam ekrano įrašui tai trys problemos iškart — spaudimas kliento atminčiai iš readFileSync, pralaidumo ir perdavimo kaina backende už failą, kurio jis net nesaugo, ir vienintelis monolitinis POST, kuriam tenka pradėti iš nulio, jei tinklas žagteli pusiaukelėje.

Verta būti sąžiningam ir dėl to, kad šis „kaip buvo" nebuvo koks nors nepaliestas pirminis dizainas. v1.8.11 funkcija suspaudė į save posūkį, įvykusį per vieną savaitę: ankstyva versija įkeldavo į Cloudflare R2 per iš anksto pasirašytus URL ir konvertuodavo WebM į MP4 su pridėtu ffmpeg-static; tai pakeitė backend tarpininkaujamas Drive srautas; o tada ffmpeg konvertavimą visai išplėšė vaizdo WebM įkėlimo kaip yra naudai (komitas pažymi, kad tai sumažino bitrate ir davė maždaug keturis kartus mažesnius failus — įvertinimas iš akies, ne benchmark). Tad net „kaip buvo" jau kartą išmoko ištrinti natyvią priklausomybę. Kitas leidimas ištrintų ir proxy.

Kaip tapo: tiesiai į Drive (v1.8.13)

v1.8.13 perrašo CloudUploader.processRecording() aplink vieną sakinį iš komito, kuris jį išleido: įkelk tiesiai į Drive, „paliekant backend tik metaduomenų įrašui + AI transkripto analizei." Naujas kelias:

  1. POST /api/geekbye/recordingstik metaduomenys (pavadinimas, trukmė, failo dydis, format: 'webm'). Komentaras kode be užuolankų: // Create backend record (metadata only — no file upload).
  2. Įkelti vaizdą tiesiai į Google Drive, visiškai apeinant backend tarpininką.
  3. PATCH /api/geekbye/recordings/${backendRecordingId} su gautu { driveUrl, driveFolderId } — backend įrašui pasakoma, kur failas atsidūrė.
  4. Transkriptas vis dar eina į …/process analizei.

Backend iš buvimo faile kelyje tapo tuo, kam apie failą pasakoma po fakto. Jis laiko eilutę ir rodyklę; baitai gyvena tik naudotojo diske ir naudotojo Drive.

Tampant tikru Drive klientu

Kad klientas kalbėtų su Drive tiesiogiai, jis turi būti tikru Google Drive API klientu, ir v1.8.13 tam prideda mašineriją: naują DriveService (Drive klientą), naują DriveAuthRepository, paremtą nauja SQLite lentele drive_auth, ir SDK googleapis kaip priklausomybę.

Autorizacija yra švarus gebėjimo perdavimas. Backend'o programos konfigūracijos endpoint'as perduoda Google OAuth kliento kredencialusclientId ir clientSecret — žemyn į programą, kuriuos CloudUploader perskaito ir perduoda į driveService.initialize(...). Iš ten desktop programa pati atlieka visą OAuth srautą: pastato laikiną http serverį atsitiktiniame 127.0.0.1 prievade, atveria sutikimo ekraną sistemos naršyklėje su shell.openExternal, pagauna peradresavimą į /callback ir iškeičia kodą į žetonus su google-auth-library. Tie žetonai išsaugomi lokaliai — saveDriveAuth(access_token, refresh_token, expiry_date) į drive_auth — su drive.file apimtimi, kuri leidžia programai valdyti tik tuos failus, kuriuos ji pati sukuria. Atnaujinimas taip pat lokalus: klausytojas oauth2Client.on('tokens', …) rašo atnaujintus žetonus tiesiai atgal į lentelę. Backend perduoda gebėjimą kartą ir tada lieka nuošalyje.

Dalis apie „atnaujinamumą", sąžiningai

Štai detalė, dėl kurios labiausiai noriu būti tiesus, nes tai būtent toks dalykas, kurį changelog suapvalina. Leidimo pastaba sako „atnaujinami įkėlimai", ir tai tiesa iš naudotojo perspektyvos. Bet GeekBye atnaujinamumo protokolo neįgyvendino. Niekur GeekBye šaltiniuose nėra nei fragmento dydžio konstantos, nei baitų poslinkio sekiklio, nei Content-Range / 308 Resume Incomplete tvarkymo. Paieškok jų grep'u — ir negausi nieko: jie gyvena SDK googleapis viduje.

Štai ką daro pačios GeekBye kodas:

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

Visą darbą atlieka du pasirinkimai. Pirma, media.body yra srautas (createReadStream), o ne buferis, — ir srauto perdavimas yra būtent tai, kas priverčia googleapis derėtis dėl atnaujinamos sesijos už tave, užuot dariusi viską vienu ypu. Antra, progresas nuskaitomas iš SDK onUploadProgress atgalinio iškvietimo pagal fileStats.size. Tai visa „atnaujinamo įkėlimo" implementacija programos lygmenyje: srautas vidun, progresas laukan. Sunkusis protokolas — atverti sesiją, įkelti fragmentais, atnaujinti nuo poslinkio po nutrūkimo — yra SDK darbas, ir teisingas inžinerinis sprendimas buvo leisti jam tą darbą atlikti, o ne išradinėti jį iš naujo.

Kas perrėmina visą leidimą. Patikimumo pagerinimas atsirado ne iš įkėlimo variklio rašymo. Jis atsirado iš proxy, stovėjusio prieš vieną tokį, ištrynimo — o anksčiau, tą pačią savaitę, iš ffmpeg žingsnio, stovėjusio prieš , ištrynimo. Mažiau kodo, daugiau atsparumo, nes atsparusis dalykas buvo nudėvėta biblioteka tą akimirką, kai nustojai ranka maitinti ją buferiu.

Dalys, kurios tikrai yra GeekBye

Perdavus perkėlimą SDK, tai nereiškia, kad statyti nebeliko ko. Jį supa du tikros programos logikos gabalai.

Našlaičio aplanko atšaukimas. Kiekvienas įrašas gauna savo Drive poaplankį (sukurtą su mimeType: 'application/vnd.google-apps.folder'), ir transkriptas nusileidžia ten antru failu šalia vaizdo. Jei įkėlimas išmeta išimtį, catch ištrina tą aplanką — this.drive.files.delete({ fileId: folderId }), žurnalizuojama kaip Rolled back orphaned Drive folder — tad nepavykęs įkėlimas nepalieka tuščių aplankų pėdsako tavo Drive. Sukurti konteinerį prieš tai, kai žinai, kad operacija pavyks, yra maža rizika; padaryti taip, kad klaidos kelias jį sutvarkytų, yra pataisymas.

Gyvavimo ciklas aplink singletoną. DriveService yra singletonas, laikantis atmintyje OAuth klientą, ir tas klientas atmintyje neišgyvena programos perkrovimo, nors žetonai išgyvena (jie duomenų bazėje). Tad CloudUploader iš naujo inicializuoja DriveService iš išsaugotų žetonų prieš kiekvieną įkėlimą, o tai reiškia, kad įrašas, padarytas iškart po paleidimo iš naujo, vis tiek įkeliamas, neprašant tavęs prisijungti iš naujo. O peržiūros praėjimas pavertė drive-connect tvarkyklę fire-and-forget, nes blokuojantis connect iškvietimas stabdė renderio statuso apklausą — UI negalėjo parodyti „jungiamasi…", jei connect iškvietimas laikė giją. Tie du — pakartotinė inicializacija iš saugyklos ir neblokuojantis connect — yra tikri tiesioginio kelio kraštutiniai atvejai, ir, kas pažymėtina, nė vienas iš jų nėra apie įkėlimo fragmentavimą. Kai deleguoji sunkiąją dalį, bagai, kurie tau lieka, yra gyvavimo ciklo bagai.

Trys dalykai, kurių šis leidimas mus išmokė

  1. Klausk, kur teka baitai, o ne tik ar tai veikia. v1.8.11 proxy veikė. Bet marširuoti kiekvieną įrašą per savo serverius kainuoja tau pralaidumą, atmintį ir atnaujinamumą — už failą, kurio nesaugai. Perbraižyti duomenų kelią taip, kad klientas kalbėtų su tikslu tiesiogiai, dažnai ir yra visa optimizacija.
  2. Geriausias atnaujinamas įkėlimas — tas, kurio nerašei. Srauto perdavimas brandžiam SDK nupirko pilną atnaujinamą sesiją už dvi kodo eilutes. Instinktas ranka sulipdyti fragmentavimą ir poslinkio matematiką būtų davęs daugiau kodo ir mažiau patikimumo.
  3. Kai deleguoji šerdį, paveldi gyvavimo ciklą, ne algoritmus. Tikri tiesioginio kliento bagai buvo „iš naujo inicializuok singletoną po perkrovimo" ir „neblokuok connect iškvietimo", ne baitų poslinkiai. Tai geras sandoris — gyvavimo ciklo bagai matomi ir lokalūs; protokolo bagai nei viena, nei kita.

Ankstesniam v1 istorijos skyriui — gimęs ir ištaisytas per dvylika minučių (v1.8.10); o visai istorijos linijai — programinės įrangos tobulybės pristatymo anatomija.

Susiję straipsniai

Trys veiksmažodžiai, kurie palaiko Web Audio gyvą
Steven
Steven8 min. skaitymo

Trys veiksmažodžiai, kurie palaiko Web Audio gyvą

Du GeekBye taškiniai leidimai, per du mėnesius vienas nuo kito ir dviejuose skirtinguose failuose, išmokė mūsų garso kodą tos pačios pamokos iš priešingų galų: liaukis elgtis su naršyklės AudioContext kaip su vienkartiniu. Vienas leidimas išmoko resume() kontekstą, kurį macOS tyliai pristabdė įrašymo viduryje; kitas išmoko suspend() vietoj close(), kad vienas po kito einantys seansai nustotų atsitrenkti į Chromium primestą maždaug šešių kontekstų lubą. Resume, suspend, close — štai ir visas siužetas.

Inžinerija
Audio
Desktop
Atskirti skambutį nuo atvertos programos
Steven
Steven7 min. skaitymo

Atskirti skambutį nuo atvertos programos

GeekBye gali pastebėti, kad prisijungei prie vaizdo susitikimo, ir pasiūlyti jį įrašyti. Aptikimas pasirodo esąs lengvoji pusė — Swift binaras, kas dešimt sekundžių skaitantis langų pavadinimus. Sunkioji pusė yra tikslumas: nesuveikti, kai Zoom tiesiog atvertas, nesiūlyti susitikimo, kurį jau įrašinėji, ir nenutildyti mikrofono skambutyje, kuriame iš tikrųjų esi. Trys leidimai, ir kiekvienas yra apsauga, kuriai teko išmokti nenugalėti pačios savęs.

Inžinerija
macOS
Desktop
Gimęs ir ištaisytas per dvylika minučių
Steven
Steven6 min. skaitymo

Gimęs ir ištaisytas per dvylika minučių

GeekBye v1.8.10 changelog sako, kad ištaisė strigtį redaguojant klaviatūros sparčiuosius klavišus. Ištaisė — bet strigtis buvo įvesta ir ištaisyta tame pačiame pull request, dvylikos minučių skirtumu, ir nepasiekė nė vieno naudotojo. Tikroji istorija — patikimumo kaskada, kuri ją pagimdė: mažas, teisingas pakeitimas to, kada spartieji klavišai pristabdomi, ir React išardymo bagas, iškritęs iš vienos eilutės, skirtos padaryti redaktorių saugesnį.

Inžinerija
React
Desktop