上一章《Camera2 开关相机》我们学习了如何开启和关闭相机,接下来我们来学习如何开启预览。
阅读完本章,你将会学到以下几个知识点:
1.如何配置预览尺寸
2.如何创建 CameraCaptureSession
3.如何创建 CaptureRequest
4.如何开启和关闭预览
5.如何适配预览画面的比例
6.如何使用 ImageReader 获取预览数据
7.设备方向的概念
8.局部坐标系的概念
9.显示方向的概念
10.摄像头传感器方向的概念
11.如何矫正图像数据的方向
1 获取预览尺寸
在第一章《Camera2 概览》我们提到了 CameraCharacteristics 是一个只读的相机信息提供者,其内部携带大量的相机信息,包括代表相机朝向的 LENS_FACING;判断闪光灯是否可用的 FLASH_INFO_AVAILABLE;获取所有可用 AE 模式的 CONTROL_AE_AVAILABLE_MODES 等等。如果你对 Camera1 比较熟悉,那么 CameraCharacteristics 有点像 Camera1 的 Camera.CameraInfo 或者 Camera.Parameters。CameraCharacteristics 以键值对的方式提供相机信息,你可以通过 CameraCharacteristics.get() 方法获取相机信息,该方法要求你传递一个 Key 以确定你要获取哪方面的相机信息,例如下面的代码展示了如何获取摄像头方向信息:
private void getCameraCharacteristics() {
CameraManager cameraManager = (CameraManager)getSystemService(Context.CAMERA_SERVICE);
try {
String[] cameraIdList = cameraManager.getCameraIdList();
CameraCharacteristics characteristics;
for (String cameraId : cameraIdList) {
characteristics = cameraManager.getCameraCharacteristics(cameraId);
int facing = characteristics.get(CameraCharacteristics.LENS_FACING);
if (CameraCharacteristics.LENS_FACING_BACK == facing) {
Log.e(TAG, "后置摄像头");
} else if (CameraCharacteristics.LENS_FACING_FRONT == facing) {
Log.e(TAG, "前置摄像头");
} else if (CameraCharacteristics.LENS_FACING_EXTERNAL == facing) {
Log.e(TAG, "外部摄像头");
}
}
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
CameraCharacteristics 有大量的 Key 定义,这里就不一一阐述,当你在开发过程中需要获取某些相机信息的时候再去查阅 API文档即可。
由于不同厂商对相机的实现都会有差异,所以很多参数在不同的手机上支持的情况也不一样,相机的预览尺寸也是,所以接下来我们就要通过 CameraCharacteristics 获取相机支持的预览尺寸列表。所谓的预览尺寸,指的就是相机把画面输出到手机屏幕上供用户预览的尺寸,通常来说我们希望预览尺寸在不超过手机屏幕分辨率的情况下,越大越好。另外,出于业务需求,我们的相机可能需要支持多种不同的预览比例供用户选择,例如 4:3 和 16:9 的比例。由于不同厂商对相机的实现都会有差异,所以很多参数在不同的手机上支持的情况也不一样,相机的预览尺寸也是。所以在设置相机预览尺寸之前,我们先通过CameraCharacteristics 获取该设备支持的所有预览尺寸:
StreamConfigurationMap configurationMap = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
Size[] sizes = configurationMap.getOutputSizes(SurfaceTexture.class);
从上面的代码可以看出预览尺寸列表并不是直接从 CameraCharacteristics 获取的,而是先通过 SCALER_STREAM_CONFIGURATION_MAP 获取 StreamConfigurationMap 对象,然后通过 StreamConfigurationMap.getOutputSizes() 方法获取尺寸列表,该方法会要求你传递一个 Class 类型,然后根据这个类型返回对应的尺寸列表,如果给定的类型不支持,则返回 null,你可以通过 StreamConfigurationMap.isOutputSupportedFor() 方法判断某一个类型是否被支持,常见的类型有:
- ImageReader:常用来拍照或接收 YUV 数据。
- MediaRecorder:常用来录制视频。
- MediaCodec:常用来录制视频。
- SurfaceHolder:常用来显示预览画面。
- SurfaceTexture:常用来显示预览画面。
由于我们使用的是 SurfaceTexture,所以显然这里我们就要传递 SurfaceTexture.class 获取支持的尺寸列表。如果我们把所有的预览尺寸都打印出来看时,会发现一个比较特别的情况,就是预览尺寸的宽是长边,高是短边,例如 1920x1080,而不是 1080x1920,这是因为相机 Sensor 的宽是长边,而高是短边。
在获取到预览尺寸列表之后,我们要根据自己的实际需求过滤出其中一个最符合要求的尺寸,并且把它设置给相机,在我们的 Demo 里,只有当预览尺寸的比例和大小都满足要求时才能被设置给相机,如下所示:
2 配置预览尺寸
在获取适合的预览尺寸之后,接下来就是配置预览尺寸使其生效了。在配置尺寸方面,Camera2 和 Camera1 有着很大的不同,Camera1 是将所有的尺寸信息都设置给相机,而 Camera2 则是把尺寸信息设置给 Surface,例如接收预览画面的SurfaceTexture,或者是接收拍照图片的 ImageReader,相机在输出图像数据的时候会根据 Surface 配置的 Buffer 大小输出对应尺寸的画面。
获取 Surface 的方式有很多种,可以通过 TextureView、SurfaceView、ImageReader 甚至是通过 OpenGL 创建,这里我们要将预览画面显示在屏幕上,所以我们选择了 TextureView,并且通过 TextureView.SurfaceTextureListener 回调接口监听 SurfaceTexture 的状态,在获取可用的 SurfaceTexture 对象之后通过 SurfaceTexture.setDefaultBufferSize() 设置预览画面的尺寸,最后使用 Surface(SurfaceTexture) 构造方法创建出预览的 Surface 对象。
首先,我们在布局文件中添加一个 TextureView,并给它取个 ID 叫 camera_preview:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextureView
android:id="@+id/camera_preview"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
然后我们在 Activity 里获取 TextureView 对象,并且注册一个TextureView.SurfaceTextureListener 用于监听 SurfaceTexture 的状态:
private TextureView.SurfaceTextureListener textureListener = new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
surfaceTexture.setDefaultBufferSize(width, height);
Surface surface = new Surface(surfaceTexture);
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
return false;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
}
};
TextureView textureView = findViewById(R.id.camera_preview);
textureView.setSurfaceTextureListener(textureListener);
当 SurfaceTexture 可用的时候会回调 onSurfaceTextureAvailable() 方法并且把 SurfaceTexture 对象和尺寸传递给我们,此时我们要做的就是通过 SurfaceTexture.setDefaultBufferSize() 设置预览画面的尺寸并且创建 Surface 对象:
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
surfaceTexture.setDefaultBufferSize(width, height);
Surface surface = new Surface(surfaceTexture);
}
到这里,用于预览的 Surface 就准备好了,接下来我们来看下如何创建 CameraCaptureSession。
3 创建 CameraCaptureSession
用于接收预览画面的 Surface 准备就绪了,接了下来我们要使用这个 Surface 创建一个 CameraCaptureSession 实例,涉及的方法是 CameraDevice.createCaptureSession(),该方法要求你传递以下三个参数:
- outputs:所有用于接收图像数据的 Surface,例如本章用于接收预览画面的 Surface,后续还会有用于拍照的 Surface,这些 Surface 必须在创建 Session 之前就准备好,并且在创建 Session 的时候传递给底层用于配置 Pipeline。
- callback:用于监听 Session 状态的 CameraCaptureSession.StateCallback 对象,就如同开关相机一样,创建和销毁 Session 也需要我们注册一个状态监听器。
- handler:用于执行 CameraCaptureSession.StateCallback 的 Handler 对象,可以是异步线程的 Handler,也可以是主线程的 Handler,在我们的 Demo 里使用的是主线程 Handler。