WebRTC ile güvenli video görüşme: sinyalleşmeden TURN'e
PsyBank'ın görüşme altyapısında medya neden sunucuya uğramaz? NAT geçişi, TURN maliyeti ve şifreleme kararları.
Bir danışan, kaldığı otelin lobisindeki paylaşımlı Wi-Fi üzerinden terapistiyle görüşmeye bağlanmaya çalışıyor. Terapist ise şehrin öbür ucunda, mobil operatörün 4G hattında. İkisi de birbirinin IP adresini bilmiyor, ikisi de kurumsal ya da operatör NAT'ının arkasında, ikisinin de router'ında dışarıya açık tek bir port yok. Ve yine de, üç saniye içinde birbirlerinin yüzünü görüp konuşmaya başlamaları gerekiyor. Üstelik aralarındaki her saniye, en hassas veri sınıfına giren bir psikoterapi seansı. PsyBank'ın görüşme altyapısını kurarken çözdüğümüz asıl problem buydu; "video" değildi.
Yıllar bize şunu öğretti: WebRTC bir video teknolojisi değil, video kılığına girmiş bir NAT geçiş problemidir. Kamera açmak, kodek seçmek, ekran paylaşmak; bunların hepsi çözülmüş işler. Zor olan, internetin ortasında birbirini hiç tanımayan iki uç noktayı, aralarına hiçbir sunucu koymadan tanıştırmak. Bu yazıda PsyBank'ın 6.443'ü aşkın terapisti ve 101.446'yı aşan danışanı taşıyan görüşme altyapısını nasıl kurduğumuzu; SDP ve ICE'nin gerçekte ne yaptığını, sinyalleşme sunucusunu neden "az ama kusursuz" iş yapacak şekilde tasarladığımızı, TURN'ün maliyet matematiğini ve seansın neden asla sunucuya uğramadığını anlatacağız.
Baştan söyleyelim: burada anlatılanların çoğu, dokümantasyonda güzel görünüp sahada çöken varsayımların üstüne kurulu. Biz o çöküşleri yaşadık, ölçtük ve mimariye ders olarak geri yazdık.
WebRTC'nin gerçek işi: iki yabancıyı NAT arkasında tanıştırmak
Bir WebRTC bağlantısının kalbinde iki kavram var: SDP ve ICE. SDP (Session Description Protocol) bir el sıkışma metnidir; "ben şu kodekleri konuşurum, şu çözünürlükleri desteklerim, şifreleme anahtarım bu" diyen bir tanışma kartı. Bir taraf teklif (offer) üretir, diğer taraf yanıt (answer) döner. Bu iki metnin karşılıklı değiş tokuşu, iki tarayıcının aynı dili konuşacağından emin olmasını sağlar. Ama SDP tek başına hiçbir bağlantı kurmaz; sadece "nasıl konuşacağımızı" anlatır, "nereden ulaşacağımızı" değil.
"Nereden" sorusunun cevabı ICE'dir (Interactive Connectivity Establishment). Her uç nokta, kendisine ulaşılabilecek olası adreslerin bir listesini çıkarır: yerel ağ adresi, NAT'ın dışarıya gösterdiği genel adres, ve gerekirse bir relay üzerinden erişilebilecek adres. Bu adreslerin her birine ICE adayı (candidate) denir. İki taraf adaylarını birbirine gönderir ve ICE motoru, çalışan bir çift bulana kadar bu adayları sistematik olarak birbirine karşı dener. Çalışan ilk çift üzerinden medya akmaya başlar. Bütün mesele budur; gerisi ayrıntı.
Burada çoğu ekibin gözden kaçırdığı gerçek şu: ICE aday toplama süreci zaman alır ve bu süre kullanıcının gördüğü "bağlanıyor..." ekranının uzunluğudur. Adayları teker teker toplayıp hepsi gelene kadar beklerseniz (vanilla ICE), kullanıcı üç-dört saniye boş ekrana bakar. Biz baştan itibaren trickle ICE kullandık: adaylar bulundukça anında karşı tarafa gönderilir, ICE motoru henüz liste tamamlanmadan denemeye başlar. Bu tek karar, medya başlama süresini ortalama 2,8 saniyeden 900 milisaniyenin altına indirdi. Bir terapi seansında ilk üç saniye, danışanın "acaba çalışıyor mu?" endişesini yaşadığı üç saniyedir; onu ortadan kaldırmak teknik bir iyileştirme değil, bir güven kararıdır.
SDP değiş tokuşunun sinsi bir tuzağı da vardır: iki taraf aynı anda teklif üretirse (glare durumu), müzakere kilitlenir. Kamera açıp kapatma, ekran paylaşımı başlatma gibi seans ortasındaki her değişiklik yeni bir SDP müzakeresi tetiklediği için bu çakışma sandığınızdan sık yaşanır. Biz "perfect negotiation" desenini uyguladık: taraflardan biri kibar (polite), diğeri kaba (impolite) rol alır; çakışma anında kibar taraf kendi teklifinden vazgeçip karşıdakini kabul eder. Bu küçük rol ayrımı olmadan, seans içinde ekran paylaşan bir terapistin görüşmeyi yanlışlıkla kilitlemesi an meselesidir. Kimin hangi rolü aldığına odaya girişte, deterministik biçimde karar veriyoruz; böylece her iki uç da aynı sonucu bağımsızca hesaplıyor.
Sinyalleşme sunucusu: az iş yapar, ama kusursuz yapar
SDP ve ICE adaylarını iki tarafın birbirine ulaştırması gerekir; ama iki taraf henüz birbirine bağlanamadığı için bu değiş tokuşu yapacak bir aracıya ihtiyaç vardır. İşte sinyalleşme sunucusu tam olarak bu aracıdır ve başka hiçbir şey değildir. Görevini bir cümleyle özetleyebiliriz: mektupları taşır, ama zarfları açmaz. Bizim sinyalleşme katmanımız Go ile yazıldı; her görüşme bir "oda" nesnesidir ve sunucu sadece o odadaki mesajları doğru alıcıya yönlendirir.
type Oda struct {
ID string
Uyeler map[string]*websocket.Conn
mu sync.RWMutex
}
// Sinyalleşme yalnızca "kim, kime, ne" bilgisini taşır.
// İçerik (SDP/ICE) sunucu için opak bir yüktür; açılmaz, saklanmaz.
func (o *Oda) Yonlendir(gonderen string, m Sinyal) {
o.mu.RLock()
defer o.mu.RUnlock()
for id, conn := range o.Uyeler {
if id == gonderen {
continue // gönderene geri yollama
}
// offer / answer / ice — üçü de aynı borudan geçer
_ = conn.WriteJSON(m)
}
}
Bu kadar. Sinyalleşme sunucusunun tuttuğu tek durum, hangi WebSocket bağlantısının hangi odada olduğudur. SDP'nin içinde ne yazdığını bilmez, umursamaz; onun için bir SDP teklifi ile bir ICE adayı, yönlendirilecek iki opak zarftan ibarettir. Bu sadeliğin bilinçli bir tasarım tercihi olduğunu vurgulamak isteriz. Sunucuya iş yükledikçe, hem saldırı yüzeyini hem de KVKK sorumluluğunu büyütürsünüz. Bilmediğiniz veri, sızdıramayacağınız veridir.
Tek bir sunucu tüm görüşmeleri taşıyamayacağı için birden çok örnek çalıştırıyoruz; ama danışan ve terapist farklı örneklere düşebilir. Oda durumunu örnekler arasında senkron tutmak için Redis pub/sub kullanıyoruz. Bir taraf odaya girdiğinde ilgili kanala yayın yapılır, diğer örnek haberdar olur ve mesajlar doğru sokete akar. Böylece kullanıcı hangi sunucuya bağlanırsa bağlansın, eşleşme kusursuz kurulur. Go'nun goroutine modeli burada doğal bir eşleşme: her WebSocket bağlantısı kendi hafif goroutine'inde yaşar, on binlerce açık bağlantı birkaç çekirdekli tek düğümde rahatça taşınır.
Sinyalleşme sunucusu tarafları tanıştırır ve çekilir. Medyayı taşıyan bir sinyalleşme sunucusu, artık sinyalleşme sunucusu değildir; onu öyle sanmaya devam ederseniz, ne maliyetini ne de mahremiyet borcunu doğru hesaplayabilirsiniz.
STUN ucuzdur, TURN pahalıdır: relay'in maliyet matematiği
İki uç noktanın çoğu, aslında birbirine doğrudan bağlanabilir; sadece kendi genel adreslerini bilmezler. STUN sunucusu tam olarak bunu çözer: uç nokta STUN'a bir paket gönderir, STUN "seni şu genel IP ve porttan görüyorum" diye cevap verir. Bu bilgiyle uç nokta, NAT'ın dışarıya gösterdiği adresi bir ICE adayı olarak listeye ekler. STUN bedava denecek kadar ucuzdur, çünkü sadece birkaç küçük paket alışverişi yapar; medyanın bir baytına bile dokunmaz. Görüşmelerimizin büyük çoğunluğu STUN'un verdiği bu genel adres üzerinden doğrudan, uçtan uca kurulur.
Ama her NAT bu kadar kibar değildir. Bazı kurumsal güvenlik duvarları ve özellikle mobil operatörlerin taşıyıcı sınıfı NAT'ları (CGNAT), simetrik NAT denilen türden davranır: her hedef için farklı bir dış port açarlar, dolayısıyla STUN'un öğrendiği adres karşı tarafla konuşurken işe yaramaz. İki taraf da bu türden bir NAT arkasındaysa, doğrudan bağlantı matematiksel olarak imkânsız hâle gelir. İşte tam bu noktada TURN devreye girer. TURN bir relay'dir: her iki uç nokta da TURN sunucusuna bağlanır ve tüm medya trafiği bu sunucu üzerinden geçer. Yani TURN'e düşen bir görüşmede, sesin ve görüntünün her baytı sunucunuzun bant genişliğini tüketir.
Maliyet farkını somutlaştıralım. Tek bir görüşülen taraf çift yönlü video için tipik olarak 1–2 Mbps tüketir. STUN üzerinden doğrudan kurulan bir görüşme sunucunuza sıfıra yakın maliyet bindirir. TURN'e düşen aynı görüşme ise, tüm süre boyunca sunucunuzun bant genişliğinden akar; binlerce eşzamanlı seansta bu, aylık bulut faturasında dört haneli dolar farkı demektir. Ölçümlerimiz şunu gösterdi:
- Sabit hatlar ve ev Wi-Fi'si: Görüşmelerin yaklaşık %85'i STUN ile doğrudan kurulur; TURN'e hiç ihtiyaç duymaz.
- Mobil operatör ağları: CGNAT nedeniyle TURN'e düşme oranı bazı operatörlerde %25–30'a kadar çıkar; iki taraf da mobildeyse oran daha da yükselir.
- Kurumsal ağlar: Katı güvenlik duvarları UDP'yi tümden kapatabilir; bu durumda TCP hatta 443 portu üzerinden TURN, bağlanabilmenin tek yoludur.
- Genel ortalama: Tüm trafiğimizde görüşmelerin yaklaşık %12'si TURN relay'ine düşüyor; bu %12, medya bant genişliği maliyetimizin neredeyse tamamını oluşturuyor.
Buradan çıkan ders net: TURN'ü lüks değil, sigorta olarak görün. Onu kapatırsanız görüşmelerin belirli bir yüzdesi sessizce hiç kurulamaz ve kullanıcı nedenini asla anlamaz; sadece "bu uygulama çalışmıyor" der ve gider. Ama TURN'ü de düşüncesizce açık bırakmak, faturayı katlar. Biz iki yönlü davrandık: TURN sunucularını medya trafiğinin coğrafi olarak yakınına yerleştirip gecikmeyi düşürdük, ve TURN kimlik bilgilerini süreli hâle getirdik. Statik bir TURN kullanıcı adı ve parolası bir kez sızdığında, bant genişliği hırsızları relay'inizi kendi trafikleri için sömürür. Çözüm, coturn'ün desteklediği kısa ömürlü HMAC tabanlı kimlik bilgileri:
// TURN için süreli kimlik: parola, paylaşılan sırla üretilen bir HMAC'tir.
// Kullanıcı adı bir son-geçerlilik zaman damgası taşır; süresi dolunca
// aynı kimlik bir daha çalışmaz. Sızsa bile ömrü dakikalarla sınırlıdır.
func turnKimlik(sir, kullaniciAdi string, omur time.Duration) (string, string) {
sonGecerlilik := time.Now().Add(omur).Unix()
kullanici := fmt.Sprintf("%d:%s", sonGecerlilik, kullaniciAdi)
mac := hmac.New(sha1.New, []byte(sir))
mac.Write([]byte(kullanici))
parola := base64.StdEncoding.EncodeToString(mac.Sum(nil))
return kullanici, parola // istemciye anlık verilir, ömrü kısadır
}
Her görüşme başlamadan istemciye taze bir kimlik veriyoruz; ömrünü seansı taşıyacak kadar, ama sızıntıyı işe yaramaz kılacak kadar kısa tutuyoruz. Bu, TURN'ün maliyet dengesinin sadece bant genişliği tarafı değil, güvenlik tarafıdır ve ikisini birlikte düşünmeyen ekipler er ya da geç ya şişmiş bir faturayla ya da sömürülmüş bir relay'le tanışır.
Medya sunucuya uğramaz: DTLS-SRTP ve KVKK'nın mimari karşılığı
PsyBank sağlık verisi işleyen bir üründür ve bir psikoterapi seansının içeriği, düşünülebilecek en hassas veridir. Bu yüzden altyapıyı kurarken pazarlıksız bir ilke koyduk: seans içeriği hiçbir koşulda sunucuya uğramaz. WebRTC'nin güzelliği, bu ilkeyi teknolojinin doğasına gömülü kılmasıdır. WebRTC'de medya trafiği zorunlu olarak şifrelidir; şifresiz WebRTC diye bir şey yoktur. Ses ve görüntü SRTP ile, aralarındaki anahtar değişimi ise DTLS ile korunur. İki uç nokta DTLS el sıkışmasında birbirinin sertifika parmak izini doğrular ve oturum anahtarlarını yalnızca aralarında üretir.
Kritik nokta şu: bu anahtarlar hiçbir zaman sinyalleşme sunucusundan geçmez. Sunucu SDP zarflarını taşırken içindeki DTLS parmak izini görür, ama oturum anahtarını asla göremez; o anahtar iki uç nokta arasında, DTLS el sıkışması sırasında doğrudan türetilir. Dolayısıyla STUN üzerinden doğrudan kurulan bir görüşmede medya, terapistin cihazından danışanın cihazına, arada hiçbir sunucuya uğramadan, uçtan uca şifreli akar. Sunucumuz teknik olarak dinleyemez; "dinlemeyiz" bir söz değil, mimarinin bir sonucudur.
Parmak izi ayrıntısı burada göründüğünden önemlidir. SDP içinde taşınan DTLS sertifika parmak izi, bir bağlama görevi görür: DTLS el sıkışmasında karşı tarafın sunduğu sertifika, SDP'de duyurulan parmak iziyle bire bir eşleşmek zorundadır. Eşleşmezse el sıkışma reddedilir. Bunun anlamı şudur; kötü niyetli bir aracı, sinyalleşme trafiğini ele geçirip araya girmeye kalksa bile, kendi sertifikasının parmak izini SDP'ye yazmadan bağlantıyı kuramaz, yazarsa da karşı taraf beklediği parmak iziyle uyuşmadığını görür. Sinyalleşme kanalının bütünlüğünü ayrıca TLS ile koruduğumuz için, bu iki katman birlikte ortadaki adam saldırısını pratikte kapatır. Güvenlik tek bir sihirli anahtardan değil, birbirini doğrulayan bu katmanlardan doğar.
KVKK uyumunun en sağlam biçimi, veriyi iyi korumak değil, veriye hiç sahip olmamaktır. Sunucunuzdan geçmeyen içeriği ne sızdırabilir, ne de mahkeme celbiyle teslim etmek zorunda kalabilirsiniz; en güvenli veri, hiç toplanmamış veridir.
Burada dürüst bir dipnot düşmek gerekir, çünkü sektörde bu konuda çok fazla pazarlama yalanı dönüyor. TURN'e düşen görüşmelerde medya, tanım gereği relay sunucusundan geçer. Ancak geçmesi, okunması demek değildir: TURN sunucusu SRTP ile şifrelenmiş paketleri sadece bir uçtan alıp öbür uca iletir, oturum anahtarına sahip olmadığı için içeriği çözemez. Yani "uçtan uca şifreli" ilkesi TURN'de de korunur; değişen tek şey, şifreli baytların bir relay üzerinden akmasıdır. Bu ayrımı kullanıcıya ve denetçiye dürüstçe anlatmak, "asla hiçbir sunucudan geçmez" diye abartmaktan çok daha sağlam bir güven zeminidir. Biz TURN sunucularını da kendi kontrolümüzdeki, kayıt tutmayan, medyayı diske yazmayan yapılandırmalarla çalıştırıyoruz; relay bir boru olarak kalır, bir depo hâline asla gelmez.
Bağlantı kalitesini izlemek ve seansı ayakta tutmak
Bağlantıyı kurmak işin yarısı; onu bir terapi seansı boyunca ayakta tutmak diğer yarısı. Bir alışveriş sepeti düştüğünde kullanıcı sinirlenir; bir terapi seansı düştüğünde ise duygusal olarak açılmış bir insan, cümlesinin ortasında yalnız kalır. Bu yüzden bağlantı kalitesini sürekli izliyor ve bozulmayı kullanıcı fark etmeden önce yakalamaya çalışıyoruz. WebRTC'nin getStats arayüzü bize saniye saniye ölçüm verir: paket kaybı oranı, gidiş-dönüş gecikmesi, jitter ve alıcı tarafının tamponlama davranışı.
Bu metrikleri üç renkli basit bir sağlık göstergesine indirdik. Paket kaybı belirli bir eşiği aştığında ya da gecikme tırmandığında, önce sessiz uyarlamalar devreye girer: video çözünürlüğünü ve bit hızını kademeli düşürürüz. Bir terapi seansında sesin sürekliliği görüntünün netliğinden çok daha önemlidir; hat daraldığında görüntüyü feda edip sesi korumak, verdiğimiz en önemli önceliklendirme kararıydı. Kullanıcı bulanık bir görüntüyle konuşmaya devam edebilir; kesik kesik bir sesle edemez.
Bağlantı büsbütün koptuğunda ise devreye ICE restart girer. Kullanıcı bir tünele girip çıktığında ya da Wi-Fi'den mobil veriye geçtiğinde IP adresi değişir ve mevcut ICE yolu ölür. Baştan yeni bir görüşme kurmak yerine, aynı oturum üzerinde ICE müzakeresini yeniden başlatıp taze adaylarla yeni bir yol buluyoruz. Bu, kullanıcının gözünde bir-iki saniyelik bir donma olarak görünür ve seans kaldığı yerden devam eder; oysa naif bir uygulama görüşmeyi tümden düşürüp danışanı yeniden bağlanma ekranına atardı.
// Bağlantı koptuğunda görüşmeyi sıfırlamayız; ICE'yi yeniden müzakere ederiz.
pc.oniceconnectionstatechange = function() {
if (pc.iceConnectionState === "disconnected" ||
pc.iceConnectionState === "failed") {
// Taze adaylarla aynı oturum üzerinde yeni bir yol ara
pc.restartIce();
yenidenBaglanmaSayaciniBaslat(); // jitter'lı, üstel geri çekilme
}
}
Yeniden bağlanma denemelerini jitter'lı üstel geri çekilme ile yapıyoruz; çünkü bir baz istasyonu ya da sunucu kısa süreli sarsıldığında, binlerce istemcinin aynı anda yeniden bağlanmaya hücum etmesi bir "bağlantı fırtınası" yaratır ve zaten zayıflamış tarafı büsbütün devirir. Denemeler arasına rastgele dağıtılmış gecikmeler koymak, bu fırtınayı düz bir dalgaya çevirir. Bu dersi Go tarafındaki sinyalleşme sunucusunun yeniden başlatmalarında da öğrenmiştik; istemci tarafında da aynı disiplin geçerli.
İzlemenin bir de seans başlamadan önceki hâli var. Danışan görüşme odasına girdiğinde, "bağlan" düğmesine basmadan önce arka planda sessiz bir ağ testi çalıştırıyoruz: kamera ve mikrofon erişimini, STUN üzerinden aday toplayabildiğini ve gerekirse TURN'e ulaşabildiğini birkaç saniyede yokluyoruz. Sonuca göre kullanıcıya net bir dille "bağlantınız görüşme için uygun" ya da "ağınız zayıf, kablolu bağlantı ya da farklı bir şebeke deneyin" diyoruz. Bu ön kontrol, seansın ortasında yaşanacak hüsranın önemli bir kısmını daha kapıda yakalıyor. Bir sorunu kullanıcı henüz duygusal olarak açılmamışken söylemek, seansın on beşinci dakikasında söylemekten çok daha az zarar verir; hüsranı erkene çekmek, onu hafifletmenin en ucuz yoludur.
Mobil ağlarda öğrendiklerimiz
Masaüstünde kusursuz çalışan bir görüşme altyapısı, mobil ağlarda bambaşka bir hayvandır. Kullanıcılarımızın önemli bir kısmı seansa telefonundan, hareket hâlindeyken ya da değişken sinyal koşullarında katılıyor; ve laboratuvarda hiç görmediğimiz sorunların çoğu ilk kez sahada, gerçek mobil ağlarda ortaya çıktı. Öğrendiklerimizi maddeler hâlinde bırakmak, benzer bir yola çıkacaklara en somut faydayı sağlar:
- Ağ geçişleri kural, istisna değil: Kullanıcı asansöre biner, Wi-Fi'den 4G'ye düşer, tünele girer. Her geçiş IP değişikliğidir. ICE restart'ı sağlam kurmadan mobil bir görüşme ürünü yayınlamak, ilk günden şikâyet üretir.
- CGNAT TURN'ü zorunlu kılar: Mobil operatörlerin taşıyıcı NAT'ı simetriktir; iki mobil kullanıcı arasında doğrudan bağlantı çoğu zaman kurulamaz. TURN olmadan bu görüşmeler sessizce hiç başlamaz.
- UDP her yerde açık değildir: Bazı kurumsal ve halka açık ağlar UDP'yi kısıtlar. TURN'ü yalnızca UDP üzerinde değil, TCP ve 443 portu üzerinden de erişilebilir tutmak, "hiç bağlanamıyorum" vakalarını belirgin biçimde düşürdü.
- Pil ve ısı gerçek kısıtlardır: Yüksek çözünürlüklü video, telefonu ısıtır ve pili tüketir. Uzun seanslarda çözünürlüğü makul bir tavana sabitlemek, hem cihazı korur hem de dar bantta kararlılık sağlar.
- Ekran kapanması bağlantıyı öldürebilir: Mobil işletim sistemleri arka plandaki uygulamaları agresif biçimde uyutur. Sesli seanslarda bunu aşmak için özel önlemler almadan, kullanıcı telefonu cebine koyduğunda görüşme sessizce ölür.
Bu maddelerin ortak dersi şu: mobil ağ, kararlı bir kanal değil, sürekli müzakere edilen bir ilişkidir. Altyapıyı "bağlantı bir kez kurulur ve durur" varsayımıyla tasarlarsanız, mobil gerçeklik onu ilk gün çürütür. Biz tersinden tasarladık: bağlantı her an kopabilir, her an yeniden kurulması gerekir, ve bu yeniden kurulum kullanıcıya mümkün olduğunca görünmez olmalıdır. Bu zihniyet değişikliği, mobil seans başarı oranımızı ilk sürüme göre kayda değer biçimde yukarı çekti ve %98'lik danışan memnuniyetinin sessiz ama en belirleyici bileşenlerinden biri oldu.
Sonuç
WebRTC ile güvenli video görüşme kurmanın özü, aslında az sayıda ama sarsılmaz karara dayanır. Sinyalleşme sunucusunu mümkün olan en aptal aracı olarak tasarlayın; ne kadar az bilirse o kadar az sızdırır. STUN'a güvenin ama TURN'ü bir sigorta olarak her zaman hazır, süreli kimliklerle korunmuş ve maliyeti ölçülmüş biçimde tutun. Medyayı uçtan uca şifreli tutup ilkenizi dürüstçe anlatın; "hiç geçmez" diye abartmak yerine "geçse bile okunamaz" diyebilecek kadar sağlam bir mimari kurun. Bağlantıyı kurmayı değil, onu ayakta tutmayı asıl iş olarak görün. Ve mobil ağı kararlı bir kanal değil, sürekli müzakere edilen bir ilişki olarak kabul edin.
Bütün bu altyapıyı PsyBank için 14 haftada, Go, Svelte, PostgreSQL ve Redis ile kurup Docker imajlarıyla AWS üzerinde yayınladık; ama asıl kazandıran şey teknoloji seçimi değil, en başta koyduğumuz ilkeydi: seans içeriği asla sunucuya uğramaz. Benzer bir görüşme altyapısı kuracaksanız, size en net tavsiyemiz şu: önce mahremiyet ilkenizi ve kopma senaryolarınızı yazın, dili ve kütüphaneyi sonra seçin. Çünkü sonunda kullanıcının hatırlayacağı şey, hangi kodeği kullandığınız değil; en kırılgan anında sesinin kesilmemiş ve kimsenin dinlememiş olmasıdır.