Android音視頻-視頻採集(Camera2預覽基礎)

Camera2是在API level 21後面取代Camera的一個API,我們以後開發的應用中實際和這個API打交到會比較多,畢竟現在很多Android使用都API21以上了。Camera2的使用我們也和上面說的Camera一樣的功能來實現一遍,瞭解其中的一些細節。

創建Camera2應用

首先我們使用最簡單的方式來使用Camera2來創建一個顯示預覽的應用。開始之前先總體瞭解一些Camera2用來拍照流程和相關的類。

  • CameraManager
    它是Android設備用來管理所有的相機設備類。例如檢測打開攝像頭,查看下面的相機特徵。

  • CameraCharacteristics
    每一個相機設備擁有一些列屬性和設置用來描述它。它可以通過CameraManager來獲取。

  • CameraCaptureSession
    當我們使用Camera2從相機設備來拍照或者預覽圖像,應用程序必須先創建CameraCaptureSession

  • SurfaceTexture
    相機預覽要一個View來顯示預覽的數據,在使用Camera的時候我們使用的是SurfaceView這個類。使用Camera2來操作相機也要一個Surface來顯示View,Surface這個對象可以通過很多類來實例話,包括SurfaceView,SurfaceTexture,MediaCodec,MediaRecorder,Allocation,和ImageReader。我們demo使用的TextureView,它內部使用的是SurfaceTexture類。

  • CaptureRequest
    相機應用程序要構建一個CaptureRequest用來拍照,它定義了相機設備拍照的需要的一些參數。

  • 使用CaptureRequest
    一旦CaptureRequest請求被設置好,他就可以被髮送到激活CaptureSession用來拍照或者連續重複使用。

看到網上有一個圖很好的描述了Camera2相關的東西。




Camera2引入了管道的概念讓Android設備和攝像頭聯繫起來。例如系統向攝像頭髮送預覽請求,攝像頭會返回CameraMetaData。它們在CameraCaptureSession中操作



這裏羅列了Camera2大致所有相關的類。以上參考自這裏

接下來我們的創建一個簡單的預覽的相機應用,先轉起來心裏舒暢一些。

  • 聲明Camera權限和Camera2使用全部特徵
<uses-permission android:name="android.permission.CAMERA" />
    <uses-feature android:name="android.hardware.camera2.full"/>
  • 創建一個View繼承TextureView
    這個TextureVie用來顯示相機傳輸過來的預覽數據。
    並且在設置監聽Surface可用的時候執行下面的步驟打開相機。

  • 請求打開相機

private void openCamera() {
        //獲取CameraManager對象
        CameraManager manager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
        Log.e(TAG, "is camera open");
        try {
            //檢測攝像頭設備ID,有幾個攝像頭
            cameraId = manager.getCameraIdList()[0];
            //獲取相機特徵對象
            CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);
            //獲取相機輸出流配置Map
            StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
            assert map != null;
            //獲取預覽輸出尺寸
            imageDimension = map.getOutputSizes(SurfaceTexture.class)[0];
            // Add permission for camera and let user grant the permission
            if (ActivityCompat.checkSelfPermission(mContext, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
                return;
            }
            //調用CameraManger對象打開相機函數
            manager.openCamera(cameraId, stateCallback, null);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
        Log.e(TAG, "openCamera X");
    }

這個方法現在很精簡,用來打開相機設備,每個函數可以看註釋,也可以自己跟蹤到源碼看大概是個什麼東西,最後執行openCamera的時候設置了一個stateCallback,這個用來接受相機已經打開的回掉。如果相機可以使用了,那麼就執行接下來的步驟把相機底層的數據顯示到TextureView上面去。

  • 使用TextureView來顯示預覽數據
protected void createCameraPreview() {
        try {
            Log.e(TAG, "createCameraPreview: ");
            //獲取當前TextureVie的SurfaceTexture
            SurfaceTexture texture = getSurfaceTexture();
            assert texture != null;
            //設置SurfaceTexture默認的緩衝區大小,爲上面得到的預覽的size大小
            texture.setDefaultBufferSize(imageDimension.getWidth(), imageDimension.getHeight());
            Surface surface = new Surface(texture);
            //創建CaptureRequest對象,並且聲明類型爲TEMPLATE_PREVIEW,可以看出是一個預覽類型
            captureRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            //設置請求的結果返回到到Surface上
            captureRequestBuilder.addTarget(surface);
            //創建CaptureSession對象
            cameraDevice.createCaptureSession(Arrays.asList(surface), new CameraCaptureSession.StateCallback() {
                @Override
                public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
                    //The camera is already closed
                    if (null == cameraDevice) {
                        return;
                    }
                    Log.e(TAG, "onConfigured: ");
                    // When the session is ready, we start displaying the preview.
                    cameraCaptureSessions = cameraCaptureSession;
                    //更新預覽
                    updatePreview();
                }

                @Override
                public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
                    Toast.makeText(mContext, "Configuration change", Toast.LENGTH_SHORT).show();
                }
            }, null);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

上面大致就是創建一個CaptureRequest,把這個請求發送到CaptureSession會話中去。最後在回話配置成功的回掉裏面更新預覽的視圖

  • 更新預覽視圖
protected void updatePreview() {
        if (null == cameraDevice) {
            Log.e(TAG, "updatePreview error, return");
        }
        Log.e(TAG, "updatePreview: ");
        //設置相機的控制模式爲自動,方法具體含義點進去(auto-exposure, auto-white-balance, auto-focus)
        captureRequestBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);
        try {
            //設置重複捕獲圖片信息
            cameraCaptureSessions.setRepeatingRequest(captureRequestBuilder.build(), null, mBackgroundHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

OK,上面的步驟完成了一個相機的預覽的功能。

我們接下來要仔細瞭解每一個步驟裏面的具體的參數的設置以及擴展上面簡單預覽界面的參數設置。

初始化相機

這裏我們主要了解的是CameraManager類和CameraCharacteristics類
出了上面的API,我們瞭解一些它們其他的API。

CameraManager

它是Camera2中的一個系統的管理服務,管理着檢測,特徵和連接CameraDevices相機設備。

  • getCamraIdList()

    這個方法返回當前連接的相機設備的ID,它包含了當前的設備所有的相機。包括其他可能正在使用的相機。

    它通過Binder遠程連接相機服務獲取相機,我們的到的string數組裏麪包含0和1。這個和Camera的ID相同,ID0代表位前置相機,ID1代表後置相機。

  • openCamera(String cameraId,CameraDevice.StateCallback callback,Handler hander)

    通過相機ID來打開相機,我們上面設置前面兩個參數,第三個Hander設爲null,如果我們知道onPreViewFrame的回掉機制,那麼這個看一下就知道第三個參數的含義,它會隱形第二個參數的callback回掉的執行線程,如果設置爲空就默認爲主線程回掉。

  • registerAvailabilityCallback(CameraManager.AvailabilityCallback callback, Handler handler)


    這裏寫圖片描述

    這個很正常,剛開始我們的前置相機是沒有打開的,它收到兩個相機都可以使用的回掉,後面我們的應用前置相機打開了,它收到前置相機不可用的回掉。

    我們然後退出相機預覽界面:


    這裏寫圖片描述

    它收到前置相機可用的回掉。可以知道這個方法在第一次註冊的時候會收到設備所有相機的可以與否的回掉,然後當有某個相機的可以與否發送變化後,它會收到回掉。

    註冊了監聽回掉要記得退出時註銷監聽函數。

  • registerTorchCallback(CameraManager.TorchCallback callback, Handler handler)
    這是對手機的手電筒的一個監聽。這裏面的回掉函數的細節,以及使用的判斷到下面的實現打開手電筒的功能去詳細瞭解。

CameraCharacteristics

相機特徵類,它是一個描述相機CameraDevice的屬性的一個類。例如我們上面獲取預覽的圖片尺寸的API,我們上面的代碼直接就取的第一個尺寸,沒有做一個最佳匹配的尺寸輸出。

Camera圖片大小

Camera預覽圖片大小

我們在上一篇學習Camera的API的時候,使用Camera.Parameter.setPreviewSize來設置PreviewSize。那個選擇最佳的預覽尺寸的算法是根據顯示預覽的View的寬高和Camera支持的預覽大小做一個比較最相近的來選取。而Camera2API沒有發現具體的API對Camera進行設置,但是我們在Camera2的時候使用的TextureView進行視圖預覽的,可以對這個TextureView設置預覽大小。

  • 我們在Camera的使用使用API Camera.Parameters.getSupportedPreviewSizes()方法來獲取當前的相機支持的預覽的圖像大小,再和我們給出來的預覽View來進行一個比較,取它們直接一個接近的相機支持的大小。
  • Camera2API使用的時候通過CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(mCameraId);
    StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
    map.getOutputSizes(SurfaceTexture.class);

    Camera2API通過這種方法來獲取當前相機支持的預覽尺寸大小。拿到了這些大小再和我們給出的預覽View來比較選取一個合適的預覽大小。

在不考慮預覽方向的前提下,這個預覽尺寸設置的大小它影響我們看預覽View的時候的清晰程度,假如現在豎屏一個預覽的View,它的寬高爲1080*1692,極端一點,我們現在設置的預覽尺寸爲177*144,那麼我們會看到一個非常模糊的預覽View,它們越接近我們看到的視圖越清晰。

我們獲取到的Camera支持的預覽尺寸都是寬度大於高度的,這裏我們考慮爲這些尺寸爲當前CameraSensor的方向的尺寸,當我們下面把它順時針旋轉90度,就是我們豎屏看到的視圖和現實物理視圖一樣了。

下面給出Camera2中用來獲取相對最佳的預覽尺寸的代碼。
下面參考自這裏

private Size getPreferredPreviewSize(Size[] mapSizes, int width, int height) {
        Log.e(TAG, "getPreferredPreviewSize: surface width="+width+",surface height="+height);
        List<Size> collectorSizes = new ArrayList<>();
        for (Size option : mapSizes) {
            if (width > height) {
                if (option.getWidth() > width &&
                        option.getHeight() > height) {
                    collectorSizes.add(option);
                }
            } else {
                if (option.getWidth() > height &&
                        option.getHeight() > width) {
                    collectorSizes.add(option);
                }
            }
        }
        if (collectorSizes.size() > 0) {
            return Collections.min(collectorSizes, new Comparator<Size>() {
                @Override
                public int compare(Size lhs, Size rhs) {
                    return Long.signum(lhs.getWidth() * lhs.getHeight() - rhs.getWidth() * rhs.getHeight());
                }
            });
        }
        Log.e(TAG, "getPreferredPreviewSize: best width="+
                mapSizes[0].getWidth()+",height="+mapSizes[0].getHeight());
        return mapSizes[0];
    }

預覽的尺寸我們詳細瞭解了,他要的尺寸是和我們預覽View相當接近的一個最佳的尺寸大小。這份代碼同樣可以應用到我們的上一篇博客中Camera來獲取最佳尺寸裏面

Camera方向基礎

我們根據上一篇文章的理解,我們上次使用Camera的時候,如果不做預覽的圖片旋轉,它默認是我們看到的圖像逆時針轉了90度的,這個預覽圖像我們看着很彆扭,我們就使用Camera的API把預覽圖片圖片旋轉了90度,這下我們看到的預覽圖片和現實中的圖片的方向一樣了。還捎帶看了一個Camera Sensor這個東西。我們還沒有考慮屏幕旋轉的情況,這幾個概念要梳理一下。先有一個概念就是我們的預覽方向的計算是根據屏幕方向,CameraSensor採集圖片的方向和攝像頭是前置還是後置來決定的,爲啥這麼說,應爲Google Sample的示例代碼是這麼寫的..

屏幕方向

下面這幾張圖片示例的出處爲查看




這張圖描述了我們要了解的兩個概念,手機屏幕方向,CameraSensor的方向。我們先了解屏幕的方向:

手機設備屏幕方向
自然握持狀態下爲0度,逆時針旋狀依次爲:90度、180度、270度;這個手機設備的屏幕方向的角度信息是所有設備都相同的。我們來驗證一下。

((Activity)mContext).getWindowManager().getDefaultDisplay().getRotation();

這個方法可以獲取當前屏幕的方向信息,爲了好驗證使用模擬器來做這個比較方便,旁邊有那個逆時針旋轉屏幕的功能按鈕。通過看數值,90度和270都完美的驗證了,有個180度上面的方法無法驗證,我們要的效果其實就是把豎屏的倒置過來,通過設置Activity的screenOrientation爲reversePortrait這就把Activity倒過來了,完美的驗證了角度爲180度。證明我們上面的概念沒有問題。

CameraSensor方向
這個方向概念來自官網的描述來自Camera的CameraInfo.orientation屬性,它是相機圖像方向,這也是我們前面上篇文章寫Camera的時候說的圖像採集Sensor的方向。這裏在貼一張圖。




這個和上面的圖看出區別就是兩個模擬器型號不同,然後重要的一點不同就是CameraSensor不同。這裏我沒有這兩個設備,沒有去驗證,而模擬器獲取的CameraSensor值也都是0。有設備了再驗證。。。

CameraSensor對大多數設備都是屏幕方向90度的方向,但是例如Nexus5X它的CameraSensor的方向就是屏幕方向270度的方向。得出一個結論:CameraSensor的值和設備製造的硬件廠商有關,和Android API的版本無關的。

  • 如何獲取CameraSensor方向
    • Camera:
      如果使用的Camera 的API可以使用Camera.CameraInfo.orientaion來獲取CameraSensor的方向。
    • Camera2:
      Camera2的API可以通過
      StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
      assert map != null;
      // sensor orientation
      int sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
      這個方法來獲取CameraSensor的方向。

我們在前面的文章屏幕豎屏的時候使用Camera來操作,把後置相機的預覽設置爲90度,把前置相機攝像頭設置爲270度就可以完美的預覽(屏幕方向和相機預覽方向一致),爲啥呢?

我們瞭解了上面的CameraSensor的概念可以完全解答這個問題。我使用的小米5手機測試的它的前置相機攝像頭爲90度。獲取它的後置攝像頭的CameraSensor爲270度。這裏我們進行把相機旋轉90和270度,這裏實際是把默認的後置相機攝像頭採集的90度的方向圖片幀數據,順時針轉90度到屏幕方向0度的方向,這個時候圖片的方向和我們的豎屏View顯示的方向一致了。同理後置的270也是這麼來的(這個理解應該沒啥問題吧)。

如何設置預覽方向

我們上一篇的Camera就針對豎屏進行了處理,沒有考慮到橫屏預覽啥效果,當然會有一些問題。這裏針對Camera和Camera2這兩個API看分別如何設置。

Camera設置預覽方向

預覽方向由當前的屏幕的方向和CameraSensor的方向來決定,通過算法計算它們兩個來得到預覽的方向。
我們在使用Camera的setDisplayOrientation的時候這個方法的源碼註釋上面有這麼一些註釋代碼:

public static void setCameraDisplayOrientation(Activity activity,
              int cameraId, android.hardware.Camera camera) {
          android.hardware.Camera.CameraInfo info =
                  new android.hardware.Camera.CameraInfo();
          android.hardware.Camera.getCameraInfo(cameraId, info);
          int rotation = activity.getWindowManager().getDefaultDisplay()
                  .getRotation();
          int degrees = 0;
          switch (rotation) {
              case Surface.ROTATION_0: degrees = 0; break;
              case Surface.ROTATION_90: degrees = 90; break;
              case Surface.ROTATION_180: degrees = 180; break;
              case Surface.ROTATION_270: degrees = 270; break;
          }

          int result;
          if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
              result = (info.orientation + degrees) % 360;
              result = (360 - result) % 360;  // compensate the mirror
          } else {  // back-facing
              result = (info.orientation - degrees + 360) % 360;
          }
          camera.setDisplayOrientation(result);
      }

把這份代碼應用到上一篇博客Camera中去,完美的解決豎直和橫屏的預覽效果。

Camera2設置預覽方向

我們上面的Demo在橫屏的時候預覽的視圖不是屏幕方向90度下來的。我們必須給它做一些變化。上面的Camera有專門的API來進行方向設置,但是Camera2我還沒有發現,網上找到一種方法就是對TextureView進行變幻和上面的預覽視圖大小一樣對TextureView進行設置即可。代碼如下:

private void transformImage(int width, int height) {
        Matrix matrix = new Matrix();
        int rotation = ((Activity) mContext).getWindowManager().getDefaultDisplay().getRotation();
        RectF textureRectF = new RectF(0, 0, width, height);
        RectF previewRectF = new RectF(0, 0, mImageDimension.getHeight(), mImageDimension.getWidth());
        float centerX = textureRectF.centerX();
        float centerY = textureRectF.centerY();
        if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) {
            previewRectF.offset(centerX - previewRectF.centerX(),
                    centerY - previewRectF.centerY());
            matrix.setRectToRect(textureRectF, previewRectF, Matrix.ScaleToFit.FILL);
            float scale = Math.max((float) width / mImageDimension.getWidth(),
                    (float) height / mImageDimension.getHeight());
            matrix.postScale(scale, scale, centerX, centerY);
            matrix.postRotate(90 * (rotation - 2), centerX, centerY);
        }
        setTransform(matrix);
    }

預覽方向總結

Camera 中通過API setDisplayOrientation來進行設置,難點是如何獲取這個方向的值,因爲在橫豎屏的時候有一些變動,我們必須的弄明白屏幕方向,CameraSensor方向這兩個概念,因爲這個方向是由它們兩可以動態的計算得來的。

Camera2的API中我現在沒有發現具體方法對預覽的大小和方向進行設置,但是Camera2通過TextureView來進行視圖預覽的。我們通過對TextureView設置大小和角度變幻。

這裏有一個疑問。Camera2的圖像顯示數據,沒有使用CameraSensor的那個原理來顯示圖像了麼,我在不做任何設置的時候,豎屏的時候,視圖仍然是正常的顯示,根據對於上一節的CameraSensor的瞭解,它的方向是90度,沒有對視圖進行變幻應該是一個方向異常的。這裏以後瞭解了。

這裏我們對於Camera2的初始化的一部分API做了瞭解,重點理解了相機的預覽大小和方向的概念和一些基礎知識。還有一點對於拍照輸出的圖片的方向,Camera使用API Camera.Parameters.setRotation方法設置即可,對於Camera2的拍照圖片設置我們在下一節去看了。這些內容有點亂了混在一起。下一篇重點了解Camera2來實現一些功能。

有錯誤的地方希望指正。

參考鏈接:
簡單的Camera2應用如何去搭建
對於CameraSensor和屏幕方向的概念
對於細節知識點的講解

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章