音视频采集

在整个音视频处理的过程中,位于发送端的音视频采集工作无疑是整个音视频链路的开始。在 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。

Android 音视频采集那些事的更多相关文章

  1. 手机Android音视频采集与直播推送,实现单兵、移动监控类应用

    从安卓智能手机.平板,到可穿戴的Android Ware.眼镜.手表.再到Android汽车.智能家居.电视,甚至最近看新闻,日本出的几款机器人都是Android系统的,再把目光放回监控行业,传统监控 ...

  2. Android 音视频开发(七): 音视频录制流程总结

    在前面我们学习和使用了AudioRecord.AudioTrack.Camera.MediaExtractor.MediaMuxer API.MediaCodec. 学习和使用了上述的API之后,相信 ...

  3. Android 音视频开发(一):PCM 格式音频的播放与采集

    什么是 PCM 格式 声音从模拟信号转化为数字信号的技术,经过采样.量化.编码三个过程将模拟信号数字化. 采样 顾名思义,对模拟信号采集样本,该过程是从时间上对信号进行数字化,例如每秒采集 44100 ...

  4. Android 音视频开发学习思路

    Android 音视频开发这块目前的确没有比较系统的教程或者书籍,网上的博客文章也都是比较零散的.只能通过一点点的学习和积累把这块的知识串联积累起来. 初级入门篇: Android 音视频开发(一) ...

  5. Android 音视频开发(一) : 通过三种方式绘制图片

    版权声明:转载请说明出处:http://www.cnblogs.com/renhui/p/7456956.html 在 Android 音视频开发学习思路 里面,我们写到了,想要逐步入门音视频开发,就 ...

  6. Android音视频开发(1):H264 基本原理

    前言 H264 视频压缩算法现在无疑是所有视频压缩技术中使用最广泛,最流行的.随着 x264/openh264 以及 ffmpeg 等开源库的推出,大多数使用者无需再对H264的细节做过多的研究,这大 ...

  7. android音视频点/直播模块开发

      音视频 版权声明:本文为博主原创文章,未经博主允许不得转载. 前言 随着音视频领域的火热,在很多领域(教育,游戏,娱乐,体育,跑步,餐饮,音乐等)尝试做音视频直播/点播功能,那么作为开发一个小白, ...

  8. Android 音视频开发(六): MediaCodec API 详解

    在学习了Android 音视频的基本的相关知识,并整理了相关的API之后,我们应该对基本的音视频有一定的轮廓了. 下面开始接触一个Android音视频中相当重要的一个API: MediaCodec.通 ...

  9. Android 音视频深入 七 学习之路的总结和资料分享

    说个实话一开始我对基于Android如何开发音视频很迷茫,甚至对音视频开发都不是很明白,我看了Android 音视频开发入门指南 http://blog.51cto.com/ticktick/1956 ...

  10. Android 音视频同步(A/V Sync)

    1.  音视频同步原理 1)时间戳 音视频同步主要用于在音视频流的播放过程中,让同一时刻录制的声音和图像在播放的时候尽可能的在同一个时间输出. 解决音视频同步问题的最佳方案就是时间戳:首先选择一个参考 ...

随机推荐

  1. centos7 七步教你安装搭建 LAMP 服务

    先说说LAMP是什么 LAMP是指一组通常一起使用来运行动态网站或者服务器的自由软件名称首字母缩写: Linux,操作系统 Apache,网页服务器 MariaDB或MySQL,数据库管理系统(或者数 ...

  2. Mardown学习

    Mardown学习 标题: 一级标题:'#'+'空格'+'标题名字'+'回车' 二级标题:'##'+'空格'+'标题名字'+'回车' 三级标题:'###'+'空格'+'标题名字'+'回车' 四级标题: ...

  3. 20192305 王梓全Python程序设计实验三报告

    20192305 王梓全Python程序设计实验三报告 课程:<Python程序设计> 班级: 1923 姓名: 王梓全 学号:20192305 实验教师:王志强 实验日期:2021年5月 ...

  4. 学习&资源分享

    零 方法 1 学校图书馆官网(注意查看学校购买的数据库以及校外访问方法) 2 各省市图书馆官网(注册各地图书馆账号,注册方法见视频01:28)   一 论文 1 知网(图书馆进入) 2 Web of ...

  5. 2020年第11届蓝桥杯C/C++B组 第二轮省赛

    # JJU-干干 试题A :门牌制作[问题描述]小蓝要为一条街的住户制作门牌号.这条街一共有 2020 位住户,门牌号从 1 到 2020 编号.小蓝制作门牌的方法是先制作 0 到 9 这几个数字字符 ...

  6. ES6知识点总结

    声明变量      let 不能重复声明 块级作用域 可修改let变量的值 , const 不能重复声明 块级作用域 不能修改const 变量的值 2. 箭头函数 而箭头函数的this指向函数定义时所 ...

  7. 尝试改善科研V2

    参考链接: https://fulequn.github.io/2022/09/26/Article202209261/ https://www.xljsci.com/ https://apps.an ...

  8. Python+Django(1)——建立项目

    为项目新建一个目录,将其命名为learning_log,再在终端中切换到这个目录(Python 3): 运行模块venv 来创建一个名为ll_env的虚拟环境:python -m venv ll_en ...

  9. LOJ数列分块入门九题(中)

    #6281. 数列分块入门 5 - 题目 - LibreOJ (loj.ac) 区间开方,区间求和题. 显然,针对区间维护开方操作很难做到,于是考虑其值的性质,显然,int范围内的值最多开方6次就会变 ...

  10. Linux_MySQL

    MySQL 安装 AB复制 安装 1.编译安装 2.yum安装 [https://www.mysql.com/] yum安装的方式 1.在官网下载mysql rpm包 # wget https://d ...