Steven
Steven8 Min. Lesezeit

Das Backend aus dem Upload-Pfad nehmen

GeekBye nimmt deinen Bildschirm auf und speichert das Video auf deinem Google Drive. Die erste Version schickte jede Aufnahme unterwegs durch GeekByes eigene Server; eine Version später ging die Datei direkt von deiner Maschine zu Drive, und das Backend wurde zum Halten eines einzigen Zeigers degradiert. Das Interessante ist, wie wenig Code die „direkte, fortsetzbare“ Version tatsächlich enthält — denn die Fortsetzbarkeit kam vom Löschen eines Proxys, nicht vom Schreiben eines.

Technik
Architektur
Desktop
GeekBye-Releases
Das Backend aus dem Upload-Pfad nehmen

GeekBye kann deinen Bildschirm aufnehmen — Video, Systemaudio und Mikrofon — und die fertige Aufnahme automatisch in dein Google Drive ablegen. Zwischen v1.8.11 und v1.8.13 änderte sich die nutzerseitige Beschreibung der Funktion kaum, aber der Pfad, den das Video zu Drive nimmt, wurde komplett umgeleitet. In der ersten Version reiste deine Aufnahme durch GeekByes Server. In der zweiten ging sie direkt von deiner Maschine zu deinem Drive, und GeekByes Backend sah nie ein Byte davon. Diese Umleitung ist die ganze Geschichte, und das Befriedigende daran ist, dass die „bessere“ Version auffällig weniger Code enthält als die, die sie ersetzte.

Die Pipeline, von Ende zu Ende

Die Aufnahme passiert im Renderer. ScreenRecordingService.ts erstellt einen MediaRecorder über dem Capture-Stream mit { mimeType: 'video/webm;codecs=vp9,opus', videoBitsPerSecond: 1_000_000 } und startet ihn mit einem Ein-Sekunden-Timeslice (CHUNK_INTERVAL_MS = 1000). Jeder ondataavailable-Blob wird über IPC an den Hauptprozess geschickt, wo der recording:save-chunk-Handler ihn mit fs.promises.appendFile an eine Datei auf der Festplatte anhängt. Früh erwähnenswert, weil es verlockend falsch zu lesen ist: dieser Ein-Sekunden-Takt ist der Festplatten-Schreib-Rhythmus, keine Upload-Chunk-Größe. Wenn der Upload startet, liegt eine einzige fertige WebM-Datei auf der Festplatte.

Was sich zwischen den beiden Releases unterscheidet, ist nur, was mit dieser fertigen Datei passiert.

Das Davor: alles durch das Backend (v1.8.11)

Der Upload-Pfad der v1.8.11, in CloudUploader.processRecording(), ist das naheliegende Design und eine völlig vernünftige erste Version:

  1. POST /api/geekbye/recordings, um einen Backend-Datensatz mit den Metadaten der Aufnahme zu erstellen.
  2. Das ganze Video in den Speicher lesen und es als multipart per POST an das Backend schicken — const fileBuffer = fs.readFileSync(filePath) — an POST /api/geekbye/recordings/${id}/upload. Das Backend lädt diese Datei dann zu Drive hoch und gibt einen driveUrl zurück.
  3. POST …/process, um das Transkript zur KI-Analyse zu senden.

Das tragende Detail ist Schritt 2: der Client lädt die ganze Aufnahme in den Speicher, und jedes ihrer Bytes durchquert GeekByes eigene Server auf dem Weg zu Drive. Für einen kurzen Clip ist das in Ordnung. Für eine lange Bildschirmaufnahme sind es drei Probleme auf einmal — Speicherdruck auf dem Client durch readFileSync, Bandbreiten- und Relay-Kosten auf dem Backend für eine Datei, die es nicht einmal behält, und ein einziger monolithischer POST, der bei null neu starten muss, wenn das Netzwerk auf halbem Weg schluckt.

Es lohnt sich auch, ehrlich zu sein: dieses „Davor“ war kein makelloses Originaldesign. Die Funktion der v1.8.11 quetschte einen Pivot innerhalb einer Woche zusammen: eine frühe Fassung lud über vorsignierte URLs zu Cloudflare R2 hoch und konvertierte WebM mit einem gebündelten ffmpeg-static zu MP4; das wurde durch den backend-vermittelten Drive-Flow ersetzt; und dann wurde die ffmpeg-Konvertierung ganz herausgerissen zugunsten des Hochladens des WebM wie es ist (der Commit notiert, dass es die Bitrate senkte und ungefähr viermal kleinere Dateien erzeugte — eine beiläufige Schätzung, kein Benchmark). Also hatte selbst das „Davor“ schon einmal gelernt, eine native Abhängigkeit zu löschen. Das nächste Release würde auch den Proxy löschen.

Das Danach: direkt zu Drive (v1.8.13)

v1.8.13 schreibt CloudUploader.processRecording() um einen einzigen Satz aus dem Commit herum, der es auslieferte: direkt zu Drive hochladen, "keeping backend for metadata record + AI transcript analysis only." Der neue Pfad:

  1. POST /api/geekbye/recordingsnur Metadaten (Titel, Dauer, Dateigröße, format: 'webm'). Der Kommentar im Code ist unverblümt: // Create backend record (metadata only — no file upload).
  2. Das Video direkt zu Google Drive hochladen und den Backend-Mittelsmann komplett überspringen.
  3. PATCH /api/geekbye/recordings/${backendRecordingId} mit dem resultierenden { driveUrl, driveFolderId } — dem Backend-Datensatz wird mitgeteilt, wo die Datei gelandet ist.
  4. Das Transkript geht weiterhin zur Analyse an …/process.

Das Backend ging davon, im Pfad der Datei zu sein, dazu über, nach der Tatsache über die Datei informiert zu werden. Es hält eine Zeile und einen Zeiger; die Bytes leben nur auf der Festplatte des Nutzers und im Drive des Nutzers.

Ein echter Drive-Client werden

Damit der Client direkt mit Drive spricht, muss er ein tatsächlicher Google-Drive-API-Client sein, und v1.8.13 fügt die Maschinerie dafür hinzu: einen neuen DriveService (den Drive-Client), ein neues DriveAuthRepository, gestützt auf eine neue drive_auth-SQLite-Tabelle, und das googleapis-SDK als Abhängigkeit.

Die Autorisierung ist eine saubere Fähigkeits-Übergabe. Der App-Config-Endpoint des Backends leitet Google-OAuth-Client-Credentials — eine clientId und ein clientSecret — an die App hinunter, die CloudUploader liest und an driveService.initialize(...) übergibt. Von da an führt die Desktop-App den ganzen OAuth-Flow selbst aus: sie stellt einen temporären http-Server auf einem zufälligen 127.0.0.1-Port auf, öffnet den Zustimmungsbildschirm im Systembrowser mit shell.openExternal, fängt die Weiterleitung auf einem /callback ab, und tauscht den Code mit google-auth-library gegen Tokens. Diese Tokens werden lokal gespeichert — saveDriveAuth(access_token, refresh_token, expiry_date) in drive_auth — mit dem drive.file-Scope, der die App nur die von ihr erstellten Dateien verwalten lässt. Das Erneuern ist auch lokal: ein oauth2Client.on('tokens', …)-Listener schreibt erneuerte Tokens direkt zurück in die Tabelle. Das Backend übergibt die Fähigkeit einmal und bleibt dann außerhalb der Schleife.

Der „fortsetzbare“ Teil, ehrlich

Hier ist das Detail, bei dem ich am meisten geradeheraus sein will, weil es genau die Art von Sache ist, die ein Changelog abrundet. Die Release-Note sagt „fortsetzbare Uploads“, und das stimmt aus Sicht des Nutzers. Aber GeekBye hat das resumable-Protokoll nicht implementiert. Es gibt keine Chunk-Größen-Konstante, keinen Byte-Offset-Tracker und keine Content-Range / 308 Resume Incomplete-Behandlung irgendwo in GeekByes Quellcode. Grepe danach und du bekommst nichts — sie leben im googleapis-SDK.

Was GeekByes eigener Code tut, ist dies:

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

Zwei Entscheidungen erledigen die ganze Arbeit. Erstens ist media.body ein Stream (createReadStream), kein Buffer — und einen Stream zu übergeben ist genau das, was googleapis dazu bringt, in deinem Namen eine fortsetzbare Sitzung auszuhandeln, statt alles auf einmal zu tun. Zweitens wird der Fortschritt vom onUploadProgress-Callback des SDK gegen fileStats.size gelesen. Das ist die gesamte „fortsetzbarer Upload“-Implementierung auf Anwendungsebene: Stream rein, Fortschritt raus. Das harte Protokoll — eine Sitzung öffnen, in Chunks hochladen, nach einem Abbruch von einem Offset fortsetzen — ist die Aufgabe des SDK, und die richtige Engineering-Entscheidung war, es diese Aufgabe tun zu lassen, statt sie neu zu erfinden.

Was das ganze Release neu rahmt. Die Zuverlässigkeitsverbesserung kam nicht vom Schreiben einer Upload-Engine. Sie kam vom Löschen des Proxys, der vor einer saß — und, früher in derselben Woche, vom Löschen des ffmpeg-Schritts, der vor dem saß. Weniger Code, mehr Widerstandsfähigkeit, weil das widerstandsfähige Ding eine gut abgehangene Bibliothek war, sobald du aufhörtest, sie von Hand mit einem Buffer zu füttern.

Die Teile, die wirklich GeekByes sind

Den Transfer an das SDK zu übergeben heißt nicht, dass nichts mehr zu bauen blieb. Zwei Stücke echter Anwendungslogik umgeben ihn.

Ein Rollback für verwaiste Ordner. Jede Aufnahme bekommt ihren eigenen Drive-Unterordner (erstellt mit mimeType: 'application/vnd.google-apps.folder'), und das Transkript landet dort als zweite Datei neben dem Video. Wenn der Upload wirft, löscht das catch diesen Ordner — this.drive.files.delete({ fileId: folderId }), protokolliert als Rolled back orphaned Drive folder — damit ein fehlgeschlagener Upload keine Spur leerer Ordner in deinem Drive hinterlässt. Einen Container zu erstellen, bevor du weißt, dass die Operation gelingt, ist eine kleine Bürde; den Fehlerpfad ihn aufräumen zu lassen, ist die Behebung.

Lebenszyklus um ein Singleton. DriveService ist ein Singleton, das einen In-Memory-OAuth-Client hält, und dieser In-Memory-Client überlebt einen App-Neustart nicht, obwohl die Tokens es tun (sie sind in der Datenbank). Also reinitialisiert CloudUploader DriveService vor jedem Upload aus den gespeicherten Tokens, was bedeutet, dass eine direkt nach dem Neustart gemachte Aufnahme trotzdem hochlädt, ohne dich zum Neuverbinden aufzufordern. Und der Review-Durchgang machte den drive-connect-Handler zu fire-and-forget, weil ein blockierender Connect-Aufruf das Status-Polling des Renderers ins Stocken brachte — die UI konnte kein „Verbinden…“ zeigen, wenn der Connect-Aufruf den Thread festhielt. Diese beiden — Reinit-aus-dem-Speicher und nicht-blockierender Connect — sind die echten Randfälle des direkten Pfads, und bemerkenswerterweise geht es bei keinem von ihnen um Upload-Chunking. Wenn du den harten Teil delegierst, sind die Bugs, die dir bleiben, Lebenszyklus-Bugs.

Drei Dinge, die uns dieses Release lehrte

  1. Frag, wohin die Bytes fließen, nicht nur, ob es funktioniert. Der Proxy der v1.8.11 funktionierte. Aber jede Aufnahme durch deine eigenen Server zu leiten kostet dich Bandbreite, Speicher und Neustartfähigkeit für eine Datei, die du nicht behältst. Den Datenpfad so neu zu zeichnen, dass der Client direkt mit dem Ziel spricht, ist oft die ganze Optimierung.
  2. Der beste fortsetzbare Upload ist der, den du nicht geschrieben hast. Einen Stream an ein ausgereiftes SDK zu übergeben erkaufte eine vollständige fortsetzbare Sitzung für zwei Zeilen Code. Der Instinkt, Chunking und Offset-Mathematik von Hand zu bauen, hätte mehr Code und weniger Zuverlässigkeit erzeugt.
  3. Wenn du den Kern delegierst, erbst du Lebenszyklus, keine Algorithmen. Die echten Bugs des direkten Clients waren „das Singleton nach dem Neustart reinitialisieren“ und „den Connect-Aufruf nicht blockieren“, keine Byte-Offsets. Das ist ein guter Handel — Lebenszyklus-Bugs sind sichtbar und lokal; Protokoll-Bugs sind keins von beiden.

Für das vorige Kapitel der v1-Geschichte, geboren und behoben in zwölf Minuten (v1.8.10); und für den ganzen Bogen, die Anatomie, Software bis zur Perfektion auszuliefern.

Ähnliche Artikel

Die drei Verben, die Web Audio am Leben halten
Steven
Steven9 Min. Lesezeit

Die drei Verben, die Web Audio am Leben halten

Zwei punktuelle GeekBye-Releases, zwei Monate auseinander und in zwei verschiedenen Dateien, brachten unserem Audio-Code dieselbe Lektion von entgegengesetzten Enden bei: den AudioContext des Browsers nicht länger als Wegwerfobjekt zu behandeln. Ein Release lernte, einen Kontext per resume() wiederaufzunehmen, den macOS mitten in der Aufnahme still suspendiert hatte; das andere lernte, suspend() statt close() aufzurufen, damit aufeinanderfolgende Sitzungen nicht länger gegen Chromiums Decke von etwa sechs Kontexten krachen. Resume, suspend, close — das ist die ganze Handlung.

Technik
Audio
Desktop
Einen Anruf von einer offenen App unterscheiden
Steven
Steven8 Min. Lesezeit

Einen Anruf von einer offenen App unterscheiden

GeekBye kann bemerken, dass du einem Videomeeting beigetreten bist, und anbieten, es aufzunehmen. Die Erkennung erweist sich als die einfache Hälfte — ein Swift-Binary, das alle zehn Sekunden Fenstertitel liest. Die harte Hälfte ist Präzision: nicht auszulösen, wenn Zoom bloß offen ist, nicht für ein Meeting zu fragen, das du bereits aufnimmst, und nicht das Mikrofon in dem Anruf stummzuschalten, in dem du tatsächlich bist. Drei Releases, und jedes ist ein Wächter, der lernen musste, sich nicht selbst zu besiegen.

Technik
macOS
Desktop
Geboren und behoben in zwölf Minuten
Steven
Steven7 Min. Lesezeit

Geboren und behoben in zwölf Minuten

Das Changelog von GeekBye v1.8.10 sagt, es habe einen Crash beim Bearbeiten von Tastaturkürzeln behoben. Hat es — aber der Crash wurde im selben Pull Request eingeführt und behoben, zwölf Minuten auseinander, und erreichte keinen einzigen Nutzer. Die eigentliche Geschichte ist die Zuverlässigkeitskaskade, die ihn hervorbrachte: eine kleine, korrekte Änderung daran, wann Kürzel suspendiert werden, und der React-Teardown-Bug, der aus einer einzigen Zeile fiel, die den Editor sicherer machen sollte.

Technik
React
Desktop