Steven
Steven5 นาทีที่อ่าน

เอา backend ออกจากเส้นทางอัปโหลด

GeekBye บันทึกหน้าจอของคุณและเซฟวิดีโอลง Google Drive ของคุณ เวอร์ชันแรกส่งทุกการบันทึกผ่านเซิร์ฟเวอร์ของ GeekBye เองระหว่างทางไปที่นั่น; อีกหนึ่งรีลีสถัดมา ไฟล์ก็วิ่งตรงจากเครื่องของคุณไป Drive และ backend ถูกลดขั้นเหลือเพียงถือ pointer หนึ่งตัว ส่วนที่น่าสนใจคือเวอร์ชัน «ตรง, resumable» นั้นจริง ๆ แล้วบรรจุโค้ดน้อยแค่ไหน — เพราะความ resumable มาจากการลบ proxy ทิ้ง ไม่ใช่จากการเขียนมันขึ้นมา

วิศวกรรม
สถาปัตยกรรม
Desktop
GeekBye รีลีส
เอา backend ออกจากเส้นทางอัปโหลด

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() คือดีไซน์ที่เห็นชัด และเป็นเวอร์ชันแรกที่สมเหตุสมผลอย่างสมบูรณ์:

  1. POST /api/geekbye/recordings เพื่อสร้าง backend record พร้อม metadata ของการบันทึก
  2. อ่านวิดีโอทั้งไฟล์เข้า memory แล้ว POST มันไปที่ backend เป็น multipart — const fileBuffer = fs.readFileSync(filePath) — ยิงไปที่ POST /api/geekbye/recordings/${id}/upload จากนั้น backend อัปโหลดไฟล์นั้นขึ้น Drive แล้วคืน driveUrl
  3. 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." เส้นทางใหม่:

  1. POST /api/geekbye/recordingsmetadata เท่านั้น (title, duration, ขนาดไฟล์, format: 'webm') คอมเมนต์ในโค้ดพูดตรง ๆ: // Create backend record (metadata only — no file upload)
  2. อัปโหลดวิดีโอ ตรงไป Google Drive ข้ามคนกลาง backend ไปทั้งหมด
  3. PATCH /api/geekbye/recordings/${backendRecordingId} พร้อมด้วย { driveUrl, driveFolderId } ที่ได้ — backend record ถูก_บอก_ว่าไฟล์ไปจบที่ไหน
  4. 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

สามสิ่งที่รีลีสนี้สอนเรา

  1. ถามว่า byte ไหลไปไหน ไม่ใช่แค่ว่ามันใช้ได้ไหม proxy ของ v1.8.11 ใช้ได้ แต่การ route ทุกการบันทึกผ่านเซิร์ฟเวอร์ของคุณเอง ทำให้คุณเสีย bandwidth, memory, และความ restart ได้ สำหรับไฟล์ที่คุณไม่ได้เก็บ การวาดเส้นทางข้อมูลใหม่ให้ client คุยกับปลายทางโดยตรง บ่อยครั้งคือ optimization ทั้งหมด
  2. resumable upload ที่ดีที่สุดคืออันที่คุณไม่ได้เขียน การส่ง stream ให้ SDK ที่โตเต็มที่ ซื้อ session แบบ resumable เต็มรูปแบบมาได้ด้วยโค้ดสองบรรทัด สัญชาตญาณที่จะทำ chunking และเลข offset ด้วยมือ คงได้ผลเป็นโค้ดมากขึ้นและความน่าเชื่อถือน้อยลง
  3. เมื่อคุณมอบหมายแกนกลาง คุณรับมรดกเป็น lifecycle ไม่ใช่ algorithm บั๊กจริงของ client แบบตรงคือ «re-init singleton หลัง restart» และ «อย่าบล็อก connect call» ไม่ใช่ byte offset นั่นเป็นการแลกที่ดี — บั๊ก lifecycle มองเห็นได้และเป็นเฉพาะที่; บั๊กโปรโตคอลไม่เป็นทั้งสองอย่าง

สำหรับบทก่อนหน้าในเรื่องยุค v1 ดูถือกำเนิดและถูกแก้ในสิบสองนาที (v1.8.10); และสำหรับภาพรวมทั้งเส้นเรื่อง ดูกายวิภาคของการส่งมอบซอฟต์แวร์ให้สมบูรณ์แบบ

บทความที่เกี่ยวข้อง

สามกริยาที่ทำให้ Web Audio ยังมีชีวิต
Steven
Steven5 นาทีที่อ่าน

สามกริยาที่ทำให้ Web Audio ยังมีชีวิต

GeekBye สอง point release ห่างกันสองเดือนและอยู่คนละไฟล์ สอนโค้ดเสียงของเราด้วยบทเรียนเดียวกันจากปลายสองด้านตรงข้าม: เลิกมอง AudioContext ของ browser ว่าเป็นของใช้แล้วทิ้ง release หนึ่งเรียนรู้ที่จะ resume() context ที่ macOS แอบ suspend ไปกลางการอัด; อีกอันเรียนรู้ที่จะ suspend() แทน close() เพื่อให้ session ที่ต่อกันเป็นพรืดเลิกพุ่งชนเพดานราวหก context ของ Chromium resume, suspend, close — นั่นคือทั้งเรื่อง

วิศวกรรม
Audio
Desktop
แยกการโทรออกจากแอปที่แค่เปิดค้างไว้
Steven
Steven4 นาทีที่อ่าน

แยกการโทรออกจากแอปที่แค่เปิดค้างไว้

GeekBye สังเกตได้ว่าคุณเข้าร่วมการประชุมวิดีโอแล้ว และเสนอบันทึกมันให้ด้วยคลิกเดียว การตรวจจับกลายเป็นครึ่งที่ง่าย — Swift binary ที่อ่านชื่อหน้าต่างทุกสิบวินาที ครึ่งที่ยากคือความแม่นยำ: ไม่ยิงตอน Zoom แค่เปิดค้างอยู่, ไม่เตือนสำหรับการประชุมที่คุณกำลังบันทึกอยู่แล้ว, และไม่ mute ไมค์ในการโทรที่คุณอยู่จริง ๆ สามรีลีส และแต่ละอันคือ guard ที่ต้องเรียนรู้ที่จะไม่เอาชนะตัวเอง

วิศวกรรม
macOS
Desktop
ถือกำเนิดและถูกแก้ในสิบสองนาที
Steven
Steven4 นาทีที่อ่าน

ถือกำเนิดและถูกแก้ในสิบสองนาที

changelog ของ GeekBye v1.8.10 บอกว่ามันแก้ crash ตอนแก้ไข keyboard shortcut มันแก้จริง — แต่ crash นั้นถูกใส่เข้ามาและถูกแก้ใน pull request เดียวกัน ห่างกันสิบสองนาที และไม่เคยไปถึงผู้ใช้แม้แต่คนเดียว เรื่องจริงคือปฏิกิริยาลูกโซ่ด้านความน่าเชื่อถือที่ผลิตมันออกมา: การเปลี่ยนแปลงเล็ก ๆ ที่ถูกต้องเรื่องว่าจะ suspend shortcut เมื่อไหร่ และ React teardown bug ที่หล่นออกมาจากโค้ดบรรทัดเดียวที่ตั้งใจให้ editor ปลอดภัยขึ้น

วิศวกรรม
React
Desktop