Camera2录制视频(一):音频的录制及编码

原创文章,未经作者允许不得转载

山黛远,月波长

暮云秋影蘸潇湘

醉魂应逐凌波梦,分付西风此夜凉
https://github.com/JadynAi/MediaLearn/blob/feature-0.9/mediakit
在Android开发方面,音视频占据了不小领域。对于想往这方面了解的小伙伴们,往往不知道从何处下手开始学习。
我接触音视频开发有一段日子,作为自己学习的回顾和补充,也一直在记录一些音视频开发的博客。
对往期博客有兴趣的朋友们可以先了解一二。
MediaCodeC硬编码将图片集编码为视频Mp4文件MediaCodeC编码视频

MediaCodeC将视频完整解码,并存储为图片文件。使用两种不同的方式,硬编码解码视频

MediaCodeC解码视频指定帧硬编码解码指定帧

概述

最近再次回顾所学,觉得还有许多不足。遂决定写几篇小总结,以Android平台录制视频项目为例,整理自己的音视频开发知识。如果有小伙伴想学习音视频开发,但又不知道从何着手,可以模仿博主做一个相关的Demo来学习。
本文的项目地址入口在Camera2Record入口界面,业务功能实现在Camera2Recorder
在这个项目中,我会尽可能地将音视频涉及到的功能模块化,减少耦合性。让这些零散的功能尽可能的适应更多的业务场景。

API

项目中视频方面采用的技术逻辑为:

使用Camera2API,配合MediaCodeC + Surface + OpenGL将原始帧数据编码为H264码流

音频方面采用技术逻辑为:

AudioRecord录音,MediaCodeC将PCM数据编码为AAC数据

音视频编码使用的是MediaMuxer,将视频帧数据和音频帧数据封装为MP4文件。整体而言涉及到的API有:

  • MediaCodeC
  • AudioRecord
  • MediaMuxer
  • OpenGL(不用详细了解)

架构设计【注1】

作为一个简单的音视频录制应用,并没有什么花哨的功能(暂时没有,以后会慢慢追加)。整体业务逻辑就是直截了当的录制视频 ——> 产出视频。业务再细分的话,主要有三个部分:一是画面,即视频部分;二是声音,即音频部分;三是混合器,即将视频和音频混合,并生成视频文件。
将业务略作区分后,我们由结果向前反推,既然要生成MP4文件,那么需要提供一些什么数据呢?所以我们根据输出——即混合器部分,梳理各个模块的详细功能。

视频封装

混合器模块,使用了Android提供的MediaMuxer作为视频封装输出工具。MediaMuxer支持三种输出格式,分别为MP4、Webm和3GP文件,本次项目的混合器输出自然选择的是MP4文件。

MP4是MPEG-4的官方容器格式定义的广义文件扩展名,可以流媒体化并支持众多多媒体的内容:多音轨、视频流、字幕、图片、可变帧率、码率【注2】。
在制作MP4文件时,应该优先选用MPEG-4标准下的视频/音频格式,一般来说,对于MP4容器的封装,相对而言比较常见的有两种编码方式:

  • H264视频编码,AAC音频编码
  • Xvid视频编码,MP4音频编码

视频编码算法

在本项目中,博主采用的视频编码算法为H264。H264作为压缩率最高的视频压缩格式,与其他编码格式相比,同等画面质量,体积最小。它有两个名称,一个是沿用ITU_T组织的H.26x名称——H.264;另一个是MPEG-4AVC,AVC即为高级视频编码,而MP4格式则是H264编码制定使用的标准封装格式【注3】。

音频编码算法

博主采用的音频编码算法为AAC。AAC可以同时支持48个音轨,15个低频音轨,相比MP3,AAC可以在体积缩小30%的前提下提供更好的音质【注4】。
AAC最初是基于MPEG-2的音频编码技术,后来MPEG_4标准出台,AAC重新集成了其他技术,变更为现在的MPEG-4 AAC标准。一般而言,目前常用的AAC编码指代的就是MPEG-4 AAC。
MPEG-4 AAC有六种子规格:

  • MPEG-4 AAC LC 低复杂度规格(Low Complexity)—现在的手机比较常见的MP4文件中的音频部份就包括了该规格音频文件
  • MPEG-4 AAC Main 主规格 注:包含了除增益控制之外的全部功能,其音质最好
  • MPEG-4 AAC SSR 可变采样率规格(Scaleable SampleRate)
  • MPEG-4 AAC LTP 长时期预测规格(Long TermPredicition)
  • MPEG-4 AAC LD 低延迟规格(Low Delay)
  • MPEG-4 AAC HE高效率规格(HighEfficiency)—这种规格用于低码率编码,有NeroACC 编码器支持

目前最流行的就是LC和HE了。需要注意的是MPEG-4 AAC LC这种规格为“低复杂度规格”,一般应用于中等码率。而中等码率,一般指96kbps~192kbps,所以如果使用了LC编码,请将码率控制在这个范围内会比较好一点。

工作流程

将业务逻辑梳理清楚之后,那么各个模块更具体的功能就清晰了很多。这里有一个大致的工作流程图以作参考:
先从视频模块开始,VideoRecorder运行在一个独立的工作线程,使用OpenGL+Surface+MediaCodeC对接Camera2,接受相机回调画面并编码为H264码流。这个类对外回调可用的视频帧数据VideoPacket对象。这个数据类型是工程中自行定义的对象,封装了这一帧视频的数据——ByteArray类型,以及这一帧数据携带的信息——BufferInfo:主要是这一帧的时间戳以及其他
接下来是音频模块,考虑到录音模块或许日后有机会复用,所以将录音模块单独分离出来。AudioRecorder在开始录制后不停运行,对外回调PCM原始数据——ByteArray类型。AudioRecord类可以对外提供两种类型,ShortArray和ByteArray,因为视频对外的数据类型为ByteArray,所以这里也选择了ByteArray。这一段PCM数据会被添加到一个外部的链表中,而AudioEncoder音频编码模块,也持有PCM数据链表。在开始录制后,AudioEncoder不断循环地从PCM链表中提取数据,编码为AAC格式的原始帧数据。这里的AAC原始数据,指的是没有添加ADTS头信息的数据
与此同时,视频模块输出的视频帧数据和音频模块输出的AAC音频帧数据,会被提交到Mux模块中,在这个模块中,持有两个视频帧数据和音频帧数据的链表。Mux模块会不断循环地从这两个链表中提取数据,使用MediaMuxer将帧数据封装到各自的轨上,最终输出MP4文件。

音频录制及编码

音频模块分为录音以及编码两个小模块,分别运行在两个独立的工作线程。录音模块不用多提,完全是基于AudioRecord的二次封装,这里是代码地址AudioRecorder
这里主要说一下音频编码模块AudioEncoder,音频录制模块在运行后拿到可用PCM数据并回调到外部,封装到一个线程安全的链表中。而AudioEncoder则会不停地从链表中提取数据,再使用MediaCodeC将PCM数据编码为AAC格式的音频帧数据。由于MediaMuxer封装AAC音频轨,并不需要ADTS头信息,所以AudioEncoder得到的AAC原始帧数据也无须再作二次处理了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
 var presentationTimeUs = 0L
val bufferInfo = MediaCodec.BufferInfo()
// 循环的拿取PCM数据,编码为AAC数据。
while (isRecording.isNotEmpty() || pcmDataQueue.isNotEmpty()) {
val bytes = pcmDataQueue.popSafe()
bytes?.apply {
val (id, inputBuffer) = codec.dequeueValidInputBuffer(1000)
inputBuffer?.let {
totalBytes += size
it.clear()
it.put(this)
it.limit(size)
// 当输入数据全部处理完,需要向Codec发送end——stream的Flag
codec.queueInputBuffer(id, 0, size
, presentationTimeUs,
if (isEmpty()) MediaCodec.BUFFER_FLAG_END_OF_STREAM else 0)
// 1000000L/ 总数据 / audio channel / sampleRate
presentationTimeUs = 1000000L * (totalBytes / 2) / format.sampleRate
}
}

loopOut@ while (true) {
// 获取可用的输出缓存队列
val outputBufferId = dequeueOutputBuffer(bufferInfo, defTimeOut)
if (outputBufferId == MediaCodec.INFO_TRY_AGAIN_LATER) {
break@loopOut
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// audio format changed
} else if (outputBufferId >= 0) {
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
break@loopOut
}
val outputBuffer = codec.getOutputBuffer(it)
if (bufferInfo.size > 0) {
frameCount++
dataCallback.invoke(outputBuffer, bufferInfo)
}
codec.releaseOutputBuffer(it, false)
}
}
}

这里的工作流程是这样的:只有PCM链表中有数据,MediaCodeC就会将这些数据填入到可用的输入队列中。每一段PCM的数据长度并不一定是一帧音频数据所对应的长度,所以工程要做的是,不停地想编码器输入数据,而编码器也需要不停地往外输出数据,直至将编码器内部的输入数据编码完毕。
还有一个需要注意的点,就是MediaCodec当输入数据全部填充完毕时,需要发送一个==BUFFER_FLAG_END_OF_STREAM==标示,用来标示数据输入END。如果没有发送这个标示的话,那么编码完后的音频数据会丢失掉最后一小段时间的音频。
除此之外,还有一个很重要的点,就是AAC编码的时间戳计算问题,相关部分的知识请阅读博主之前的博客解决AAC编码时间戳问题

未完待续

由于篇幅有限,这篇文章只分享了音频的编码,在下一篇文章里博主会分享视频的录制和编码~~
以上

相关文章

Camera2录制视频(二):MediaCodeC+OpenGL视频编码