Android 音视频采集那些事

音视频采集 在整个音视频处理的过程中,位于发送端的音视频采集工作无疑是整个音视频链路的开始。在 Android 或者 IOS 上都有相关的硬件设备——Camera 和麦克风作为输入源。本章我们来分析如何在 Android 上通过 Camera 以及录音设备采集数据。本章可结合之前发布的文章Android 音视频 - MediaCodec 编解码音视频做一个完整的 Demo。
Camera 在 Android 上的图片/视频采集设备无疑就是 Camera 了,在 Android SDK API21 之前的版本只能使用 Camera1 ,在 API 21 之后 Camera1 已经被标记为 Deprecated ,Google 推荐使用 Camera2,下面我们来分别看一下。
Camera1 我们先来看一下 Camera1 体系的部分类图。
Android 音视频采集那些事
文章图片

Camera 类是 Camera1 体系的核心类,该类还有好多内部类,如上图:

Camera.CameraInfo 类表达 Camera 的前后(facing)和旋转(orientation)等 Camera 相关的信息。
Camera.Parameters 类是 Camera 相关的参数设置比如设置预览 Size 以及设置旋转角度等。
Camera 类拥有打开 Camera、设置参数、设置预览等 API,下面我们来看使用 Camera API 打开系统照相机的流程。
Android 音视频采集那些事
文章图片

1.在开启 Camera 之前先释放 Camera,这一步的目的是重置 Camera 的状态重置 Camera 的 previewCallback 为 null。
调用 Camera 的 release 释放
把 Camera 对象设置为 null
/** *释放Camera */ private fun releaseCamera() { //重置previewCallback为空 cameraInstance!!.setPreviewCallback(null) cameraInstance!!.release() cameraInstance = null }

2.获取 Camera 的 Id
/** *获取Camera Id */ private fun getCurrentCameraId(): Int { val cameraInfo = Camera.CameraInfo() //遍历所有的Camera id,比较CameraInfo facing for (id in 0 until Camera.getNumberOfCameras()) { Camera.getCameraInfo(id, cameraInfo) if (cameraInfo.facing == cameraFacing) { return id } } return 0 }

3.打开 Camera 获取 Camera 对象
/** *获取Camera 实例 */ private fun getCameraInstance(id: Int): Camera { return try { //调用Camera的open函数获取Camera的实例 Camera.open(id) } catch (e: Exception) { throw IllegalAccessError("Camera not found") } }

【Android 音视频采集那些事】4.设置 Camera 的相关参数
//[3]设置参数 val parameters = cameraInstance!!.parametersif (parameters.supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) { parameters.focusMode = Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE } cameraInstance!!.parameters = parameters

5.设置 previewDisplay
//【4】调用Camera API 设置预览Surface surfaceHolder?.let { cameraInstance!!.setPreviewDisplay(it) }

6.设置预览回调
//【5】 调用Camera API设置预览回调 cameraInstance!!.setPreviewCallback { data, camera -> if (data =https://www.it610.com/article/= null || camera == null) { return@setPreviewCallback } val size = camera.parameters.previewSize onPreviewFrame?.invoke(data, size.width, size.height) }

7.开启预览
//【6】 调用Camera API开启预览 cameraInstance!!.startPreview()

上面代码中的【3】【4】【5】【6】都是调用 Camera 类的 API 来完成,
经过上面的流程之后,Camera 的预览会显示在传入的 Surface 上,并且在 Camera 停止前会一直回调函数onPreviewFrame(byte[] data,Camera camera),其中 byte[] data 中存储的就是实时的 YUV 图像数据。byte[] data 的格式是 YUV 格式中的 NV21
YUV 图像格式 色彩空间
这里我们只讲常用到的两种色彩空间。
RGBRGB 的颜色模式应该是我们最熟悉的一种,在现在的电子设备中应用广泛。通过 R G B 三种基础色,可以混合出所有的颜色。
YUV 这里着重讲一下 YUV,这种色彩空间并不是我们熟悉的。这是一种亮度与色度分离的色彩格式。
早期的电视都是黑白的,即只有亮度值,即 Y。有了彩色电视以后,加入了 UV 两种色度,形成现在的 YUV,也叫 YCbCr。
Y:亮度,就是灰度值。除了表示亮度信号外,还含有较多的绿色通道量。
U:蓝色通道与亮度的差值。
V:红色通道与亮度的差值。
采用 YUV 有什么优势呢?
人眼对亮度敏感,对色度不敏感,因此减少部分 UV 的数据量,人眼却无法感知出来,这样可以通过压缩 UV 的分辨率,在不影响观感的前提下,减小视频的体积。
RGB 和 YUV 的换算
Y = 0.299R + 0.587G + 0.114B
U = -0.147R - 0.289G + 0.436B
V = 0.615R - 0.515G - 0.100B
——————————————————
R = Y + 1.14V
G = Y - 0.39U - 0.58V
B = Y + 2.03U
YUV 格式
YUV 存储方式分为两大类:planar 和 packed。
  • planar:先存储所有 Y,紧接着存储所有 U,最后是 V;
    Android 音视频采集那些事
    文章图片

  • packed:每个像素点的 Y、U、V 连续交叉存储。
    Android 音视频采集那些事
    文章图片

pakced 存储方式已经非常少用,大部分视频都是采用 planar 存储方式。
对于 planar 存储方式,通过省略一些色度信息,即亮度共用一些色度信息,进而节省存储空间。因此,planar 又区分了以下几种格式: YUV444、 YUV422、YUV420。
YUV 4:4:4 采样,每一个 Y 对应一组 UV 分量。
Android 音视频采集那些事
文章图片

YUV 4:2:2 采样,每两个 Y 共用一组 UV 分量。
Android 音视频采集那些事
文章图片

YUV 4:2:0 采样,每四个 Y 共用一组 UV 分量。
Android 音视频采集那些事
文章图片

其中,最常用的就是 YUV420。
YUV420 格式存储方式又分两种类型
  • YUV420P:三平面存储。数据组成为 YYYYYYYYUUVV(如 I420)或 YYYYYYYYVVUU(如 YV12)。
  • YUV420SP:两平面存储。分为两种类型 YYYYYYYYUVUV(如 NV12)或 YYYYYYYYVUVU(如 NV21)
Camera2 在 Andorid SDK API 21 之后呢,Google 就推荐使用 Camera2 体系来管理设备,Camera2 还是与 Camera1 有很大的不同的。一样的,我们先来看一下 Camera2 体系的部分类图
Android 音视频采集那些事
文章图片

Camera2 要比 Camera1 复杂的多,CameraManager CameraCaptureSession 是 Camera2 体系的核心类,CameraManager 用来管理摄像头的打开和关闭 Camera2 引入了 CameraCaptureSession 来管理拍摄会话。
我们下面来看一下更详细的流程图。
Android 音视频采集那些事
文章图片

1.在开启 Camera 之前先释放 Camera,这一步的目的是重置 Camera 的状态。
private fun releaseCamera() { imageReader?.close() cameraInstance?.close() captureSession?.close() imageReader = null cameraInstance = null captureSession = null }

2.获取 Camera 的 Id
/** *【1】 获取Camera Id */ private fun getCameraId(facing: Int): String? { return cameraManager.cameraIdList.find { id -> cameraManager.getCameraCharacteristics(id).get(CameraCharacteristics.LENS_FACING) == facing } }

3.打开 Camera
try { //【2】打开Camera,传入的 CameraDeviceCallback()是摄像机设备状态回调 cameraManager.openCamera(cameraId, CameraDeviceCallback(), null) } catch (e: CameraAccessException) { Log.e(TAG, "Opening camera (ID: $cameraId) failed.") }//设备状态回调 private inner class CameraDeviceCallback : CameraDevice.StateCallback() { override fun onOpened(camera: CameraDevice) { cameraInstance = camera //【3】开启拍摄会话 startCaptureSession() }override fun onDisconnected(camera: CameraDevice) { camera.close() cameraInstance = null }override fun onError(camera: CameraDevice, error: Int) { camera.close() cameraInstance = null } }

4.开启拍摄会话
//【3】开启拍摄会话 private fun startCaptureSession() { val size = chooseOptimalSize() //创建ImageRender并设置回调 imageReader = ImageReader.newInstance(size.width, size.height, ImageFormat.YUV_420_888, 2).apply { setOnImageAvailableListener({ reader -> val image = reader?.acquireNextImage() ?: return@setOnImageAvailableListener onPreviewFrame?.invoke(image.generateNV21Data(), image.width, image.height) image.close() }, null) }try { if (surfaceHolder == null) { //设置ImageRender的surface给cameraInstance,以便后面预览的时候数据呈现到ImageRender的surface,从而触发ImageRender的回调 cameraInstance?.createCaptureSession( listOf(imageReader!!.surface), //【4】CaptureStateCallback是CameraCaptureSession的内部类,是摄像机会话状态的回调 CaptureStateCallback(), null ) } else { cameraInstance?.createCaptureSession( listOf(imageReader!!.surface, surfaceHolder!!.surface), CaptureStateCallback(), null ) }} catch (e: CameraAccessException) { Log.e(TAG, "Failed to start camera session") } }//摄像机会话状态的回调 private inner class CaptureStateCallback : CameraCaptureSession.StateCallback() { override fun onConfigureFailed(session: CameraCaptureSession) { Log.e(TAG, "Failed to configure capture session.") } //摄像机配置完成 override fun onConfigured(session: CameraCaptureSession) { cameraInstance ?: return captureSession = session //设置预览CaptureRequest.Builder val builder = cameraInstance!!.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW) builder.addTarget(imageReader!!.surface) surfaceHolder?.let { builder.addTarget(it.surface) }try { //开启会话 session.setRepeatingRequest(builder.build(), null, null) } catch (e: CameraAccessException) { Log.e(TAG, "Failed to start camera preview because it couldn't access camera", e) } catch (e: IllegalStateException) { Log.e(TAG, "Failed to start camera preview.", e) } } }

PS
ImageRender 可以直接访问呈现在 Surface 上得图像数据,ImageRender 的工作原理是创建实例并设置回调,这个回调会在 ImageRender 所关联的 Surface 上的图像可用时调用
我们分析了上面的 Camera 采集数据,完整的代码请看文末的 Github 地址。
AudioRecord 上面分析完了视频,我们接着来看音频,录音 API 我们使用 AudioRecord,录音的流程相对于视频而言要简单许多,一样的,我们先来看一下简单类图。
Android 音视频采集那些事
文章图片

就一个类,API 也简单明了,我们来看一下流程。
Android 音视频采集那些事
文章图片

下面上代码
public void startRecord() { //开启录音 mAudioRecord.startRecording(); mIsRecording = true; //开启新线程轮询 ExecutorService executorService = Executors.newSingleThreadExecutor(); executorService.execute(new Runnable() { @Override public void run() { byte[] buffer = new byte[DEFAULT_BUFFER_SIZE_IN_BYTES]; while (mIsRecording) { int len = mAudioRecord.read(buffer, 0, DEFAULT_BUFFER_SIZE_IN_BYTES); if (len > 0) { byte[] data = https://www.it610.com/article/new byte[len]; System.arraycopy(buffer, 0, data, 0, len); //处理data } } } }); }public void stopRecord() { mIsRecording = false; mAACMediaCodecEncoder.stopEncoder(); mAudioRecord.stop(); }

AudioRecord 生成的 byte[] data 即 PCM 音频数据。
小结 本章我们对音视频的原生输入 API 进行了详细的介绍,这个也是我们后面博客的基础,有了 YUV 和 PCM 数据之后,就可以编码了,下一篇我们再来分析 MediaCodec,用 MediaCodec 对原生音视频数据进行硬编码生成 Mp4。

    推荐阅读