Android音视频学习——Camera2官方demo解析(2)

本篇主要就几个关键的类进行解释,并且对需要注意的点注释,此外再总结一下如何使用Camera2进行拍照和预览的流程。附上官方demo。

Android音视频学习——Camera2官方demo解析(2)_第1张图片

上面是Camera2的流程示意图,由于我喜欢从整体思路上分析代码,所以下面先就整个呆萌的思路拓展一下。

首先肯定是解决相机的问题啦,毕竟是主角嘛,但是相机是底层的,Android为我们抽象了CameraDevice类,用来表示各个相机。我们又知道,设备的相机通常有好几个,前后的特点都不一样,比如镜像问题,单单一个CameraDevice无法表示全。这时候可能有人会想把CameraDevice做成抽象类,每个相机作为子类各自配置自己的属性,反正打开相机的操作不依赖相机本身,只需要相机的标识就行,打开相机以后再将该相机的实例对象返回给调用者就ok了,没错,呆萌也是这样想的:

@Override
public void onOpened(@NonNull CameraDevice cameraDevice) {
    mCameraDevice = cameraDevice;
}

相机设备的问题解决了,但是谁来拍照啊,打开相机不可能让CameraDevice自己打开自己吧,所以需要一个CameraManager来管理相机的开启,相机是系统级别的,所以返回的Manager都是一样的也无妨,就将其上升到服务的层次,只要有上下文就能获取,通过下面的方法获取并进行拍照。

CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
manager.openCamera(mCameraId, mStateCallback, mBackgroundHandler);

呆萌又想,如果都交给CameraManager来管理,它还是要管理好多东西哦,比如相机的取景方向,这是个大问题,见解析(1),为了满足单一职责原则和开闭原则,不如将相机设备的属性封装成一个类,这样CameraDevice只需要专注于创建对话Session就行了, 于是CameraCharacteristics应运而生,其封装了指定ID的相机中的各种属性,例如想要获取指定ID相机的取景方向:

CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);
mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);

我们知道,相机设备是嵌入在Android设备当中的,两者之间的通信需要底层的知识,但是我们不需要知道,所以Android为我们提供了CameraCaptureSession这个上层类,翻译为相机捕捉的对话,显然这个对话为我们预览和拍摄做了封装,其作用相当于图中的pipeline

打开相机后就可以通过管道进行配置了,而相机的各种配置很多,用户的需求也是不一样的,如果在对话管道CameraCaptureSession中封装了对相机的配置,显然很臃肿,所以此处抽象出来一个CaptureRequest类,并且对其的配置使用了Builder模式,这个模式适合的场景就是Director主动去配置对象的属性,提供了默认配置,而不需要在构造函数中将属性一次性配置完成。建议如果代码中需要配置较多的属性,并且这些属性都有默认值的话,使用Builder模式更佳。呆萌是这样体现的:

//创建Builder
mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
//设置持续的自动对焦
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
//创建CaptureRequest
mPreviewRequest = mPreviewRequestBuilder.build();

配置完成后就可以进行预览和拍照了,返回的结果被封装成为CameraMetadata,这是一个控制相机和保存相机信息的基类,它描述了查询信息,拍照结果等返回的key/value组成的map,有个很重要的问题是返回的结果以什么形式呈现,我们首先想到的肯定是以方法的返回值返回,但是这里涉及到何时拍照完成的问题,以及拍照中对焦距、闪光灯等配置的调节,都需要立即呈现给Android系统,显然观察者模式是不错的选择,通过设置回调来立即接受到配置的变化以及完成与否,呆萌是这样做的:

/* process(CaptureResult)是针对不同的结果和当前的状态对相机进行设置和操纵的方法 */

private CameraCaptureSession.CaptureCallback mCaptureCallback = new CameraCaptureSession.CaptureCallback() { 
    @Override
    public void onCaptureProgressed(@NonNull CameraCaptureSession session,
                                    @NonNull CaptureRequest request,
                                    @NonNull CaptureResult partialResult) {
        process(partialResult);
    }

    @Override
    public void onCaptureCompleted(@NonNull CameraCaptureSession session,
                                   @NonNull CaptureRequest request,
                                   @NonNull TotalCaptureResult result) {
        process(result);
    }
}

有了上面这几个概念,整个流程也就差不多走通了,下面梳理一下:

  1. 首先获取CameraManager,调用其getCameraIdList()获取所有的CameraId,从中选择需要的,根据ID找到对应的CameraDevice
  2. 获取到该相机的CameraCharacteristics,获取各属性(相机取景方向,可支持的输出图像大小),根据属性配置显示的大小,并根据当前屏幕旋转位置和相机位置调整预览和拍照的方向
  3. 创建后台线程和Handler对象,提供给Camera用于后台操作
  4. 创建CameraDevice.StateCallback对象,在开启和关闭的回调中启动和释放资源,并创建预览
  5. 开启相机前动态检查相机权限,获得权限后通过manageropen方法开启相机
  6. 创建CameraCaptureSession.CaptureCallback对象,在回调中根据返回结果进行相应操作
  7. 利用CameraDevice创建CaptureRequest.builder对象进行配置,并创建Session对话,在onConfigured的回调中启动预览
  8. 拍照时与⑦基本相同,只是request的目标Surface改变,并且调用的是Sessioncapture()方法

以上就是用Camera2API拍照的全部流程,其中有不少细节需要注意,在下面将一一注释,如果真的需要用到此API,建议直接修改官方呆萌就够了。

是否支持

具体见Android设备对新Camera2 API的支持问题:以华为M2为例,现在很多推出的设备虽然都是Android5.0以上的,也就是支持Camera2的API,但是虽然它们实现了这些API,但是实际上可配置的参数比Camera少,甚至性能都比Camera差,为此厂商在系统中写明了对Camera2的支持程度,通过:

CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);
characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);

可以获得支持的程度,得到的值中有这三种:LIMITED值为0,FULL值为1,LEGACY值为2,其支持程度为FULL > LIMITED > LEGACY。关于LEGACY是这样说的:

A LEGACY device does not support per-frame control, manual sensor control, manual post-processing, arbitrary cropping regions, and has relaxed performance constraints.

翻译一下:遗留的设备不支持每帧控制、手动传感器控制、手动后处理、任意裁剪区域,并且具有宽松的性能约束。可以说兼容性很差了,我在自己手机上验证了下,发现LEGACY,属于遗留机!可以预见很多手机上并不完全支持Camera2API,所以如果手机并不是FULL支持的话,建议还是使用Camera

Semaphore

官方呆萌中使用了Semaphore防止在相机未关闭前就退出了App,因为这样会导致无法正常关闭相机,释放资源,相机服务是系统级别的,App如果一直持有其引用就无法被gc清理,造成泄露,同时也防止并发状态下多个应用频繁启动关闭相机。Semaphore简单来说就是食堂阿姨,初始化的时候给阿姨指定量的卡布奇诺,调用acquire的时候拿走拿走一杯,调用release阿姨就再多放一杯,如果卡布奇诺暂时卖完了,Semaphore值为0,acquire就阻塞,等待release释放一个,还可以设置阻塞时限,如果超时就返回false等等。在呆萌中这样写:

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

@Override
public void onResume() {
    super.onResume();
    startBackgroundThread();
    if (mTextureView.isAvailable()) {
        openCamera(mTextureView.getWidth(), mTextureView.getHeight());
    } else {
        mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
    }
}

private void openCamera(int width, int height){
    if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
        throw new RuntimeException("Time out waiting to lock camera opening.");
    }
    manager.openCamera(mCameraId, mStateCallback, mBackgroundHandler);
}

private void closeCamera(){
    try {
        mCameraOpenCloseLock.acquire();
        ...
    } finally {
        mCameraOpenCloseLock.release();
    }
}

首先我们需要直到onDisconnected方法在退出Activity后并不会被调用,然后我们假设一个场景,第一次打开Activity,此时Semaphore为1,onResume回调,mTextureView不可用,调用setSurfaceTextureListener方法,最终调用openCamera,其中尝试获取许可,获得后Semaphore为0,并打开相机,相机的onOpened回调,release()方法被调用,释放一个许可,Semaphore为1,随后退出App,在OnPause回调中关闭相机,首先调用acquire()获取许可,并调用close()方法关闭相机,注意关闭相机中并未退出Activity,此时如果其他应用调用openCamera方法,会因为Semaphore为0而处于阻塞状态,等到相机关闭后finally语句块调用,释放许可,允许其他应用访问相机。

输出图像大小

在创建CameraCaptureSession的时候需要传入一个回调对象,其中有一个回调方法是这样的:

@Override
public void onConfigureFailed(
        @NonNull CameraCaptureSession cameraCaptureSession) {
    showToast("Failed");
}

很多时候我们会遇到这个回调,特别是没有配置好的时候,这个回调发生的时机在官方文档中是这样的:

This can happen if the set of requested outputs contains unsupported sizes, or too many outputs are requested at once.

不支持的大小?没错,无论是Camera还是Camera2都有着支持的分辨率大小,如果请求输出的Surface包括了不支持的分辨率,就会回调这个方法,并且无法正确打开相机,呆萌中的几个方法都是在设置合适的分辨率大小:

private void setUpCameraOutputs(int width, int height){}

private static Size chooseOptimalSize(Size[] choices, int textureViewWidth, int textureViewHeight, int maxWidth, int maxHeight, Size aspectRatio){}

具体的筛选见呆萌,就是在最大宽高范围内,根据textureView的宽高,分为大的Size和不大的Size,如果有大的,在大的中选最小的,反之在小中选最大的。然后根据屏幕旋转和相机的方向,以及当前屏幕的Orientation进行调整。总之输出的Surface的大小也是很讲究的。

剩下的难点其实就是CaptureResult中各个配置常量的含义,这点太多了,就不一一列举了,大家用到的时候自己去发掘吧。

你可能感兴趣的:(android)