Playing Voip

11:06下午 四月 07, 2013 in category Java by ingramchen

最近正在寫 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 啊。在技術上是可以突破的,但戰略上呢 ?

迴響[1]

Android Development with AndroVM

07:00下午 一月 27, 2013 in category Java by ingramchen

Android Development with AndroVM

iOS 的開發一直都有 iOS Simulator 可用,速度快功能也全,讓開發的步調變快許多。Android 也有一個 Android Emulator,功能更多,但是開啟慢,執行更慢到爆,所以一直沒什麼人在用,大家都是拿實機來開發。後來 Android Emulator 出了 Intel x86 版 (現在出到 4.1),這玩意我實際裝過後發現的確比原有的快很多,但是裡面沒有 Google Api,我們公司的 App 跑不起來 (Cubie Messenger 有用到 Google Map)。所以 x86 emulator 跟我們無緣了。

今天要介紹的是另一個 Open Source Android VM,叫 AndroVM,我們公司拿它來開發了 Android 版 Cubie 一個月左右,我可以說 Android 的開發節奏總算能跟上 iOS 了。AndroVM 官網有一個詳細的安裝教學,建議有空的人自己去按它的步驟安裝即可。不過,AndroVM 有一個特別的 AndroVM Player,要用它才能真正達到順暢的開發。官網對它的說明不大夠,所以很多人不會用它的 Player。下面是一步一步安裝和設定教學:

AndroVM 是跑在 Virtualbox VM 裡,再透過內部網路跟本機 OS 溝通,所以要先安裝 VirtualBox,到官網下載直接裝就好,沒什麼難度。

再來是設定 VirtualBox 本機的 DHCP。開啟 VirtualBox 後,到 Preference -> Network

新增本機網路後,應該會出現一個 'vboxnet0' 的網路,double click 開啟它,切到 DHCP Server -> 勾選 Enable Server,再填入以下的值:

如果你知道 DHCP 是什麼原理你可以自己修改上面的值符合你的需求。不然先用上面的值吧。

接下來下載 AndroVM image,先到 AndroiVM 官網有幾個 .OVA 可供下載,一般來說你是開發手機 App應該是下載 "vbox86p version with gapps & houdini" 這個版本。不過你可以根據開發的需求選擇不同版本,下面是代號說明:

  • vbox86p : 手機版, 預設解析度 480×800
  • vbox86t : Tablet 版,預設解析度 1024×600
  • vbox86tp : Tablet 版,但有電話的功能,預設解析度 1024×600
  • gapps : 代表包含 Google 相關服務 (有 Google Play)
  • houdini: 代表包含 Arm emulation (AndroidVM 是跑在 x86 上,所以反過來需要去模擬 Arm)

接下來就是匯入 AndroVM .OVA 檔到 VirtualBox 裡。double click 剛下載的 .ova 檔,就會出現匯入畫面:

原來的名字太長了,你可以自己改。改完成功匯入後,接下來要幫 AndroVM 設定兩張網卡,選擇剛才匯入的 VM,再選 Settings -> Network -> Adaptor1 -> Attatch to: [Host-Only Adaptor]

第一張一定要是 Host-Only,這是給 AndroVM 跟本機 OS 溝通用的,第二張則是 Android 本身要用的 (在 Android 裡會以 WIFI 呈現):

第二張的設定就要看本機用什麼網路了。如果你的本機是用 WIFI,那就要如上圖選 Bridged Adapter,名稱那邊再選 WIFI,如果本機是直接接網路線的,那 Attached To: 應該可以設為 NAT。

好了,到這一步終於設好了,可以開啟 AndroVM 了,選擇 AndroidVM 再按 Start 開機:

沒兩下就進 Android 的歡迎畫面了,開機超快啊!!

接下來你要檢查你的 Android 網路有沒有通,如果右上角出現藍色的 WIFI 圖示,表示你剛才設定的第二張網卡是對的,如果是灰色或黑色的圖示表示網卡設錯,Android 不能上網了,請回到設定第二張網卡那改變設定,再重開一次。

當你點擊到 AndroVM 時,你的滑鼠會被 VirtualBox 抓住,變成只能在 VM 裡移動,出不來。這時只要看右下角顯示的 Host 鍵,然後在鍵盤上按一下就可以出來了。(Host 鍵每個 OS 不同。圖例顯示的是 Mac 的左蝴蝶鍵)

好,把 welcome 步驟跑完後,進入 Android Home Screen,你可以大概玩一下體驗流暢的感覺。Android 的控制鍵對應到主機鍵盤是:

  Android Back Key  -> Esc
  Android Menu Key  -> F1
  Android Power Key -> F4
  解鎖              -> 按 Esc 或 F1

目前 AndroidVM 跑的模式是軟體的 OpenGL ES,但已經很快了。接下來我們要設定硬體加速和 AndroidVM Player。Home screen 上有個設定用的 App 叫 androVM Configuration:

androVM Configuration 裡可以看到第一張網卡的 IP,這個 IP 會用在 AndroVM Player 和 adb 的連線上。另外也提供改變實體鍵盤的行為,以及軟體 OpenGL 模式下的解析度。預設的鍵盤設定是不會開虛擬鍵盤的,這樣和實際手機上的環境差太多,不利開發測試,下面將它改成虛擬鍵盤:

設定好,Save 完 AndroVM 要重開。

到此階段其實就可以拿來開發了,但是滑鼠會被 AndroVM 搶走很難用,軟體 OpenGL 也不夠快。接下來要設定 AndroVM Player 來解決這個問題。先到官網下載 AndroVM Player,依照你的OS版本挑選,本文下載 MacOS 64bit: AndroVMplayer-mac64-20121119.tgz。下載後解開壓縮檔,放在你自己的工具目錄,接下來的範例會用 ~/develop/AndroVMplayer-mac64 這個目錄來說明。

AndroidVM Player 通常是直接下 command line 來開啟,要跑 run.sh 這個 script:

   $ cd ~/develop/AndroVMplayer-mac64
   $ ./run.sh 480 800 240

執行後,你會看到類似下面的輸出,這樣代表成功了。

   Window ID: 0x7fac98c47360
   [OpenGL init OK]
   creating vinput server on TCP port 22469 returns 6
   [Input Server init OK]
   You shall know start the AndroVM Virtual Machine configured for OpenGL Hardware support - waiting for connection from VM ...

不過開啟的 Player 視窗本身是一片空白,因為它在等待 VirtualBox 裡的 AndroVM 連接上。AndroVM 只有在硬體 OpenGL 模式下,才會和 Player 連線。我們先放Player 到一邊,回到 AndroVM Configuration 裡。勾選 Hardware OpenGL,Save 後,重開 AndroVM。

AndroVM 重開後,你會發現它不會直接進入 Android 了,它會開始搜尋 Player,它們之間是透過第一張網卡連線的,所以第一張要正確設定為 Host-Only 才能正確連接。成功連上後,AndroVM 會出現類似的訊息:

然後,AndroVM 會將畫面 "投射" 到 Player 上。你就可以在 Player 上操作 AndroVM 了,速度快而且滑鼠不會被搶走:

AndroVM Player 可以正常運作後,接下來就是讓 eclipse 開發環境可以連上它。我們要使用 Android SDK 提供的 adb 工具來連上 AndroVM:

   $ cd ~/develop/android-sdk-mac_x86  # 這是你的 android sdk 目錄位置 
   $ cd platform-tools
   $ ./adb connect 192.168.56.101      # 192.168.56.101 這個 IP 要代入你的 AndroVM 的第一張網卡的 IP。

請注意 adb connect 後面接的 IP Address 就是 AndroVM 第一張網卡的 IP,你可以在 androVM 的 virtualbox 畫面上看到。也可以到 Android 裡的 AndroVM Configuration 這個 App 裡查到。 正確連上後,可以用下列指令查一下:

   $ ./adb devices 
   List of devices attached 
   192.168.56.101:5555	device

應該要看到一台用該 IP 顯示的裝置才是。這樣在 Eclipse 的 DDMS 就可以看到了,當然也可以直接佈署和 Debug 囉:

到此為止,androVM 開發環境算是設定好了。要注意的是,我們已經開啟 AndroVM 的硬體 OpenGL 模式,現在 AndroVM 開機後,就會開始等待 Player ,而不會直接進入軟體模式的 Android,除非它一分鐘內找不到 Player,它才會退回軟體模式。一旦進入軟體模式後,這時再開啟 Player 就不會互連了,你要重開 AndroVM 才行。

所以每次準備開發環境的執行順序是:

  1. 執行 run.sh
  2. 開啟 virtualbox 的 AndroVM
  3. 執行 adb connect

有興趣的人可以自己寫一個 script 將這三件事自動化。我們公司還沒有去寫這 script 因為我們的開發機從來不關機 :)

最後,AndroVM Player 還有另一個功能 -- 切換 Android 的解析度。剛才我們啟動 Player 時的 run.sh,後面接的三個參數就是解析度的參數,依序是 寬度、高度、DPI。下面列出常用的標準解析度:

  ./run.sh  240  320 120     # ldpi
  ./run.sh  320  480 160     # mdpi
  ./run.sh  480  800 240     # hdpi
  ./run.sh  540  960 256     # hdpi, qhd
  ./run.sh  720 1280 320     # xhdpi
  ./run.sh 1080 1920 440     # Full HD,不過沒人會用這模式開發吧...

當然這三組數字可以任意填啦,但一般開發通常會選 hdpi 的解析度。另外要注意切換 Player 解析度通常還要重開 AndroVM 才行。只重開 Player 通常解析度還是會怪怪的。也許這 bug 之後會修好。

Windows 用戶注意事項

  • Windows 版的 AndroVM Player 沒有 run.sh 這個 script,請用 AndroVMplayer.exe 取代
  • 如果發生無法 Player 和 AndroVM 無法連線,請先檢查 Windows 防火牆的設定。看是要關閉防火牆還是將 AndroVMplayer 加入白名單都可解決問題。
  • AndroVM 已知問題:

    • 有時候 AndroVM 內的無線網路會連不上,即使第二張網卡是設對的。這時候要等一下才會連上線。等不及的就重開試試。
    • AndroVM 裡的 browser 和 WebView 在某些顯示卡下會破圖 (nvidia ?),這可能要到下一版才會修正。
    • 沒有前後 Camera

    結語

    AndroVM 的下一版應該會加入 "軟體模式下滑鼠不會被搶走" 的功能,這樣就可以少掉 Player 的步驟。這個專案目前已經 Open Source 了,未來發展應該會加速,不過現階段已經很好用了。即使有了好用的 VM,用手機實測 App 這一步還是免不了的,但大多數的時間都可以用快速的 VM 開發實在是很爽啊。

    參考資料

    迴響[2]

    Composable Future API

    12:26上午 一月 14, 2013 in category Java by ingramchen

    當我們撰寫 async 程式時,最常用的就是 callback:

       openFileAsync("a.png", (file) { 
          print( file.length ); 
       });
    

    但是一旦 API 設計成 callback,後來就會沒完沒了:

      
       openFileAsync("a.png", (file) { 
          readImageAsync(file, (image) { 
               rotateImageAsync(image, 90, (rotated) {
                    saveImageAsync(rotated......
              }); 
          });
       });
    
    

    對於這個問題,觀察近年來的發展,有一個共同的趨勢,就是額外產生一個 Future 物件 (或叫做 Promise,名稱依語言不同而不同) 來管理這成串的地獄:

    
       Future future = openFileAsync("a.png");
       future.then( (file) { 
          print( file.length ); 
       });
    
    

    當 async task 變多時,可以變成:

    
       openFileAsync("a.png")
           .chain( readImageAsync )    //chain() 也是回傳 Future
           .chain( (image) => rotateImageAsync(image, 90) )
           .then( saveImageAsync );
    
    

    多了一個 Future 物件後,整個 async chain 程式更為明暸,也更容易維護。而且更可以操作更進階的 cancel, error handling, map, filter 等等功能。我可以說 這種提供 Composability 的 Future 取代 callback 的趨勢已經確立了。下面收集了一些最近看到的 API 發展

    JQuery

    最有名最多人碰過的應該是 JQuery 的 Deferred ,從 1.5 版開始就有了:

    http://api.jquery.com/category/deferred-object

    JQuery 的 API 功能完整,用起來最為簡潔,因為 method 的 parameter 的 type 很鬆。

    Akka

    再來是 Akka 內附的 Future API (Both Scala/Java)

    http://doc.akka.io/docs/akka/2.0.1/scala/futures.html

    Akka 的 Future 是個 monad,所以 composability 很高。另外,Twitter 那裡也用 scala 開發了一堆 framework,裡面他們也另外造了一個 Future。

    Java Jdk8

    Java 最早有 concurrent.Future,但那是 old style (只能 pull,不能 push,也不能 compose) 不過 jdk8 Doug Lea 重新設計了一個 CompletableFuture

    http://cs.oswego.edu/pipermail/concurrency-interest/2012-December/010423.html

    這個就是全功能了,配合 lambda 應該可以寫的很簡潔。不過目前的 method 有點混亂,還需要一點時間成熟。

    Google Guava

    Google 的 Guava 替 concurrent.Future 補強,多了個 ListenableFuture,這 jdk6 就能用了,大部份功能都有,但因為沒有 lambda 所以語法很慘。

    http://code.google.com/p/guava-libraries/wiki/ListenableFutureExplained

    Python 3

    Python 3.2 把 java concurrent 整包移植過去了,當然也包含 Future,不過這個 future 只多了 add_done_callback method,沒有 composability

    http://docs.python.org/dev/library/concurrent.futures.html

    Dart

    Dart 因為是 web,一大堆的 api 都是 async 的,所以從一開始就加了 Future,composability 還不錯,使用上也算簡潔。最近 Dart M3 正在大改 Future/Stream/Iterator/DomEvent 相關的 API,打算把這些 listener 全部整合成統一的概念。

    http://code.google.com/p/dart/source/browse/branches/bleeding_edge/dart/sdk/lib/async

    Objective C

    Objective C 在 github 上有看到一些,不過不是很完整,也不大像現在的 Composable Future 的設計趨勢。Composable Future 這個潮流大概還沒吹到 Objective C 吧... Objective C 有 Block 語法可用,但也就停在這裡了。偏偏 iOS 開發上 async 的操作很多,最後我只好自己刻了一個陽春的 Future 來克服這個問題。

    node.js

    node.js 我沒什麼研究,有看到一個 async library

    https://github.com/caolan/async

    設計的不錯。它雖然沒有 Future 的物件,但是提供 callback function 很好的 composability

    其他

    其他語言我就沒什麼涉獵了,Ruby/C# 那裡應該也有類似的。

    ps. 上面我用 Composable 這個字用的很兇,我是參考這篇文章的定義:

    http://blog.softmemes.com/2012/06/18/the-way-of-the-future-futures-in-net-java-and-javascript

    迴響[0]