Steven
Steven6 min lukuaika

Backendin poistaminen latauspolusta

GeekBye tallentaa näyttösi ja tallentaa videon Google Driveesi. Ensimmäinen versio toimitti jokaisen tallenteen GeekByen omien palvelimien kautta matkalla sinne; yksi julkaisu myöhemmin tiedosto meni suoraan koneeltasi Driveen, ja backend alennettiin yhden osoittimen pitämiseen. Kiinnostava osa on se, miten vähän koodia 'suora, jatkettava' versio oikeasti sisältää — koska jatkettavuus tuli välityspalvelimen poistamisesta, ei sen kirjoittamisesta.

Ohjelmistokehitys
Arkkitehtuuri
Desktop
GeekBye-julkaisut
Backendin poistaminen latauspolusta

GeekBye osaa tallentaa näyttösi — video, järjestelmän ääni ja mikrofoni — ja pudottaa valmiin tallenteen automaattisesti Google Driveesi. v1.8.11:n ja v1.8.13:n välillä ominaisuuden käyttäjälle näkyvä kuvaus tuskin muuttui, mutta polku jonka video kulkee Driveen ohjattiin täysin uudelleen. Ensimmäisessä versiossa tallenteesi matkasi GeekByen palvelimien läpi. Toisessa se meni suoraan koneeltasi Driveesi, eikä GeekByen backend nähnyt siitä ainuttakaan tavua. Tuo uudelleenohjaus on koko tarina, ja tyydyttävää on se, että "parempi" versio sisältää silmiinpistävän vähemmän koodia kuin se jonka se korvasi.

Putki, päästä päähän

Tallennus tapahtuu renderöijässä. ScreenRecordingService.ts luo MediaRecorder:n kaappausvirran päälle { mimeType: 'video/webm;codecs=vp9,opus', videoBitsPerSecond: 1_000_000 }:lla ja käynnistää sen yhden sekunnin aikaviipaleella (CHUNK_INTERVAL_MS = 1000). Jokainen ondataavailable-blob lähetetään IPC:n yli pääprosessiin, jossa recording:save-chunk-käsittelijä liittää sen levyllä olevaan tiedostoon fs.promises.appendFile:lla. Kannattaa merkitä varhain, koska tätä on houkuttelevaa lukea väärin: tuo yhden sekunnin tahti on levylle-kirjoituksen rytmi, ei latauksen palakoko. Siihen mennessä kun lataus alkaa, levyllä on yksi valmis WebM-tiedosto.

Se mikä eroaa kahden julkaisun välillä on vain se mitä tuolle valmiille tiedostolle tapahtuu.

Ennen: kaikki backendin läpi (v1.8.11)

v1.8.11:n latauspolku tiedostossa CloudUploader.processRecording() on ilmeinen suunnittelu ja täysin järkevä ensimmäinen versio:

  1. POST /api/geekbye/recordings luodaksesi backend-tietueen tallenteen metadatalla.
  2. Lue koko video muistiin ja POST se backendiin multipart-muodossa — const fileBuffer = fs.readFileSync(filePath) — osuen POST /api/geekbye/recordings/${id}/upload:iin. Backend sitten lataa tuon tiedoston Driveen ja palauttaa driveUrl:n.
  3. POST …/process lähettääksesi transkription AI-analyysiin.

Kantava yksityiskohta on vaihe 2: asiakas lataa koko tallenteen muistiin, ja jokainen sen tavu kulkee GeekByen omien palvelimien läpi matkalla Driveen. Lyhyelle klipille se on ihan hyvä. Pitkälle näyttötallenteelle se on kolme ongelmaa kerralla — muistipaine asiakkaalle readFileSync:stä, kaistanleveys- ja välityskustannus backendille tiedostosta jota se ei edes säilytä, ja yksi monoliittinen POST jonka on käynnistyttävä nollasta jos verkko nikottelee puolivälissä.

Kannattaa myös olla rehellinen siitä että tämä "ennen" ei ollut mikään koskematon alkuperäinen suunnittelu. v1.8.11:n ominaisuus litisti yhteen saman viikon sisäisen käännöksen: varhainen versio latasi Cloudflare R2:een esiallekirjoitettujen URL:ien kautta ja muunsi WebM:n MP4:ksi niputetulla ffmpeg-static:lla; se korvattiin backend-välitteisellä Drive-kululla; ja sitten ffmpeg-muunnos revittiin kokonaan pois WebM:n lataamisen hyväksi sellaisenaan (commit huomauttaa että se laski bittinopeutta ja tuotti noin nelinkertaisesti pienempiä tiedostoja — sisäinen arvio, ei vertailutesti). Joten jopa "ennen" oli jo oppinut poistamaan natiiviriippuvuuden kerran. Seuraava julkaisu poistaisi myös välityspalvelimen.

Jälkeen: suoraan Driveen (v1.8.13)

v1.8.13 kirjoittaa CloudUploader.processRecording():n uudelleen yhden virkkeen ympärille sen commitista joka sen toimitti: lataa suoraan Driveen, "keeping backend for metadata record + AI transcript analysis only." Uusi polku:

  1. POST /api/geekbye/recordingsvain metadata (otsikko, kesto, tiedostokoko, format: 'webm'). Kommentti koodissa on tyly: // Create backend record (metadata only — no file upload).
  2. Lataa video suoraan Google Driveen, ohittaen backend-välikäden kokonaan.
  3. PATCH /api/geekbye/recordings/${backendRecordingId} syntyneellä { driveUrl, driveFolderId }:llä — backend-tietueelle kerrotaan minne tiedosto päätyi.
  4. Transkriptio menee yhä …/process:iin analyysia varten.

Backend siirtyi olemasta tiedoston polulla siihen että sille kerrotaan tiedostosta jälkikäteen. Se pitää rivin ja osoittimen; tavut asuvat vain käyttäjän levyllä ja käyttäjän Drivessä.

Oikeaksi Drive-asiakkaaksi tuleminen

Jotta asiakas voisi puhua Driven kanssa suoraan, sen on oltava aito Google Drive API -asiakas, ja v1.8.13 lisää siihen koneiston: uuden DriveService:n (Drive-asiakas), uuden DriveAuthRepository:n jota tukee uusi drive_auth SQLite-taulu, ja googleapis-SDK:n riippuvuutena.

Valtuutus on siisti kyvykkyyden luovutus. Backendin sovelluskonfiguraatio-päätepiste välittää Google OAuth -asiakastunnuksetclientId:n ja clientSecret:n — alas sovellukseen, jotka CloudUploader lukee ja välittää driveService.initialize(...):lle. Siitä työpöytäsovellus ajaa koko OAuth-kulun itse: se pystyttää väliaikaisen http-palvelimen satunnaiseen 127.0.0.1-porttiin, avaa suostumusnäytön järjestelmäselaimessa shell.openExternal:lla, nappaa uudelleenohjauksen /callback:ssa ja vaihtaa koodin tokeneihin google-auth-library:lla. Nuo tokenit tallennetaan paikallisesti — saveDriveAuth(access_token, refresh_token, expiry_date) drive_auth:iin — drive.file-scopella, joka antaa sovelluksen hallita vain luomiaan tiedostoja. Päivitys on myös paikallinen: oauth2Client.on('tokens', …)-kuuntelija kirjoittaa päivitetyt tokenit suoraan takaisin tauluun. Backend luovuttaa kyvykkyyden kerran ja pysyy sitten silmukan ulkopuolella.

"Jatkettava"-osa, rehellisesti

Tässä on yksityiskohta josta haluan eniten olla suora, koska se on juuri sellainen asia jonka muutosloki pyöristää. Julkaisumuistiinpano sanoo "resumable uploads", ja siitä mihin käyttäjä seisoo se on totta. Mutta GeekBye ei toteuttanut jatkettavaa protokollaa. GeekByen lähdekoodissa ei ole missään palakoko-vakiota, tavusiirtymä-seurainta eikä Content-Range / 308 Resume Incomplete -käsittelyä. Grep niitä etkä saa mitään — ne asuvat googleapis-SDK:n sisällä.

Se mitä GeekByen oma koodi tekee on tämä:

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

Kaksi valintaa tekee koko työn. Ensiksi, media.body on virta (createReadStream), ei puskuri — ja virran välittäminen on juuri se mikä saa googleapis:n neuvottelemaan jatkettavan istunnon puolestasi sen sijaan että se tekisi yhden laukauksen. Toiseksi, edistyminen luetaan SDK:n onUploadProgress-takaisinkutsusta fileStats.size:ä vasten. Siinä on koko "jatkettavan latauksen" toteutus sovellustasolla: virta sisään, edistyminen ulos. Vaikea protokolla — avaa istunto, lataa paloissa, jatka siirtymästä katkoksen jälkeen — on SDK:n työtä, ja oikea insinööripäätös oli antaa sen tehdä tuo työ sen sijaan että sen keksisi uudelleen.

Mikä kehystää koko julkaisun uudelleen. Luotettavuusparannus ei tullut latausmoottorin kirjoittamisesta. Se tuli välityspalvelimen poistamisesta joka istui sen edessä — ja aiemmin samalla viikolla ffmpeg-vaiheen poistamisesta sen edessä. Vähemmän koodia, enemmän kestävyyttä, koska kestävä asia oli hyvin kulunut kirjasto sillä hetkellä kun lopetit sen käsin syöttämisen puskurilla.

Palat jotka ovat aidosti GeekByen

Siirron luovuttaminen SDK:lle ei tarkoita ettei jäänyt mitään rakennettavaa. Kaksi palaa aitoa sovelluslogiikkaa ympäröi sitä.

Orpokansion palautus. Jokainen tallenne saa oman Drive-alikansionsa (luotu mimeType: 'application/vnd.google-apps.folder':lla), ja transkriptio laskeutuu sinne toisena tiedostona videon viereen. Jos lataus heittää, catch poistaa tuon kansion — this.drive.files.delete({ fileId: folderId }), lokitettuna nimellä Rolled back orphaned Drive folder — niin ettei epäonnistunut lataus jätä tyhjien kansioiden jälkeä Driveesi. Säiliön luominen ennen kuin tiedät operaation onnistuvan on pieni vastuu; epäonnistumispolun laittaminen siivoamaan se on korjaus.

Elinkaari singletonin ympärillä. DriveService on singleton joka pitää muistinvaraista OAuth-asiakasta, eikä tuo muistinvarainen asiakas selviä sovelluksen uudelleenkäynnistyksestä vaikka tokenit selviävät (ne ovat tietokannassa). Joten CloudUploader alustaa DriveService:n tallennetuista tokeneista uudelleen ennen jokaista latausta, mikä tarkoittaa että heti uudelleenkäynnistyksen jälkeen tehty tallenne latautuu yhä pyytämättä sinua yhdistämään uudelleen. Ja katselmointikierros muutti drive-connect-käsittelijän tulita-ja-unohda-tyyppiseksi, koska estävä yhdistämiskutsu jumitti renderöijän tilan pollausta — UI ei voinut näyttää "yhdistetään…" jos yhdistämiskutsu piti säiettä. Nämä kaksi — uudelleenalustus tallennuksesta ja estämätön yhdistäminen — ovat suoran polun todelliset reunatapaukset, eikä kumpikaan niistä huomionarvoisesti koske latauksen palottelua. Kun delegoit vaikean osan, jäljelle jäävät bugit ovat elinkaaribugeja.

Kolme asiaa jotka tämä julkaisu opetti meille

  1. Kysy mihin tavut virtaavat, älä vain toimiiko se. v1.8.11:n välityspalvelin toimi. Mutta jokaisen tallenteen reitittäminen omien palvelimiesi läpi maksaa sinulle kaistanleveyttä, muistia ja jatkettavuutta tiedostosta jota et säilytä. Datapolun uudelleenpiirtäminen niin että asiakas puhuu määränpään kanssa suoraan on usein koko optimointi.
  2. Paras jatkettava lataus on se jota et kirjoittanut. Virran välittäminen kypsälle SDK:lle osti täyden jatkettavan istunnon kahdella koodirivillä. Vaisto tehdä palottelu ja siirtymämatematiikka käsin olisi tuottanut enemmän koodia ja vähemmän luotettavuutta.
  3. Kun delegoit ytimen, perit elinkaaren, et algoritmeja. Suoran asiakkaan todelliset bugit olivat "alusta singleton uudelleen käynnistyksen jälkeen" ja "älä estä yhdistämiskutsua", ei tavusiirtymiä. Se on hyvä vaihtokauppa — elinkaaribugit ovat näkyviä ja paikallisia; protokollabugit eivät kumpaakaan.

Edellinen luku v1-tarinassa: syntyi ja korjattiin kahdessatoista minuutissa (v1.8.10); ja koko kaarelle, ohjelmiston toimittamisen anatomia täydellisyyteen asti.

Aiheeseen liittyvät artikkelit

Kolme verbiä, jotka pitävät Web Audion elossa
Steven
Steven7 min lukuaika

Kolme verbiä, jotka pitävät Web Audion elossa

Kaksi GeekBye-julkaisua, kahden kuukauden välein ja kahdessa eri tiedostossa, opettivat ääni-koodillemme saman opetuksen vastakkaisista päistä: lakkaa kohtelemasta selaimen AudioContextia kertakäyttöisenä. Yksi julkaisu oppi kutsumaan resume() kontekstille, jonka macOS oli hiljaa keskeyttänyt kesken tallennuksen; toinen oppi kutsumaan suspend() eikä close(), niin että peräkkäiset istunnot lakkaavat törmäämästä Chromiumin noin-kuuden-kontekstin kattoon. Resume, suspend, close — siinä on koko juoni.

Ohjelmistokehitys
Audio
Desktop
Puhelun erottaminen avoimesta sovelluksesta
Steven
Steven7 min lukuaika

Puhelun erottaminen avoimesta sovelluksesta

GeekBye voi huomata, että olet liittynyt videokokoukseen, ja tarjoutua tallentamaan sen. Tunnistus osoittautuu helpommaksi puoliskoksi — Swift-binääri, joka lukee ikkunoiden otsikoita joka kymmenes sekunti. Vaikea puolisko on tarkkuus: olla laukeamatta kun Zoom on vain auki, olla tarjoamatta kokousta jota jo tallennat, ja olla mykistämättä mikrofonia puhelussa jossa oikeasti olet. Kolme julkaisua, ja jokainen niistä on vartija jonka oli opittava olemaan kukistamatta itseään.

Ohjelmistokehitys
macOS
Desktop
Syntyi ja korjattiin kahdessatoista minuutissa
Steven
Steven6 min lukuaika

Syntyi ja korjattiin kahdessatoista minuutissa

GeekBye v1.8.10:n muutosloki sanoo että se korjasi kaatumisen näppäinoikoteitä muokatessa. Niin se teki — mutta kaatuminen esiteltiin ja korjattiin samassa pull requestissa, kahdentoista minuutin välein, eikä se koskaan tavoittanut ainuttakaan käyttäjää. Todellinen tarina on luotettavuuskaskadi joka sen tuotti: pieni, oikea muutos siihen milloin oikotiet keskeytetään, ja React-purkubugi joka putosi yhdestä rivistä jonka piti tehdä editorista turvallisempi.

Ohjelmistokehitys
React
Desktop