
Wyjmowanie backendu ze ścieżki przesyłania
GeekBye nagrywa twój ekran i zapisuje wideo na twój Google Drive. Pierwsza wersja przesyłała każde nagranie przez własne serwery GeekBye po drodze; wydanie później plik szedł prosto z twojej maszyny na Drive, a backend został zdegradowany do trzymania jednego wskaźnika. Ciekawą częścią jest to, jak mało kodu naprawdę zawiera wersja „bezpośrednia, wznawialna" — bo wznawialność wzięła się z usunięcia proxy, a nie z napisania go.
GeekBye potrafi nagrać twój ekran — wideo, dźwięk systemowy i mikrofon — i automatycznie upuścić gotowe nagranie na twój Google Drive. Między v1.8.11 a v1.8.13 opis funkcji widziany przez użytkownika ledwie się zmienił, ale ścieżka, którą wideo pokonuje, by dotrzeć na Drive, została całkowicie przekierowana. W pierwszej wersji twoje nagranie podróżowało przez serwery GeekBye. W drugiej szło prosto z twojej maszyny na twój Drive, a backend GeekBye nigdy nie zobaczył ani jednego jego bajtu. To przekierowanie jest całą historią, a satysfakcjonującą częścią jest to, że „lepsza" wersja zawiera zauważalnie mniej kodu niż ta, którą zastąpiła.
Potok, od początku do końca
Nagrywanie dzieje się w rendererze. ScreenRecordingService.ts tworzy MediaRecorder na strumieniu przechwytywania z { mimeType: 'video/webm;codecs=vp9,opus', videoBitsPerSecond: 1_000_000 } i uruchamia go z jednosekundowym wycinkiem czasu (CHUNK_INTERVAL_MS = 1000). Każdy blob ondataavailable jest wysyłany przez IPC do procesu głównego, gdzie handler recording:save-chunk dokleja go do pliku na dysku za pomocą fs.promises.appendFile. Warto zaznaczyć od razu, bo łatwo to źle odczytać: ten jednosekundowy rytm to rytm zapisu na dysk, a nie rozmiar fragmentu przesyłania. Zanim rozpocznie się przesyłanie, na dysku leży pojedynczy gotowy plik WebM.
To, co różni oba wydania, to tylko to, co dzieje się z tym gotowym plikiem.
Przedtem: wszystko przez backend (v1.8.11)
Ścieżka przesyłania w v1.8.11, w CloudUploader.processRecording(), to oczywisty projekt i całkowicie rozsądna pierwsza wersja:
POST /api/geekbye/recordings, by utworzyć rekord backendu z metadanymi nagrania.- Wczytaj całe wideo do pamięci i wyślij je POST-em na backend jako multipart —
const fileBuffer = fs.readFileSync(filePath)— trafiając wPOST /api/geekbye/recordings/${id}/upload. Backend następnie wgrywa ten plik na Drive i zwracadriveUrl. POST …/process, by wysłać transkrypcję do analizy AI.
Kluczowym szczegółem jest krok 2: klient wczytuje całe nagranie do pamięci, a każdy jego bajt przechodzi przez własne serwery GeekBye w drodze na Drive. Dla krótkiego klipu to w porządku. Dla długiego nagrania ekranu to trzy problemy naraz — presja na pamięć klienta ze strony readFileSync, koszt przepustowości i przekazywania po stronie backendu za plik, którego nawet nie zatrzymuje, oraz pojedynczy monolityczny POST, który musi zacząć od zera, jeśli sieć czknie w połowie.
Warto też być uczciwym, że to „przedtem" nie było jakimś nieskazitelnym pierwotnym projektem. Funkcja z v1.8.11 sklejała w sobie zwrot dokonany w ciągu jednego tygodnia: wczesna wersja wgrywała na Cloudflare R2 przez wstępnie podpisane URL-e i konwertowała WebM na MP4 dołączonym ffmpeg-static; zostało to zastąpione przepływem Drive pośredniczonym przez backend; a potem konwersja ffmpeg została całkowicie wyrwana na rzecz wgrywania WebM tak, jak jest (commit odnotowuje, że obniżyło to bitrate i dało pliki mniej więcej czterokrotnie mniejsze — szacunek z głowy, nie benchmark). Więc nawet „przedtem" już raz nauczyło się usuwać natywną zależność. Następne wydanie usunęłoby też proxy.
Potem: prosto na Drive (v1.8.13)
v1.8.13 przepisuje CloudUploader.processRecording() wokół jednego zdania z commita, który je wysłał: wgrywaj bezpośrednio na Drive, „zostawiając backend tylko dla rekordu metadanych + analizy transkrypcji AI." Nowa ścieżka:
POST /api/geekbye/recordings— tylko metadane (tytuł, czas trwania, rozmiar pliku,format: 'webm'). Komentarz w kodzie jest bez ogródek:// Create backend record (metadata only — no file upload).- Wgraj wideo bezpośrednio na Google Drive, całkowicie pomijając pośrednika-backend.
PATCH /api/geekbye/recordings/${backendRecordingId}z wynikowym{ driveUrl, driveFolderId }— rekordowi backendu mówi się, gdzie plik wylądował.- Transkrypcja wciąż idzie do
…/processpo analizę.
Backend przeszedł od bycia w ścieżce pliku do bycia informowanym o pliku po fakcie. Trzyma wiersz i wskaźnik; bajty żyją tylko na dysku użytkownika i na jego Drive.
Stając się prawdziwym klientem Drive
By klient mógł rozmawiać z Drive bezpośrednio, musi być faktycznym klientem Google Drive API, i v1.8.13 dodaje do tego maszynerię: nowy DriveService (klient Drive), nowe DriveAuthRepository oparte na nowej tabeli SQLite drive_auth oraz SDK googleapis jako zależność.
Autoryzacja to czyste przekazanie uprawnień. Endpoint konfiguracji aplikacji na backendzie przekazuje poświadczenia klienta Google OAuth — clientId i clientSecret — w dół do aplikacji, którą CloudUploader odczytuje i przekazuje do driveService.initialize(...). Stamtąd aplikacja desktopowa sama przeprowadza cały przepływ OAuth: stawia tymczasowy serwer http na losowym porcie 127.0.0.1, otwiera ekran zgody w przeglądarce systemowej za pomocą shell.openExternal, łapie przekierowanie na /callback i wymienia kod na tokeny za pomocą google-auth-library. Te tokeny są zapisywane lokalnie — saveDriveAuth(access_token, refresh_token, expiry_date) do drive_auth — z zakresem drive.file, który pozwala aplikacji zarządzać tylko plikami, które sama tworzy. Odświeżanie też jest lokalne: nasłuch oauth2Client.on('tokens', …) zapisuje odświeżone tokeny prosto z powrotem do tabeli. Backend przekazuje uprawnienie raz, a potem trzyma się z dala.
Część „wznawialna", uczciwie
Oto szczegół, co do którego najbardziej chcę być szczery, bo to dokładnie ten rodzaj rzeczy, który changelog zaokrągla. Nota do wydania mówi „wznawialne przesyłanie" i to prawda z perspektywy użytkownika. Ale GeekBye nie zaimplementował protokołu wznawialnego. Nigdzie w źródle GeekBye nie ma stałej rozmiaru fragmentu, trackera przesunięcia bajtowego ani obsługi Content-Range / 308 Resume Incomplete. Poszukaj ich grepem, a nie dostaniesz nic — żyją wewnątrz SDK googleapis.
To, co robi własny kod GeekBye, jest takie:
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)
},
},
)
Dwa wybory robią całą robotę. Po pierwsze, media.body to strumień (createReadStream), a nie bufor — i przekazanie strumienia to dokładnie to, co sprawia, że googleapis negocjuje w twoim imieniu sesję wznawialną, zamiast robić to za jednym zamachem. Po drugie, postęp jest odczytywany z callbacku onUploadProgress SDK względem fileStats.size. To cała implementacja „wznawialnego przesyłania" na poziomie aplikacji: strumień w środku, postęp na zewnątrz. Trudny protokół — otwórz sesję, wgrywaj we fragmentach, wznów od przesunięcia po zerwaniu — to zadanie SDK, a poprawną decyzją inżynierską było pozwolić mu wykonać to zadanie, zamiast wynajdywać je od nowa.
Co przeformułowuje całe wydanie. Poprawa niezawodności nie wzięła się z napisania silnika przesyłania. Wzięła się z usunięcia proxy, które stało przed jednym — a wcześniej tego samego tygodnia, z usunięcia kroku ffmpeg stojącego przed tamtym. Mniej kodu, więcej odporności, bo odporną rzeczą była wysłużona biblioteka w chwili, gdy przestałeś ręcznie karmić ją buforem.
Kawałki, które są autentycznie GeekBye
Przekazanie transferu do SDK nie znaczy, że nie zostało nic do zbudowania. Dwa kawałki prawdziwej logiki aplikacji go otaczają.
Wycofanie osieroconego folderu. Każde nagranie dostaje własny podfolder Drive (utworzony z mimeType: 'application/vnd.google-apps.folder'), a transkrypcja ląduje tam jako drugi plik obok wideo. Jeśli przesyłanie rzuci wyjątek, catch usuwa ten folder — this.drive.files.delete({ fileId: folderId }), logowane jako Rolled back orphaned Drive folder — więc nieudane przesyłanie nie zostawia śladu pustych folderów na twoim Drive. Tworzenie kontenera, zanim wiesz, że operacja się powiedzie, to małe ryzyko; sprawienie, że ścieżka błędu go sprząta, jest naprawą.
Cykl życia wokół singletona. DriveService to singleton trzymający w pamięci klienta OAuth, a ten klient w pamięci nie przeżywa restartu aplikacji, mimo że tokeny tak (są w bazie danych). Więc CloudUploader re-inicjalizuje DriveService z zapisanych tokenów przed każdym przesłaniem, co znaczy, że nagranie zrobione tuż po ponownym uruchomieniu wciąż się wgrywa, nie prosząc cię o ponowne połączenie. A przegląd zamienił handler drive-connect w fire-and-forget, bo blokujące wywołanie connect zatrzymywało odpytywanie statusu przez renderer — UI nie mógł pokazać „łączenie…", jeśli wywołanie connect trzymało wątek. Te dwa — re-inicjalizacja z pamięci i nieblokujący connect — to prawdziwe przypadki brzegowe ścieżki bezpośredniej, i co znamienne, żaden z nich nie dotyczy fragmentowania przesyłania. Gdy delegujesz trudną część, błędy, które ci zostają, to błędy cyklu życia.
Trzy rzeczy, których nauczyło nas to wydanie
- Pytaj, gdzie płyną bajty, nie tylko czy działa. Proxy z v1.8.11 działało. Ale trasowanie każdego nagrania przez własne serwery kosztuje cię przepustowość, pamięć i wznawialność — za plik, którego nie zatrzymujesz. Przerysowanie ścieżki danych tak, by klient rozmawiał z celem bezpośrednio, jest często całą optymalizacją.
- Najlepsze wznawialne przesyłanie to takie, którego nie napisałeś. Przekazanie strumienia dojrzałemu SDK kupiło pełną sesję wznawialną za dwie linijki kodu. Instynkt, by ręcznie sklecić fragmentowanie i matematykę przesunięć, dałby więcej kodu i mniej niezawodności.
- Gdy delegujesz rdzeń, dziedziczysz cykl życia, nie algorytmy. Prawdziwymi błędami klienta bezpośredniego były „re-inicjalizuj singleton po restarcie" i „nie blokuj wywołania connect", a nie przesunięcia bajtowe. To dobry handel — błędy cyklu życia są widoczne i lokalne; błędy protokołu nie są ani jednym, ani drugim.
Po poprzedni rozdział historii v1 — narodzony i naprawiony w dwanaście minut (v1.8.10); a po cały łuk — anatomia dostarczania oprogramowania do perfekcji.