Android 视频数据采集系列的最后一篇出炉了,和前两篇文章想比,这篇文章从系统API层面进行一些探索,涉及到的细节更多。初次接触 Camera2 API 会觉得它的使用有些繁琐,涉及到的类有些多,不过就像第一次使用Activity, Fragment 的API 一样,只要多加练习,熟练掌握这些 API 只是时间问题。
Andrid 系统最初提供的操控相机的 API android.hardware.camera 现已弃用,新的API android.hardware.camera2 在andrid L 上开始使用,这里只讨论和学习 Camera2 的使用。根据谷歌官方的说法,重新设计 Camera API 的目的在于大幅提高应用对于 Android 设备上的相机子系统的控制能力,同时重新组织 API,提高其效率和可维护性。借助额外的控制能力,开发者可以更轻松地在 Android 设备上构建高品质的相机应用,这些应用可在多种产品上稳定运行,同时仍会尽可能使用设备专用算法来最大限度地提升质量和性能。
关于Camera API 和 Camere API2 的对比,官方介绍 Camera2 的视频里没有详细说明,只是一笔带过,提到了以下两点:
1. Camera API 不支持捕捉未被压缩的画面,或者不支持在新的硬件上运行,预览效率被限定在一到三秒每帧;
2. Camere2 API 拍照的时间间隔更短,支持在多台相机上预览,可以直接加特效或者滤膜。
Camera2 API 把摄像头设备建模为管道,该管道接收一个捕获单个帧的请求做为输入,输出一个捕获结果元数据包,以及该请求的一组输出图像缓冲区。这些请求包含有关帧的捕获和处理的所有配置信息,其中包括分辨率和像素格式,手动传感器、镜头和闪光灯控件,3A 操作模式,RAW 到 YUV 处理控件等。捕获单个帧的请求按顺序处理,并且多个请求可以同时进行,摄像头设备处理数据需要经过多道工序的加工处理,在大多数Android 设备上需要有多个正在运行的请求才能保持完整的帧率。
完整的相机模型可以用下面一张图表示:
如果想全面了解相机模型的细节,这张图比较合适,可是这么多大大小小的框框和没见过的类对于我这样的小白不太友好,我们来看一张精简的图片:
第二张图是相机核心操作的模型图,其中出现的一些类 CameraDevice, CameraRequest等看起来有些陌生,不用着急待会儿给他们做一一介绍。
为了方便理解,我们可以把摄像头设备看作一个工厂车间,捕获一张图片的过程看作车间里一道完整的流水线,把 Camera2 API 相关的类看作车间里的师傅们,这些师傅各司其职,协同工作完成捕获图像的任务。下面我们来看完成这个任务需要的师傅们。
CameraManager,类似于LocationManager、ConnectivityManager ,是一个系统级别的服务管理器,负责枚举、查询和打开可用的相机设备。
CameraDevice,是连接到 Android 设备的单个相机的表示形式,可以对以高帧速率捕获图像和后期处理进行细粒度控制。CameraDevice 描述了硬件设备以及该设备的可用设置和输出参数。这些信息通过 CameraCharacteristics 对象提供。
CameraCharacteristics,用来描述 CameraDevice 的属性,比如支持的 JPEG 缩略图大小等, 这些属性对于给定的 CameraDevice 是固定的,可以通过CameraManager.getCameraCharacteristics 查询。
CameraCaptureSession,是 CameraDevice 捕获图像的会话,用于捕获来自摄像机的图像或重新处理先前在同一会话中捕获的图像。创建 CameraCaptureSession 需要配置摄像头设备的内部管道并分配用于将图像发送到所需目标的内存缓冲区,是一项耗费资源的异步操作,可能需要几百毫秒。
CaptureRequest,是从摄像机设备捕获单个图像所需的一组不变的设置,包含捕获硬件(传感器,镜头,闪光灯),处理管线,控制算法和输出缓冲区的配置。还包含捕获后的图像数据发送的目标 Surface 列表。
CaptureResult,是 CameraDevice 处理 CaptureRequest之后产生的,从图像传感器捕获的单个图像结果的子集。包含捕获硬件(传感器,镜头,闪光灯),处理管线,控制算法和输出缓冲区的最终配置的子集。
Image,是与媒体源(例如MediaCodec或CameraDevice)一起使用的单个完整图像缓冲区。Image 允许通过一个或多个 ByteBuffer 高效的直接访问 Image 中的像素数据。每个缓冲区数据封装在一个描述像素数据的 Plane中,由于这种直接访问的方式,Image不能直接用作UI资源。
Image 通常是由硬件组件直接生成或使用的,是整个系统共享的有限资源,应在不再需要时及时关闭。例如,当使用 ImageReader 类从各种媒体源中读取图像时,一旦达到 ImageReader.getMaxImages 的数量限制,不关闭旧的 Image 对象将阻止新图像的可用性。
ImageReader 人如其名,用来读取 Image 数据,也可以做为 Image 的存储缓冲区,允许应用程序直接访问渲染到 Surface 中的图像数据。CameraDevice 捕获的图像数据被封装在 Image 对象中,Surface 使用 ImageReader读取这些数据。使用 ImageReader 可以同时访问多个Imagge对象,发送到 ImageReader 的图像将排队等待,ImageReader 的工作方式类似于生产者消费者模式,直到之前的图像被访问取走,新的图像才能被存到队列里。由于内存限制,如果 ImageReader 未能以等于生产速率的速率获取和释放图像,则图像源为了尝试把图像渲染到 Surface上,最终将停止发送或者丢掉一些图像。
SurfaceView,老熟人了,音视频开发中出镜率最高的 View 之一, 用于渲染 Camera 设备的预览画面和捕获的图像。
DngCreator,用于将原始像素数据写入 DNG 文件的类。通常与CameraDevice 可用的 ImageFormat.RAW_SENSOR 缓冲区一起使用,或与应用程序生成的 Bayer-type 原始像素数据一起使用。
DNG(Digital Negative)文件格式是 Adobe 公司发表的一种跨平台文件格式,旨在统一数码相机广泛使用的图像文件格式“RAW”。DNG 文件允许在用户定义的颜色空间中定义像素数据和关联的元数据,该元数据允许在后期处理期间将该像素数据转换为标准CIE XYZ颜色空间。
以上就是车间里参与捕获图像的主要的师傅们,等师傅们准备就绪后,就可以开始动工了。从准备到捕获一张图像一共需要经过五个流程:
准备渲染图像的 SurfaceView
整个捕获图像的过程中,SurfaceView 负责相机画面的渲染工作,它内部使用 Surface 来展示这些图像。Surface 的创建是一个异步操作,等 Surface 创建完毕后就可以进行下一步操作。
打开摄像头设备,初始化 CameraDevice
这一步需要参与的类有 CameraManager、CameraId, 以及 CameraHandler。打开摄像头是一个耗时操作,为了不阻塞主线程,需要在新的线程里执行。告诉 CameraManager 要打开的摄像头ID(前置或者后置摄像头)以及执行该操作的handler, CameraManager 会通过接口回调通知你操作的结果,打开摄像头成功,或者错误,或者摄像头不可用。整个异步操作使用 kotlin 协程完成:
/** Opens the camera and returns the opened device (as the result of the suspend coroutine) */@ExperimentalCoroutinesApi@SuppressLint("MissingPermission")private suspend fun openCamera( cameraManager: CameraManager, cameraId: String, cameraHandler: Handler): CameraDevice = suspendCancellableCoroutine { cont -> cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() { override fun onOpened(camera: CameraDevice) = cont.resume(camera) override fun onDisconnected(camera: CameraDevice) { Log.w(TAG, "Camera $cameraId has been disconnected") requireActivity().finish() } override fun onError(camera: CameraDevice, error: Int) { val msg = when (error) { ERROR_CAMERA_DEVICE -> "Fatal device" ERROR_CAMERA_DISABLED -> "Device policy" ERROR_CAMERA_IN_USE -> "Camera in use" ERROR_CAMERA_SERVICE -> "Fatal service" ERROR_MAX_CAMERAS_IN_USE -> "Maximum cameras in use" else -> "Unknown" } val exception = RuntimeException("Camera $cameraId error:($error) $msg") Log.e(TAG, exception.message, exception) if (cont.isActive) cont.resumeWithException(exception) } }, cameraHandler) }
3. 配置 CameraCaptureSession,开启图像预览
摄像头成功打开以后,就可以调用设备的硬件和软件资源,创建图像预览了,这一步需要 CameraDevice、CameraSession、ImageReader 协作完成。ImageReader 被创建作为捕获静态图像的缓存,使用 CameraDevice 创建 CameraSession时,把用于预览图像的 SurfaceView 中的 Surface 和 ImageReader 中的 Surface 作为图像帧数据的接收者。调用 CameraSession.setRepeatingRequest()启动摄像头连续画面预览。
// Initialize an image reader which will be used to capture still photosval size = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! .getOutputSizes(args.pixelFormat).maxBy { it.height * it.width }!!imageReader = ImageReader.newInstance(size.width, size.height, args.pixelFormat, IMAGE_BUFFER_SIZE)// Creates list of surfaces where the camera will output framesval targets = listOf(viewFinder.holder.surface, imageReader.surface)// Start a capture session using our open camera and list of surfaces where frames will gosession = createCaptureSession(camera, targets, cameraHandler)val captureRequest = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply { addTarget(viewFinder.holder.surface)}// Keep sending the capture request as frequently as possible until the// session is torn down or session.stopRepeating() is calledsession.setRepeatingRequest(captureRequest.build(), null, cameraHandler)
到这里,捕获图像前的准备工作已经完成,下面的操作就是等待用户按下捕获图像的按钮,拍摄图像和保存图像结果。
4. 捕获图像
捕获图像的操作由 CameraCaptureSession 完成,它使用保存的 CameraDevice 创建一个 CaptureRequest.Builder 用来设置捕获图像的参数以及展示图像的 Surface, 把 CaptureRequest 和捕获图像后的回调函数 CameraCaptureSession.CaptureCallback 交给 CameraCaptureSession 后,它会通过 CaptureCallback 及时通知外界捕获图像的进度。这里使用 ImageReader 作为捕获图像的缓冲区,捕获完成后,CameraCaptureSession 返回捕获结果 TotalCaptureResult。捕获图像属于IO 密集型操作,同样需要异步实现:
/*** Helper function used to capture a still image using the [CameraDevice.TEMPLATE_STILL_CAPTURE]template.* It performs synchronization between the [CaptureResult] and the [Image] resulting* from the single capture, and outputs a [CombinedCaptureResult] object.*/private suspend fun takePhoto(): CombinedCaptureResult = suspendCoroutine { cont ->// Fulsh any images left in the image reader@Suppress("ControlFlowWithEmptyBody") while (imageReader.acquireNextImage() != null) {} // Start a new image queueval imageQueue = ArrayBlockingQueue(IMAGE_BUFFER_SIZE)imageReader.setOnImageAvailableListener({ reader -> val image = reader.acquireNextImage() Log.d(TAG, "Image available in queue:${image.timestamp}") imageQueue.add(image)}, imageReaderHandler)val captureResult = session.device.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE) .apply { addTarget(imageReader.surface) }session.capture(captureResult.build(), object : CameraCaptureSession.CaptureCallback() { override fun onCaptureStarted( session: CameraCaptureSession, request: CaptureRequest, timestamp: Long, frameNumber: Long ) { super.onCaptureStarted(session, request, timestamp, frameNumber) // Start the animation to inform the user that capture begin viewFinder.post(animationTask) } override fun onCaptureCompleted( session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult ) { // Save capture result and other operations... }, cameraHandler)}
5. 保存图像
拿到捕获的图像后,到了最后一道工序,保存捕获的图像。如果图像的格式是 JPEG 或者 DEPTH_JPEG,直接保存图像的字节流,如果是原始格式 RAW_SENSOR,需要使用 DngCreator 把图像数据保存为跨平台的 DNG 格式,方便以后使用,保存成功以后返回一个 File 文件:
/** Helper function used to save a [CombinedCaptureResult] into a [File] */private suspend fun saveResult(result: CombinedCaptureResult): File = suspendCoroutine { cont -> when (result.format) { // When the format is JPEG or DEPTH JPEG we can simply save the bytes as-is ImageFormat.JPEG, ImageFormat.DEPTH_JPEG -> { val buffer = result.image.planes[0].buffer val bytes = ByteArray(buffer.remaining()).apply { buffer.get(this) } try { val output = createFile(requireContext(), "jpg") FileOutputStream(output).use { it.write(bytes) } cont.resume(output) } catch (exc: IOException) { Log.e(TAG, "Unable to write JPEG image to file", exc) cont.resumeWithException(exc) } } // When the format is RAW we use the DngCreator utility library ImageFormat.RAW_SENSOR -> { val dngCreator = DngCreator(characteristics, result.metadata) try { val output = createFile(requireContext(), "dng") FileOutputStream(output).use { dngCreator.writeImage(it, result.image) } cont.resume(output) } catch (exc: IOException) { Log.e(TAG, "Unable to write DNG image to file", exc) cont.resumeWithException(exc) } } // No other formats are supported by this sample else -> { val exc = RuntimeException("Unknown image format: ${result.image.format}") Log.e(TAG, exc.message, exc) cont.resumeWithException(exc) } } }
到这里就完成了使用 Camera2 API 捕获一张图像的任务,使用 Camera2 API 可以拿到图像的原始数据用作后期各种处理,并且对捕获图像的过程进行更细粒度的控制,获取完成的示例代码请移步 https://github.com/android/camera-sample 或者 https://github.com/Hiwensen/StreamingTour
Android 设备采集视频数据系列也到此结束,三种方法:使用系统已安装的相机应用,使用 Jetpack CameraX 库或者使用 Camera2 API,各有优缺点和不同的适用场景,总有一种能满足你的需求。文中只介绍了基本的捕获图像的用法,至于录制视频和更多功能,后期有更多时间了继续探索。