Steven
Stevenโ€ขโ€ข8 menit baca

Mengeluarkan Backend dari Jalur Unggah

GeekBye merekam layarmu dan menyimpan videonya ke Google Drive-mu. Versi pertama mengirim setiap rekaman melalui server milik GeekBye sendiri dalam perjalanannya; satu rilis kemudian, file itu pergi langsung dari mesinmu ke Drive, dan backend diturunkan menjadi hanya memegang satu pointer. Bagian menariknya adalah betapa sedikit kode yang sebenarnya dimuat versi 'langsung, resumable' itu โ€” karena kemampuan-melanjutkan datang dari menghapus sebuah proxy, bukan dari menulis satu.

Engineering
Architecture
Desktop
GeekBye Releases
Mengeluarkan Backend dari Jalur Unggah

GeekBye bisa merekam layarmu โ€” video, audio sistem, dan mik โ€” dan menjatuhkan rekaman yang selesai ke Google Drive-mu secara otomatis. Antara v1.8.11 dan v1.8.13, deskripsi fitur yang menghadap-pengguna nyaris tak berubah, tapi jalur yang ditempuh video untuk sampai ke Drive dialihkan sepenuhnya. Di versi pertama, rekamanmu bepergian melalui server GeekBye. Di yang kedua, ia pergi langsung dari mesinmu ke Drive-mu, dan backend GeekBye tak pernah melihat satu byte pun darinya. Pengalihan itu adalah keseluruhan ceritanya, dan bagian yang memuaskan adalah bahwa versi "lebih baik" memuat kode yang mencolok lebih sedikit daripada yang ia gantikan.

Pipeline itu, ujung ke ujung

Perekaman terjadi di renderer. ScreenRecordingService.ts membuat sebuah MediaRecorder di atas capture stream dengan { mimeType: 'video/webm;codecs=vp9,opus', videoBitsPerSecond: 1_000_000 } dan memulainya dengan sebuah timeslice satu detik (CHUNK_INTERVAL_MS = 1000). Setiap blob ondataavailable dikirim lewat IPC ke main process, di mana handler recording:save-chunk menambahkannya ke sebuah file di disk dengan fs.promises.appendFile. Layak ditandai lebih awal, karena ini hal yang menggoda untuk disalahbaca: irama satu-detik itu adalah ritme tulis-ke-disk, bukan sebuah ukuran potongan unggahan. Pada saat unggahan mulai, ada satu file WebM selesai yang duduk di disk.

Yang berbeda antara kedua rilis hanyalah apa yang terjadi pada file yang selesai itu.

Sebelumnya: semua lewat backend (v1.8.11)

Jalur unggah v1.8.11, di CloudUploader.processRecording(), adalah desain yang jelas dan sebuah versi pertama yang sepenuhnya masuk akal:

  1. POST /api/geekbye/recordings untuk membuat sebuah record backend dengan metadata rekaman.
  2. Baca seluruh video ke memori dan POST ke backend sebagai multipart โ€” const fileBuffer = fs.readFileSync(filePath) โ€” mengenai POST /api/geekbye/recordings/${id}/upload. Backend lalu mengunggah file itu ke Drive dan mengembalikan sebuah driveUrl.
  3. POST โ€ฆ/process untuk mengirim transkrip demi analisis AI.

Detail penyangganya adalah langkah 2: klien memuat seluruh rekaman ke memori dan setiap byte-nya melintasi server milik GeekBye sendiri dalam perjalanan ke Drive. Untuk klip pendek itu tak apa. Untuk rekaman layar panjang itu tiga masalah sekaligus โ€” tekanan memori pada klien dari readFileSync, biaya bandwidth dan relay pada backend untuk sebuah file yang bahkan tak ia simpan, dan satu POST monolitik yang harus memulai ulang dari nol jika jaringan tersendat di tengah jalan.

Layak juga jujur bahwa "sebelumnya" ini bukanlah suatu desain asli yang murni. Fitur v1.8.11 memampatkan sebuah pivot dalam-seminggu: sebuah potongan awal mengunggah ke Cloudflare R2 via URL yang telah ditandatangani sebelumnya dan mengonversi WebM ke MP4 dengan sebuah ffmpeg-static yang dibundel; itu diganti oleh alur Drive yang dimediasi backend; dan lalu konversi ffmpeg dicabut seluruhnya demi mengunggah WebM apa adanya (commit-nya mencatat bahwa itu menurunkan bitrate dan menghasilkan file kira-kira empat kali lebih kecil โ€” sebuah perkiraan inline, bukan sebuah benchmark). Jadi bahkan "sebelumnya" pun sudah pernah belajar menghapus sebuah dependensi native sekali. Rilis berikutnya akan menghapus proxy juga.

Sesudahnya: langsung ke Drive (v1.8.13)

v1.8.13 menulis ulang CloudUploader.processRecording() seputar satu kalimat dari commit yang mengirimnya: unggah langsung ke Drive, "keeping backend for metadata record + AI transcript analysis only." Jalur baru:

  1. POST /api/geekbye/recordings โ€” hanya metadata (judul, durasi, ukuran file, format: 'webm'). Komentar di kode itu blak-blakan: // Create backend record (metadata only โ€” no file upload).
  2. Unggah video langsung ke Google Drive, melewati perantara backend sepenuhnya.
  3. PATCH /api/geekbye/recordings/${backendRecordingId} dengan { driveUrl, driveFolderId } yang dihasilkan โ€” record backend diberi tahu di mana file berakhir.
  4. Transkrip masih pergi ke โ€ฆ/process untuk analisis.

Backend berubah dari berada di dalam jalur file menjadi diberi tahu tentang file setelah faktanya. Ia memegang sebuah baris dan sebuah pointer; byte-nya hidup hanya di disk pengguna dan di Drive pengguna.

Menjadi klien Drive sungguhan

Agar klien bicara ke Drive secara langsung, ia harus menjadi sebuah klien Google Drive API yang sebenarnya, dan v1.8.13 menambahkan mesin untuk itu: sebuah DriveService baru (klien Drive), sebuah DriveAuthRepository baru yang didukung oleh sebuah tabel SQLite drive_auth baru, dan SDK googleapis sebagai sebuah dependensi.

Otorisasinya adalah serah-terima kapabilitas yang bersih. Endpoint app-config milik backend meneruskan kredensial klien Google OAuth โ€” sebuah clientId dan clientSecret โ€” turun ke aplikasi, yang CloudUploader baca dan oper ke driveService.initialize(...). Dari sana aplikasi desktop menjalankan seluruh alur OAuth sendiri: ia mendirikan sebuah server http sementara di sebuah port 127.0.0.1 acak, membuka layar persetujuan di browser sistem dengan shell.openExternal, menangkap redirect di sebuah /callback, dan menukar kode itu dengan token menggunakan google-auth-library. Token-token itu disimpan secara lokal โ€” saveDriveAuth(access_token, refresh_token, expiry_date) ke drive_auth โ€” dengan scope drive.file, yang membuat aplikasi hanya mengelola file yang ia buat. Penyegaran juga lokal: sebuah listener oauth2Client.on('tokens', โ€ฆ) menulis token yang disegarkan langsung kembali ke tabel. Backend menyerahkan kapabilitas sekali dan lalu tetap keluar dari lingkaran.

Bagian "resumable", sejujurnya

Inilah detail yang paling ingin kujelaskan terus-terang, karena ini persis jenis hal yang dibulatkan sebuah changelog. Catatan rilis bilang "resumable uploads," dan itu benar dari tempat pengguna berdiri. Tapi GeekBye tidak mengimplementasikan protokol resumable. Tak ada konstanta ukuran-potongan, tak ada pelacak byte-offset, dan tak ada penanganan Content-Range / 308 Resume Incomplete di mana pun dalam sumber GeekBye. Grep untuk mereka dan kamu tak dapat apa-apa โ€” mereka hidup di dalam SDK googleapis.

Yang dilakukan kode GeekBye sendiri adalah ini:

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

Dua pilihan melakukan semua pekerjaan. Pertama, media.body adalah sebuah stream (createReadStream), bukan sebuah buffer โ€” dan mengoper sebuah stream persis adalah yang membuat googleapis menegosiasikan sebuah sesi resumable atas namamu alih-alih melakukan sekali tembak. Kedua, progres dibaca dari callback onUploadProgress milik SDK terhadap fileStats.size. Itulah keseluruhan implementasi "resumable upload" di tingkat aplikasi: stream masuk, progres keluar. Protokol yang sulit โ€” membuka sebuah sesi, mengunggah dalam potongan, melanjutkan dari sebuah offset setelah sebuah putus โ€” adalah pekerjaan SDK, dan keputusan rekayasa yang benar adalah membiarkannya melakukan pekerjaan itu alih-alih menciptakannya ulang.

Yang membingkai ulang keseluruhan rilis. Peningkatan keandalan tak datang dari menulis sebuah mesin unggah. Ia datang dari menghapus proxy yang duduk di depan satu โ€” dan, lebih awal di minggu yang sama, menghapus langkah ffmpeg di depan itu. Lebih sedikit kode, lebih banyak ketahanan, karena hal yang tahan itu adalah sebuah library yang sudah teruji begitu kamu berhenti menyuapinya sebuah buffer dengan tangan.

Bagian-bagian yang sungguh-sungguh milik GeekBye

Menyerahkan transfer ke SDK tak berarti tak ada yang tersisa untuk dibangun. Dua potong logika aplikasi nyata mengelilinginya.

Sebuah rollback folder-yatim. Setiap rekaman mendapat subfolder Drive-nya sendiri (dibuat dengan mimeType: 'application/vnd.google-apps.folder'), dan transkrip mendarat di sana sebagai file kedua di samping video. Jika unggahan melempar, catch menghapus folder itu โ€” this.drive.files.delete({ fileId: folderId }), dicatat sebagai Rolled back orphaned Drive folder โ€” sehingga sebuah unggahan yang gagal tak meninggalkan jejak folder kosong di Drive-mu. Membuat sebuah kontainer sebelum kamu tahu operasinya akan berhasil adalah sebuah liabilitas kecil; membuat jalur kegagalan membersihkannya adalah perbaikannya.

Siklus hidup di seputar sebuah singleton. DriveService adalah sebuah singleton yang memegang sebuah klien OAuth in-memory, dan klien in-memory itu tak selamat dari sebuah restart aplikasi walau token selamat (mereka ada di database). Jadi CloudUploader menginisialisasi ulang DriveService dari token yang tersimpan sebelum setiap unggahan, yang berarti sebuah rekaman yang dibuat tepat setelah peluncuran-ulang tetap terunggah tanpa memintamu terhubung kembali. Dan lintasan tinjauan mengubah handler drive-connect menjadi fire-and-forget, karena sebuah panggilan connect yang memblokir sedang mengganjal polling status renderer โ€” UI tak bisa menampilkan "connectingโ€ฆ" jika panggilan connect menahan thread. Dua itu โ€” inisialisasi-ulang-dari-penyimpanan dan connect non-blocking โ€” adalah kasus tepi sejati dari jalur langsung, dan yang penting keduanya bukan tentang chunking unggahan. Ketika kamu mendelegasikan bagian yang sulit, bug yang tersisa padamu adalah bug siklus hidup.

Tiga hal yang diajarkan rilis ini kepada kami

  1. Tanyakan ke mana byte mengalir, bukan hanya apakah ia bekerja. Proxy v1.8.11 bekerja. Tapi merutekan setiap rekaman melalui server-mu sendiri membebanimu bandwidth, memori, dan kemampuan-restart untuk sebuah file yang tak kamu simpan. Menggambar ulang jalur data agar klien bicara ke tujuan secara langsung sering kali adalah keseluruhan optimasinya.
  2. Unggahan resumable terbaik adalah yang tak kamu tulis. Mengoper sebuah stream ke sebuah SDK matang membeli sebuah sesi resumable penuh seharga dua baris kode. Insting untuk membuat sendiri chunking dan matematika offset akan menghasilkan lebih banyak kode dan lebih sedikit keandalan.
  3. Ketika kamu mendelegasikan inti, kamu mewarisi siklus hidup, bukan algoritma. Bug sejati klien langsung adalah "inisialisasi ulang singleton setelah restart" dan "jangan blokir panggilan connect," bukan byte offset. Itu pertukaran yang bagus โ€” bug siklus hidup terlihat dan lokal; bug protokol bukan keduanya.

Untuk bab sebelumnya dalam kisah v1, lahir dan diperbaiki dalam dua belas menit (v1.8.10); dan untuk keseluruhan alurnya, anatomi merilis software sampai sempurna.

Artikel Terkait

Tiga Kata Kerja yang Menjaga Web Audio Tetap Hidup
Steven
Stevenโ€ขโ€ข8 menit baca

Tiga Kata Kerja yang Menjaga Web Audio Tetap Hidup

Dua point release GeekBye, terpaut dua bulan dan di dua berkas berbeda, mengajari kode audio kami pelajaran yang sama dari ujung yang berlawanan: berhenti memperlakukan AudioContext milik browser sebagai barang sekali pakai. Satu rilis belajar untuk resume() sebuah context yang diam-diam disuspend macOS di tengah perekaman; yang lain belajar untuk suspend() alih-alih close() agar sesi beruntun berhenti menabrak plafon kira-kira enam context milik Chromium. Resume, suspend, close โ€” itulah keseluruhan plotnya.

Engineering
Audio
Desktop
Membedakan Panggilan dari Aplikasi yang Terbuka
Steven
Stevenโ€ขโ€ข8 menit baca

Membedakan Panggilan dari Aplikasi yang Terbuka

GeekBye bisa memperhatikan bahwa kamu telah bergabung ke sebuah rapat video dan menawarkan untuk merekamnya. Deteksinya ternyata paruh yang mudah โ€” sebuah binary Swift yang membaca judul jendela setiap sepuluh detik. Paruh yang sulit adalah presisi: tidak memicu saat Zoom sekadar terbuka, tidak menawari sebuah rapat yang sudah kamu rekam, dan tidak membisukan mikrofon di panggilan yang sebenarnya kamu ikuti. Tiga rilis, dan masing-masing adalah sebuah penjaga yang harus belajar untuk tidak mengalahkan dirinya sendiri.

Engineering
macOS
Desktop
Lahir dan Diperbaiki dalam Dua Belas Menit
Steven
Stevenโ€ขโ€ข7 menit baca

Lahir dan Diperbaiki dalam Dua Belas Menit

Changelog GeekBye v1.8.10 bilang ia memperbaiki sebuah crash saat mengedit pintasan keyboard. Memang begitu โ€” tapi crash itu diperkenalkan dan diperbaiki dalam pull request yang sama, terpaut dua belas menit, dan tak pernah mencapai satu pun pengguna. Kisah sebenarnya adalah kaskade keandalan yang menghasilkannya: sebuah perubahan kecil dan benar tentang kapan pintasan ditangguhkan, dan bug teardown React yang jatuh dari satu baris yang dimaksudkan untuk membuat editor lebih aman.

Engineering
React
Desktop