Steven
Steven9 मिनट पढ़ें

अपलोड के रास्ते से backend को हटा देना

GeekBye आपकी स्क्रीन record करता है और video को आपके Google Drive में save कर देता है। पहला version हर recording को वहाँ तक पहुँचने के रास्ते में GeekBye के अपने servers से होकर भेजता था; एक release बाद, file आपकी मशीन से सीधे Drive चली जाती थी, और backend को महज़ एक pointer थामे रखने तक घटा दिया गया। दिलचस्प हिस्सा यह है कि वह «सीधा, resumable» version असल में कितना कम code रखता है — क्योंकि resumability एक proxy लिखने से नहीं, बल्कि उसे मिटाने से आई।

इंजीनियरिंग
आर्किटेक्चर
डेस्कटॉप
GeekBye रिलीज़
अपलोड के रास्ते से backend को हटा देना

GeekBye आपकी स्क्रीन — video, system audio, और mic — record कर सकता है और तैयार recording को आपके Google Drive में अपने-आप गिरा सकता है। v1.8.11 और v1.8.13 के बीच, इस feature का user-facing विवरण मुश्किल से बदला, पर video जो रास्ता Drive तक पहुँचने के लिए लेती है वह पूरी तरह फिर से रूट कर दिया गया। पहले version में, आपकी recording GeekBye के servers से होकर सफ़र करती थी। दूसरे में, वह आपकी मशीन से सीधे आपके Drive चली गई, और GeekBye के backend ने उसका एक byte भी नहीं देखा। वही फिर-से-रूट करना पूरी कहानी है, और संतोषजनक हिस्सा यह है कि वह «बेहतर» version, जिसे उसने बदला उससे साफ़ तौर पर कम code रखता है।

pipeline, सिरे से सिरे तक

recording renderer में होती है। ScreenRecordingService.ts capture stream के ऊपर { mimeType: 'video/webm;codecs=vp9,opus', videoBitsPerSecond: 1_000_000 } के साथ एक MediaRecorder बनाता है और उसे एक-सेकंड timeslice (CHUNK_INTERVAL_MS = 1000) के साथ शुरू करता है। हर ondataavailable blob को IPC के ज़रिए main process पर भेजा जाता है, जहाँ recording:save-chunk handler उसे fs.promises.appendFile से disk पर एक file में जोड़ देता है। जल्दी झंडा गाड़ देना क़ीमती है, क्योंकि यह ग़लत पढ़ लेने वाली चीज़ है: वह एक-सेकंड की लय disk-write की लय है, कोई upload chunk size नहीं। जब तक upload शुरू होता है, disk पर एक अकेली तैयार WebM file बैठी होती है।

दोनों releases के बीच जो फ़र्क़ है वह सिर्फ़ यह है कि उस तैयार file के साथ क्या होता है।

पहले: सब कुछ backend से होकर (v1.8.11)

v1.8.11 का upload path, CloudUploader.processRecording() में, स्पष्ट design है और एक पूरी तरह वाजिब पहला version है:

  1. POST /api/geekbye/recordings से recording के metadata वाला एक backend record बनाओ।
  2. पूरी video को memory में पढ़ो और उसे multipart के तौर पर backend पर POST करो — const fileBuffer = fs.readFileSync(filePath)POST /api/geekbye/recordings/${id}/upload पर मारते हुए। फिर backend उस file को Drive पर upload करता है और एक driveUrl लौटाता है।
  3. POST …/process से transcript को AI analysis के लिए भेजो।

भार उठाने वाली बारीक़ी step 2 है: client पूरी recording को memory में लादता है और उसका हर byte Drive के रास्ते में GeekBye के अपने servers से गुज़रता है। एक छोटी clip के लिए वह ठीक है। एक लंबी screen recording के लिए वह एक साथ तीन समस्याएँ हैं — readFileSync से client पर memory का दबाव, एक ऐसी file के लिए backend पर bandwidth और relay की लागत जिसे वह रखता तक नहीं, और एक अकेला monolithic POST जिसे नेटवर्क बीच में हिचकी ले तो शून्य से फिर शुरू करना पड़ता है।

यह ईमानदार होने लायक़ भी है कि यह «पहले» कोई बेदाग़ मूल design नहीं था। v1.8.11 feature ने एक हफ़्ते-भीतर के pivot को एक साथ ठूँस दिया था: एक शुरुआती cut ने pre-signed URLs के ज़रिए Cloudflare R2 पर upload किया और एक bundled ffmpeg-static से WebM को MP4 में बदला; उसे backend-मध्यस्थ Drive flow ने बदल दिया; और फिर ffmpeg conversion को WebM को जस-का-तस upload करने के हक़ में पूरी तरह उखाड़ फेंका गया (commit नोट करता है कि इसने bitrate घटाया और मोटे तौर पर चार-गुना छोटी files बनाईं — एक inline अनुमान, कोई benchmark नहीं)। तो «पहले» भी एक native dependency मिटाना एक बार सीख चुका था। अगली release proxy को भी मिटा देगी।

बाद में: सीधे Drive (v1.8.13)

v1.8.13 उसे ship करने वाले commit के एक वाक्य के इर्द-गिर्द CloudUploader.processRecording() को फिर से लिखता है: सीधे Drive पर upload करो, "keeping backend for metadata record + AI transcript analysis only." नया path:

  1. POST /api/geekbye/recordingsसिर्फ़ metadata (title, duration, file size, format: 'webm')। code में comment बेलाग है: // Create backend record (metadata only — no file upload)
  2. video को सीधे Google Drive पर upload करो, backend बिचौलिए को पूरी तरह छोड़ते हुए।
  3. PATCH /api/geekbye/recordings/${backendRecordingId} को नतीजे वाले { driveUrl, driveFolderId } के साथ — backend record को बता दिया जाता है कि file कहाँ जाकर बैठी।
  4. transcript अब भी analysis के लिए …/process जाता है।

backend file के path के भीतर होने से, बाद में file के बारे में बताए जाने की स्थिति में आ गया। वह एक row और एक pointer थामता है; bytes सिर्फ़ user की disk में और user के Drive में जीते हैं।

एक असली Drive client बनना

client को Drive से सीधे बात करने के लिए, उसे एक असल Google Drive API client होना पड़ता है, और v1.8.13 उसके लिए यंत्र-व्यवस्था जोड़ता है: एक नया DriveService (वह Drive client), एक नई drive_auth SQLite table के सहारे टिका एक नया DriveAuthRepository, और एक dependency के तौर पर googleapis SDK।

authorization एक साफ़-सुथरा capability का हस्तांतरण है। backend का app-config endpoint Google OAuth client credentials — एक clientId और एक clientSecret — app तक रिले करता है, जिसे CloudUploader पढ़ता है और driveService.initialize(...) को पास करता है। वहाँ से desktop app पूरा OAuth flow ख़ुद चलाता है: वह एक random 127.0.0.1 port पर एक अस्थायी http server खड़ा करता है, shell.openExternal से system browser में consent screen खोलता है, एक /callback पर redirect पकड़ता है, और google-auth-library से code को tokens में बदलता है। वे tokens locally save होते हैं — drive_auth में saveDriveAuth(access_token, refresh_token, expiry_date)drive.file scope के साथ, जो app को सिर्फ़ उन्हीं files को manage करने देता है जो वह बनाता है। refresh भी local है: एक oauth2Client.on('tokens', …) listener refresh किए गए tokens को सीधे table में वापस लिख देता है। backend capability एक बार सौंप देता है और फिर loop से बाहर रहता है।

«resumable» वाला हिस्सा, ईमानदारी से

यही वह बारीक़ी है जिसके बारे में मैं सबसे ज़्यादा सीधा रहना चाहता हूँ, क्योंकि यह ठीक वैसी चीज़ है जिसे एक changelog गोल कर देता है। release note कहता है "resumable uploads," और जहाँ user खड़ा है वहाँ से वह सच है। पर GeekBye ने resumable protocol को implement नहीं किया। GeekBye के source में कहीं भी कोई chunk-size constant नहीं, कोई byte-offset tracker नहीं, और कोई Content-Range / 308 Resume Incomplete handling नहीं। उन्हें grep करो तो कुछ नहीं मिलता — वे googleapis SDK के भीतर रहते हैं।

GeekBye का अपना code जो करता है वह यह है:

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 एक buffer नहीं बल्कि एक stream (createReadStream) है — और एक stream पास करना ठीक वही है जो googleapis को एक शॉट में करने के बजाय आपकी ओर से एक resumable session negotiate करवाता है। दूसरा, progress को SDK के onUploadProgress callback से fileStats.size के मुक़ाबले पढ़ा जाता है। application स्तर पर «resumable upload» का पूरा implementation बस यही है: stream अंदर, progress बाहर। कठिन protocol — एक session खोलना, chunks में upload करना, एक drop के बाद एक offset से resume करना — SDK का काम है, और सही engineering फ़ैसला यही था कि उसे उस काम को करने दिया जाए, उसे फिर से ईजाद न किया जाए।

जो पूरी release को दोबारा फ्रेम कर देता है। reliability का सुधार एक upload engine लिखने से नहीं आया। वह उस proxy को मिटाने से आया जो एक के आगे बैठा था — और, उसी हफ़्ते पहले, उसके आगे वाले ffmpeg step को मिटाने से। कम code, ज़्यादा लचीलापन, क्योंकि लचीली चीज़ एक ख़ूब घिसी-पिटी library थी, उसी पल से जब आपने उसे हाथ से एक buffer खिलाना बंद किया।

वे हिस्से जो सचमुच GeekBye के अपने हैं

transfer को SDK के हवाले करने का मतलब यह नहीं कि बनाने को कुछ बचा ही नहीं था। असली application logic के दो टुकड़े उसे घेरे हुए हैं।

एक orphaned-folder rollback। हर recording को अपना ख़ुद का Drive subfolder मिलता है (mimeType: 'application/vnd.google-apps.folder' से बना), और transcript वहाँ video के बग़ल में एक दूसरी file के तौर पर आ बैठता है। अगर upload throw कर दे, तो catch उस folder को delete कर देता है — this.drive.files.delete({ fileId: folderId }), Rolled back orphaned Drive folder के रूप में log किया गया — ताकि एक नाकाम upload आपके Drive में ख़ाली folders की एक क़तार न छोड़े। यह जानने से पहले कि operation कामयाब होगा एक container बना देना एक छोटी देनदारी है; नाकामी वाले path से उसे साफ़ करवाना ही उसका fix है।

एक singleton के इर्द-गिर्द lifecycle। DriveService एक in-memory OAuth client थामे एक singleton है, और वह in-memory client एक app restart को नहीं झेल पाता, भले ही tokens झेल जाते हैं (वे database में हैं)। तो CloudUploader हर upload से पहले store किए गए tokens से DriveService को re-initialize करता है, जिसका मतलब है कि relaunch के ठीक बाद बनाई गई एक recording भी आपसे दोबारा connect करने को कहे बिना upload हो जाती है। और review pass ने drive-connect handler को fire-and-forget बना दिया, क्योंकि एक blocking connect call renderer की status polling को रोक रहा था — अगर connect call thread पकड़े रहता तो UI «connecting…» नहीं दिखा सकती थी। वे दो — re-init-from-storage और non-blocking connect — सीधे path के असली edge cases हैं, और ग़ौरतलब है कि उनमें से कोई भी upload chunking के बारे में नहीं है। जब आप कठिन हिस्सा सौंप देते हैं, तो जो bugs आपके पास बचते हैं वे lifecycle bugs होते हैं।

तीन चीज़ें जो इस release ने हमें सिखाईं

  1. पूछो कि bytes कहाँ बहते हैं, सिर्फ़ यह नहीं कि यह चलता है या नहीं। v1.8.11 का proxy चलता था। पर हर recording को अपने ख़ुद के servers से रूट करना आपको एक ऐसी file के लिए bandwidth, memory, और restartability चुकवाता है जिसे आप रखते तक नहीं। data path को इस तरह दोबारा खींचना कि client सीधे मंज़िल से बात करे, अक्सर पूरी optimization होती है।
  2. सबसे अच्छा resumable upload वही है जो आपने लिखा ही नहीं। एक परिपक्व SDK को एक stream पास करना दो लाइनों के code में एक पूरा resumable session ख़रीद लाया। chunking और offset के गणित को हाथ से गढ़ने की सहज-वृत्ति ज़्यादा code और कम reliability पैदा करती।
  3. जब आप core को सौंपते हैं, तो आपको algorithms नहीं, lifecycle विरासत में मिलती है। सीधे client के असली bugs «restart के बाद singleton को re-init करो» और «connect call को block मत करो» थे, byte offsets नहीं। यह एक अच्छा सौदा है — lifecycle bugs दिखते हैं और स्थानीय होते हैं; protocol bugs इनमें से कुछ भी नहीं।

v1 कहानी के पिछले अध्याय के लिए, देखें बारह मिनट में जन्मा और ठीक हुआ (v1.8.10); और पूरे चाप के लिए, सॉफ़्टवेयर को परिपूर्णता तक पहुँचाकर भेजने की शारीर-रचना

संबंधित लेख

तीन क्रियाएँ जो Web Audio को ज़िंदा रखती हैं
Steven
Steven10 मिनट पढ़ें

तीन क्रियाएँ जो Web Audio को ज़िंदा रखती हैं

GeekBye की दो point releases, दो महीने के फ़ासले पर और दो अलग files में, हमारे audio code को उलटे छोरों से एक ही सबक़ सिखा गईं: browser के AudioContext को इस्तेमाल-कर-फेंको जैसा मानना बंद करो। एक release ने एक ऐसे context को resume() करना सीखा जिसे macOS ने record के बीचोंबीच चुपचाप suspend कर दिया था; दूसरी ने close() के बजाय suspend() करना सीखा ताकि पीठ-दर-पीठ चलती sessions Chromium की लगभग-छह-context वाली छत से टकराना बंद कर दें। resume, suspend, close — यही पूरी कहानी है।

इंजीनियरिंग
Audio
डेस्कटॉप
एक call को एक खुले हुए app से अलग पहचानना
Steven
Steven10 मिनट पढ़ें

एक call को एक खुले हुए app से अलग पहचानना

GeekBye यह ताड़ सकता है कि आप एक video meeting में शामिल हो गए हैं और एक क्लिक में उसे record करने की पेशकश करता है। पता चलता है कि detection ही आसान वाला आधा था — एक Swift binary जो हर दस सेकंड window titles पढ़ती है। मुश्किल आधा है सटीकता: जब Zoom बस खुला हो तब न भड़कना, एक ऐसी meeting के लिए न टोकना जिसे आप पहले से record कर रहे हैं, और उस call में microphone को mute न करना जिसमें आप असल में हैं। तीन releases, और हर एक एक ऐसा guard है जिसे ख़ुद को न हराना सीखना पड़ा।

इंजीनियरिंग
macOS
डेस्कटॉप
बारह मिनट में जन्मा और ठीक हुआ
Steven
Steven9 मिनट पढ़ें

बारह मिनट में जन्मा और ठीक हुआ

GeekBye v1.8.10 का changelog कहता है कि उसने keyboard shortcuts को edit करते वक़्त होने वाले एक crash को ठीक किया। किया भी — पर वह crash एक ही pull request में, बारह मिनट के फ़ासले पर, आया और ठीक हुआ, और एक भी user तक कभी नहीं पहुँचा। असली कहानी वह reliability श्रृंखला है जिसने उसे पैदा किया: shortcuts कब suspend हों, इस पर एक छोटा, सही बदलाव, और editor को ज़्यादा सुरक्षित बनाने के इरादे वाली एक लाइन से गिरा वह React teardown bug।

इंजीनियरिंग
React
डेस्कटॉप