WebRTC源碼分析(一):安卓相機採集實現分析

WebRTC 的代碼量不小,一次性看明白不太現實,在本系列中,我將試圖搞清楚三個問題:java

  1. 客戶端之間如何創建鏈接?
  2. 客戶端之間如何實現數據傳輸?
  3. 音視頻數據的採集、預覽、編碼、傳輸、解碼、渲染完整流程。

本文是第一篇,我將從最熟悉的採集入手,分析一下 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

  • 建立 Camera 對象:Camera.open;
  • 設置預覽 SurfaceTexture,用來接收幀數據(位於顯存中):camera.setPreviewTexture;
  • 選擇合適的相機預覽參數(尺寸、幀率、對焦):Parameters 和 camera.setParameters;
  • 若是須要獲取內存數據回調,則須要設置 buffer 和 listener:camera.addCallbackBuffer 和 camera.setPreviewCallbackWithBuffer;
  • 若是須要相機服務爲咱們調整數據方向,則能夠設置旋轉角度:camera.setDisplayOrientation;
  • 開啓預覽:camera.startPreview;
  • 中止預覽:camera.stopPreview 和 camera.release;

Camera2

  • 建立 CameraManager 對象,相機操做始於「相機管家」:context.getSystemService(Context.CAMERA_SERVICE);
  • 建立 CameraDevice 對象:cameraManager.openCamera;
  • 和 Camera1 不一樣,Camera2 的操做都是異步的,調用 openCamera 時咱們會傳入一個回調,在其中接收相機操做狀態的事件;
  • 建立成功:CameraDevice.StateCallback#onOpened;
  • 建立相機對象後,開啓預覽 session,設置數據回調:camera.createCaptureSession,一樣,這個操做也會傳入一個回調;
  • session 開啓成功:CameraCaptureSession.StateCallback#onConfigured;
  • 開啓 session 後,設置數據格式(尺寸、幀率、對焦),發出數據請求:CaptureRequest.Builder 和 session.setRepeatingRequest;
  • 中止預覽:cameraCaptureSession.stop 和 cameraDevice.close;

 

 

相機方向

 

一般前置攝像頭輸出的圖像方向是逆時針旋轉 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 有幾點注意事項:

  • 建立 OpenGL texture 時所在的線程須要準備好 GL 上下文,WebRTC 中將這部分邏輯封裝在 EglBase 類中;
  • 建立 SurfaceTexture 所在的線程,將是其數據回調 onFrameAvailable 發生的線程;不過 API 21 引入了一個新的重載版本,支持指定回調所在線程的 Handler;

// 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.

 

有哪些坑

  • 低版本(5.0 之前)的系統上,Camera1 中止預覽時,不要手賤地調用下列接口設置 null 值:setPreviewDisplay/setPreviewCallback/setPreviewTexture(文檔中確實也說過不要調用…),不然可能致使系統服務全線崩潰,最終致使手機重啓:

 

  • Camera1 中止預覽可能存在死鎖(沒有解決):

//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();

  • Camera2 相關的代碼在 4.4.2 以前的系統上遇到 VerifyError:

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 接收幀數據,有些機型可能獲取到的數據是黑屏(MX5 遇到過):須要設置 SurfaceTexture 的 buffer size,surfaceTexture.setDefaultBufferSize
  • 利用 SurfaceTexture 接收幀數據,經過 SurfaceTexture.getTimestamp 接口獲取時間戳,這個時間戳是相對時間,並且前面會有幾幀值爲 0:相對時間的問題能夠在首幀記錄下和物理時間的差值,而後計算後續每幀的物理時間戳,但頭幾幀時間戳爲 0,因此咱們記下差值就得等到非零時,而頭幾幀則能夠直接使用物理時間做爲時間戳;
  • surfaceTexture.updateTexImage 和 eglSwapBuffers 會發生死鎖,咱們須要自行加鎖:

// 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);}

  • 有些機型上,用 TextureView 實現預覽,onSurfaceTextureAvailable 回調不會被調用,致使沒法開啓預覽,這個問題有可能能夠經過開啓硬件加速得以解決(參考 StackOverflow 這個問題,我還頂過),但有可能這個辦法也無論用,那麼恭喜你,得再費一番腦細胞了。我就遇到過這種狀況,在一款 OPPO 4.3 的手機上,折騰半天發現延遲一下子重設一次 LayoutParams 就能觸發,因此就先這麼搞了;

內存抖動優化

 

運行 AppRTC-Android 程序,咱們會發現內存抖動很是嚴重:

 

這塊咱們能夠利用 Allocation Tracker 進行分析和優化

 

 

 

https://blog.piasy.com/2017/07/24/WebRTC-Android-Camera-Capture/

相關文章
相關標籤/搜索