音视频采集

在整个音视频处理的过程中,位于发送端的音视频采集工作无疑是整个音视频链路的开始。在 Android 或者 IOS 上都有相关的硬件设备——Camera 和麦克风作为输入源。本章我们来分析如何在 Android 上通过 Camera 以及录音设备采集数据。本章可结合之前发布的文章Android 音视频 - MediaCodec 编解码音视频做一个完整的 Demo。

Camera

在 Android 上的图片/视频采集设备无疑就是 Camera 了,在 Android SDK API21 之前的版本只能使用 Camera1 ,在 API 21 之后 Camera1 已经被标记为 Deprecated ,Google 推荐使用 Camera2,下面我们来分别看一下。

Camera1

我们先来看一下 Camera1 体系的部分类图。

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

Camera.CameraInfo 类表达 Camera 的前后(facing)和旋转(orientation)等 Camera 相关的信息。

Camera.Parameters 类是 Camera 相关的参数设置比如设置预览 Size 以及设置旋转角度等。

Camera 类拥有打开 Camera、设置参数、设置预览等 API,下面我们来看使用 Camera API 打开系统照相机的流程。

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")      }  }

4.设置 Camera 的相关参数

//[3]设置参数val parameters = cameraInstance!!.parameters        if (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 == 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;

  • packed:每个像素点的 Y、U、V 连续交叉存储。

pakced 存储方式已经非常少用,大部分视频都是采用 planar 存储方式。

对于 planar 存储方式,通过省略一些色度信息,即亮度共用一些色度信息,进而节省存储空间。因此,planar 又区分了以下几种格式: YUV444、 YUV422、YUV420。

YUV 4:4:4 采样,每一个 Y 对应一组 UV 分量。

YUV 4:2:2 采样,每两个 Y 共用一组 UV 分量。

YUV 4:2:0 采样,每四个 Y 共用一组 UV 分量。

其中,最常用的就是 YUV420

YUV420 格式存储方式又分两种类型

  • YUV420P:三平面存储。数据组成为 YYYYYYYYUUVV(如 I420)或 YYYYYYYYVVUU(如 YV12)。
  • YUV420SP:两平面存储。分为两种类型 YYYYYYYYUVUV(如 NV12)或 YYYYYYYYVUVU(如 NV21)

Camera2

在 Andorid SDK API 21 之后呢,Google 就推荐使用 Camera2 体系来管理设备,Camera2 还是与 Camera1 有很大的不同的。一样的,我们先来看一下 Camera2 体系的部分类图

Camera2 要比 Camera1 复杂的多,CameraManager CameraCaptureSession 是 Camera2 体系的核心类,CameraManager 用来管理摄像头的打开和关闭 Camera2 引入了 CameraCaptureSession 来管理拍摄会话。

我们下面来看一下更详细的流程图。

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,录音的流程相对于视频而言要简单许多,一样的,我们先来看一下简单类图。

就一个类,API 也简单明了,我们来看一下流程。

下面上代码

   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 = 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。

更多相关文章

  1. Android各种访问权限Permission详解
  2. AndroidStudio安装SDKComponentSetup无法勾选AndroidSDK选项
  3. Android(安卓)控件Edittext详解
  4. androidManifest.xml之uses-permission(权限-安全控制)
  5. 关于android的tween animation
  6. Android中的通知—Notification
  7. android控件的属性
  8. android 自定义AlertDialog 与Activity相互传递数据
  9. 安卓系统新手入门教程

随机推荐

  1. android中能不能new Activity()对象引发
  2. Android蓝牙开发—经典蓝牙详细开发流程
  3. android 学习笔记一
  4. Android adb shell 命令
  5. Android之SAX解析xml
  6. 总结安卓各大版本(4.0-9.0)的区别、新特
  7. android彻底杀死当前activity和线程方法
  8. emulator:ERROR:Unable to load VM from
  9. Android View相关-View的绘制流程浅析
  10. android之getevent/sendevent/input