Android使用CameraX采集视频数据
上篇文章提到 Android 视频数据的采集大致有以下三种并介绍了最简单的第一种方法,
使用系统预装、或者已经安装的相机应用(例如美图等)采集视频数据;
使用 Jetpack CameraX 库采集视频数据;
使用 android 系统提供的 Camera API 开发自己的相机应用。
下面一起学习第二种方法:使用 Jetpack CameraX 库采集视频数据。
Jetpack 是 Google 18年推出的一套库、工具和指南,帮助开发者快速高效开发优质应用,包括满足后台调度需求的 WorkManager, 实现数据存储持久性的 Room, 管理应用导航流程的 Nabigation 等。
CameraX 是谷歌为了帮助开发者快速开发相机类应用, 在19年 I/O 大会上推出的包含在 Jetpack 里的一个库。Android camera platform team 的产品经理 在会上对 CameraX 做了一个简单的介绍,CameraX 之前,要开发自己的相机应用是一项繁琐的工作,开发者需要面对繁琐的 CameraAPI,Android 系统的碎片化和设备的多样化,要想开发出来的应用兼容不同的设备,需要在各种设备上做大量测试。谷歌 工程师说他们在中国的 Google Developer Day(GDD)上听说有些开发团队需要在上百台设别上测试应用,于是谷歌打造了一个 CameraX 自动化测试实验室,可以7x24小时,在不同的设备上测试,根据测试结果,谷歌工程师发现和修复了以下问题,包括:
1. 前置和后置摄像头转换的崩溃;
2. 闪光灯不工作;
3. 优化摄像头预览跟随屏幕旋转,摄像头的关闭等。
截止到目前(2020.8)CameraX 包含了以下几个库:
androidx.camera.camera2
androidx.camera.core
androidx.camera.extensions
androidx.camera.lifecycle
androidx.camera.view
需要注意的是核心库 androidx.camera.core 目前处于测试阶段。Beta 版功能稳定,并且具有功能完整的 API Surface。它们可以投入实际使用,但可能包含错误。
那么使用CameraX 有哪些优点呢?听一听谷歌工程师的介绍:
1. 使用简单,CameraX 基于 Camera2 API 做的开发和封装,跟camera2 API 相比,使用CameraX 可以减少70%的代码量;
例如 camera 360 这款相机应用,使用 CameraX 后,减少了很多特定设备的测试,减少了75%的代码,缩小 APK 包大小。
2. 向后兼容到 AndroidL;
3. 提供一致的开发API, 如果要使用底层 Camera API, 不用考虑选择使用 Camera 还是 Camera2 API;
4. 感知生命周期,不需要开发者自己处理;
5. 不用自己创建线程操作相机。
如果你之前使用 CameraAPI 开发相机应用,能体会到上面的几点绝对是开发者的福音,AndroidDeveloper 提供了比较详细的文档介绍了 CameraX 库,并给出了两个示例工程,一个入门应用,实现基本的拍照功能,另一个CameraXBasic 实现了拍照,存储及存储照片的预览,有这些资料,足够我们对 CameraX 有一个全面的认识。
下面参考这两个示例工程,结合代码学习 CameraX 的使用。
一个相机应用的基本功能大致包括:图像预览,图像拍摄和图像分析,CameraX 把实现这些功能的类封装成了用例 (UseCase) 方便开发者理解和使用,这些用例包括 Preview, ImageAnalysis, ImageCapture, VideoCapture。实现拍照功能,需要用到的用例和类有:
PreviewView
用于预览图像的自定义 View, 继承自FrameLayout,管理 Surface 生命周期以及预览图像的长宽比和方向,内部使用 TextureView 或者 SurfaceView 渲染图像;
Preview
提供在屏幕上显示相机预览流的用例。预览流已经连接到了显示的 Surface, Preview 决定了 Surface 的显示方式,并负责管理 Surface 的生命周期。
ImageCapture
用于拍照的用例,提供 takePicture() 函数拍摄图片,保存图片到内存或者文件并提供图片元数据,相机聚焦以后会以自动模式拍摄照片。ImageCapture 可以通过 Preview 控制对焦和曝光测光区域,当拍摄的照片保存在内存时,通过 ImageProxy 获取和使用图像。
ImageAnalysis
为应用提供 CPU 可访问的图像,并对其进行分析的用例。ImageAnalysis 通过 ImageReader 从相机获取图像,每个图像都通过 ImageProxy 提供给 ImageAnalysis.Analyzer 函数进行数据分析。
ProcessCameraProvider
用于将相机的生命周期绑定到应用程序进程中的任何 LifecycleOwner的单例对象。
下面我们尝试把这几个主要的类串联起来,完成拍照的功能。
首先创建一个Fragment 的子类 CameraXFragment, 在它的布局文件中放置一个 PreviewView 用来预览相机画面:
1 |
|
2 | <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" |
3 | xmlns:app="http://schemas.android.com/apk/res-auto" |
4 | android:id="@+id/camera_container" |
5 | android:background="@android:color/black" |
6 | android:layout_width="match_parent" |
7 | android:layout_height="match_parent"> |
8 | |
9 | <androidx.camera.view.PreviewView |
10 | android:id="@+id/view_finder" |
11 | android:layout_width="match_parent" |
12 | android:layout_height="match_parent" /> |
13 | |
14 | </androidx.constraintlayout.widget.ConstraintLayout> |
接下来要做的是在 CameraXFragment 的生命周期函数里,执行以下一系列操作设置和绑定拍照有关的操作,为了方便理解,这里省略与主流程关系不大的细节。
- 获取屏幕的宽高比 screenAspectRatio 和 屏幕方向 rotation
1 | // Get screen metrics used to setup camera for full screen resolution |
2 | val metrics = DisplayMetrics().also { viewFinder.display.getRealMetrics(it) } |
3 | val screenAspectRatio = aspectRatio(metrics.widthPixels, metrics.heightPixels) |
4 | val rotation = viewFinder.display.rotation |
- 设置 ProcessCameraProvider 和当前使用的摄像头:
1 | val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext()) |
2 | cameraProviderFuture.addListener(Runnable{ |
3 | cameraProvider = cameraProviderFuture.get() |
4 | |
5 | val lensFacing = when{ |
6 | hasBackCamera() -> CameraSelector.LENS_FACING_BACK |
7 | hasFrontCamera() -> CameraSelector.LENS_FACING_FRONT |
8 | else -> throw IllegalStateException("Back and front camera are unavailable") |
9 | } |
10 | }, ContextCompat.getMainExecutor(requireContext() |
- 构建 Preview :
1 | preview = Preview.Builder() |
2 | // We request aspect ratio but no resolution |
3 | .setTargetAspectRatio(screenAspectRatio) |
4 | // Set initial target rotation |
5 | .setTargetRotation(rotation) |
6 | .build() |
- 设置camera 和 preview
1 | private lateinit var viewFinder: PreviewView |
2 | //... |
3 | |
4 | // A variable number of use-cases can be passed here - |
5 | // camera provides access to CameraControl & CameraInfo |
6 | camera = cameraProvider.bindToLifecycle( |
7 | this, cameraSelector, preview, imageCapture, imageAnalyzer |
8 | ) |
9 | |
10 | // Attach the viewfinder's surface provider to preview use case |
11 | preview?.setSurfaceProvider(viewFinder.createSurfaceProvider()) |
- 构建 ImageCapture:
1 | imageCapture = ImageCapture.Builder() |
2 | .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) |
3 | // We request aspect ratio but no resolution to match preview config, but letting |
4 | // CameraX optimize for whatever specific resolution best fits our use cases |
5 | .setTargetAspectRatio(screenAspectRatio) |
6 | // Set initial target rotation, we will have to call this again if rotation changes |
7 | // during the lifecycle of this use case |
8 | .setTargetRotation(rotation) |
9 | .build() |
- 构建 ImageAnalysis,这里简单的分析图像亮度的变化:
1 | imageAnalyzer = ImageAnalysis.Builder() |
2 | // We request aspect ratio but no resolution |
3 | .setTargetAspectRatio(screenAspectRatio) |
4 | // Set initial target rotation, we will have to call this again if rotation changes |
5 | // during the lifecycle of this use case |
6 | .setTargetRotation(rotation) |
7 | .build() |
8 | // The analyzer can then be assigned to the instance |
9 | .also { |
10 | it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma -> |
11 | // Values returned from our analyzer are passed to the attached listener |
12 | // We log image analysis results here - you should do something useful |
13 | // instead! |
14 | Log.d(TAG, "Average luminosity: $luma") |
15 | }) |
16 | } |
处理手机屏幕的旋转
需要获取 DisplayManager,并向其注册一个 DisplayListener,屏幕方向变化后及时更新 imageCapture 的 targetRotation:
1 | private val displayManager by lazy { |
2 | requireContext().getSystemService(Context.DISPLAY_SERVICE) as DisplayManager |
3 | } |
4 | |
5 | /** |
6 | * We need a display listener for orientation changes that do not trigger a configuration |
7 | * change, for example if we choose to override config change in manifest or for 180-degree |
8 | * orientation changes. |
9 | */ |
10 | private val displayListener = object : DisplayManager.DisplayListener { |
11 | override fun onDisplayChanged(displayId: Int) = |
12 | view?.let { |
13 | if (displayId == this@CameraxFragment.displayId) { |
14 | imageCapture?.targetRotation = it.display.rotation |
15 | } |
16 | } ?: Unit |
17 | |
18 | override fun onDisplayAdded(displayId: Int) = Unit |
19 | override fun onDisplayRemoved(displayId: Int) = Unit |
20 | } |
21 | |
22 | override fun onViewCreated(view: View, savedInstanceState: Bundle?){ |
23 | // Every time the orientation of the device changes, update the rotation for use case |
24 | displayManager.registerDisplayListener(displayListener, null) |
25 | //... |
26 | } |
- 响应拍照按钮的点击事件,保存为本地文件:
1 | controllerView.findViewById<ImageButton>(R.id.camera_capture_button).setOnClickListener { |
2 | // Get a stable reference of the modifiable image capture use case |
3 | imageCapture?.let { imageCapture -> |
4 | // Create output file to hold the image |
5 | val photoFile = createFile(outputDirectory, FILENAME, PHOTO_EXTENSION) |
6 | |
7 | // Setup image capture metadata |
8 | val metadata = Metadata().apply { |
9 | // Mirror image when using the front camera |
10 | isReversedHorizontal = lensFacing == CameraSelector.LENS_FACING_FRONT |
11 | } |
12 | |
13 | // Create output options object which contains file + metadata |
14 | val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile) |
15 | .setMetadata(metadata) |
16 | .build() |
17 | |
18 | // Setup image capture listener which is triggered after photo has been taken |
19 | imageCapture.takePicture(outputOptions, cameraExecutor, object : ImageCapture.OnImageSavedCallback { |
20 | override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { |
21 | val savedUri = outputFileResults.savedUri ?: Uri.fromFile(photoFile) |
22 | |
23 | // We can only change the foreground Drawable using API level 23+ API |
24 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { |
25 | // Update the gallery thumbnail with latest picture taken |
26 | setGalleryThumbnail(savedUri) |
27 | } |
28 | } |
29 | |
30 | override fun onError(exception: ImageCaptureException) { |
31 | Log.e(TAG, "Photo capture failed:${exception.message}", exception) |
32 | } |
33 | }) |
34 | } |
35 | } |
到这里就完成了使用 CameraX 拍摄照片的基本功能,要想把项目做的丰富些,还应该添加查看已拍摄照片,删除已拍摄的照片,分享照片到其它应用的功能,这些已经在完整的示例工程中实现。要获取完成的代码细节,请移步官方示例工程 CameraXBasic 或者 StreamingTour。