
Bir Görüşmeyi Açık Bir Uygulamadan Ayırt Etmek
GeekBye bir video görüşmesine katıldığınızı fark edip onu kaydetmeyi teklif edebilir. Tespit, kolay yarı olarak çıkıyor — her on saniyede pencere başlıklarını okuyan bir Swift ikili dosyası. Zor yarı kesinliktir: Zoom sadece açıkken tetiklenmemek, zaten kaydettiğiniz bir görüşme için sormamak, ve gerçekten içinde bulunduğunuz görüşmede mikrofonu susturmamak. Üç sürüm, ve her biri kendini yenmemeyi öğrenmek zorunda kalan bir korumadır.
GeekBye'in kulağa önemsiz gelen ama öyle olmayan bir özelliği var: bir video görüşmesine katıldığınızı fark eder ve tek tıkla onu kaydetmeye başlamayı teklif eder. Söylemi tek bir cümle. Uygulaması ise, ilginç problemin asla "bir görüşme tespit et" olmadığını — "onun hakkında haklı olmak" olduğunu öğrenmenin üç sürümü. Gerçekten bir görüşmede mi olduğun yoksa sadece uygulamayı açık mı tuttuğun hakkında haklı olmak. İki kez sormamak hakkında haklı olmak. Yanlış mikrofonu susturmamak hakkında haklı olmak. Üç sürümün her biri, özünde, kendi üzerine takılıp düşmemeyi öğretilmesi gereken bir korumadır.
Pencere başlıkları, süreçler değil
İlk ve en sonuç doğuran karar, bir dipnot olarak değil, bir tasarım beyanı olarak bir commit mesajına yazılmıştır:
Detection uses window title matching (not process detection) to distinguish "app is open" from "in an active call."
Bu ayrım tüm özelliktir. Görüşmeleri hangi uygulamaların çalıştığına bakarak tespit etseydin, Zoom başlar başlamaz tetiklenirdin — ve Zoom, tek bir görüşme olmadan bir sürü masaüstünde bütün gün açık durur. Bu yüzden GeekBye süreçleri izlemez. macOS'ta, her on saniyede ekrandaki pencereler için CGWindowListCopyWindowInfo'yu çağıran ve her birinin başlığını kCGWindowName aracılığıyla okuyan küçük bir native Swift ikili dosyası, MeetingMonitor çalıştırır. "Zoom Meeting" başlıklı bir pencere bir görüşmede olduğun anlamına gelir; "Zoom Workplace" penceresi uygulamanın sadece açık olduğu anlamına gelir. Sinyal "Zoom canlı mı" değil, "ekranındaki bir pencere canlı bir görüşme gibi mi görünüyor."
Yoklama aralığı sabit bir sabittir — pollIntervalSeconds: Double = 10.0 — artı başlangıçta bir anlık kontrol, böylece bir görüşme, penceresi belirdikten sonra yaklaşık on saniye içinde fark edilir. (Bu bir kod değeridir, ölçülmüş bir gecikme değil; "~10 s'ye kadar"yı bir benchmark değil, aritmetik olarak ele al.)
Tarayıcı görüşmeleri bir adım daha gerektirir, çünkü "Meet - Weekly Sync" başlıklı bir Chrome penceresi umut vericidir ama kesinlik URL'dedir. Yani en öndeki uygulama bilinen bir tarayıcıysa — Chrome, Safari, Edge, Brave, ve bundle id ile tanımlanan bir avuç daha — ikili dosya aktif sekmenin URL'sini AppleScript aracılığıyla çıkarır ve onu meet.google.com/, zoom.us/j/, ve teams.microsoft.com/ gibi kalıplara karşı kontrol eder. Başka bir uygulamaya AppleScript takılabilir, bu yüzden o çağrı, bir DispatchSemaphore ile korunan bir arka plan DispatchQueue'sunda 3 saniyelik zaman aşımıyla çalışır; sıkışmış bir tarayıcı, tüm detektörü dondurmak yerine zaman aşımına uğrar.
Zor kısım tetiklenmemektir
İşte kalıp listesini okuduğumda beni şaşırtan kısım. Bir görüşmeyi tespit etmek birkaç dize eşleştirmesidir. Bir görüşmeyi tespit etmemek — istemediğinde tetiklenmemek — tüm özenin gittiği yerdir. Pencere-başlığı kalıplarının listesi, bir dizi kasıtlı dışlama gibi okunur:
- Google Meet,
"Meet - "ile ve onun uzun-tire varyantı"Meet – "ile eşleşir, ama çıplak"Google Meet"dizesiyle eşleşmez — çünkü o, bir görüşmeden önce açtığın iniş sayfasıdır. - Zoom,
"Zoom Meeting"ve"Zoom Webinar"ile eşleşir, ama"Zoom Workplace"veya çıplak bir"Zoom"ile eşleşmez — açık duran uygulama. - Teams,
"Meeting with","Meeting in"," | Call"," | Meeting"ile eşleşir — görüşme-içi başlık biçimleri, boştaki istemci değil. - Slack huddle'ları, Webex odaları ve WhatsApp aramaları, her biri kendi yalnızca-görüşme-içi parçalarını alır.
Bunların her biri, bir uygulamanın açık olduğu ama bir görüşmede olmadığın yaygın durumda sessiz kalma kararıdır. Tasarım hedefi maksimum geri çağırma değildir; Slack okurken bir "kaydedelim mi?" banner'ı patlatan araç olmamaktır. Buradaki bir yanlış pozitif, bir yanlış negatifin olmadığı bir şekilde pahalıdır: bir görüşmeyi kaçır ve kullanıcı manuel olarak kaydetmeye başlar; açık bir uygulamada yanlış ateşle ve kullanıcı banner'ını görmezden gelmeyi öğrenir. Tüm sezgisel yöntem, ikinci başarısızlığın nadir olmasına doğru ayarlanmıştır.
Bu kadar basit bir sezgisel yöntemin sınırlarını adlandırmak dürüstçedir. Kalıplar yalnızca İngilizcedir — yerelleştirilmiş bir işletim sistemi veya İngilizce olmayan bir görüşme uygulaması eşleşmez, kabul edilmiş bir v1 sınırlaması. Ve diğer uygulamaların pencere başlıkları yalnızca Ekran Kaydı izni verildiğinde görünür olduğundan, tespit onsuz sessizce hiçe iner. Basit, okunaklı, ve ekibin gizlemek yerine yazdığı yollarla kusurlu.
Teklif, ve onu dırdır etmekten alıkoyan mekanizma
Bir kalıp eşleştiğinde, GeekBye köşede küçük, çerçevesiz, hep-üstte bir banner gösterir — kendi penceresi, bir işletim sistemi bildirimi değil — bir Start Recording düğmesiyle. Ona tıkla ve bir IPC mesajı (meeting:start-recording) banner'ı gizler, ana pencereyi öne getirir, ve renderer'a bir oturum başlatmasını söyler. Onu reddet ve banner gider.
Ama bunun naif bir versiyonu çıldırtıcı olurdu. Başlıklar bir görüşme sırasında sürekli değişir — bir katılımcı katılır, görüşmeyi yeniden adlandırırsın, bir ekran paylaşımı pencere adını değiştirir — ve her değişiklik, tam dizeye kilitlenen bir detektörü yeniden ateşlerdi. Bu yüzden detektör, MeetingDetector.ts içinde, tüm işi kendini tutmak olan küçük bir durum makinesi taşır:
- Platform başına kararlı bir
meetingId. Swift tarafıgoogle-meet-activegibi bir id yayar, kasıtlı olarak uçucu pencere başlığından türetilmez, böylece bir görüşme içindeki katılmalar, ayrılmalar ve yeniden adlandırmalar yeni bir görüşme olarak okunmaz. - Bir
dismissedMeetings: Set<string>. Banner'ı reddet ve o görüşmenin id'si yok sayılır — ta ki görüşme gerçekten bitene kadar, o noktada id temizlenir, böylece sonraki gerçek görüşme yeni baştan sorabilir. - Bir
notificationActiveboole değeri. Bir seferde yalnızca bir banner, böylece zaten bir Meet'teyken beliren bir Slack huddle'ı ikinci bir istem yığmaz.
İşte özellik, gerçekten: biraz tespit ve bir sürü "can sıkıcı olma." Ki bu tam olarak sonraki iki sürümün neden var olduğudur — çünkü o korumalardan ikisi kendini yenmiş çıktı.
Gerçek görüşmeni susturan susturma (v1.8.18)
Kaydın bir sustur düğmesi var, ve ilk uygulama "susturmayı" en düz anlamıyla ele aldı. v1.8.15'te gönderilen sürüm, mikrofonu işletim sisteminin giriş sesini sıfıra ayarlayarak susturdu:
osascript -e "set volume input volume 0"
önceki ses, geri yüklenebilmesi için veritabanına kaydedilmiş, waveInSetVolume aracılığıyla bir Windows eşdeğeri, ve susturmayı geri aldığından emin olmak için bir before-quit güvenlik ağı ile. Kod bunu bir özellik olarak bile tanımladı — "İKİ düzeyde" susturmak, sistem ve uygulama, böylece mikrofon tüm uygulamalar için engellendi.
O son ifade, bir övünme olarak dile getirilmiş hatadır. Mikrofonu tüm uygulamalar için engellemek, gerçekten içinde oturduğun Zoom veya Meet görüşmesi için engellemek demektir. Onun seni duymasını durdurmak için GeekBye'de sustura basarsın, ve gerçek görüşmende sessizleşmiş olursun. v1.8.18 düzeltmesi bir rütbe indirmesidir: işletim sistemi giriş sesine dokunmayı tamamen bırak, ve yalnızca GeekBye'in kendi ses işlem hattını sustur. Listen modunda, RealtimeAudioCaptureService susturulmuş mikrofon karelerini bir sıfır tamponuyla değiştirir, böylece karıştırıcıya sessizlik akar; video modunda, ScreenRecordingService bir Web Audio GainNode'unu sıfıra ayarlar. Sistem-geneli susturma, kaydedilmiş-ses kurtarması, tüm çapraz-platform aygıtı — hepsi silindi. Susturmak "bu uygulama dinlemeyi bırakır" anlamına gelmeli, asla "mikrofonun herkes için kapanır" değil.
Kendini sıfırlayan koruma (v1.8.19)
Kendini yenen diğer koruma notificationActive'di. Kağıt üzerinde zaten "biri varsa banner gösterme"yi yanıtlıyordu — ve "zaten kaydettiğim bir görüşmeyi kaydetmeyi teklif etme"yi de kapsaması gerekirmiş gibi görünüyordu. Kapsamıyordu, ve onu düzelten commit nedenini tam olarak açıklıyor:
The notificationActive flag resets when the meeting detector cycles through ended/detected states, causing notifications to reappear mid-recording.
Detektör canlı bir yoklamadır; uzun bir görüşme boyunca kısaca "ended" ve sonra tekrar "detected" okuyabilir — bir başlık titremesi, anlık olarak ekran dışı bir pencere — ve her döngü bayrağı sıfırladı. Böylece koruma, sen kaydederken sessizce kendini yeniden kurmaya devam etti, ve banner zaten kaydettiğin görüşmeyi kaydetmeyi teklif ederek yeniden belirirdi. Düzeltme, yerel bir bayrağa güvenmeyi bırakır ve bunun yerine gerçeğin kaynağına sorar: main.ts'ten enjekte edilen ve gerçek isRealtimeTranscriptionActive durumuna bağlanan, herhangi bir bildirim gösterilmeden önce kontrol edilen bir recordingActiveChecker geri çağırması.
// Don't show notification if recording/transcription is already active
if (this.recordingActiveChecker?.()) {
return
}
Ders küçük ve geneldir: durumunu sahiplendiğin bir koruma, kendi kontrol akışın tarafından ayağının altından sıfırlanabilir. Koruduğu gerçek durumu okuyan bir koruma sıfırlanamaz. notificationActive, "kaydediyor muyuz" için bir vekil izliyordu; düzeltme, kaydedip kaydetmediğimizi sorar.
Bu sürümlerin bize öğrettiği üç şey
- Görmesi kolay olanı değil, kastettiğin durumu tespit et. "Uygulama çalışıyor" kontrol etmesi önemsiz ve yanlıştır. "Canlı bir görüşme gibi görünen bir pencere var" biraz daha zor ve doğrudur. Bu ikisi arasındaki boşluk, tüm özelliğin ta kendisidir.
- Proaktif bir özellik için, kesinlik üründür. Kimse doğru tespit ettiğin görüşmeyi hatırlamaz; bir görüşmede değilken onları böldüğün üç seferi hatırlarlar. Kalıp istisnaları ve anti-dırdır durum makinesi cila değildir — yardımcı bir banner ile kendini görmezden gelmeye alıştırdığın bir banner arasındaki farktır.
- Kendini sıfırlayabilen bir koruma, koruma değildir. Her iki takip sürümü de aynı biçimdedir: gerçek durum (işletim sistemi mikrofonu, transkripsiyon durumu) yerine kontrol ettiği bir vekile (kaydedilmiş bir ses, yerel bir boole) atıfta bulunan bir güvenlik mekanizması. Korumaları bir kopyaya değil, gerçeğe yönelt.
v1 hikâyesinin bir önceki bölümü için, backend'i yükleme yolundan çıkarmak (v1.8.11–v1.8.13); ve tüm yay için, yazılımı kusursuzluğa kadar sevk etmenin anatomisi.