1. 程式人生 > >Android音視訊學習——Camera2官方demo解析(2)

Android音視訊學習——Camera2官方demo解析(2)

本篇主要就幾個關鍵的類進行解釋,並且對需要注意的點註釋,此外再總結一下如何使用Camera2進行拍照和預覽的流程。附上官方demo

這裡寫圖片描述

上面是Camera2的流程示意圖,由於我喜歡從整體思路上分析程式碼,所以下面先就整個呆萌的思路拓展一下。

首先肯定是解決相機的問題啦,畢竟是主角嘛,但是相機是底層的,Android為我們抽象了CameraDevice類,用來表示各個相機。我們又知道,裝置的相機通常有好幾個,前後的特點都不一樣,比如映象問題,單單一個CameraDevice無法表示全。這時候可能有人會想把CameraDevice做成抽象類,每個相機作為子類各自配置自己的屬性,反正開啟相機的操作不依賴相機本身,只需要相機的標識就行,開啟相機以後再將該相機的例項物件返回給呼叫者就ok了,沒錯,呆萌也是這樣想的:

@Override
public void onOpened(@NonNull CameraDevice cameraDevice) {
    mCameraDevice = cameraDevice;
}

相機裝置的問題解決了,但是誰來拍照啊,開啟相機不可能讓CameraDevice自己開啟自己吧,所以需要一個CameraManager來管理相機的開啟,相機是系統級別的,所以返回的Manager都是一樣的也無妨,就將其上升到服務的層次,只要有上下文就能獲取,通過下面的方法獲取並進行拍照。

CameraManager manager = (CameraManager) activity.getSystemService
(Context.CAMERA_SERVICE); manager.openCamera(mCameraId, mStateCallback, mBackgroundHandler);

呆萌又想,如果都交給CameraManager來管理,它還是要管理好多東西哦,比如相機的取景方向,這是個大問題,見解析(1),為了滿足單一職責原則和開閉原則,不如將相機裝置的屬性封裝成一個類,這樣CameraDevice只需要專注於建立對話Session就行了, 於是CameraCharacteristics應運而生,其封裝了指定ID的相機中的各種屬性,例如想要獲取指定ID相機的取景方向:

CameraCharacteristics characteristics = manager.getCameraCharacteristics
(cameraId); mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);

我們知道,相機裝置是嵌入在Android裝置當中的,兩者之間的通訊需要底層的知識,但是我們不需要知道,所以Android為我們提供了CameraCaptureSession這個上層類,翻譯為相機捕捉的對話,顯然這個對話為我們預覽和拍攝做了封裝,其作用相當於圖中的pipeline

開啟相機後就可以通過管道進行配置了,而相機的各種配置很多,使用者的需求也是不一樣的,如果在對話管道CameraCaptureSession中封裝了對相機的配置,顯然很臃腫,所以此處抽象出來一個CaptureRequest類,並且對其的配置使用了Builder模式,這個模式適合的場景就是Director主動去配置物件的屬性,提供了預設配置,而不需要在建構函式中將屬性一次性配置完成。建議如果程式碼中需要配置較多的屬性,並且這些屬性都有預設值的話,使用Builder模式更佳。呆萌是這樣體現的:

//建立Builder
mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
//設定持續的自動對焦
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
//建立CaptureRequest
mPreviewRequest = mPreviewRequestBuilder.build();

配置完成後就可以進行預覽和拍照了,返回的結果被封裝成為CameraMetadata,這是一個控制相機和儲存相機資訊的基類,它描述了查詢資訊,拍照結果等返回的key/value組成的map,有個很重要的問題是返回的結果以什麼形式呈現,我們首先想到的肯定是以方法的返回值返回,但是這裡涉及到何時拍照完成的問題,以及拍照中對焦距、閃光燈等配置的調節,都需要立即呈現給Android系統,顯然觀察者模式是不錯的選擇,通過設定回撥來立即接受到配置的變化以及完成與否,呆萌是這樣做的:

/* process(CaptureResult)是針對不同的結果和當前的狀態對相機進行設定和操縱的方法 */

private CameraCaptureSession.CaptureCallback mCaptureCallback = new CameraCaptureSession.CaptureCallback() { 
    @Override
    public void onCaptureProgressed(@NonNull CameraCaptureSession session,
                                    @NonNull CaptureRequest request,
                                    @NonNull CaptureResult partialResult) {
        process(partialResult);
    }

    @Override
    public void onCaptureCompleted(@NonNull CameraCaptureSession session,
                                   @NonNull CaptureRequest request,
                                   @NonNull TotalCaptureResult result) {
        process(result);
    }
}

有了上面這幾個概念,整個流程也就差不多走通了,下面梳理一下:

  1. 首先獲取CameraManager,呼叫其getCameraIdList()獲取所有的CameraId,從中選擇需要的,根據ID找到對應的CameraDevice
  2. 獲取到該相機的CameraCharacteristics,獲取各屬性(相機取景方向,可支援的輸出影象大小),根據屬性配置顯示的大小,並根據當前螢幕旋轉位置和相機位置調整預覽和拍照的方向
  3. 建立後臺執行緒和Handler物件,提供給Camera用於後臺操作
  4. 建立CameraDevice.StateCallback物件,在開啟和關閉的回撥中啟動和釋放資源,並建立預覽
  5. 開啟相機前動態檢查相機許可權,獲得許可權後通過manageropen方法開啟相機
  6. 建立CameraCaptureSession.CaptureCallback物件,在回撥中根據返回結果進行相應操作
  7. 利用CameraDevice建立CaptureRequest.builder物件進行配置,並建立Session對話,在onConfigured的回撥中啟動預覽
  8. 拍照時與⑦基本相同,只是request的目標Surface改變,並且呼叫的是Sessioncapture()方法

以上就是用Camera2API拍照的全部流程,其中有不少細節需要注意,在下面將一一註釋,如果真的需要用到此API,建議直接修改官方呆萌就夠了。

是否支援

具體見Android裝置對新Camera2 API的支援問題:以華為M2為例,現在很多推出的裝置雖然都是Android5.0以上的,也就是支援Camera2的API,但是雖然它們實現了這些API,但是實際上可配置的引數比Camera少,甚至效能都比Camera差,為此廠商在系統中寫明瞭對Camera2的支援程度,通過:

CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);
characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);

可以獲得支援的程度,得到的值中有這三種:LIMITED值為0,FULL值為1,LEGACY值為2,其支援程度為FULL > LIMITED > LEGACY。關於LEGACY是這樣說的:

A LEGACY device does not support per-frame control, manual sensor control, manual post-processing, arbitrary cropping regions, and has relaxed performance constraints.

翻譯一下:遺留的裝置不支援每幀控制、手動感測器控制、手動後處理、任意裁剪區域,並且具有寬鬆的效能約束。可以說相容性很差了,我在自己手機上驗證了下,發現LEGACY,屬於遺留機!可以預見很多手機上並不完全支援Camera2API,所以如果手機並不是FULL支援的話,建議還是使用Camera

Semaphore

官方呆萌中使用了Semaphore防止在相機未關閉前就退出了App,因為這樣會導致無法正常關閉相機,釋放資源,相機服務是系統級別的,App如果一直持有其引用就無法被gc清理,造成洩露,同時也防止併發狀態下多個應用頻繁啟動關閉相機。Semaphore簡單來說就是食堂阿姨,初始化的時候給阿姨指定量的卡布奇諾,呼叫acquire的時候拿走拿走一杯,呼叫release阿姨就再多放一杯,如果卡布奇諾暫時賣完了,Semaphore值為0,acquire就阻塞,等待release釋放一個,還可以設定阻塞時限,如果超時就返回false等等。在呆萌中這樣寫:

private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
    @Override
    public void onOpened(@NonNull CameraDevice cameraDevice) {
        // This method is called when the camera is opened.  We start camera preview here.
        mCameraOpenCloseLock.release();
        mCameraDevice = cameraDevice;
        createCameraPreviewSession();
    }
    @Override
    public void onDisconnected(@NonNull CameraDevice cameraDevice) {
        mCameraOpenCloseLock.release();
        cameraDevice.close();
        mCameraDevice = null;
    }
    @Override
    public void onError(@NonNull CameraDevice cameraDevice, int error) {
        mCameraOpenCloseLock.release();
        cameraDevice.close();
        mCameraDevice = null;
        Activity activity = getActivity();
        if (null != activity) {
            activity.finish();
        }
    }
}

@Override
public void onResume() {
    super.onResume();
    startBackgroundThread();
    if (mTextureView.isAvailable()) {
        openCamera(mTextureView.getWidth(), mTextureView.getHeight());
    } else {
        mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
    }
}

private void openCamera(int width, int height){
    if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
        throw new RuntimeException("Time out waiting to lock camera opening.");
    }
    manager.openCamera(mCameraId, mStateCallback, mBackgroundHandler);
}

private void closeCamera(){
    try {
        mCameraOpenCloseLock.acquire();
        ...
    } finally {
        mCameraOpenCloseLock.release();
    }
}

首先我們需要直到onDisconnected方法在退出Activity後並不會被呼叫,然後我們假設一個場景,第一次開啟Activity,此時Semaphore為1,onResume回撥,mTextureView不可用,呼叫setSurfaceTextureListener方法,最終呼叫openCamera,其中嘗試獲取許可,獲得後Semaphore為0,並開啟相機,相機的onOpened回撥,release()方法被呼叫,釋放一個許可,Semaphore為1,隨後退出App,在OnPause回撥中關閉相機,首先呼叫acquire()獲取許可,並呼叫close()方法關閉相機,注意關閉相機中並未退出Activity,此時如果其他應用呼叫openCamera方法,會因為Semaphore為0而處於阻塞狀態,等到相機關閉後finally語句塊呼叫,釋放許可,允許其他應用訪問相機。

輸出影象大小

在建立CameraCaptureSession的時候需要傳入一個回撥物件,其中有一個回撥方法是這樣的:

@Override
public void onConfigureFailed(
        @NonNull CameraCaptureSession cameraCaptureSession) {
    showToast("Failed");
}

很多時候我們會遇到這個回撥,特別是沒有配置好的時候,這個回調發生的時機在官方文件中是這樣的:

This can happen if the set of requested outputs contains unsupported sizes, or too many outputs are requested at once.

不支援的大小?沒錯,無論是Camera還是Camera2都有著支援的解析度大小,如果請求輸出的Surface包括了不支援的解析度,就會回撥這個方法,並且無法正確開啟相機,呆萌中的幾個方法都是在設定合適的解析度大小:

private void setUpCameraOutputs(int width, int height){}

private static Size chooseOptimalSize(Size[] choices, int textureViewWidth, int textureViewHeight, int maxWidth, int maxHeight, Size aspectRatio){}

具體的篩選見呆萌,就是在最大寬高範圍內,根據textureView的寬高,分為大的Size和不大的Size,如果有大的,在大的中選最小的,反之在小中選最大的。然後根據螢幕旋轉和相機的方向,以及當前螢幕的Orientation進行調整。總之輸出的Surface的大小也是很講究的。

剩下的難點其實就是CaptureResult中各個配置常量的含義,這點太多了,就不一一列舉了,大家用到的時候自己去發掘吧。