Steven
Steven7 dk okuma

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.

Mühendislik
macOS
Desktop
GeekBye Releases
Bir Görüşmeyi Açık Bir Uygulamadan Ayırt Etmek

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-active gibi 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 notificationActive boole 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

  1. 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.
  2. 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.
  3. 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.

İlgili Makaleler

Web Audio'yu Hayatta Tutan Üç Fiil
Steven
Steven8 dk okuma

Web Audio'yu Hayatta Tutan Üç Fiil

İki ay arayla ve iki farklı dosyada, iki GeekBye nokta sürümü, ses kodumuza aynı dersi zıt uçlardan öğretti: tarayıcının AudioContext'ini tek kullanımlıkmış gibi ele almayı bırak. Bir sürüm, macOS'un kayıt ortasında sessizce askıya aldığı bir bağlamı resume() etmeyi öğrendi; diğeri, ardışık oturumların Chromium'un yaklaşık altı bağlamlık tavanına toslamayı bırakması için close() yerine suspend() yapmayı öğrendi. Resume, suspend, close — bütün olay örgüsü bu.

Mühendislik
Audio
Desktop
Backend'i Yükleme Yolundan Çıkarmak
Steven
Steven7 dk okuma

Backend'i Yükleme Yolundan Çıkarmak

GeekBye ekranınızı kaydeder ve videoyu Google Drive'ınıza kaydeder. İlk sürüm her kaydı oraya giderken GeekBye'in kendi sunucuları üzerinden geçirdi; bir sürüm sonra, dosya makinenizden doğrudan Drive'a gitti, ve backend tek bir işaretçiyi tutmaya indirgendi. İlginç kısım, 'doğrudan, devam ettirilebilir' sürümün aslında ne kadar az kod içerdiğidir — çünkü devam ettirilebilirlik bir proxy yazmaktan değil, bir proxy'yi silmekten geldi.

Mühendislik
Mimari
Desktop
On İki Dakikada Doğdu ve Düzeltildi
Steven
Steven6 dk okuma

On İki Dakikada Doğdu ve Düzeltildi

GeekBye v1.8.10'in changelog'u klavye kısayollarını düzenlerken bir çökmeyi düzelttiğini söyler. Düzeltti — ama çökme aynı pull request'te, on iki dakika arayla eklendi ve düzeltildi, ve tek bir kullanıcıya bile ulaşmadı. Asıl hikâye onu üreten güvenilirlik çağlayanıdır: kısayolların ne zaman askıya alındığına dair küçük, doğru bir değişiklik, ve editörü daha güvenli kılmak için tasarlanmış tek bir satırdan düşen React teardown bug'ı.

Mühendislik
React
Desktop