Android音視訊(一) Camera2 API採集資料

MzDavid發表於2019-03-04

這是Android音視訊的第一篇文章,終於回到了我的老本行上。之前好像從來沒有做過Camera相關的開發,這塊對我來說還是有點陌生,所以從頭開始學起。

Camera2

在Android API21中Google釋出了Camera2來取代原本的Camera,兩者的變動也是比較大的。

Android音視訊(一) Camera2 API採集資料

Camera2中Google採用了pipeline(管道)的概念,將Camera Device相機裝置和Android Device安卓裝置連線起來, Android Device通過管道傳送CaptureRequest請求給Camera Device,Camera Device通過管道返回CameraMetadata資料給Android Device,這一切建立在一個叫作CameraCaptureSession的會話中。

Camera2主要類說明

在Camera2 架構在核心參與類角色有:CameraManager、CameraDevice、CameraCharacteristics、CameraRequest與CameraRequest.Builder、CameraCaptureSession以及CaptureResult。

CameraManager

位於android.hardware.camera2.CameraManager下,也是Android 21(5.0)新增的,和其他系統服務一樣通過 Context.getSystemService(Context.CAMERA_SERVICE) 來完成初始化,主要用於管理系統攝像頭。

  • manager.getCameraIdList() 獲取Android裝置的攝像頭列表
  • manager.getCameraCharacteristics(cameraId) 獲取指定攝像頭的相關特性
  • manager.openCamera(String cameraId, CameraDevice.StateCallback callback, Handler handler) 開啟指定Id的攝像頭,StateCallback 是開啟狀態的一個監聽回撥,Handler 表示使用哪個執行緒處理回撥,如果為null則表示當前執行緒。

CameraDevice

CameraDevice是Camera2抽象出來的一個物件,直接與系統硬體攝像頭相聯絡。

  • 通過CameraDevice.StateCallback監聽攝像頭的狀態

    private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback(){
    
        @Override
        public void onOpened(@NonNull CameraDevice camera) {
            //攝像頭開啟,可以建立會話,開始預覽
        }
    
        @Override
        public void onDisconnected(@NonNull CameraDevice camera) {
    
        }
    
        @Override
        public void onError(@NonNull CameraDevice camera, int error) {
    
        }
    };
    複製程式碼
  • 管理CameraCaptureSession會話,相當於Android Device和Camera Device之間的管道,後面的資料交流都在這個會話中完成。

  • 管理CaptureRequest,主要包括通過createCaptureRequest(int templateType)建立捕獲請求,在需要預覽、拍照、再次預覽的時候都需要通過建立請求來完成。

CameraCaptureSession

正如前面所說,系統向攝像頭髮送 Capture 請求,而攝像頭會返回 CameraMetadata,這一切都是在由對應的CameraDevice建立的CameraCaptureSession 會話完成,當程式需要預覽、拍照、再次預覽時,都需要先通過會話。CameraCaptureSession一旦被建立,直到對應的CameraDevice關閉才會死掉。雖然CameraCaptureSession會話用於從攝像頭中捕獲影像,但是隻有同一個會話才能再次從同一攝像頭中捕獲影像。

  • 管理CameraCaptureSession.StateCallback狀態回撥,用於接收有關CameraCaptureSession狀態的更新的回撥物件,主要回撥方法有兩個當CameraDevice 完成配置,對應的會話開始處理捕獲請求時觸發onConfigured(CameraCaptureSession session)方法,反之配置失敗時候觸發onConfigureFailed(CameraCaptureSession session)方法。
  • 管理CameraCaptureSession.CaptureCallback捕獲回撥,用於接收捕獲請求狀態的回撥,當請求觸發捕獲已啟動時、捕獲完成時、在捕獲影像時發生錯誤的情況下都會觸發該回撥對應的方法。
  • 通過呼叫方法capture(CaptureRequest request, CameraCaptureSession.CaptureCallback listener, Handler handler)提交捕獲影像請求,即拍照。
  • 通過呼叫方法setRepeatingRequest(CaptureRequest request, CameraCaptureSession.CaptureCallback listener, Handler handler)請求不斷重複捕獲影像,即實現預覽。
  • 通過方法呼叫stopRepeating()實現停止捕獲影像,即停止預覽。

CameraCharacteristics

描述Cameradevice屬性的物件,可以使用CameraManager通過getCameraCharacteristics(String cameraId)進行查詢。

CameraRequest與CameraRequest.Builder

CameraRequest代表了一次捕獲請求

CameraRequest.Builder用於描述捕獲圖片的各種引數設定,包含捕獲硬體(感測器,鏡頭,快閃記憶體),對焦模式、曝光模式,處理流水線,控制演算法和輸出緩衝區的配置,然後傳遞到對應的會話中進行設定。CameraRequest.Builder負責生成CameraRequest物件。

CaptureResult

CaptureRequest描述是從影像感測器捕獲單個影像的結果的子集的物件。

程式碼實現

谷歌有寫兩個示例程式,介紹如何使用Camera2

android-Camera2Basic

android-Camera2Video

示例程式中有預覽、拍照、錄影等功能,非常好的入門學習程式碼。

在這裡還是過一遍整個流程,加深理解。

1、開啟攝像頭

private void openCamera(int width, int height) {
        // 判斷許可權
        if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.CAMERA)
                != PackageManager.PERMISSION_GRANTED) {
            requestCameraPermission();
            return;
        }
        // 設定引數,獲取攝像頭ID、設定預覽寬高等
        setUpCameraOutputs(width, height);
        // 配置TextureView的紋理轉換,解決Camera顯示變形問題
        configureTransform(width, height);
        Activity activity = getActivity();
        CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
        try {
            //Semaphore 訊號量,保證只能有一條執行緒使用Camera裝置
            if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
                throw new RuntimeException("Time out waiting to lock camera opening.");
            }
            //真正去開啟攝像頭,在mStateCallback中獲取成功或失敗的回撥
            manager.openCamera(mCameraId, mStateCallback, mBackgroundHandler); 
        } catch (CameraAccessException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            throw new RuntimeException("Interrupted while trying to lock camera opening.", e);
        }
    }



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

    };

複製程式碼

2、建立會話,開始預覽

private void createCameraPreviewSession() {
    try {
        SurfaceTexture texture = mTextureView.getSurfaceTexture();
        assert texture != null;

        // We configure the size of default buffer to be the size of camera preview we want.
        texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());

        // This is the output Surface we need to start preview.
        Surface surface = new Surface(texture);

        // We set up a CaptureRequest.Builder with the output Surface.
        mPreviewRequestBuilder
                = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
        //這裡是指實時影像資料的輸出目標,以後錄製視訊、直播等都需要在這裡新增對應的Target
        mPreviewRequestBuilder.addTarget(surface);

        // Here, we create a CameraCaptureSession for camera preview.
        mCameraDevice.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()),
                new CameraCaptureSession.StateCallback() {

                    @Override
                    public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
                        // The camera is already closed
                        if (null == mCameraDevice) {
                            return;
                        }

                        // When the session is ready, we start displaying the preview.
                        mCaptureSession = cameraCaptureSession;
                        try {
                            // 設定自動對焦
                            mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
                                    CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
                            // Flash is automatically enabled when necessary.
                            setAutoFlash(mPreviewRequestBuilder);

                            // Finally, we start displaying the camera preview.
                            mPreviewRequest = mPreviewRequestBuilder.build();
                            //不斷捕獲影像,顯示預覽影像
                            mCaptureSession.setRepeatingRequest(mPreviewRequest,
                                    mCaptureCallback, mBackgroundHandler);
                        } catch (CameraAccessException e) {
                            e.printStackTrace();
                        }
                    }

                    @Override
                    public void onConfigureFailed(
                            @NonNull CameraCaptureSession cameraCaptureSession) {
                        showToast("Failed");
                    }
                }, null
        );
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}
複製程式碼

3、拍照

/**
 * Initiate a still image capture.
 */
private void takePicture() {
    lockFocus();
}

/**
 * Lock the focus as the first step for a still image capture.
 */
private void lockFocus() {
    try {
        // 告訴攝像機開始對焦
        mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
                CameraMetadata.CONTROL_AF_TRIGGER_START);
        // Tell #mCaptureCallback to wait for the lock.
        mState = STATE_WAITING_LOCK;
        // 傳送CaptureRequest要求攝像機捕捉影像
        mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback,
                mBackgroundHandler);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}
複製程式碼
private CameraCaptureSession.CaptureCallback mCaptureCallback
            = new CameraCaptureSession.CaptureCallback() {

        private void process(CaptureResult result) {
              //處理對焦、閃光燈等
              .......
             
              case STATE_WAITING_LOCK: {
                    .......
                    //對焦完成,儲存一張圖片
                    captureStillPicture();
                    .......
                    break;
                }
              
        }
  }
複製程式碼
private void captureStillPicture() {
    try {
        final Activity activity = getActivity();
        if (null == activity || null == mCameraDevice) {
            return;
        }
        // This is the CaptureRequest.Builder that we use to take a picture.
        // 將mImageReader作為目標,獲得的圖片資料會交給mImageReader處理
        // 初始化時 mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler);
        // 所以資料最終會在mOnImageAvailableListener中處理
        final CaptureRequest.Builder captureBuilder =
                mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
        captureBuilder.addTarget(mImageReader.getSurface());

        // Use the same AE and AF modes as the preview.
        captureBuilder.set(CaptureRequest.CONTROL_AF_MODE,
                CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
        setAutoFlash(captureBuilder);

        // Orientation
        int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
        captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation));

        // 拍照事件的最終回撥,圖片處理完後呼叫onCaptureCompleted
        CameraCaptureSession.CaptureCallback CaptureCallback
                = new CameraCaptureSession.CaptureCallback() {

            @Override
            public void onCaptureCompleted(@NonNull CameraCaptureSession session,
                                           @NonNull CaptureRequest request,
                                           @NonNull TotalCaptureResult result) {
                showToast("Saved: " + mFile);
                Log.d(TAG, mFile.toString());
                unlockFocus();
            }
        };

        mCaptureSession.stopRepeating();
        mCaptureSession.abortCaptures();
        mCaptureSession.capture(captureBuilder.build(), CaptureCallback, null);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}
複製程式碼
// 資料處理、儲存
private final ImageReader.OnImageAvailableListener mOnImageAvailableListener
        = new ImageReader.OnImageAvailableListener() {

    @Override
    public void onImageAvailable(ImageReader reader) {
        //mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage(), mFile));
        Image mImage = reader.acquireNextImage();
        ByteBuffer buffer = mImage.getPlanes()[0].getBuffer();
        byte[] bytes = new byte[buffer.remaining()];
        buffer.get(bytes);
        // 檔案操作
        // ......
        // 這裡獲取到的是視訊的原始資料,要對資料做進一步的演算法處理、或者採用第三方的編碼庫進行壓縮、以及網路傳輸等,都可以從這裡拿到資料。
        mImage.close();
    }
};
複製程式碼

4、錄影

主要看一下MediaRecorder錄製視訊相關程式碼

private void startRecordingVideo() {
    if (null == mCameraDevice || !mTextureView.isAvailable() || null == mPreviewSize) {
        return;
    }
    try {
        // 關閉之前的會話,新的會話會新增錄影的Target
        closePreviewSession();
        // 配置MediaRecorder,音訊、視訊來源,編碼格式等
        setUpMediaRecorder();
        SurfaceTexture texture = mTextureView.getSurfaceTexture();
        assert texture != null;
        texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
        // 建立一個適合視訊錄製的請求
        mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
        List<Surface> surfaces = new ArrayList<>();

        // Set up Surface for the camera preview
        Surface previewSurface = new Surface(texture);
        surfaces.add(previewSurface);
        mPreviewBuilder.addTarget(previewSurface);

        // Set up Surface for the MediaRecorder 重要的一步,視訊資訊會交給mMediaRecorder處理
        Surface recorderSurface = mMediaRecorder.getSurface();
        surfaces.add(recorderSurface);
        mPreviewBuilder.addTarget(recorderSurface);

        // Start a capture session
        // Once the session starts, we can update the UI and start recording
        mCameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() {

            @Override
            public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
                mPreviewSession = cameraCaptureSession;
                updatePreview();
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        // UI
                        mButtonVideo.setText(R.string.stop);
                        mIsRecordingVideo = true;

                        // 開始錄製
                        mMediaRecorder.start();
                    }
                });
            }

            @Override
            public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
                Activity activity = getActivity();
                if (null != activity) {
                    Toast.makeText(activity, "Failed", Toast.LENGTH_SHORT).show();
                }
            }
        }, mBackgroundHandler);
    } catch (CameraAccessException | IOException e) {
        e.printStackTrace();
    }

}
複製程式碼
// 配置MediaRecorder
private void setUpMediaRecorder() throws IOException {
    final Activity activity = getActivity();
    if (null == activity) {
        return;
    }
    // 設定要用於錄製的音訊源。
    mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
    // 設定要用於錄製的視訊源。
    mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
    // 設定錄製期間生成的輸出檔案的格式。
    mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
    
    // 生成MP4檔案路徑
    if (mNextVideoAbsolutePath == null || mNextVideoAbsolutePath.isEmpty()) {
        mNextVideoAbsolutePath = getVideoFilePath(getActivity());
    }
    mMediaRecorder.setOutputFile(mNextVideoAbsolutePath);
    
    // 設定用於錄製的視訊編碼位元率。
    mMediaRecorder.setVideoEncodingBitRate(10000000);
    // 設定要捕獲的視訊的幀速率。
    mMediaRecorder.setVideoFrameRate(30);
    mMediaRecorder.setVideoSize(mVideoSize.getWidth(), mVideoSize.getHeight());
    // 設定要用於錄製的視訊編碼器。
    mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
    // 設定要用於錄製的音訊編碼器。
    mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
    int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
    switch (mSensorOrientation) {
        case SENSOR_ORIENTATION_DEFAULT_DEGREES:
            mMediaRecorder.setOrientationHint(DEFAULT_ORIENTATIONS.get(rotation));
            break;
        case SENSOR_ORIENTATION_INVERSE_DEGREES:
            mMediaRecorder.setOrientationHint(INVERSE_ORIENTATIONS.get(rotation));
            break;
    }
    // 在呼叫start前必須的一步
    mMediaRecorder.prepare();
}
複製程式碼
/**
 * 常規使用MediaRecorder去錄製視訊的例子如下:
 * MediaRecorder recorder = new MediaRecorder();
 * recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
 * recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
 * recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
 * recorder.setOutputFile(PATH_NAME);
 * recorder.prepare();
 * recorder.start();   // Recording is now started
 * ...
 * recorder.stop();
 * recorder.reset();   // You can reuse the object by going back to setAudioSource() step
 * recorder.release(); // Now the object cannot be reused
 **/
複製程式碼

Camera2預覽、拍照和錄影的大概流程就是這樣,相對來說挺複雜的,但也非常重要,後面會繼續深入分析原理和原始碼。

相關文章