Android音视频开发系列-Camera1和Camera2开发小结

752 阅读7分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第21天,点击查看活动详情

前言

Android开发中相机调用有两种方式:一种通过Intent调起系统相机拍照,另一种通过CameraAPI定制相机拍照。两者都能实现业务开发中拍照需求,两者不同点在于第二种方式有更高相机自定义功能场景需求,例如添加滤镜、人脸标签、裁剪照片尺寸等(虽然目前系统相机很多也已经支持这类功能)。正好最近在做相机开发工作总结相机开发中一些相关知识。

Camera1

在Android8.0之前的CameraAPI调用的是android.hardware.Camera。 在谷歌官方文档可以找到旧版Android Camera架构图。Camera API v1的版本Java层很简单只有Camera类,添加录像功能MediaRecorder就两个类。

image.png

API调用

Camera1的接口调用非常简单。大致分为以下四个步骤:

  1. 第一步打开相机 当然在开启相机之前需要确认一下相机权限,依据需要前后摄像头打开相机。
///相机分前置摄像头和后置摄像头:CameraInfo.CAMERA_FACING_BACK、CameraInfo.CAMERA_FACING_FRONT。
Camera mCamera = Camera.open(CameraInfo.CAMERA_FACING_BACK); //打开摄像头
  1. 配置相机基本参数 Camera.Parameters是获取对应相机参数信息的入口。可以通过它获取到对应相机的一些基本参数:是否支持某些设置以及预览尺寸大小、图像输出尺寸大小等参数信息(前后摄像头获取的参数信息会略有不同)。比如通过Camera.Parameters.getSupportedXXX判断是否支持某个相机特性,通过 Camera.Parameters.set()设置相机某个参数。通过Camera.setParameters把设置后所有参数应用到底层。
Camera.Parameters parameters = mCamera.getParameters(); // 获取相机配置参数
...省略配置参数过程。
mCamera.setParameters(parameters);
  1. 设置窗口预览 窗口预览可设置TextureView、SurfaceView或者是自建SurfaceTexture通过OpenGL渲染。
mCamera.setPreviewTexture(mOutputTexture);
mCamera.setPreviewDisplay();//或调用该方法添加预览窗口 两者实现最终目标一致。
mCamera.setPreviewCallback(this); //获取每帧画面预览data数据。
  1. 开启/关闭预览
mCamera.startPreview();//开启预览
mCamera.stopPreview();//关闭预览

Camera1获取Camera实例必须在open之后才能获取,因此相机配置信息也只能在确认开启相机之后才能获取到。

Camera2

在Android8.0之后官方推出Camera2,废弃使用Camera1,相机调用架包调整到android.hardware.camera2。从官方架构图中可以看到Camera2分成CameraDevice和CameraManager两部分。 image.png

API调用

  1. 获取相机配置信息 Camera2以系统服务形式获取CameraManager对象,遍历getCameraIdList获取每个摄像头信息。 CameraCharacteristics是摄像头相机参数信息获取:预览尺寸大小、输出图片尺寸大小、是否支持闪光灯等。
CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
     try {
         for (String cameraId : manager.getCameraIdList()) {
           CameraCharacteristics characteristics
                        = manager.getCameraCharacteristics(cameraId);
             //.... 省略配置获取
             mCameraId = cameraId;
             return;
         }
     } catch (CameraAccessException e) {
        e.printStackTrace();
     } catch (NullPointerException e) {
        e.printStackTrace();
     }
  1. 打开相机 选取需要的CameraId之后调用openCamera方法打开相机,该方法需要创建异步线程Handler和StateCallback回调接口:相机工作在独立线程中,相机状态通过StateCallback返回。
private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {

        @Override
        public void onOpened(@NonNull CameraDevice cameraDevice) {
            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;
        }
    };
mBackgroundThread = new HandlerThread("CameraBackground");
mBackgroundThread.start();
mBackgroundHandler = new Handler(mBackgroundThread.getLooper());   
CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
manager.openCamera(mCameraId, mStateCallback, mBackgroundHandler);
 
  1. 预览 打开相机之后在StateCallback回调的onOpened中创建mCameraDevice.createCaptureSession预览会话。在创建回调中onConfigured方法获取会话实例设置预览请求实现窗口预览。
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);

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

CameraManager

CameraManager是获取Camera2相关功能的系统服务,关键功能:

  1. 获取CameraId
  2. 获取CameraCharacteristics
  3. 开启相机。

CameraCharacteristics

CameraCharacteristics是相机信息提供对象,可以对标Camera1中Camera.CameraInfo和 Camera.Parameters。区别在CameraCharacteristics只读不写,Camera2的参数配置不在该对象中设置。

CameraDevice

CameraDevice则是CameraManager打开相机后获取的当前相机实例,它有以下关键功能:

  1. 通过createCaptureSession创建CameraCaptureSession实例。
  2. 通过createCaptureRequest创建CaptureRequest实例。

CameraCaptureSession

CameraCaptureSession就是相机会话,创建会话后就可以执行拍照、录制、设置闪光灯、对焦设置、显示预览画面等功能。

CaptureRequest

CaptureRequest 是向 CameraCaptureSession 提交 Capture 请求时的信息载体。每一个 CaptureRequest 表示一帧画面的操作,可以精准控制相机输出每一帧内容。

高级特性

Camera2比起Camera1来讲API调用确实复杂繁琐的多,但比起Camera1来讲Camera2可以做自定义功能更多。

  • 相机信息可以在相机开启前获取:Camera2将相机信息剥离到CameraCharacteristics实例,相比Camera1不需要在开启相机即可获取相机信息。
  • 在不开启预览的情况下拍照:Camera2不强制要求你必须先开启预览才能拍照,满足更多实际业务需求。
  • 一次拍摄多张不同格式和尺寸的图片:Camera2支持一次拍摄多张照片(OpenGL方式除外),因为Camera2架构机制情况可以输入多个Surface,图像结果则是从Surface中获取。
  • 控制曝光时间:Camera2可控制曝光时间,实现拍摄长曝光图片。在光线较差环境也能拍摄出一定亮度的照片。
  • 灵活的 3A 控制:3A(AF、AE、AWB)的控制在 Camera2 上得到了最大化的放权。

相机开发常见问题

预览方向

由于摄像头传感器特殊性,默认情况下相机成像可能与屏幕成 90° 夹角,导致最终屏幕预览时画面方向不正确。

  • 情况1 设备竖屏放置,屏幕角度为0°,传感器角度为90°,画面成像内容和设备成90°垂直显示。这时就需要将摄像头数据采集方向顺时针旋转90°才能得到正确的成像结果。
  • 情况2 设备横屏放置,屏幕角度为90°,传感器角度为90°,画面成像内容和设备成180°颠倒显示。这时就需要将摄像头数据采集方向顺时针旋转180°才能得到正确的成像结果。

可以通过以下代码得出屏幕成像与传感器之间的夹角大小,将计算得到到最终结果赋值给Camera来旋转相机传感器输出正确角度数据。

private int calculateCameraPreviewOrientation() {
        Camera.CameraInfo info = new Camera.CameraInfo();
        Camera.getCameraInfo(CameraParam.getInstance().cameraId, info);
        // 屏幕角度
        int rotation = 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;
        } else {
            result = (info.orientation - degrees + 360) % 360;
        }
        return result;
    }

预览/输出尺寸

通常相机输出成像用到TextureView或是SurfaceView。每个设备配置的相机传感器不同会导致所支持的输出预览尺寸也会有所不同。但屏幕尺寸比例是3:4,相机最终预览尺寸输出尺寸比例是1:1时,最终在屏幕成像上肯定会有明显拉伸情况。

比如需要3:4尺寸比,一般解决方案先遍历相机支持的所有预览尺寸比并找出合适的3:4尺寸。相机具备该尺寸后设置该尺寸预览,然后通过View的onMeasure方法重新计算成像的TextureView或SurfaceView尺寸大小。

parameters.getSupportedPreviewSizes();//支持的预览尺寸集合
parameters.getSupportedPictureSizes();//支持的拍摄尺寸集合

总结

在此之前有对CameraX开箱即用做过介绍,目前Android官方也推荐开发者使用CameraX来实现相机功能。但因为一直处于beta未实现正式版本,所以在正式环境中暂时还是使用旧版Camera(主要当前需求够用未做升级)。

参考