
एक call को एक खुले हुए app से अलग पहचानना
GeekBye यह ताड़ सकता है कि आप एक video meeting में शामिल हो गए हैं और एक क्लिक में उसे record करने की पेशकश करता है। पता चलता है कि detection ही आसान वाला आधा था — एक Swift binary जो हर दस सेकंड window titles पढ़ती है। मुश्किल आधा है सटीकता: जब Zoom बस खुला हो तब न भड़कना, एक ऐसी meeting के लिए न टोकना जिसे आप पहले से record कर रहे हैं, और उस call में microphone को mute न करना जिसमें आप असल में हैं। तीन releases, और हर एक एक ऐसा guard है जिसे ख़ुद को न हराना सीखना पड़ा।
GeekBye में एक feature है जो सुनने में मामूली लगता है पर है नहीं: यह ताड़ लेता है कि आप कब एक video meeting में शामिल हुए और पेशकश करता है, एक क्लिक में, उसे record करना शुरू करने की। बिक्री की बात एक अकेला वाक्य है। लागू करना, तीन releases है इस सीख का कि दिलचस्प समस्या कभी «एक meeting detect करना» थी ही नहीं — वह «उसके बारे में सही होना» थी। सही इस बारे में कि आप असल में एक call में हैं या बस app खोले हुए हैं। सही इस बारे में कि दोबारा न पूछें। सही इस बारे में कि ग़लत microphone को mute न करें। तीन releases में से हर एक, अपने मूल में, एक ऐसा guard है जिसे यह सिखाना पड़ा कि ख़ुद पर ठोकर न खाए।
window titles, processes नहीं
पहला और सबसे भारी नतीजे वाला फ़ैसला एक commit message में एक footnote के तौर पर नहीं, बल्कि एक design वक्तव्य के तौर पर लिखा है:
Detection uses window title matching (not process detection) to distinguish "app is open" from "in an active call."
वही भेद पूरा feature है। अगर आप यह देखकर meetings detect करते कि कौन-से apps चल रहे हैं, तो Zoom के launch होते ही भड़क उठते — और Zoom बहुत सारे desktops पर दिन-भर बिना एक भी call के खुला रहता है। तो GeekBye processes को नहीं ताकता। macOS पर यह एक छोटी native Swift binary, MeetingMonitor, चलाता है, जो हर दस सेकंड screen पर मौजूद windows के लिए CGWindowListCopyWindowInfo को call करती है और kCGWindowName के ज़रिए हर एक का title पढ़ती है। "Zoom Meeting" title वाली एक window का मतलब है आप एक call में हैं; "Zoom Workplace" window का मतलब है app बस खुला है। संकेत «Zoom ज़िंदा है क्या» नहीं है, वह «आपकी screen की एक window एक जीवित call जैसी दिखती है क्या» है।
poll अंतराल एक कड़ा constant है — pollIntervalSeconds: Double = 10.0 — साथ में startup पर एक तत्काल जाँच, ताकि एक meeting उसकी window के प्रकट होने के लगभग दस सेकंड के भीतर ताड़ ली जाए। (वह एक code मान है, कोई मापी गई latency नहीं; «ज़्यादा से ज़्यादा ~10 से» को benchmark नहीं, अंकगणित समझें।)
browser meetings को एक क़दम और चाहिए, क्योंकि "Meet - Weekly Sync" title वाली एक Chrome window आशाजनक है पर पक्का पता URL में है। तो जब सबसे आगे वाला app एक जाना-पहचाना browser हो — Chrome, Safari, Edge, Brave, और bundle id से पहचाने गए एक मुट्ठी-भर और — तब binary AppleScript के ज़रिए active tab का URL निकालती है और उसे meet.google.com/, zoom.us/j/, और teams.microsoft.com/ जैसे patterns के विरुद्ध जाँचती है। किसी दूसरे app में AppleScript अटक सकती है, तो वह call एक background DispatchQueue पर चलती है जिसे एक 3-सेकंड timeout वाला DispatchSemaphore संभालता है; एक फँसा हुआ browser पूरे detector को जमा देने के बजाय timeout हो जाता है।
मुश्किल हिस्सा है न भड़कना
यहीं वह हिस्सा है जिसने pattern सूची पढ़ते वक़्त मुझे चौंकाया। एक call detect करना कुछ string matches है। एक को न detect करना — जब आप न चाहें तब न भड़कना — वहीं सारी सावधानी गई। window-title patterns की सूची जान-बूझकर की गई बहिष्कारों के एक समुच्चय जैसी पढ़ी जाती है:
- Google Meet
"Meet - "और उसके en-dash रूप"Meet – "से match करता है, पर नंगे string"Google Meet"से नहीं — क्योंकि वह वह landing page है जिसे आप एक call से पहले खोलते हैं। - Zoom
"Zoom Meeting"और"Zoom Webinar"से match करता है, पर"Zoom Workplace"या नंगे"Zoom"से नहीं — खुला बैठा app। - Teams
"Meeting with","Meeting in"," | Call"," | Meeting"से match करता है — call-के-दौरान के title आकार, न कि निष्क्रिय client। - Slack huddles, Webex rooms, और WhatsApp calls, हर एक को अपने ख़ुद के सिर्फ़-call-के-दौरान वाले टुकड़े मिलते हैं।
उनमें से हर एक एक फ़ैसला है कि उस आम हालत में चुप रहा जाए जहाँ एक app खुला है पर आप एक meeting में नहीं हैं। design लक्ष्य अधिकतम recall नहीं है; वह यह है कि वह औज़ार न बना जाए जो आपके Slack पढ़ते वक़्त एक «record करना है?» banner उछाल दे। यहाँ एक false positive उस तरह महँगा है जिस तरह एक false negative नहीं: एक call चूको तो user ख़ुद-ब-ख़ुद record करना शुरू कर देता है; एक खुले app पर ग़लत भड़को तो user आपके banner को अनदेखा करना सीख जाता है। पूरी heuristic इस ओर ट्यून है कि दूसरी नाकामी विरल हो।
इतनी सरल एक heuristic की सीमाओं को नाम देना ईमानदारी है। patterns सिर्फ़-अंग्रेज़ी हैं — एक localize किया हुआ OS या एक ग़ैर-अंग्रेज़ी meeting app match नहीं होगा, एक स्वीकृत v1 सीमा। और चूँकि दूसरे apps के window titles सिर्फ़ Screen Recording अनुमति दिए जाने पर ही दिखते हैं, उसके बिना detection चुपचाप कुछ-न-कुछ से घटकर कुछ-नहीं हो जाता है। सरल, सुपाठ्य, और उन तरीक़ों से अपूर्ण जिन्हें team ने छिपाने के बजाय लिख डाला।
पेशकश, और वह मशीनरी जो इसे टोकाटोकी से रोके रखती है
जब एक pattern match होता है, GeekBye कोने में एक छोटा frameless हमेशा-सबसे-ऊपर banner दिखाता है — इसकी अपनी window, कोई OS notification नहीं — एक Start Recording button के साथ। इसे क्लिक करो और एक IPC message (meeting:start-recording) banner को छिपा देता है, मुख्य window को ऊपर लाता है, और renderer को एक session शुरू करने को कहता है। इसे ख़ारिज करो और banner चला जाता है।
पर इसका एक भोला version पागल कर देने वाला होता। एक call के दौरान titles लगातार बदलते हैं — एक participant जुड़ता है, आप meeting का नाम बदलते हैं, एक screen-share window का नाम बदल देता है — और हर बदलाव एक ऐसे detector को दोबारा भड़का देता जो ठीक उसी string पर टिका था। तो MeetingDetector.ts में मौजूद detector एक छोटी state machine संभाले रखता है जिसका सारा काम संयम है:
- प्रति-platform एक स्थिर
meetingId। Swift पक्षgoogle-meet-activeजैसा एक id उत्सर्जित करता है, जान-बूझकर उस चंचल window title से व्युत्पन्न नहीं, ताकि एक ही call के भीतर joins, leaves, और renames एक नई meeting के रूप में न पढ़े जाएँ। - एक
dismissedMeetings: Set<string>। banner ख़ारिज करो और उस meeting का id अनदेखा हो जाता है — जब तक meeting असल में ख़त्म न हो जाए, जिस बिंदु पर id साफ़ हो जाता है ताकि अगली असली meeting नए सिरे से टोक सके। - एक
notificationActiveboolean। एक बार में सिर्फ़ एक banner, ताकि जब आप पहले से एक Meet में हों तब उछलता हुआ एक Slack huddle एक दूसरा prompt न ठेल दे।
असल में वही यह feature है: थोड़ा-सा detection और बहुत सारा «कष्टप्रद न होना»। ठीक इसीलिए अगली दो releases मौजूद हैं — क्योंकि उन guards में से दो ख़ुद को हराते निकले।
वह mute जिसने आपकी असली meeting को mute कर दिया (v1.8.18)
record करने में एक mute button है, और पहले implementation ने «mute» को उसके सबसे शाब्दिक अर्थ में लिया। v1.8.15 में ship हुआ, इसने operating system के input volume को शून्य पर सेट करके microphone को mute किया:
osascript -e "set volume input volume 0"
पिछले volume को database में सहेजा गया ताकि उसे बहाल किया जा सके, waveInSetVolume के ज़रिए एक Windows समतुल्य, और यह पक्का करने के लिए एक before-quit सुरक्षा-जाल कि इसने un-mute कर दिया। code ने तो इसे एक feature के रूप में वर्णित तक किया — «दो स्तरों पर» mute करना, system और app, ताकि mic सभी apps के लिए ब्लॉक हो जाए।
वह आख़िरी वाक्यांश ही एक शेख़ी के तौर पर कहा गया bug है। सभी apps के लिए mic ब्लॉक करने का मतलब है उस Zoom या Meet call के लिए इसे ब्लॉक करना जिसमें आप असल में बैठे हैं। आप GeekBye में mute दबाते हैं ताकि वह आपको सुनना बंद करे, और आप अपनी असली meeting में चुप हो गए। v1.8.18 का fix एक अवनति है: OS के input volume को छूना पूरी तरह बंद करो, और सिर्फ़ GeekBye की अपनी audio pipeline को mute करो। Listen mode में, RealtimeAudioCaptureService mute किए गए mic frames को एक शून्य buffer से बदल देता है ताकि सन्नाटा mixer तक बहे; video mode में, ScreenRecordingService एक Web Audio GainNode को शून्य पर सेट कर देता है। वह system-wide mute, सहेजे गए volume की बहाली, वह पूरा cross-platform तंत्र — सब मिटा दिया गया। mute का मतलब «यह app सुनना बंद करता है» होना चाहिए, कभी «आपका microphone सबके लिए बंद हो जाता है» नहीं।
वह guard जिसने ख़ुद को रीसेट कर दिया (v1.8.19)
दूसरा ख़ुद को हराने वाला guard notificationActive था। काग़ज़ पर यह पहले से «अगर एक banner ऊपर है तो banner मत दिखाओ» का जवाब देता था — और यह लगता था कि इसे «एक ऐसी meeting record करने की पेशकश मत करो जिसे मैं पहले से record कर रहा हूँ» को भी संभालना चाहिए। इसने नहीं संभाला, और जो commit इसे ठीक करती है वह सटीकता से समझाती है कि क्यों:
The notificationActive flag resets when the meeting detector cycles through ended/detected states, causing notifications to reappear mid-recording.
detector एक जीवित poll है; एक लंबी call के दौरान यह क्षण-भर «ended» और फिर दोबारा «detected» पढ़ सकता है — एक title की टिमटिमाहट, एक पल के लिए screen से बाहर हुई window — और हर चक्र flag को रीसेट कर देता था। तो guard आपके record करते वक़्त चुपचाप ख़ुद को फिर-फिर से लोड करता रहा, और banner उस call को record करने की पेशकश करते हुए दोबारा प्रकट हो जाता जिसे आप पहले से record कर रहे थे। fix एक local flag पर भरोसा करना बंद करता है और उसके बजाय सत्य के स्रोत से पूछता है: एक recordingActiveChecker callback, main.ts से inject किया हुआ और असली isRealtimeTranscriptionActive state से जुड़ा हुआ, किसी भी notification के दिखने से पहले जाँचा जाता है।
// Don't show notification if recording/transcription is already active
if (this.recordingActiveChecker?.()) {
return
}
सबक़ छोटा और आम है: एक guard जिसका state आपका अपना है, आपके ही control flow के हाथों आपके पैरों तले रीसेट किया जा सकता है। एक guard जो उस असली state को पढ़ता है जिसकी वह रक्षा कर रहा है, नहीं। notificationActive «क्या हम record कर रहे हैं» के एक proxy को track कर रहा था; fix पूछता है कि क्या हम record कर रहे हैं।
तीन चीज़ें जो इन releases ने हमें सिखाईं
- वह state detect करो जो तुम्हारा मतलब है, वह नहीं जो देखने में आसान है। «app चल रहा है» जाँचना मामूली है और ग़लत है। «एक ऐसी window मौजूद है जो एक जीवित call जैसी दिखती है» थोड़ा मुश्किल है और सही है। उन दोनों के बीच की खाई ही पूरा feature है।
- एक सक्रिय feature के लिए, सटीकता ही उत्पाद है। उस meeting को कोई याद नहीं रखता जिसे आपने सही detect किया; लोग उन तीन बार को याद रखते हैं जब आपने उन्हें तब टोका जब वे किसी में थे ही नहीं। pattern अपवाद और anti-nag state machine पॉलिश नहीं हैं — वे एक मददगार banner और उस banner के बीच का अंतर हैं जिसे अनदेखा करना आप ख़ुद को सिखा लेते हैं।
- एक guard जो ख़ुद को रीसेट कर सके, guard नहीं है। दोनों follow-up releases एक ही आकार की हैं: एक सुरक्षा तंत्र जिसने असली state (OS mic, transcription स्थिति) के बजाय एक ऐसे proxy (एक सहेजा गया volume, एक local boolean) का हवाला दिया जिसे वह ख़ुद नियंत्रित करता था। guards को सत्य पर तानो, उसकी एक प्रति पर नहीं।
v1 कहानी के पिछले अध्याय के लिए, देखें अपलोड के रास्ते से backend को हटा देना (v1.8.11–v1.8.13); और पूरे चाप के लिए, सॉफ़्टवेयर को परिपूर्णता तक पहुँचाकर भेजने की शारीर-रचना।