Android音視頻-視頻採集(Camera2功能實現)

這一篇文章我們要實現Camera實現的等一些功能。熟悉Camera2API的使用,着重瞭解我們前面沒有深入瞭解的視頻錄製相關的內容。

基本功能實現

切換攝像頭

這個的實現和Camera API的步驟一摸一樣。只是換了一個API而已。Camera是通過Camera.CameraInfo去獲取相機,Camera2通過CameraManger去獲取設備相機。關鍵代碼如下:

private void getDefaultCameraId() {
        mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
        try {
            String[] cameraList = mCameraManager.getCameraIdList();
            for (int i = 0; i < cameraList.length; i++) {
                String cameraId = cameraList[i];
                if (TextUtils.equals(cameraId, CAMERA_FONT)) {
                    mCameraId = cameraId;
                    break;
                } else if (TextUtils.equals(cameraId, CAMERA_BACK)) {
                    mCameraId = cameraId;
                    break;
                }
            }
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }
    /**
     * 切換攝像頭
     */
    public void switchCamera() {
        if (TextUtils.equals(mCameraId, CAMERA_FONT)) {
            mCameraId = CAMERA_BACK;
        } else {
            mCameraId = CAMERA_FONT;
        }
        closeCamera();
        openCamera(getWidth(), getHeight());
    }

拍照

使用Camera2API來進行拍照要使用ImageRender來實現。具體步驟如下:

  • 設置ImageReader,回掉可用保存圖片
private void setupImageReader() {
        //2代表ImageReader中最多可以獲取兩幀圖像流
        mImageReader = ImageReader.newInstance(mPreviewSize.getWidth(), mPreviewSize.getHeight(),
                ImageFormat.JPEG, 2);
        mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
            @Override
            public void onImageAvailable(ImageReader reader) {
                mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage()));
            }
        }, mBackgroundHandler);
    }

    private static File mImageFile;
    private ImageReader mImageReader;

    private static class ImageSaver implements Runnable {
        private Image mImage;

        private ImageSaver(Image image) {
            mImage = image;
        }

        @Override
        public void run() {
            ByteBuffer byteBuffer = mImage.getPlanes()[0].getBuffer();
            byte[] bytes = new byte[byteBuffer.remaining()];
            byteBuffer.get(bytes);
            FileOutputStream fileOutputStream = null;
            try {
                fileOutputStream = new FileOutputStream(mImageFile);
                fileOutputStream.write(bytes);
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                mImage.close();
                if (fileOutputStream != null) {
                    try {
                        fileOutputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
  • 在預覽創建CameraCaptureSession的時候除了預覽的TextureView,把ImageReader的Surface設置進行配置
//創建CaptureSession對象
            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;
                    }
                    Log.e(TAG, "onConfigured: ");
                    // When the session is ready, we start displaying the preview.
                    mCameraCaptureSessions = cameraCaptureSession;
                    //更新預覽
                    updatePreview();
                }

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

這些代碼在創建預覽View的方法裏面。

  • 拍照

鎖定焦點

```
private void lockFocus() {
        try {
            mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_START);
            mCameraCaptureSessions.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

拍照

private void capture() {
        try {
            final CaptureRequest.Builder mCaptureBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
            int rotation = ((Activity) mContext).getWindowManager().getDefaultDisplay().getRotation();
            mCaptureBuilder.addTarget(mImageReader.getSurface());
            mCaptureBuilder.set(CaptureRequest.JPEG_ORIENTATION, ORIENTATIONS.get(rotation));
            CameraCaptureSession.CaptureCallback CaptureCallback = new CameraCaptureSession.CaptureCallback() {
                @Override
                public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
                    Toast.makeText(mContext, "Image Saved!", Toast.LENGTH_SHORT).show();
                    unLockFocus();
                    updatePreview();
                }
            };
            mCameraCaptureSessions.stopRepeating();
            mCameraCaptureSessions.capture(mCaptureBuilder.build(), CaptureCallback, null);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

解鎖焦點

private void unLockFocus() {
        try {
            mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL);
            //mCameraCaptureSession.capture(mCaptureRequestBuilder.build(), null, mCameraHandler);
            mCameraCaptureSessions.setRepeatingRequest(mPreviewRequestBuilder.build(), null, mBackgroundHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

錄製視頻

開始錄製視頻

錄製視頻同樣使用MediaRecorder來協助完成。具體步驟如下:

關閉預覽會話

private void closePreviewSession() {
        if (null != mCameraCaptureSessions) {
            mCameraCaptureSessions.close();
            mCameraCaptureSessions = null;
        }
    }

設置MediaRecorder

private void setUpMediaRecorder() throws IOException {
        mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
        mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
        mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
        mVideoPath = getOutputMediaFile(MEDIA_TYPE_VIDEO);
        mMediaRecorder.setOutputFile(mVideoPath.getAbsolutePath());
        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) mContext).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;
        }
        mMediaRecorder.prepare();
    }

構建請求創建會話

private void startRecordingVideo() {
        if (null == mCameraDevice || !isAvailable() || null == mPreviewSize) {
            return;
        }
        try {
            closePreviewSession();
            setUpMediaRecorder();
            SurfaceTexture texture = getSurfaceTexture();
            assert texture != null;
            texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
            mPreviewRequestBuilder = 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);
            mPreviewRequestBuilder.addTarget(previewSurface);

            // Set up Surface for the MediaRecorder
            Surface recorderSurface = mMediaRecorder.getSurface();
            surfaces.add(recorderSurface);
            mPreviewRequestBuilder.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) {
                    mCameraCaptureSessions = cameraCaptureSession;
                    updatePreview();
                    Toast.makeText(mContext, "start record video success", Toast.LENGTH_SHORT).show();
                    Log.e(TAG, "onConfigured: "+Thread.currentThread().getName());
                    // Start recording
                    mMediaRecorder.start();
                }

                @Override
                public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
                    Toast.makeText(mContext, "Failed", Toast.LENGTH_SHORT).show();
                }
            }, mBackgroundHandler);
        } catch (CameraAccessException | IOException e) {
            e.printStackTrace();
        }
    }

停止錄製視頻

private void stopRecordingVideo() {
        // Stop recording
        mMediaRecorder.stop();
        mMediaRecorder.reset();

        Toast.makeText(mContext, "Video saved: " + mVideoPath.getAbsolutePath(),
                Toast.LENGTH_SHORT).show();
        createCameraPreview();
    }

照片添加水印

我們簡單的實現和使用Camera的時候一樣的水印的功能,拿到攝像頭回掉的每一幀數據,然後對它進行加工處理顯示到另外一個SurfaceView上面去。

獲取每一幀的回掉

在拍照的時候我們使用了ImageReader類來進行處理,在處理每一幀的回掉的時候我們還是要藉助這個類來進行處理。

  • 在預覽請求類中添加target
mPreviewRequestBuilder.addTarget(mImageReader.getSurface());

這樣添加以後我們可以得到相機的每一幀的數據

  • 修改ImageReader的回掉函數
private void setupImageReader() {
        //2代表ImageReader中最多可以獲取兩幀圖像流
        mImageReader = ImageReader.newInstance(mPreviewSize.getWidth(), mPreviewSize.getHeight(),
                ImageFormat.JPEG, 1);
        mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
            @Override
            public void onImageAvailable(ImageReader reader) {

                        //這裏一定要調用reader.acquireNextImage()和img.close方法否則不會一直回掉了
                        Image img = reader.acquireNextImage();
                        img.close();
                        break;
                }
            }
        }, mBackgroundHandler);
    }

這裏把預覽的回掉和拍照的回掉做了一個區分的處理。

有一個要注意的是Image img = reader.acquireNextImage(); img.close();
一定要在每一幀裏面拿了Image數據,不然會出現只回掉一次的問題。

還有一個要注意的是我們每幀數據的格式我們現在設置的JPEG。這個格式有點繞下面瞭解

處理幀數據

private void setupImageReader() {
        //2代表ImageReader中最多可以獲取兩幀圖像流
        mImageReader = ImageReader.newInstance(mPreviewSize.getWidth(), mPreviewSize.getHeight(),
                ImageFormat.JPEG, 1);
        mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
            @Override
            public void onImageAvailable(ImageReader reader) {
                switch (mState) {
                    case STATE_PREVIEW:
                        //這裏一定要調用reader.acquireNextImage()和img.close方法否則不會一直回掉了
                        Image img = reader.acquireNextImage();
                        if (mIsAddWaterMark) {
                            try {
                                //獲取圖片byte數組
                                Image.Plane[] planes = img.getPlanes();
                                ByteBuffer buffer = planes[0].getBuffer();
                                buffer.rewind();
                                byte[] data = new byte[buffer.capacity()];
                                buffer.get(data);

                                //從byte數組得到Bitmap
                                Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
                                //得到的圖片是我們的預覽圖片的大小進行一個縮放到水印圖片裏面可以完全顯示
                                bitmap = ImageUtil.zoomBitmap(bitmap, mWaterMarkPreview.getWidth(),
                                        mWaterMarkPreview.getHeight());
                                //圖片旋轉 後置旋轉90度,前置旋轉270度
                                bitmap = BitmapUtils.rotateBitmap(bitmap, mCameraId.equals(CAMERA_BACK) ? 90 : 270);
                                //文字水印
                                bitmap = BitmapUtils.drawTextToCenter(mContext, bitmap,
                                        System.currentTimeMillis() + "", 16, Color.RED);
                                // 獲取到畫布
                                Canvas canvas = mWaterMarkPreview.getHolder().lockCanvas();
                                if (canvas == null) return;
                                canvas.drawBitmap(bitmap, 0, 0, new Paint());
                                mWaterMarkPreview.getHolder().unlockCanvasAndPost(canvas);
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }
                        img.close();
                        break;
                    case STATE_CAPTURE:
                        mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage()));
                        break;
                }
            }
        }, mBackgroundHandler);
    }

還是上面的函數進行了拍照和幀數據返回的區分裏面的代碼大致和使用Camera的時候一樣但是有一些坑。

設置ImageReader爲ImageFormat.YUV_420_888的時候照片返回會流暢一些。但是,它要在的到拍照圖片的時候要轉換YUV->NV->JPEG,在經過這一轉換了的圖片會有一些綠色的遮罩在上面,很不好。
問題現象
找不到合適的解決這個圖片格式轉換的問題,於是只能初始化的時候設置爲JPEG的輸出圖片了。

還有一個問題就是換了JPEG格式輸出但是在一些配置較低的手機上面明顯就感覺相機的數據看上去有些卡了,看來上面這個問題我們還是得解決,但現在找不到解決的方案了。

總結

本文Demo代碼

我們對於Camera和Camera2做的Demo都是對API有個一定的瞭解。但是要想實際項目裏面來用,那肯定是足夠的。API的兼容上面都有很大的問題。仔細看了API的使用,然後有一些參考的代碼值得學習。

google官方的一個整合Camera和Camera2的一個相機預覽拍照應用
官方Camera2API使用介紹項目
使用Camera2進行視頻錄製的應用

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