07 April 2013

最近正在寫 voip 相關的功能,(for Cubie, 未來的某一天會有吧) 我是當 pet project 在寫啦,所以都是在家裡自己慢慢手刻。voip 相關的技術很雜,橫跨網路,語音,編碼,即時處理,通訊協定,手機裝置... 等等領域,一個人不大可能面面俱到,所以盡可能尋求現有 lib 來兜。

我實際做一輪後,包含 android iOS 各寫一份(淚...) 發現細部的 library 或 framework 都已經有了,差就差在你要把它們串在一起 (當然你也要先搞懂那些 library) 。串好了,就有一個土炮 voip 可用了。voip 不是什麼新技術了,不過自己寫的第一通打通開講時還真的蠻爽的。

這一輪的實作幾乎都是用以前完全沒碰過的技術寫的: protobuf,opus,iOS Audio Unit,android ndk/jni... etc 精力也大多花在學習這些新技術上,大大滿足了我學習的慾望,尤其是有機會寫到一點 C。

心得:

c 語言... 嗯,從一個寫物件導向的眼中來看,就像是脫光衣服上戰場一般無異,所有的欄位、變數、參數都是開著讓所有人讀寫。雖然身輕如燕,但就是毫無防備,我想得練就一身好功夫才能駕馭它吧。個人還是偏好拿著武器披上盔甲再上陣,沒特別需求我大概不會用這麼低階的語言來開發吧。

iOS audio api 遠勝 android。android 只有高階 api 可控制,還零零落落的,latency 也很高,也不曉得這一大票手機種類 buffer 要設多大多小 聲音才會穩定。iOS 則是有最低層的 Audio Unit 可以使用,而且是 Real time thread,latency 可以降到最低。

voip 的手機端牽涉到 multithread programming,這時 java 的 concurrent api 和 garbage collect 的環境就比 objective c 先進多了。objective c 的 GCD, Grand central dispatch 是讓事情簡化很多,我也盡量用了,但跟 java 比,還是少了高階的元件像是 LinkedBlockingQueue 可以運用。另外在 multithread 下自己管理記憶體還是太麻煩,動不動就採錯 pointer。寫的時候要很小心,未來的維護也很困難。

opus 是個簡單又強大的編碼, royal free. 不用改什麼就可移植到 android 和 iOS,聲音清析,速度尚可 (畢竟沒有硬體加持...)。這個編碼直接提供兩個對 voip 很有用的功能,一是容許封包遺失 (掉了 30% 還是聽起來不錯),二是可以偵測背景音 -- 如果沒有在講話,只有背景音時,輸出的封包只會有一個 byte,大大降低了傳輸所需的 bit rate。opus 雖然沒有硬體加速,但 smartphone 也很夠力了,畢竟不是影像,只是聲音而已,那種白牌超爛的 android 跑起來也是 ok 的。

android ndk 不難用,難是難在要寫 jni,jni 的程式碼醜到了一個極致,一開始我花了很多時間去避開 jni,而是採另一套 JNA library 來呼叫 C 的 api,不過光是將 JNA 套進 android 環境就搞了我好幾天。最後是套進去了,也可以正常呼叫 c,但很悲慘的是 android vm 產生大量的 GC,profile 之後發現 JNA 用了很多 reflection,過程中會產生很多 string。最後還是乖乖去寫 jni,放棄 JNA 的方案。jni 寫起來好噁啊,我寫完都要洗個澡 才行 XD

反觀 objective c 呼叫 c api 就像是在家一樣 (廢話,它們本來就一家人...) 寫起來簡單多了。iOS 開發環境也支援 objective c++,所以跟 protobuf (protobuf 只支援 java/c++/python) 整合時也是很簡單, 不過同一個檔案同時混著 c c++ objective c 程式碼感覺很奇妙就是。(維護難度較高?)

voip 協定是自己設計的,用 Google protobuf 搭配 netty4 實作。protobuf 的工具來設計網路封包真的很簡單,而 netty 則是內建 protobuf 的支援,所以你要煩惱的只剩協定的流程、加密和邏輯本身。boiler plate 什麼的 protobuf 都搞定了。protobuf 的 serialization 速度目前 排行第二,離第一名 kryo 只差一點點而已。kryo 不能跨語言平台,所以再好也是不考慮的。protobuf 剛出來時 Google 被社群的人罵臭頭,說什麼又再重新發明輪子。我則是想說重新發明的好啊,這比以前所有的輪子都還圓啊。protobuf 有個資料型別叫 varint,當數字小的時候只佔 1 個 byte,數字漸漸變大時才會增加到 2, 3...4 個 byte。voip 每個 byte 都是吋土吋金,能省多少都是贏啊。protobuf 很棒,真要抱怨的話,就是 protobuf 產生的 java api 套了 builder pattern,寫起來很繁瑣。

至於協定細節的話沒什麼好提的,就根據 Cubie 的需求來客製,比較特別的是這一次的協定把一開始的 handshake 次數降到了最低 (就最低,0次)。從 Cubie 過往的經驗來看,3g 網路上做複雜的 handshake 會花上一秒到數秒的時間,使用者觀感大大扣分啊...。所以協定被設計成,socket 一連上後,下一個封包就是播打的封包,這封包包含了播打/login/加密三件事,在同一個 request 下一次完成,減少 round trip。我想這是設計自己的 protocol 唯一的好處吧,可以根據需求做最佳化,而不是要背負舊協定的包袱。

心得大概就先這樣,voip 還有一大段是處理 wifi AP hole punch,還有語音 lag/掉封包的最佳化,server scaling 等等。這個 phase 暫時不會先深入這塊了,我們得先完成 beta。不過會不會上線還是未知數咧,也有可能半路就轉彎,直接付錢橋接別人的服務了事,例如 twilio,但它在我們的測試下 latency 有點高,又會當,所以才會想自己寫... 另外,儘管 voip 是我們用戶要求最多的功能,但這功能理論上並不夠區別 Cubie 啊。在技術上是可以突破的,但戰略上呢 ?


回響

可以用 Tag <I>、<B>,程式碼請用 <PRE>