Steven
Steven8 min citire

Scoaterea backend-ului din calea de upload

GeekBye îți înregistrează ecranul și salvează videoclipul în Google Drive-ul tău. Prima versiune trimitea fiecare înregistrare prin serverele proprii ale GeekBye pe drumul într-acolo; o lansare mai târziu, fișierul mergea direct de la mașina ta către Drive, iar backend-ul a fost retrogradat la a ține un singur pointer. Partea interesantă e cât de puțin cod conține de fapt versiunea „directă, resumabilă" — pentru că resumabilitatea a venit din ștergerea unui proxy, nu din scrierea unuia.

Inginerie
Arhitectură
Desktop
Lansări GeekBye
Scoaterea backend-ului din calea de upload

GeekBye îți poate înregistra ecranul — video, audio de sistem și microfon — și poate depune automat înregistrarea finalizată în Google Drive-ul tău. Între v1.8.11 și v1.8.13, descrierea funcționalității văzută de utilizator abia s-a schimbat, dar calea pe care o parcurge videoclipul ca să ajungă la Drive a fost redirecționată complet. În prima versiune, înregistrarea ta călătorea prin serverele GeekBye. În a doua, mergea direct de la mașina ta către Drive-ul tău, iar backend-ul GeekBye n-a văzut niciodată vreun octet din ea. Această redirecționare e toată povestea, iar partea satisfăcătoare e că versiunea „mai bună" conține vizibil mai puțin cod decât cea pe care a înlocuit-o.

Pipeline-ul, de la capăt la capăt

Înregistrarea se întâmplă în renderer. ScreenRecordingService.ts creează un MediaRecorder peste stream-ul de captură cu { mimeType: 'video/webm;codecs=vp9,opus', videoBitsPerSecond: 1_000_000 } și îl pornește cu un timeslice de o secundă (CHUNK_INTERVAL_MS = 1000). Fiecare blob ondataavailable e trimis prin IPC către procesul principal, unde handler-ul recording:save-chunk îl adaugă la un fișier pe disc cu fs.promises.appendFile. Merită semnalat devreme, pentru că e un lucru tentant de interpretat greșit: acea cadență de o secundă e ritmul scrierii pe disc, nu o dimensiune de chunk de upload. Până când începe upload-ul, există un singur fișier WebM finalizat stând pe disc.

Ceea ce diferă între cele două lansări e doar ce se întâmplă cu acel fișier finalizat.

Înainte: totul prin backend (v1.8.11)

Calea de upload din v1.8.11, în CloudUploader.processRecording(), e designul evident și o primă versiune complet rezonabilă:

  1. POST /api/geekbye/recordings pentru a crea un record în backend cu metadatele înregistrării.
  2. Citește întregul video în memorie și îl trimite prin POST către backend ca multipart — const fileBuffer = fs.readFileSync(filePath) — lovind POST /api/geekbye/recordings/${id}/upload. Backend-ul apoi încarcă acel fișier în Drive și returnează un driveUrl.
  3. POST …/process pentru a trimite transcrierea pentru analiză AI.

Detaliul portant e pasul 2: clientul încarcă toată înregistrarea în memorie și fiecare octet al ei tranzitează serverele proprii ale GeekBye pe drumul către Drive. Pentru un clip scurt e în regulă. Pentru o înregistrare de ecran lungă sunt trei probleme deodată — presiune de memorie pe client de la readFileSync, cost de bandă și de retransmisie pe backend pentru un fișier pe care nici măcar nu-l păstrează, și un singur POST monolitic care trebuie să repornească de la zero dacă rețeaua sughiță la jumătatea drumului.

Merită de asemenea să fim onești că acest „înainte" nu era vreun design original imaculat. Funcționalitatea v1.8.11 a comprimat împreună un pivot din aceeași săptămână: o variantă timpurie încărca în Cloudflare R2 prin URL-uri pre-semnate și converta WebM în MP4 cu un ffmpeg-static inclus; asta a fost înlocuită de fluxul Drive mediat de backend; și apoi conversia ffmpeg a fost smulsă complet în favoarea încărcării WebM-ului ca atare (commit-ul notează că a coborât bitrate-ul și a produs fișiere de aproximativ patru ori mai mici — o estimare inline, nu un benchmark). Așa că până și „înainte"-le învățase deja o dată să șteargă o dependență nativă. Următoarea lansare avea să șteargă și proxy-ul.

După: direct în Drive (v1.8.13)

v1.8.13 rescrie CloudUploader.processRecording() în jurul unei singure propoziții din commit-ul care l-a livrat: încarcă direct în Drive, „keeping backend for metadata record + AI transcript analysis only." Noua cale:

  1. POST /api/geekbye/recordingsdoar metadate (titlu, durată, dimensiunea fișierului, format: 'webm'). Comentariul din cod e direct: // Create backend record (metadata only — no file upload).
  2. Încarcă videoclipul direct în Google Drive, sărind complet peste intermediarul backend.
  3. PATCH /api/geekbye/recordings/${backendRecordingId} cu { driveUrl, driveFolderId } rezultat — record-ului din backend i se spune unde a ajuns fișierul.
  4. Transcrierea merge în continuare la …/process pentru analiză.

Backend-ul a trecut de la a fi în calea fișierului la a i se spune despre fișier după fapt. Ține un rând și un pointer; octeții trăiesc doar pe discul utilizatorului și în Drive-ul utilizatorului.

Să devină un client Drive real

Pentru ca clientul să vorbească direct cu Drive, trebuie să fie un client Google Drive API veritabil, iar v1.8.13 adaugă mecanismele pentru asta: un nou DriveService (clientul Drive), un nou DriveAuthRepository susținut de o nouă tabelă SQLite drive_auth și SDK-ul googleapis ca dependență.

Autorizarea e un transfer curat de capabilitate. Endpoint-ul de app-config al backend-ului transmite credențialele de client Google OAuth — un clientId și un clientSecret — către aplicație, pe care CloudUploader le citește și le pasează la driveService.initialize(...). De acolo aplicația desktop rulează singură tot fluxul OAuth: ridică un server http temporar pe un port 127.0.0.1 aleatoriu, deschide ecranul de consimțământ în browserul de sistem cu shell.openExternal, prinde redirectul pe un /callback și schimbă codul pe token-uri cu google-auth-library. Acele token-uri sunt salvate local — saveDriveAuth(access_token, refresh_token, expiry_date) în drive_auth — cu scope-ul drive.file, care lasă aplicația să gestioneze doar fișierele pe care le creează. Reîmprospătarea e tot locală: un listener oauth2Client.on('tokens', …) scrie token-urile reîmprospătate direct înapoi în tabelă. Backend-ul predă capabilitatea o dată și apoi rămâne în afara buclei.

Partea „resumabilă", sincer

Iată detaliul despre care vreau cel mai mult să fiu sincer, pentru că e exact genul de lucru pe care un changelog îl rotunjește. Nota de lansare spune „resumable uploads", și asta e adevărat din locul unde stă utilizatorul. Dar GeekBye nu a implementat protocolul resumabil. Nu există nicio constantă de dimensiune de chunk, niciun tracker de offset de octeți și nicio gestionare Content-Range / 308 Resume Incomplete nicăieri în sursa GeekBye. Fă grep după ele și nu obții nimic — trăiesc în interiorul SDK-ului googleapis.

Ceea ce face codul propriu al GeekBye e asta:

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

Două alegeri fac toată treaba. Întâi, media.body e un stream (createReadStream), nu un buffer — și pasarea unui stream e exact ceea ce face googleapis să negocieze o sesiune resumabilă în numele tău în loc să facă totul dintr-un foc. Al doilea, progresul e citit din callback-ul onUploadProgress al SDK-ului față de fileStats.size. Asta e întreaga implementare de „resumable upload" la nivel de aplicație: stream la intrare, progres la ieșire. Protocolul greu — deschide o sesiune, încarcă în chunk-uri, reia de la un offset după o cădere — e treaba SDK-ului, iar decizia corectă de inginerie a fost să-l lăsăm să facă acea treabă în loc s-o reinventăm.

Ceea ce reîncadrează toată lansarea. Îmbunătățirea de fiabilitate n-a venit din scrierea unui motor de upload. A venit din ștergerea proxy-ului care stătea în fața unuia — și, mai devreme în aceeași săptămână, din ștergerea pasului ffmpeg din fața aceluia. Mai puțin cod, mai multă reziliență, pentru că lucrul rezilient era o bibliotecă bine încercată în momentul în care ai încetat s-o hrănești manual cu un buffer.

Bucățile care sunt cu adevărat ale GeekBye

Predarea transferului către SDK nu înseamnă că n-a mai rămas nimic de construit. Două bucăți de logică reală de aplicație îl înconjoară.

Un rollback pentru foldere orfane. Fiecare înregistrare primește propriul subfolder Drive (creat cu mimeType: 'application/vnd.google-apps.folder'), iar transcrierea aterizează acolo ca al doilea fișier lângă video. Dacă upload-ul aruncă o excepție, catch-ul șterge acel folder — this.drive.files.delete({ fileId: folderId }), logat ca Rolled back orphaned Drive folder — astfel încât un upload eșuat să nu lase o dâră de foldere goale în Drive-ul tău. Crearea unui container înainte să știi că operația va reuși e o mică datorie; a face calea de eșec să-l curețe e reparația.

Ciclul de viață în jurul unui singleton. DriveService e un singleton care ține un client OAuth în memorie, iar acel client din memorie nu supraviețuiește unei reporniri a aplicației, chiar dacă token-urile o fac (sunt în bază de date). Așa că CloudUploader reinițializează DriveService din token-urile stocate înainte de fiecare upload, ceea ce înseamnă că o înregistrare făcută imediat după relansare încă se încarcă fără să-ți ceară să te reconectezi. Iar trecerea de review a transformat handler-ul drive-connect în fire-and-forget, pentru că un apel de conectare blocant bloca polling-ul de status al renderer-ului — UI-ul nu putea afișa „connecting…" dacă apelul de conectare ținea thread-ul. Acestea două — reinițializarea-din-stocare și conectarea neblocantă — sunt cazurile-limită reale ale căii directe și, în mod notabil, niciunul dintre ele nu e despre chunking de upload. Când delegi partea grea, bug-urile cu care rămâi sunt bug-uri de ciclu de viață.

Trei lucruri pe care ni le-a învățat această lansare

  1. Întreabă pe unde curg octeții, nu doar dacă funcționează. Proxy-ul din v1.8.11 funcționa. Dar rutarea fiecărei înregistrări prin serverele tale proprii te costă bandă, memorie și capacitate de repornire pentru un fișier pe care nu-l păstrezi. Redesenarea căii datelor astfel încât clientul să vorbească direct cu destinația e adesea toată optimizarea.
  2. Cel mai bun upload resumabil e cel pe care nu l-ai scris. Pasarea unui stream către un SDK matur a cumpărat o sesiune resumabilă completă pentru două linii de cod. Instinctul de a scrie manual chunking-ul și matematica offset-ului ar fi produs mai mult cod și mai puțină fiabilitate.
  3. Când delegi nucleul, moștenești ciclul de viață, nu algoritmi. Bug-urile reale ale clientului direct erau „reinițializează singleton-ul după repornire" și „nu bloca apelul de conectare", nu offset-uri de octeți. E un compromis bun — bug-urile de ciclu de viață sunt vizibile și locale; bug-urile de protocol nu sunt niciuna.

Pentru capitolul anterior din povestea v1, născut și reparat în douăsprezece minute (v1.8.10); iar pentru întregul arc, anatomia livrării de software până la perfecțiune.

Articole Similare

Cele trei verbe care mențin Web Audio în viață
Steven
Steven9 min citire

Cele trei verbe care mențin Web Audio în viață

Două lansări punctuale GeekBye, la două luni distanță și în două fișiere diferite, au învățat codul nostru audio aceeași lecție din capete opuse: nu mai trata AudioContext-ul browserului ca pe ceva de unică folosință. O lansare a învățat să apeleze resume() pe un context pe care macOS îl suspendase în tăcere în mijlocul înregistrării; cealaltă a învățat să apeleze suspend() în loc de close(), ca sesiunile consecutive să nu mai izbească plafonul Chromium de aproximativ șase contexte. Resume, suspend, close — asta e toată intriga.

Inginerie
Audio
Desktop
A deosebi un apel de o aplicație deschisă
Steven
Steven9 min citire

A deosebi un apel de o aplicație deschisă

GeekBye poate observa că te-ai alăturat unei ședințe video și îți poate oferi s-o înregistreze. Detecția se dovedește a fi jumătatea ușoară — un binar Swift care citește titlurile ferestrelor la fiecare zece secunde. Jumătatea grea e precizia: să nu se declanșeze când Zoom e doar deschis, să nu întrebe pentru o ședință pe care deja o înregistrezi și să nu silențieze microfonul din apelul în care chiar te afli. Trei lansări, iar fiecare e o gardă care a trebuit să învețe să nu se învingă singură.

Inginerie
macOS
Desktop
Născut și reparat în douăsprezece minute
Steven
Steven8 min citire

Născut și reparat în douăsprezece minute

Changelog-ul GeekBye v1.8.10 spune că a reparat un crash în timpul editării scurtăturilor de tastatură. Chiar a făcut-o — dar crash-ul a fost introdus și reparat în același pull request, la douăsprezece minute distanță, și nu a ajuns niciodată la vreun utilizator. Povestea reală e cascada de fiabilitate care l-a produs: o schimbare mică și corectă a momentului când scurtăturile sunt suspendate, și bug-ul de demontare React care a căzut dintr-o singură linie menită să facă editorul mai sigur.

Inginerie
React
Desktop