1. 程式人生 > 其它 >Android Camera2 學習01_API 的簡單描述和呼叫(預覽、拍照、錄影)

Android Camera2 學習01_API 的簡單描述和呼叫(預覽、拍照、錄影)

技術標籤:javaandroidpython程式語言安卓

Android 5.1 以後,添加了Camera2 的API,能夠滿足更多控制camera的場景。當然,相對應camera1的呼叫,也變的複雜一點。

一、涉及到的關鍵類

CameraManager -------------- 獲取連線的camera情況,執行開啟攝像頭的操作;

CameraDevice -------------- 當前連線的攝像頭物件;

CaptureRequest -------------- camera資料的請求,比如預覽、拍照、錄影等;

CaptureSession -------------- 傳送請求後,就建立了一個會話,可以在當前建立的會話上切換各種請求,不需要的時候可以執行關閉;

二、程式碼實現

下面程式碼是基於Google提供的demohttps://github.com/googlesamples/android-Camera2Basic

後面自己個人又建了個獨立的分支,程式碼都是基於Google Demo 來的https://github.com/yorkZJC/AndroidCamera2Sample

Camera2BaseFragment.java

2.1這裡採用的是TextureView來進行顯示,在onResume()的時候,進行判斷,如果當前TextureView 可用了,則執行開啟攝像頭的操作,否則等待TextureView available,第一次開啟的是,TextureView還沒建立完成,所以會在TextureView available回撥中執行開啟camera的操作。

@Override
    public void onResume() {
        super.onResume();
        startBackgroundThread();


        if (mTextureView.isAvailable()) {
            openCamera(mTextureView.getWidth(), mTextureView.getHeight());
        } else {
            mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
        }
    }
private final TextureView.SurfaceTextureListener mSurfaceTextureListener
            = new TextureView.SurfaceTextureListener() {


        @Override
        public void onSurfaceTextureAvailable(SurfaceTexture texture, int width, int height) {
            openCamera( width, height);
        }


        @Override
        public void onSurfaceTextureSizeChanged(SurfaceTexture texture, int width, int height) {
            configureTransform(width, height);
        }


        @Override
        public boolean onSurfaceTextureDestroyed(SurfaceTexture texture) {
            return true;
        }


        @Override
        public void onSurfaceTextureUpdated(SurfaceTexture texture) {
        }
    };

2.2接下來看下openCamera()的實現

這邊完成了camera資訊的獲取的配置,並呼叫CameraManager 的openCamera開啟攝像頭,camera開啟狀態在CameraDevice.StateCallback中進行回撥.

 private void openCamera(int width, int height) {
        if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.CAMERA)
                != PackageManager.PERMISSION_GRANTED) {
            requestCameraPermission();
            return;
        }
        setUpCameraOutputs(width, height);
        configureTransform(width, height);
        Activity activity = getActivity();
        CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
        try {
            if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
                throw new RuntimeException("Time out waiting to lock back camera opening.");
            }
            manager.openCamera(mCameraId,mStateCallback, mBackgroundHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            throw new RuntimeException("Interrupted while trying to lock camera opening.", e);
        }
    }

2.3在camera開啟的回撥中,可以獲取到當前的camera對應的CameraDevice,在onOpened()中執行開啟預覽的操作。

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.4下來就是開啟預覽的過程,主要做了下面幾件事情:

1、預覽影象顯示在哪裡,這就需要繫結surface,這裡可以進行多個surface的繫結,如果是上層需要拿到預覽資料,則可以設定ImageReader的surface進去;

2、傳送預覽請求;

3、建立預覽會話;

完成這幾步,我們就可以看到預覽影象了。

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


            mPreviewRequestBuilder.addTarget(surface);


            //request builder可以設定多個target,如果需要拿到實時的預覽資料,則把imageReader 的surface 也設進去
//            mPreviewRequestBuilder.addTarget(mImageReader.getSurface());


            // 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.
                            mPreviewCaptureSession = cameraCaptureSession;
                            try {
                                // Auto focus should be continuous for camera preview.
                                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();
//                                mPreviewCaptureSession.setRepeatingRequest(mPreviewRequest,
//                                        mCaptureCallback, mBackgroundHandler);
                                mPreviewCaptureSession.setRepeatingRequest(mPreviewRequest,
                                        null, mBackgroundHandler);
                            } catch (CameraAccessException e) {
                                e.printStackTrace();
                            }
                        }


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

2.5拍照

Camera2 的API,拍照時通過ImageReader返回jpeg資料給上層,交由上層進行儲存;

如下面程式碼所示:

1、首先需要初始化一個JPEG型別的ImageReader,用來接收底層資料回撥;

2、設定CameraDevice.TEMPLATE_STILL_CAPTURE 型別的請求,請求拍照;請求成功後,我們需要恢復正常的預覽型別請求;

3、在ImageReader回撥中將接收到的jpeg資料進行儲存;

 /**
  * 初始化一個jpeg型別的imageReader
  **/
 private void initJpegImageReader(int width, int height) {
      
       StreamConfigurationMap map = mCameraCharacteristics.get(
                 CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
         if (map == null) {
             return;
         }
         
      Size largest = Collections.max(
                  Arrays.asList(map.getOutputSizes(ImageFormat.JPEG)),
                  new CompareSizesByArea());


        mJpegCpatureWidth = largest.getWidth();
        mJpegCaptureHeight = largest.getHeight();
        


      mJpegImageReader = ImageReader.newInstance(largest.getWidth(), largest.getHeight(),
                  ImageFormat.JPEG, 2);
      mJpegImageReader.setOnImageAvailableListener(mJpegImageAvailableListener, mBackgroundHandler);
}
 


 private final ImageReader.OnImageAvailableListener mJpegImageAvailableListener = new ImageReader.OnImageAvailableListener() {


    @Override
    public void onImageAvailable(ImageReader reader) {
        Log.v(TAG, "--- mJpegImageAvailableListener();reader: " + reader);
        Image image = reader.acquireLatestImage();
        if(image == null){
          return;
        }
        
        ByteBuffer buffer = image.getPlanes()[0].getBuffer();
            byte[] bytes = new byte[buffer.remaining()];
            buffer.get(bytes);
        
            //將接收到的資料交由獨立的執行緒進行檔案的儲存操作
        mBackgroundHandler.post(new ImageSaver(bytes,mJpegCpatureWidth,mJpegCaptureHeight, generateJpegFile(),mCaptureListener));
        image.close();
    }
   };




private void captureStillPicture() {
       
        try {
            if (null == mCameraDevice || mCapturing || mPreviewSession == null) {
        
                return;
            }
            
            mCapturing = true;


            // This is the CaptureRequest.Builder that we use to take a picture.
            mPreviewBuilder =
//設定拍照請求                    
mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
            mPreviewBuilder.addTarget(mJpegImageReader.getSurface());


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


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


            CameraCaptureSession.CaptureCallback captureCallback
                    = new CameraCaptureSession.CaptureCallback() {
                @Override
                public void onCaptureCompleted(@NonNull CameraCaptureSession session,
                                               @NonNull CaptureRequest request,
                                               @NonNull TotalCaptureResult result) {
                
                      //拍照請求成功後,恢復正常的預覽模式
                    startPreview();
                  
                  mCapturing = false;
                }
            };
            mPreviewSession.stopRepeating();
            mPreviewSession.abortCaptures();
            mPreviewSession.capture(mPreviewBuilder.build(),captureCallback , mBackgroundHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
}

2.6錄影

Android API文件(https://developer.android.google.cn/reference/android/hardware/camera2/package-summary?hl=en)描述有下面這麼一段話,我們可以看到MediaRecorder 的surface也是可以作為target Surface進行資料的請求的。那就很簡單了,錄影編碼需要資料來源,而這個source就是通過MediaRecorder.getsurface,然後把該surface設定為target surface,那麼MediaRecorder就可以拿到Camera資料了。

下面看下具體的程式碼實現:

private void startRecordingVideo() {
        if (null == mCameraDevice || !mTextureView.isAvailable() || null == mPreviewSize) {
            return;
        }
        try {
            closePreviewSession();
            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
            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;


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


三、寫在後面

看了上面簡單的程式碼流程,我們應該有這樣簡單的概念。對Camera的操作,無非就是獲取到硬體裝置相關屬性,比如當前掛載了哪些攝像頭,攝像頭支援哪些解析度等屬性,這個我們需要用到CameraManager來獲取;

獲取到Camera相關屬性後,那麼就需要對硬體裝置進行操作,操作就是開啟Camera,獲取預覽資料這些了,通過CameraManager,我們能開啟對應Id的camera,然後獲取到該id對應的Camera裝置例項,這個就是CameraDevice了;

那麼接下來就是怎麼怎麼把Camera資料顯示到UI上,這時就用到Surface了,我們可以這樣理解,Surface是影象顯示的介質,Camera2 API 允許我們設定多個Surface為輸出目標,比如上面我們說的ImageReader、SurfaceTexutre、MediaRecorder相關的Surface都可以設為目標Surface,底層會幫我們進行資料的填充和顯示。這些Surface我們需要預先初始化好引數;

那麼有了顯示的載體後,就可以進行顯示了,Camera2裡面就用到了個CaptureRequest 來觸發資料的請求,這個request又可以根據自己的使用場景設定不同的請求型別,比如是 預覽場景,則可以設定請求型別為 CameraDevice.TEMPLATE_PREVIEW,錄影場景下,則設定為CameraDevice.TEMPLATE_RECORD,拍照場景下,則設定為CameraDevice.TEMPLATE_STILL_CAPTURE;

完成了上面這些後,還需要最後一步,就是建立會話了,也就是CaptureSession。我們可以理解為,上面所做的準備,都是為了建立會話,建立了會話後,和Camera之間的互動才真正建立起來。這個會話可以隨時關閉,也可以修改引數。

===========================================================

想聯絡我的,關注我的個人公眾號(小馳筆記)吧,公眾號會記錄自己開發的點滴,還有日常的生活,希望和更多的小夥伴一起交流~~(ps:本人目前在深圳上班)