1. 程式人生 > >Android Camera2 API和拍照與錄影過程

Android Camera2 API和拍照與錄影過程

這裡寫圖片描述

簡介

Android 5.0開始出現了新的相機Camera 2 API,用來替代以前的camera api。

Camera2 API不僅提高了android系統的拍照效能,還支援RAW照片輸出,還可以設定相機的對焦模式,曝光模式,快門等等。

Camera2 中主要的API類

  • CameraManager類 : 攝像頭管理類,用於檢測、開啟系統攝像頭,通過getCameraCharacteristics(cameraId)可以獲取攝像頭特徵。

  • CameraCharacteristics類:相機特性類,例如,是否支援自動調焦,是否支援zoom,是否支援閃光燈一系列特徵。

  • CameraDevice類: 相機裝置,類似早期的camera類。

  • CameraCaptureSession類:用於建立預覽、拍照的Session類。通過它的setRepeatingRequest()方法控制預覽介面 , 通過它的capture()方法控制拍照動作或者錄影動作。

  • CameraRequest類:一次捕獲的請求,可以設定一些列的引數,用於控制預覽和拍照引數,例如:對焦模式,曝光模式,zoom引數等等。

接下來,進一步介紹,Camera2 API中的各種常見類和抽象類。

CameraManager類

CameraCharacteristics cameraCharacteristics 
=manager.getCameraCharacteristics(cameraId);

通過以上程式碼可以獲取攝像頭的特徵物件,例如: 前後攝像頭,解析度等。

CameraCharacteristics類

相機特性類

CameraCharacteristics是一個包含相機引數的物件,可以通過一些key獲取對應的values.

以下幾種常用的引數

  • LENS_FACING:獲取攝像頭方向。LENS_FACING_FRONT是前攝像頭,LENS_FACING_BACK是後攝像頭。

  • SENSOR_ORIENTATION:獲取攝像頭拍照的方向。

  • FLASH_INFO_AVAILABLE:獲取是否支援閃光燈。

  • SCALER_AVAILABLE_MAX_DIGITAL_ZOOM:獲取最大的數字調焦值,也就是zoom最大值。

  • LENS_INFO_MINIMUM_FOCUS_DISTANCE:獲取最小的調焦距離,某些手機上獲取到的該values為null或者0.0。前攝像頭大部分有固定焦距,無法調節。

  • INFO_SUPPORTED_HARDWARE_LEVEL:獲取攝像頭支援某些特性的程度。

    以下手機中支援的若干種程度:

    • INFO_SUPPORTED_HARDWARE_LEVEL_FULL:全方位的硬體支援,允許手動控制全高清的攝像、支援連拍模式以及其他新特性。

    • INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED:有限支援,這個需要單獨查詢。

    • INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY:所有裝置都會支援,也就是和過時的Camera API支援的特性是一致的。

CameraDevice類

CameraDevice的reateCaptureRequest(int templateType)方法建立CaptureRequest.Builder。

templateType引數有以下幾種:

  • TEMPLATE_PREVIEW :預覽

  • TEMPLATE_RECORD:拍攝視訊

  • TEMPLATE_STILL_CAPTURE:拍照

  • TEMPLATE_VIDEO_SNAPSHOT:建立視視訊錄製時截圖的請求

  • TEMPLATE_ZERO_SHUTTER_LAG:建立一個適用於零快門延遲的請求。在不影響預覽幀率的情況下最大化影象質量。

  • TEMPLATE_MANUAL:建立一個基本捕獲請求,這種請求中所有的自動控制都是禁用的(自動曝光,自動白平衡、自動焦點)。

CameraDevice.StateCallback抽象類

該抽象類用於CemeraDevice相機裝置狀態的回撥。

    /**
     * 當相機裝置的狀態發生改變的時候,將會回撥。
     */
    protected final CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
        /**
         * 當相機開啟的時候,呼叫
         * @param cameraDevice
         */
        @Override
        public void onOpened(@NonNull CameraDevice cameraDevice) {

            mCameraDevice = cameraDevice;
            startPreView();
        }

        @Override
        public void onDisconnected(@NonNull CameraDevice cameraDevice) {

            cameraDevice.close();
            mCameraDevice = null;
        }

        /**
         * 發生異常的時候呼叫
         *
         * 這裡釋放資源,然後關閉介面
         * @param cameraDevice
         * @param error
         */
        @Override
        public void onError(@NonNull CameraDevice cameraDevice, int error) {
            cameraDevice.close();
            mCameraDevice = null;

        }
        /**
         *當相機被關閉的時候
         */
        @Override
        public void onClosed(@NonNull CameraDevice camera) {
            super.onClosed(camera);
        }
    };

CameraCaptureSession.StateCallback抽象類

該抽象類用於Session過程中狀態的回撥。

public static abstract class StateCallback {

        //攝像頭完成配置,可以處理Capture請求了。
        public abstract void onConfigured(@NonNull CameraCaptureSession session);

        //攝像頭配置失敗
        public abstract void onConfigureFailed(@NonNull CameraCaptureSession session);

        //攝像頭處於就緒狀態,當前沒有請求需要處理
        public void onReady(@NonNull CameraCaptureSession session) {}

        //攝像頭正在處理請求
        public void onActive(@NonNull CameraCaptureSession session) {}

        //請求佇列中為空,準備著接受下一個請求。
        public void onCaptureQueueEmpty(@NonNull CameraCaptureSession session) {}

        //會話被關閉
        public void onClosed(@NonNull CameraCaptureSession session) {}

        //Surface準備就緒
        public void onSurfacePrepared(@NonNull CameraCaptureSession session,@NonNull Surface surface) {}

}

接下來,是介紹拍照和錄影流程步驟。

使用流程:

1. 開啟指定的方向的相機

最先獲取CameraManager物件,通過該物件的getCameraIdList()獲取到一些列的攝像頭引數。

通過迴圈匹配,獲取到指定方向的攝像頭,例如後攝像頭等。

 CameraManager manager = (CameraManager)getSystemService(Context.CAMERA_SERVICE);

//獲取到可用的相機
for (String cameraId : manager.getCameraIdList()) {

      //獲取到每個相機的引數物件,包含前後攝像頭,解析度等
     CameraCharacteristics  cameraCharacteristics = manager.getCameraCharacteristics(cameraId);
     //攝像頭的方向
     Integer facing = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING);

     if(facing==null){
         continue;
     }
     //匹配方向,指定開啟後攝像頭
     if(facing!=CameraCharacteristics.LENS_FACING_BACK){
          continue; 
     }

     //開啟指定的攝像頭
    manager.openCamera(mCameraId, stateCallback, workThreadManager.getBackgroundHandler());

    return;
}

當然,實際開發中,還需要獲取相機支援的特性(閃光燈,zoom調焦,手動調焦等),和設定攝像頭的引數(例如:預覽的Size)。

2. 建立預覽的介面

建立 CameraDevice.StateCallback 物件,且開啟一個相機。當相機開啟後,將出現相機預覽介面。

CameraDevice.StateCallback 物件傳入CameraManager中openCamera(mCameraId, stateCallback, workThreadManager.getBackgroundHandler())的第二個引數,用於監聽攝像頭的狀態。

/**
  * 相機裝置
  */
 protected CameraDevice mCameraDevice;


 /**
   * 當相機裝置的狀態發生改變的時候,將會回撥。
   */
 protected final CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
        /**
         * 當相機開啟的時候,呼叫
         * @param cameraDevice
         */
        @Override
        public void onOpened(@NonNull CameraDevice cameraDevice) {
            mCameraDevice = cameraDevice;
            createCameraPreviewSession();
        }

       // 省略該狀態介面的部分方法
       ...............

 };

 /**
  * 預覽請求的Builder
  */
 private CaptureRequest.Builder mPreviewRequestBuilder;


 /**
 * 相機開始預覽,建立一個CameraCaptureSession物件
 */
 private void createCameraPreviewSession() {

      // 將CaptureRequest的構建器與Surface物件繫結在一起    
      mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);

      // 為相機預覽,建立一個CameraCaptureSession物件
      mCameraDevice.createCaptureSession(Arrays.asList(surface, imageReader.getSurface()), stateCallback, null);        
 }

建立完預覽的介面後,接下來需要開始重新整理。

3. 在預覽介面過程中,需要間隔重新整理介面

相機預覽使用TextureView來實現。建立一個CameraCaptureSession ,通過一個用於預覽介面的CaptureRequest,間隔複用給CameraCaptureSession。

 private CameraCaptureSession mCaptureSession;

 CameraCaptureSession.StateCallback stateCallback=new CameraCaptureSession.StateCallback() {
                @Override
                public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {

                //當cameraCaptureSession已經準備完成,開始顯示預覽介面
                    mCaptureSession = cameraCaptureSession;
                    setCameraCaptureSession();
                }

                //省略該介面的部分方法
                .......
 }

 /**
  * 設定CameraCaptureSession的特徵:
  * <p>
  * 自動對焦,閃光燈
  */
 private void setCameraCaptureSession() {

     //設定預覽介面的特徵,通過mPreviewRequestBuilder.set()方法,例如,閃光燈,zoom調焦等
     ..........

      //為CameraCaptureSession設定間隔的CaptureRequest,用間隔重新整理預覽介面。
     mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), mCaptureCallback, workThreadManager.getBackgroundHandler()); 
 }

只要未開始拍照動作或者錄影動作,該複用的CaptureRequest會重複的重新整理預覽介面。

接下來,等待使用者點選拍照按鈕或者錄影按鈕,進行拍照動作,或者錄影動作。

4. 拍照動作

首先鎖住焦點,通過在相機預覽介面個性CaptureRequest。然後,以類似方式,需要執行一個預捕獲序列。接下來,可已經準備好捕捉圖片。建立一個新的CaptureRequest,且拍照。

  /**
     * 拍照一個靜態的圖片
     * ,當在CaptureCallback監聽器響應的時候呼叫該方法。
     * <p>
     * 當數字調焦縮放的時候,在寫入圖片數中也要設定。
     */
private void captureStillPicture() {
    try {

            // 建立一個拍照的CaptureRequest.Builder
            final CaptureRequest.Builder captureBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);

             captureBuilder.addTarget(imageReader.getSurface());

            //設定一系列的拍照引數,這裡省略
            ...........

            //先停止以前的預覽狀態
            mCaptureSession.stopRepeating();
            mCaptureSession.abortCaptures();

            //執行拍照動作
            mCaptureSession.capture(captureBuilder.build(), captureCallback, null);
    } catch (CameraAccessException e) {
            e.printStackTrace();
    }
}

拍照介面產生的資料只是在手機記憶體中,圖片是一個磁碟檔案,還需要一個將拍照產生資料寫入檔案中的操作類ImageReader。

先是建立ImageReader物件,和設定監聽器等一些列引數。

    /**
     * 處理靜態圖片的輸出
     */
    private ImageReader imageReader;

      //對於靜態圖片,使用可用的最大值來拍攝。
      Size largest = Collections.max(Arrays.asList(map.getOutputSizes(ImageFormat.JPEG)), new CompareSizeByArea());
      //設定ImageReader,將大小,圖片格式
      imageReader = ImageReader.newInstance(largest.getWidth(), largest.getHeight(), ImageFormat.JPEG, /*maxImages*/2);
      imageReader.setOnImageAvailableListener(onImageAvailableListener, workThreadManager.getBackgroundHandler());    

接下來,將ImageReader的surface配置到captureBuilder物件中captureBuilder.addTarget(imageReader.getSurface());

最後,當拍照完成後,會在該監聽狀態中回撥:

   /**
     * ImageReader的回撥監聽器
     * <p>
     * onImageAvailable被呼叫的時候,已經拍照完,準備儲存的操作
     * 通常寫入磁碟檔案中。
     */
    protected final ImageReader.OnImageAvailableListener onImageAvailableListener = (ImageReader reader)
            -> writePictureData(reader.acquireNextImage());


    public void writePictureData(Image image) {
        if (camera2ResultCallBack != null) {
            camera2ResultCallBack.callBack(ObservableBuilder.createWriteCaptureImage(appContext, image));
        }
    }        

    /**
     * 將JPEG圖片的資料,寫入磁碟中
     *
     * @param context
     * @param mImage
     * @return
     */
    public static Observable<String> createWriteCaptureImage(final Context context, final Image mImage) {
        Observable<String> observable = Observable.create(subscriber -> {
            File file = FileUtils.createPictureDiskFile(context, FileUtils.createBitmapFileName());
            ByteBuffer buffer = mImage.getPlanes()[0].getBuffer();
            byte[] bytes = new byte[buffer.remaining()];
            buffer.get(bytes);
            FileOutputStream output = null;
            try {
                output = new FileOutputStream(file);
                output.write(bytes);
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                mImage.close();
                if (null != output) {
                    try {
                        output.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
            subscriber.onNext(file.getAbsolutePath());
        });
        return observable;
    }

這裡採用RxJava+RxAndroid非同步通訊,避免太多回調介面。

5. 錄影動作

錄影是長時間的動作,錄影過程中需要重複性的重新整理錄製介面。其餘的步驟和拍照動作基本類似。

 /**
   * 開始視訊錄製。
   */
private void startRecordingVideo() {
    try {
            //建立錄製的session會話中的請求
            mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);

            //設定錄製引數,這裡省略
            .........

            // 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;
                     Log.i(TAG, " startRecordingVideo  正式開始錄製 ");
                     updatePreview();
                }
                //該介面的方法,部分省略
                .............

            }, workThreadManager.getBackgroundHandler());
        } catch (CameraAccessException | IOException e) {
            e.printStackTrace();
        }
}

//錄製過程中,不斷重新整理錄製介面
private void updatePreview() {

        try {
            mPreviewSession.setRepeatingRequest(mPreviewBuilder.build(), null, workThreadManager.getBackgroundHandler());
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
}

和拍照類似,將視訊資料寫入磁碟檔案中,也是需要一個操作類 MediaRecorder來實現的。

先是建立該操作類物件,設定一些列引數:

    /**
     * MediaRecorder
     */
private MediaRecorder mMediaRecorder;

 /**
     * 設定媒體錄製器的配置引數
     * <p>
     * 音訊,視訊格式,檔案路徑,頻率,編碼格式等等
     *
     * @throws IOException
     */
    private void setUpMediaRecorder() throws IOException {

        mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
        mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
        mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
        mNextVideoAbsolutePath = FileUtils.createVideoDiskFile(appContext, FileUtils.createVideoFileName()).getAbsolutePath();
        mMediaRecorder.setOutputFile(mNextVideoAbsolutePath);
        mMediaRecorder.setVideoEncodingBitRate(10000000);
        //每秒30幀
        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(ORIENTATIONS.get(rotation));
                break;
            default:
                break;
        }
        mMediaRecorder.prepare();
    }

間隔性的隨著視訊錄製而輸出資料到檔案中。

// 為 MediaRecorder設定Surface
Surface recorderSurface = mMediaRecorder.getSurface();
surfaces.add(recorderSurface);
mPreviewBuilder.addTarget(recorderSurface);

最後,當錄製視訊結束後,停止輸出:

 // 停止錄製
 mMediaRecorder.stop();
 mMediaRecorder.reset();

6. 恢復到預覽介面

完成一些列拍照或錄影動作後,重新恢復到預覽介面。

/**
  * 完成一些列拍照或錄影動作後,釋放焦點。
  */
private void unlockFocus() {
   try {
         //向session重新發送,預覽的間隔性請求,出現預覽介面。
         mCaptureSession.setRepeatingRequest(mPreviewRequest, mCaptureCallback, workThreadManager.getBackgroundHandler());
    } catch (CameraAccessException e) {
            e.printStackTrace();
    }
}

當然,還有關閉相機操作,和與Activity生命週期繫結的操作,這裡不再做介紹了。

下一篇,開始介紹,如何開發一個真正的相機程式。

資源參考