WebRTC 的代碼量不小,一次性看明白不太現實,在本系列中,我將試圖搞清楚三個問題:java
本文是第一篇,我將從最熟悉的採集入手,分析一下 WebRTC-Android 相機採集的實現。android
WebRTC-Android 的相機採集主要涉及到如下幾個類:Enumerator,Capturer,Session,SurfaceTextureHelper。web
其中 Enumerator 建立 Capturer,Capturer 建立 Session,實現對相機的操做,SurfaceTextureHelper 實現用 SurfaceTexture 接收數據。安全
Enumeratorsession
CameraEnumerator 接口以下:多線程
public interface CameraEnumerator { public String[] getDeviceNames(); public boolean isFrontFacing(String deviceName); public boolean isBackFacing(String deviceName); public List<CaptureFormat> getSupportedFormats(String deviceName); public CameraVideoCapturer createCapturer( String deviceName, CameraVideoCapturer.CameraEventsHandler eventsHandler); }app
主要是獲取設備列表、檢查朝向、建立 Capturer。異步
Captureride
WebRTC 視頻採集的接口定義爲 VideoCapturer,其中定義了初始化、啓停、銷燬等操做,以及接收啓停事件、數據的回調。函數
相機採集的實現是 CameraCapturer,針對不一樣的相機 API 又分爲 Camera1Capturer 和 Camera2Capturer。相機採集大部分邏輯都封裝在 CameraCapturer 中,只有建立 CameraSession 的代碼在兩個子類中有不一樣的實現。
下面分別看看 VideoCapturer 幾個重要的 API 實現邏輯。
initialize
initialize 比較簡單,只是保存一下傳入的相關對象。
startCapture
startCapture 則會先檢查當前是否正在建立 session,或者已有 session 正在運行,這裏保證了不會同時存在多個 session 在運行。而衆多狀態成員的訪問都經過 stateLock 進行保護,避免多線程安全問題。
若是須要建立 session,則在相機操做線程建立 session,同時在主線程檢測相機操做的超時。全部相機的操做都切換到了單獨的相機線程,以免形成主線程阻塞,而檢查超時天然不能在相機線程,不然相機線程被阻塞住以後超時回調也不會執行。
咱們發現 capturer 中並無實際相機操做的代碼,開啓相機、預覽的代碼都封裝在了 CameraSession 中,那這樣 capturer 的邏輯就獲得了簡化,切換攝像頭、失敗重試都只須要建立 session 便可,capturer 能夠專一於狀態維護和錯誤處理的邏輯。
CameraCapturer 狀態維護和錯誤處理的邏輯仍是很是全面的:相機開啓狀態、相機運行狀態、切換攝像頭狀態、錯誤重試、相機開啓超時,所有都考慮到了。另外相機切換、開關相機、錯誤事件,通通都有回調通知。這裏就充分體現出了 demo 和產品的差異,開啓相機預覽的 demo 十行代碼就能搞定,而要全面考慮各類異常狀況,就須要費一番苦心了。
不過這裏仍有一點小瑕疵,錯誤回調的參數是字符串,雖然能夠很方便的打入日誌,但不利於代碼判斷錯誤類型。最好是參數使用錯誤碼,而後準備一個錯誤碼到錯誤信息的轉換函數。
stopCapture
stopCapture 時會先判斷是否正在建立 session,若是正在建立,那就須要等待其建立完畢。經過檢查後,若是當前有 session 正在運行,就在相機線程關閉 session。
changeCaptureFormat
改變採集格式須要重啓採集,即先 stopCapture,再 startCapture。這倆操做都是異步的,會不會有問題?這就涉及到 Handler 的一點知識了,向 Handler 提交的消息、任務,都會被加入到同一個隊列中,提交到隊列中的任務會保證按序執行,即先提交必定會先執行,因此這裏咱們沒必要擔憂關閉相機和開啓相機順序錯亂。
switchCamera
switchCamera 也會先中止老的 session,再建立新的 session,只不過還須要檢查相機個數、實現切換狀態通知邏輯。
這塊代碼應該有個小問題:startCapture 會把 openAttemptsRemaining 設置爲 MAX_OPEN_CAMERA_ATTEMPTS,但切換攝像頭時只會將其設置爲 1,這個不對稱應該沒什麼道理,因此我認爲應該保持一致。
Session
前面咱們已經知道,和相機 API 實際打交道的代碼都在 CameraSession 中,這裏咱們就一探其究竟。
開啓相機、開啓預覽、設置事件回調的代碼都在建立 session 的工廠方法 Camera1Session.create 和 Camera2Session.create 中。中止相機和預覽則定義了一個 stop 接口。
具體的相機 API 使用就比較簡單了。
Camera1
Camera2
相機方向
一般前置攝像頭輸出的圖像方向是逆時針旋轉 270° 的,後置攝像頭是 90°,但存在一些意外狀況,例如 Nexus 5X 先後置都是 270°。
在 Camera1 裏咱們能夠經過 camera.setDisplayOrientation 接口來控制相機的輸出圖像角度,但實際上不管是獲取內存數據,仍是獲取顯存數據(SurfaceTexture),這個調用都不會改變數據,它只是影響了相機輸出數據時攜帶的變換矩陣的方向。Camera2 裏沒有相應的接口,但相機服務會自動爲咱們合理調整變換矩陣方向,因此至關於咱們正確地調用了相似的接口。
若是利用 camera.setPreviewDisplay 或者 camera.setPreviewTexture 實現預覽,那 camera.setDisplayOrientation 確實會讓預覽出來的圖像方向發生變化,由於相機服務在渲染到 SurfaceView/TextureView 時會應用變換矩陣,使得預覽畫面是旋轉以後的畫面。
除了方向還有一個鏡像的問題,Camera1 在前置攝像頭時會自動爲咱們翻轉一下畫面(固然也只是修改了變換矩陣),例如前置攝像頭輸出的圖像方向是逆時針旋轉 270° 時,那就會把圖像上下翻轉,若是咱們再設置一個旋轉 90°,把圖像旋正,那就至關因而左右翻轉,也就達到了鏡像的效果,即:前置攝像頭咱們用左手摸左邊的臉,預覽裏也是顯示在屏幕左邊(但預覽在和咱們四目相對,因此實際是「他」的右邊,是有點繞…)。
至於怎麼設置 camera.setPreviewDisplay 的參數,使得直接預覽能夠方向正確,可使用如下代碼:
privatestaticintgetRotationDegree(intcameraId){intorientation=0;WindowManagerwm=(WindowManager)applicationContext.getSystemService(Context.WINDOW_SERVICE);switch(wm.getDefaultDisplay().getRotation()){caseSurface.ROTATION_90:orientation=90;break;caseSurface.ROTATION_180:orientation=180;break;caseSurface.ROTATION_270:orientation=270;break;caseSurface.ROTATION_0:default:orientation=0;break;}if(cameraInfo.facing==Camera.CameraInfo.CAMERA_FACING_FRONT){return(720-(cameraInfo.orientation+orientation))%360;}else{return(360-orientation+cameraInfo.orientation)%360;}}
SurfaceTextureHelper
SurfaceTextureHelper 負責建立 SurfaceTexture,接收 SurfaceTexture 數據,相機線程的管理。
建立 SurfaceTexture 有幾點注意事項:
// The onFrameAvailable() callback will be executed on the SurfaceTexture ctor thread. // See: http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/// android/5.1.1_r1/android/graphics/SurfaceTexture.java#195.// Therefore, in order to control the callback thread on API lvl < 21, // the SurfaceTextureHelper is constructed on the |handler| thread.
有哪些坑
//Note: stopPreview or other driver code might deadlock. Deadlock in// android.hardware.Camera._stopPreview(Native Method) has been observed on// Nexus 5 (hammerhead), OS version LMY48I.camera.stopPreview();
try{returncameraManager.getCameraIdList();// On Android OS pre 4.4.2, a class will not load because of VerifyError if it contains a// catch statement with an Exception from a newer API, even if the code is never executed.// https://code.google.com/p/android/issues/detail?id=209129}catch(/* CameraAccessException */AndroidExceptione){Logging.e(TAG,"Camera access exception: "+e);returnnewString[]{};}
// SurfaceTexture.updateTexImage apparently can compete and deadlock with eglSwapBuffers,// as observed on Nexus 5. Therefore, synchronize it with the EGL functions.// See https://bugs.chromium.org/p/webrtc/issues/detail?id=5702 for more info.synchronized(EglBase.lock){surfaceTexture.updateTexImage();}synchronized(EglBase.lock){EGL14.eglSwapBuffers(eglDisplay,eglSurface);}
內存抖動優化
運行 AppRTC-Android 程序,咱們會發現內存抖動很是嚴重:
這塊咱們能夠利用 Allocation Tracker 進行分析和優化
https://blog.piasy.com/2017/07/24/WebRTC-Android-Camera-Capture/