一、Camera2实现预览、拍照、录像三大基础功能的流程框架图
Camera2关键几个类:
CameraManager
管理手机上的所有摄像头设备。管理手机上的所有摄像头设备,它的作用主要是获取摄像头列表和打开(openCamera)指定的摄像头。
它其实是一个系统服务,通过getSystemService(Context.CAMERA_SERVICE)获取,它的作用主要是获取摄像头列表和打开指定的摄像头。
通过CameraManager对象可以得到一些相机的基本信息,这个信息就存储在CameraCharacteristic对象中。
除了获取Camera的属性信息外,CameraManager对象最重要的作用就是打开相机(openCamera),通过CameraManager才能真正的拿到CameraDevice对象去操作相机。
CameraDevice
具体的摄像头设备,它有一系列参数(预览尺寸、拍照尺寸等),可以通过CameraManager的getCameraCharacteristics()方法获取。它的作用主要是创建CameraCaptureSession和CaptureRequest。
CameraCaptureSession
相机捕获会话,代表上层与底层之间的一个会话,通过这个会话可以下发指令给相机,让相机执行预览、拍照录像等操作,在mCameraDevice.createCaptureSession的回调中获得。主要作用是用于处理拍照和预览的工作(很重要)。
想从相机设备中获取Image,首先要创建一个CameraCaptureSession把接收数据的载体传给相机设备,目前能接收相机数据的载体是Surface和SurfaceTexture。
通过情况下,相机的预览数据可以使用SurfaceView和TextureView接收。拍照使用ImageReader、录视频使用MediaCodec或MediaRecorder。
绝大部分的相机操作都是通过向 CameraCaptureSession 提交一个 Capture 请求实现的,例如拍照、连拍、设置闪光灯模式、触摸对焦、显示预览画面等等。
CaptureRequest
CaptureRequest代表相机捕获请求,当CaptureCaptureSession创建好之后,就可以使用这个会话来下发指令,表明当前是需要预览、拍照、还是录像。CaptureRequest对象可以携带设置的参数,比如是否自动对焦、自动曝光、自动白平衡等。CaptureRequest对象的创建使用了建造者模式,需要通过CaptureRequest.Builder来创建,通过调用build方法。
CaptureRequest 是向 CameraCaptureSession 提交 Capture 请求时的信息载体,其内部包括了本次 Capture 的参数配置和接收图像数据的 Surface。CaptureRequest 可以配置的信息非常多,包括图像格式、图像分辨率、传感器控制、闪光灯控制、3A 控制等等,可以说绝大部分的相机参数都是通过 CaptureRequest 配置的。值得注意的是每一个 CaptureRequest 表示一帧画面的操作,这意味着你可以精确控制每一帧的 Capture 操作。
CaptureRequest定义了输出缓冲区以及显示界面(TextureView或SurfaceView)等。
CaptureResut
CaptureResut代表捕获请求返回的一些结果信息,从里面可以获取一些Metadata数据信息。
CaptureResult 是每一次 Capture 操作的结果,里面包括了很多状态信息,包括闪光灯状态、对焦状态、时间戳等等。例如你可以在拍照完成的时候,通过 CaptureResult 获取本次拍照时的对焦状态和时间戳。需要注意的是,CaptureResult 并不包含任何图像数据,前面我们在介绍 Surface 的时候说了,图像数据都是从 Surface 获取的。
Surface
Surface 是一块用于填充图像数据的内存空间,例如你可以使用 SurfaceView 的 Surface 接收每一帧预览数据用于显示预览画面,也可以使用 ImageReader 的 Surface 接收 JPEG 或 YUV 数据。每一个 Surface 都可以有自己的尺寸和数据格式,你可以从 CameraCharacteristics 获取某一个数据格式支持的尺寸列表。
二、概念
Capture
相机的所有操作和参数配置最终都是服务于图像捕获,例如对焦是为了让某一个区域的图像更加清晰,调节曝光补偿是为了调节图像的亮度。因此,在 Camera2 里面所有的相机操作和参数配置都被抽象成 Capture(捕获),所以不要简单的把 Capture 直接理解成是拍照,因为 Capture 操作可能仅仅是为了让预览画面更清晰而进行对焦而已。如果你熟悉 Camera1,那你可能会问 setFlashMode() 在哪?setFocusMode() 在哪?takePicture() 在哪?告诉你,它们都是通过 Capture 来实现的。
Capture 从执行方式上又被细分为【单次模式】、【多次模式】和【重复模式】三种,我们来一一解释下:
-
单次模式(One-shot):指的是只执行一次的 Capture 操作,例如设置闪光灯模式、对焦模式和拍一张照片等。多个一次性模式的 Capture 会进入队列按顺序执行。
-
多次模式(Burst):指的是连续多次执行指定的 Capture 操作,该模式和多次执行单次模式的最大区别是连续多次 Capture 期间不允许插入其他任何 Capture 操作,例如连续拍摄 100 张照片,在拍摄这 100 张照片期间任何新的 Capture 请求都会排队等待,直到拍完 100 张照片。多组多次模式的 Capture 会进入队列按顺序执行。
-
重复模式(Repeating):指的是不断重复执行指定的 Capture 操作,当有其他模式的 Capture 提交时会暂停该模式,转而执行其他被模式的 Capture,当其他模式的 Capture 执行完毕后又会自动恢复继续执行该模式的 Capture,例如显示预览画面就是不断 Capture 获取每一帧画面。该模式的 Capture 是全局唯一的,也就是新提交的重复模式 Capture 会覆盖旧的重复模式 Capture。
一些只有 Camera2 才支持的高级特性
如果要我给出强有力的理由解释为什么要使用 Camera2,那么通过 Camera2 提供的高级特性可以构建出更加高质量的相机应用程序应该是最佳理由了。
1)、在开启相机之前检查相机信息
出于某些原因,你可能需要先检查相机信息再决定是否开启相机,例如检查闪光灯是否可用。在 Caemra1 上,你无法在开机相机之前检查详细的相机信息,因为这些信息都是通过一个已经开启的相机实例提供的。在 Camera2 上,我们有了和相机实例完全剥离的 CameraCharacteristics 实例专门提供相机信息,所以我们可以在不开启相机的前提下检查几乎所有的相机信息。
2)、在不开启预览的情况下拍照
在 Camera1 上,开启预览是一个很重要的环节,因为只有在开启预览之后才能进行拍照,因此即使显示预览画面与实际业务需求相违背的时候,你也不得不开启预览。而 Camera2 则不强制要求你必须先开启预览才能拍照。
3)、一次拍摄多张不同格式和尺寸的图片
在 Camera1 上,一次只能拍摄一张图片,更不同谈多张不同格式和尺寸的图片了。而 Camera2 则支持一次拍摄多张图片,甚至是多张格式和尺寸都不同的图片。例如你可以同时拍摄一张 1440x1080 的 JPEG 图片和一张全尺寸的 RAW 图片。
4)、控制曝光时间
在暗环境下拍照的时候,如果能够适当延长曝光时间,就可以让图像画面的亮度得到提高。在 Camera2 上,你可以在规定的曝光时长范围内配置拍照的曝光时间,从而实现拍摄长曝光图片,你甚至可以延长每一帧预览画面的曝光时间让整个预览画面在暗环境下也能保证一定的亮度。而在 Camera1 上你只能 YY 一下。
5)、连拍
连拍 30 张图片这样的功能在 Camera2 出现之前恐怕只有系统相机才能做到了(通过 OpenGL 截取预览画面的做法除外),也可能是出于这个原因,市面上的第三方相机无一例外都不支持连拍。有了 Camera2,你完全可以让你的相机应用程序支持连拍功能,甚至是连续拍 30 张使用不同曝光时间的图片。
6)、灵活的 3A 控制
3A(AF、AE、AWB)的控制在 Camera2 上得到了最大化的放权,应用层可以根据业务需求灵活配置 3A 流程并且实时获取 3A 状态,而 Camera1 在 3A 的控制和监控方面提供的接口则要少了很多。例如你可以在拍照前进行 AE 操作,并且监听本这次拍照是否点亮闪光灯。
一些从 Camera1 迁移到 Camera2 的建议
如果你熟悉 Camera1,并且打算从 Camera1 迁移到 Camera2 的话,希望以下几个建议可以对你起到帮助:
1)、Camera1 严格区分了预览和拍照两个流程,而 Camera2 则把这两个流程都抽象成了 Capture 行为,只不过一个是不断重复的 Capture,一个是一次性的 Capture 而已。
2)、Camera2 的一些 API 调用也会耗时,所以建议你使用独立的线程执行所有的相机操作,尽量避免直接在主线程调用 Camera2 的 API,HandlerThread 是一个不错的选择。
3)、Camera2 所有的相机操作都可以注册相关的回调接口,然后在不同的回调方法里写业务逻辑,这可能会让你的代码因为不够线性而错综复杂,建议你可以尝试使用子线程的阻塞方式来尽可能地保证代码的线性执行。例如在子线程阻塞等待 CaptureResult,然后继续执行后续的操作,而不是将代码拆分到到 CaptureCallback.onCaptureCompleted() 方法里。