
เอา backend ออกจากเส้นทางอัปโหลด
GeekBye บันทึกหน้าจอของคุณและเซฟวิดีโอลง Google Drive ของคุณ เวอร์ชันแรกส่งทุกการบันทึกผ่านเซิร์ฟเวอร์ของ GeekBye เองระหว่างทางไปที่นั่น; อีกหนึ่งรีลีสถัดมา ไฟล์ก็วิ่งตรงจากเครื่องของคุณไป Drive และ backend ถูกลดขั้นเหลือเพียงถือ pointer หนึ่งตัว ส่วนที่น่าสนใจคือเวอร์ชัน «ตรง, resumable» นั้นจริง ๆ แล้วบรรจุโค้ดน้อยแค่ไหน — เพราะความ resumable มาจากการลบ proxy ทิ้ง ไม่ใช่จากการเขียนมันขึ้นมา
GeekBye บันทึกหน้าจอของคุณได้ — วิดีโอ, เสียงระบบ, และไมค์ — และหย่อนการบันทึกที่เสร็จแล้วลง Google Drive ของคุณโดยอัตโนมัติ ระหว่าง v1.8.11 กับ v1.8.13 คำอธิบายที่ผู้ใช้เห็นของฟีเจอร์นี้แทบไม่เปลี่ยน แต่เส้นทางที่วิดีโอใช้เพื่อไปถึง Drive ถูกเปลี่ยนใหม่ทั้งหมด ในเวอร์ชันแรก การบันทึกของคุณเดินทางผ่านเซิร์ฟเวอร์ของ GeekBye ในเวอร์ชันที่สอง มันวิ่งตรงจากเครื่องของคุณไป Drive ของคุณ และ backend ของ GeekBye ไม่เคยเห็น byte ของมันเลยสักตัว การเปลี่ยนเส้นทางนั้นคือเรื่องราวทั้งหมด และส่วนที่น่าพอใจคือเวอร์ชัน «ดีกว่า» นั้นบรรจุโค้ดน้อยกว่าตัวที่มันแทนที่อย่างเห็นได้ชัด
ไปป์ไลน์ ตั้งแต่ต้นจนจบ
การบันทึกเกิดใน renderer ScreenRecordingService.ts สร้าง MediaRecorder บน capture stream ด้วย { mimeType: 'video/webm;codecs=vp9,opus', videoBitsPerSecond: 1_000_000 } และเริ่มมันด้วย timeslice หนึ่งวินาที (CHUNK_INTERVAL_MS = 1000) ทุก blob จาก ondataavailable ถูกส่งข้าม IPC ไปที่ main process ซึ่ง handler recording:save-chunk ต่อท้ายมันเข้ากับไฟล์บนดิสก์ด้วย fs.promises.appendFile คุ้มที่จะปักธงไว้แต่เนิ่น ๆ เพราะมันเป็นสิ่งที่ชวนอ่านผิด: จังหวะหนึ่งวินาทีนั้นคือจังหวะ_เขียนดิสก์_ ไม่ใช่ chunk size ของการอัปโหลด พอถึงตอนที่การอัปโหลดเริ่ม บนดิสก์ก็มีไฟล์ WebM ที่เสร็จแล้วเพียงไฟล์เดียวนั่งอยู่
สิ่งที่ต่างกันระหว่างสองรีลีส มีเพียงว่าเกิดอะไรขึ้นกับไฟล์ที่เสร็จแล้วนั้น
ก่อนหน้า: ทุกอย่างผ่าน backend (v1.8.11)
เส้นทางอัปโหลดของ v1.8.11 ใน CloudUploader.processRecording() คือดีไซน์ที่เห็นชัด และเป็นเวอร์ชันแรกที่สมเหตุสมผลอย่างสมบูรณ์:
POST /api/geekbye/recordingsเพื่อสร้าง backend record พร้อม metadata ของการบันทึก- อ่านวิดีโอทั้งไฟล์เข้า memory แล้ว POST มันไปที่ backend เป็น multipart —
const fileBuffer = fs.readFileSync(filePath)— ยิงไปที่POST /api/geekbye/recordings/${id}/uploadจากนั้น backend อัปโหลดไฟล์นั้นขึ้น Drive แล้วคืนdriveUrl POST …/processเพื่อส่ง transcript ไปวิเคราะห์ด้วย AI
รายละเอียดที่รับน้ำหนักคือขั้นที่ 2: client โหลดการบันทึกทั้งอันเข้า memory และทุก byte ของมันวิ่งผ่านเซิร์ฟเวอร์ของ GeekBye เองระหว่างทางไป Drive สำหรับคลิปสั้น ๆ นั่นไม่เป็นไร สำหรับการบันทึกหน้าจอยาว ๆ มันคือสามปัญหาพร้อมกัน — แรงกด memory บน client จาก readFileSync, ต้นทุน bandwidth และการรีเลย์บน backend สำหรับไฟล์ที่มันไม่ได้เก็บด้วยซ้ำ, และ POST ก้อนเดียวเป็นแผ่นเดียวที่ต้อง_เริ่มใหม่จากศูนย์_ถ้าเน็ตสะอึกกลางคัน
ยังคุ้มที่จะซื่อสัตย์ว่า «ก่อนหน้า» นี้ไม่ได้เป็นดีไซน์ดั้งเดิมที่บริสุทธิ์อะไร ฟีเจอร์ v1.8.11 อัดการ pivot ภายในสัปดาห์ไว้ด้วยกัน: คัตแรก ๆ อัปโหลดไป Cloudflare R2 ผ่าน pre-signed URL และแปลง WebM เป็น MP4 ด้วย ffmpeg-static ที่ bundle มา; นั่นถูกแทนด้วย flow ของ Drive ที่ backend เป็นสื่อกลาง; แล้วการแปลง ffmpeg ก็ถูกดึงออกทั้งหมดเพื่อหันมาอัปโหลด WebM ตามที่เป็น (commit จดว่ามันลด bitrate ลงและได้ไฟล์เล็กลงราวสี่เท่า — เป็นการประเมินในบรรทัด ไม่ใช่ benchmark) ดังนั้นแม้แต่ «ก่อนหน้า» ก็ได้เรียนรู้ที่จะลบ native dependency ไปแล้วหนึ่งครั้ง รีลีสถัดไปจะลบ proxy ทิ้งด้วย
หลังจากนั้น: ตรงไป Drive (v1.8.13)
v1.8.13 เขียน CloudUploader.processRecording() ใหม่รอบประโยคหนึ่งจาก commit ที่ ship มัน: อัปโหลดตรงไป Drive, "keeping backend for metadata record + AI transcript analysis only." เส้นทางใหม่:
POST /api/geekbye/recordings— metadata เท่านั้น (title, duration, ขนาดไฟล์,format: 'webm') คอมเมนต์ในโค้ดพูดตรง ๆ:// Create backend record (metadata only — no file upload)- อัปโหลดวิดีโอ ตรงไป Google Drive ข้ามคนกลาง backend ไปทั้งหมด
PATCH /api/geekbye/recordings/${backendRecordingId}พร้อมด้วย{ driveUrl, driveFolderId }ที่ได้ — backend record ถูก_บอก_ว่าไฟล์ไปจบที่ไหน- transcript ยังไปที่
…/processเพื่อวิเคราะห์
backend เปลี่ยนจากการอยู่_ใน_เส้นทางของไฟล์ เป็นการถูก_บอกเกี่ยวกับ_ไฟล์หลังจากเหตุการณ์ผ่านไป มันถือแถวหนึ่งกับ pointer หนึ่ง; byte อยู่แค่บนดิสก์ของผู้ใช้และใน Drive ของผู้ใช้เท่านั้น
การกลายเป็น Drive client จริง ๆ
เพื่อให้ client คุยกับ Drive ได้โดยตรง มันต้องเป็น Google Drive API client จริง ๆ และ v1.8.13 เพิ่มเครื่องจักรสำหรับสิ่งนั้น: DriveService ใหม่ (ตัว Drive client), DriveAuthRepository ใหม่ที่หนุนหลังด้วยตาราง drive_auth SQLite ใหม่, และ googleapis SDK เป็น dependency
การ authorize คือการส่งมอบความสามารถที่สะอาด endpoint app-config ของ backend รีเลย์ client credentials ของ Google OAuth — clientId กับ clientSecret — ลงมาที่แอป ซึ่ง CloudUploader อ่านและส่งต่อให้ driveService.initialize(...) จากตรงนั้นเดสก์ท็อปแอปรัน OAuth flow ทั้งหมดเอง: มันตั้งเซิร์ฟเวอร์ http ชั่วคราวบนพอร์ต 127.0.0.1 แบบสุ่ม, เปิดหน้าจอ consent ใน browser ของระบบด้วย shell.openExternal, รับ redirect ที่ /callback, และแลก code เป็น token ด้วย google-auth-library token พวกนั้นถูกเซฟไว้ในเครื่อง — saveDriveAuth(access_token, refresh_token, expiry_date) ลงใน drive_auth — พร้อม scope drive.file ซึ่งให้แอปจัดการได้เฉพาะไฟล์ที่มันสร้างเอง การ refresh ก็เป็น local เช่นกัน: listener oauth2Client.on('tokens', …) เขียน token ที่ refresh แล้วกลับเข้าตารางตรง ๆ backend ส่งมอบความสามารถหนึ่งครั้ง แล้วก็อยู่นอกลูป
ส่วน «resumable» อย่างซื่อสัตย์
นี่คือรายละเอียดที่ผมอยากพูดให้ตรงที่สุด เพราะมันเป็นสิ่งที่ changelog ปัดเศษทิ้งเป๊ะ ๆ release note บอกว่า "resumable uploads," และจากจุดที่ผู้ใช้ยืนอยู่ นั่นจริง แต่ GeekBye ไม่ได้ implement โปรโตคอล resumable ไม่มี chunk-size constant, ไม่มี byte-offset tracker, และไม่มีการจัดการ Content-Range / 308 Resume Incomplete ที่ไหนเลยใน source ของ GeekBye grep หาพวกมันแล้วคุณจะไม่ได้อะไร — พวกมันอยู่ใน googleapis SDK
สิ่งที่โค้ดของ GeekBye เองทำคือสิ่งนี้:
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)
},
},
)
สองทางเลือกทำงานทั้งหมด อย่างแรก media.body เป็น stream (createReadStream) ไม่ใช่ buffer — และการส่ง stream นี่แหละที่ทำให้ googleapis เจรจา session แบบ resumable แทนคุณ แทนที่จะยิงทีเดียวจบ อย่างที่สอง progress ถูกอ่านจาก callback onUploadProgress ของ SDK เทียบกับ fileStats.size นั่นคือ implementation ของ «resumable upload» ทั้งหมดในระดับแอปพลิเคชัน: stream เข้า, progress ออก โปรโตคอลส่วนที่ยาก — เปิด session, อัปโหลดเป็น chunk, resume จาก offset หลังหลุด — เป็นงานของ SDK และการตัดสินใจทางวิศวกรรมที่ถูกต้องคือปล่อยให้มันทำงานนั้น แทนที่จะคิดค้นมันขึ้นใหม่
ซึ่งจัดกรอบทั้งรีลีสใหม่ การปรับปรุงความน่าเชื่อถือไม่ได้มาจากการเขียน upload engine มันมาจากการ_ลบ_ proxy ที่นั่งอยู่ข้างหน้ามัน — และก่อนหน้านั้นในสัปดาห์เดียวกัน คือการลบขั้น ffmpeg ที่อยู่ข้างหน้า_อันนั้น_ โค้ดน้อยลง, ทนทานมากขึ้น เพราะสิ่งที่ทนทานคือ library ที่ใช้กันจนช่ำชอง ในวินาทีที่คุณเลิกป้อน buffer ให้มันด้วยมือ
ส่วนที่เป็นของ GeekBye จริง ๆ
การยกการถ่ายโอนให้ SDK ไม่ได้แปลว่าไม่เหลืออะไรให้สร้าง มีตรรกะแอปพลิเคชันจริง ๆ สองชิ้นล้อมรอบมันอยู่
orphaned-folder rollback การบันทึกแต่ละอันได้ subfolder Drive ของตัวเอง (สร้างด้วย mimeType: 'application/vnd.google-apps.folder') และ transcript ลงไปที่นั่นเป็นไฟล์ที่สองข้างวิดีโอ ถ้าการอัปโหลด throw, catch ลบโฟลเดอร์นั้น — this.drive.files.delete({ fileId: folderId }), log เป็น Rolled back orphaned Drive folder — เพื่อไม่ให้อัปโหลดที่ล้มเหลวทิ้งร่องรอยโฟลเดอร์เปล่าไว้ใน Drive ของคุณ การสร้าง container ก่อนที่คุณจะรู้ว่า operation จะสำเร็จเป็นภาระเล็ก ๆ; การทำให้เส้นทางล้มเหลวเก็บกวาดมันคือทางแก้
lifecycle รอบ ๆ singleton DriveService เป็น singleton ที่ถือ OAuth client ใน memory และ client ใน memory นั้นไม่รอดจากการ restart แอป ทั้งที่ token รอด (พวกมันอยู่ในฐานข้อมูล) ดังนั้น CloudUploader re-initialize DriveService จาก token ที่เก็บไว้ ก่อนทุกการอัปโหลด ซึ่งแปลว่าการบันทึกที่ทำทันทีหลัง relaunch ก็ยังอัปโหลดได้โดยไม่ต้องขอให้คุณเชื่อมต่อใหม่ และรอบ review เปลี่ยน handler drive-connect ให้เป็น fire-and-forget เพราะ connect call ที่บล็อกกำลังทำให้ status polling ของ renderer ค้าง — UI แสดง «กำลังเชื่อมต่อ…» ไม่ได้ถ้า connect call ถือ thread ไว้ สองอย่างนั้น — re-init-จาก-storage และ connect ที่ไม่บล็อก — คือ edge case จริงของเส้นทางตรง และที่น่าสังเกตคือไม่มีอันไหนเกี่ยวกับการ chunk การอัปโหลดเลย เมื่อคุณมอบหมายส่วนที่ยากออกไป บั๊กที่เหลือให้คุณคือบั๊ก lifecycle
สามสิ่งที่รีลีสนี้สอนเรา
- ถามว่า byte ไหลไปไหน ไม่ใช่แค่ว่ามันใช้ได้ไหม proxy ของ v1.8.11 ใช้ได้ แต่การ route ทุกการบันทึกผ่านเซิร์ฟเวอร์ของคุณเอง ทำให้คุณเสีย bandwidth, memory, และความ restart ได้ สำหรับไฟล์ที่คุณไม่ได้เก็บ การวาดเส้นทางข้อมูลใหม่ให้ client คุยกับปลายทางโดยตรง บ่อยครั้งคือ optimization ทั้งหมด
- resumable upload ที่ดีที่สุดคืออันที่คุณไม่ได้เขียน การส่ง stream ให้ SDK ที่โตเต็มที่ ซื้อ session แบบ resumable เต็มรูปแบบมาได้ด้วยโค้ดสองบรรทัด สัญชาตญาณที่จะทำ chunking และเลข offset ด้วยมือ คงได้ผลเป็นโค้ดมากขึ้นและความน่าเชื่อถือน้อยลง
- เมื่อคุณมอบหมายแกนกลาง คุณรับมรดกเป็น lifecycle ไม่ใช่ algorithm บั๊กจริงของ client แบบตรงคือ «re-init singleton หลัง restart» และ «อย่าบล็อก connect call» ไม่ใช่ byte offset นั่นเป็นการแลกที่ดี — บั๊ก lifecycle มองเห็นได้และเป็นเฉพาะที่; บั๊กโปรโตคอลไม่เป็นทั้งสองอย่าง
สำหรับบทก่อนหน้าในเรื่องยุค v1 ดูถือกำเนิดและถูกแก้ในสิบสองนาที (v1.8.10); และสำหรับภาพรวมทั้งเส้นเรื่อง ดูกายวิภาคของการส่งมอบซอฟต์แวร์ให้สมบูรณ์แบบ