音视频开发之Android录制音频

由于FFmpeg一直都没有支持Android平台的音频输入设备API,所以无法使用FFmpeg在Android上录制音频。

Android平台音频录制API

Android上音频输入的API比较繁琐,有多套实现。有Java层的实现AudioRecord,也有native层实现OpenSLES,在 Android O之后又推出了新的native层实现AAudio,并且提供了Oboe库,对OpenSLES和AAudio进行了封装。

下面来详细的介绍下每套API的使用

AudioRecord

AudioRecord是Android平台提供的录制音频的Java层API,使用起来非常简单。通过设置采集参数就可以创建一个AudioRecord对象,调用了start方法后,就可以开始读取数据了,通过read方法将数据读取到指定的缓冲区中,调用stop后就停止采集。

创建AudioRecord
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
// 一个音频帧的字节大小 = 声道数 *(位数 /8)
final int bytesPerFrame = channels * (BITS_PER_SAMPLE / 8);
// BUFFERS_PER_SECOND = 100,预测一秒钟回调100次
// 那么44100的采样率,每次回调应该给的数据帧个数为4410个
final int framesPerBuffer = sampleRate / BUFFERS_PER_SECOND;
// 一次回调给的总数据字节大小 = 一次给的数据帧个数 * 一个数据帧的字节大小
// 创建一个buffer用来接收回调数据,使用堆外内存,在通过JNI传递的时候避免内存拷贝
byteBuffer = ByteBuffer.allocateDirect(bytesPerFrame * framesPerBuffer);

emptyBytes = new byte[byteBuffer.capacity()];
// 避免每次读取数据都需要传递buffer到JNI层,这里提前将buffer的地址保存在JNI层
nativeCacheDirectBufferAddress(byteBuffer, nativeAudioRecord);

final int channelConfig = channelCountToConfiguration(channels);
// 根据音频参数(采样率,声道数,位数)得到系统最小需要的缓冲区大小,创建AudioRecord时设置的缓冲区大小必须大于这个值
int minBufferSize =
AudioRecord.getMinBufferSize(sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT);

// BUFFER_SIZE_FACTOR = 2
// 缓冲区一般要大一点,所以下面比较了两倍的最小缓冲区和我们自己计算的缓冲区大小
// 也就是说设置的缓冲区大小最小也要是最小缓冲区的两倍。
int bufferSizeInBytes = Math.max(BUFFER_SIZE_FACTOR * minBufferSize, byteBuffer.capacity());
try {
// 根据音频参数创建AudioRecord,audioSource是指音频采集的来源
audioRecord = new AudioRecord(audioSource, sampleRate, channelConfig,
AudioFormat.ENCODING_PCM_16BIT, bufferSizeInBytes);
} catch (IllegalArgumentException e) {
return -1;
}
if (audioRecord == null || audioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
return -1;
}

音视频开发之FFmpeg录制视频

视频录制流程

前面讲解录制音频时讲过,FFmpeg对各大平台的输入和输出API进行了统一的封装,这里就不在阐述。

视频的录制相比音频的录制流程基本差不多,主要是去掉了FIFO队列的逻辑。另外对于视频帧来说,FFmpeg提供的SwsContext API处理较慢,一般不太使用,可以用libyuv库,或者使用OpenGL API用GPU来提速。

0c9c218a544d7c258f06aeded22f3197

打开视频输入

注册视频设备

所有的输入和输出设备默认都是没有注册的,如果需要使用,需要手动调用下面的注册函数。

1
avdevice_register_all();
创建输入上下文

FFmpeg将对视频文件的解封装和封装等操作都抽象到AVFormatContext中,所以视频文件的打开和输出都是要通道AVFormatContext来完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Mac平台上依旧使用avfoundation API
AVInputFormat* format = av_find_input_format("avfoundation");
// 视频的采集必须设置采集的分辨率和帧率以及像素格式,如果设置的组合在设备上不支持,会出错
AVDictionary* options = nullptr;
av_dict_set(&options, "video_size", "1280x720", 0);
av_dict_set(&options, "framerate", "30", 0);
av_dict_set(&options, "pixel_format", "nv12", 0);
// 开启输入上下文
if ((error = avformat_open_input(&fmt_ctx_in_, "0", format, &options)) < 0) {
std::cout << "Could not open input device " << av_err2str(error)
<< std::endl;
fmt_ctx_in_ = nullptr;
return error;
}

音视频开发之FFmpeg录制音频

音频录制流程

FFmpeg对各大平台的音频输入和输出的API进行了统一的封装,将输入API封装成AVInputFormat,将输出API封装成AVOutputFormat,并且将音频采集设备封装成一个音频文件进行处理,所以我们在采集音频数据的时候,可以将音频设备当做一个无限大的音频文件,不断的从音频文件中读取数据。

FFmpeg录制音频

打开音频输入

注册音频设备

所有的输入和输出设备默认都是没有注册的,如果需要使用,需要手动调用下面的注册函数。

1
avdevice_register_all();
创建输入上下文

FFmpeg将对音频文件的解封装和封装等操作都抽象到AVFormatContext中,所以音频文件的打开和输出都是要通道AVFormatContext来完成。

1
2
3
4
5
6
7
8
9
10
11
// 设置音频设备API Mac上为avfoundation
AVInputFormat* format = av_find_input_format("avfoundation");
// :0 为音频第一个设备,开启音频设备,FFmpeg中将音频设备封装为一个音频文件
// video:audio 如果视频设备为空,则写成 :音频设备
// 获取到输入上下文
if ((error = avformat_open_input(&fmt_ctx_in_, ":0", format, nullptr)) < 0) {
std::cout << "Could not open input device " << av_err2str(error)
<< std::endl;
fmt_ctx_in_ = nullptr;
return error;
}

音视频开发之OpenGL-ES入门

名词解释

OpenGL

通俗来讲,OpenGL是一个图形库,提供了一系列操作图形图像的API。因为OpenGL只是一个规范,具体的实现一般是由显卡提供。类似的图形库还有Windows平台的DirectX。

OpenGL-ES

OpenGL-ES是专为嵌入式设备准备的一个OpenGL的裁剪版(去除了一些复杂的图元)。OpenGL-ES支持Android、iOS等平台,并且它也是WebGL的基础。

EGL

EGL是OpenGL-ES与本地窗口系统之间的一个中间层API,一般由系统实现。EGL作为OpenGL-ES与设备之间的桥梁,将OpenGL-ES绘制在设备上。

WebGL

WebGL是OpenGL-ES在浏览器上的实现,WebGL1.0基于OpenGL-ES2.0实现,WebGL2.0基于OpenGl-ES3.0实现。

图形渲染管线

图形渲染管线也叫做渲染流水线,指的是将输入的原始图形数据经过渲染管线处理,输出一帧想要的图像的过程。

在OpenGL-ES中,任何事物都在3D空间中,而屏幕和窗口却都是2D的。所以图形渲染管线主要是在将输入的3D坐标转换成2D坐标,再讲2D坐标转换成实际有颜色的像素。(将输入的3D坐标画在3D坐标系中,然后根据视锥范围截取一个2D平面,将截取的平面转换为平面坐标显示)

2D坐标精确的表示一个点在2D空间的位置;而像素是这个点的近似值,像素的显示收到屏幕分辨率的限制

与CPU不同,GPU适合处理的问题都有这样的特征,一个问题可以分解为多个相同的小问题,每个问题之间相互独立不影响,所以GPU拥有大量的处理单元,处理单元数量越多,性能也就越好。而每个单元都可以并行处理,所以GPU的并行能力非常强。

可编程的渲染管线

自OpenGL-ES 2.0开始,采用的都是可编程的渲染管线,也就是开发者可以自定义部分流水线中的着色器,使得可以更细致的控制图形渲染管线。

着色器是运行在GPU上的小程序,这些小程序在图形渲染管线的某个特定部分运行,着色器是一个非常独立的程序,是一种把输入转化为输出的程序,着色器与着色器之间无法通信。

开发者可编程的阶段有两个,一个是顶点着色器,一个是片段着色器。

OpenGL-ES渲染管线

音视频开发之Android硬件编解码

MediaCodec介绍

MediaCodec是Android平台上用来访问编码器和解码器的组件。

工作流程

mediacodec-processes

MediaCodec中维护了两个BufferQueue,一个用来存放输入数据,一个用来存放输出数据。对于编码来说,InputBufferQueue用来接收视频原始YUV数据,经过Codec编码处理后,将编码后的数据放入到OutputBufferQueue中输出。而对于解码来说,InputBufferQueue用来接收视频编码数据,经过Codec解码处理后,将解码后的视频原始YUV数据放入到OutputBufferQueue中输出。

BufferQueue的数量是有限的,每次通过dequeueInputBuffer从InputBufferQueue中获取一个空的buffer索引,将输入数据填充到该buffer,通过queueInputBuffer通知Codec该索引位置的buffer已经填充了数据,可以开始处理了。然后通过dequeueOutputBuffer从OutputBufferQueue中获取处理完毕的buffer索引,获取到该buffer中的数据进行渲染或封装,使用完后,通过releaseOutputBuffer通知Codec释放该buffer中的数据(也可以让Codec进行数据的渲染)。

MediaCodec的数据分为两种,一种是原始音视频数据,一种是压缩数据。上面讲到这两种数据都是使用bytebuffer来处理的,输入层获取到空的buffer后需要将数据copy到该buffer中,而输出层获取到buffer后也需要将数据copy出来进行消费,大量的数据copy是很影响性能的。

针对原始音视频数据,MediaCodec提供了更加高效的方式来避免了数据的copy,那就是Surface。对于解码器来说,可以通过在configure的时候,设置OutputSurface来接收输出的Buffer,这样就不需要从OutputBufferQueue中copy数据了。而对于编码器来说,可以通过createInputSurface方法创建一个用来输入的Surface,这样就不需要往InputBufferQueue里copy数据了。

mediacodec-surface-processes

数据类型

原始数据

音频原始数据类型是一个PCM音频数据帧,视频原始数据类型由color_format决定,常用的有以下两种:

  • Surface Format :这种类型的format数据,表明使用的GraphicBuffer,这是一个内存共享的缓冲区。(CodecCapabilities#COLOR_FormatSurface )。
  • YUV Format :YUV颜色格式,支持多种YUV格式(CodecCapabilities#COLOR_FormatYUV420Flexible )。

在configure的时候,对于编码器来说可以指定编码器输出的数据的format,对于解码器来说可以指定解码器输入的format。

对于编码器,如果输入的时候使用的是Surface,则format需要设置为COLOR_FormatSurface。而对于解码器来说输入的format一般从媒体文件中读取。

压缩数据

对于视频来说,buffer中是一帧的压缩数据,对于音频来说,buffer中是一个单元的压缩数据,buffer中包含的都是完整的一帧或一个单元的数据。

音视频开发之Android硬件解封装

硬件解封装

Android平台针对音视频封装提供了MediaMuxer API,支持.mp4格式的封装;针对解封装提供了MediaExtractor API,支持.mp4等格式。

API架构

frameworks/base/media 文件夹中提供了所有音视频相关的Java层API,本文中讲述的解封装API都定义在该处。而具体的服务实现都在 frameworks/av 文件夹中

android_muxer_demuxer

自7.0开始,MediaService被拆分成多个服务,每个服务都运行在各自的进程中。

MediaExtractor 解封装

MediaExtractor类是官方提供的音视频解封装类。其官方使用Demo如下所示:

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
// 创建解封装器 
MediaExtractor extractor = new MediaExtractor();
// 设置数据源
extractor.setDataSource(...);
// 遍历所有轨道
int numTracks = extractor.getTrackCount();
for (int i = 0; i < numTracks; ++i) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
// 根据轨道类型选择想要的轨道
if (weAreInterestedInThisTrack) {
extractor.selectTrack(i);
}
}
ByteBuffer inputBuffer = ByteBuffer.allocate(...)
// 读取选中轨道的样本数据
while (extractor.readSampleData(inputBuffer, ...) >= 0) {
int trackIndex = extractor.getSampleTrackIndex();
long presentationTimeUs = extractor.getSampleTime();
...
// 下一个样本
extractor.advance();
}
extractor.release();
extractor = null;

支持的格式

android_muxer_demuxer

注册解封装器

MediaExtractorService被创建的时候,会注册所有的Extractor实现。

音视频开发之基础篇

本篇重点

音视频基础概念与相关术语的讲解

什么是视频

图像的组成

在说视频之前,我们来聊一下图像,图像是人对视觉感知的物质再现。图像是使用像素点阵来表示的,每个像素点都分配有特定的颜色和位置值。像素也是图像显示的基本单位,通常一幅1080x1920的图片,就是长度1920个像素点,宽度1080个像素点组成的矩阵。

颜色空间

像素点除了有位置外还需要有颜色信息,那么颜色在计算机中是如何表示呢?

在计算机中颜色空间有很多,这里我们介绍下常用的两种颜色空间,RGB颜色空间和YUV颜色空间。

RGB颜色空间

RGB颜色空间是由红绿蓝三原色来定义,通过三原色混合成所有的颜色,这应该是我们最熟悉的一种颜色空间。

YUV颜色空间

Y表示亮度,U和V表示色度和浓度。早期的YUV实际上是用在彩色电视和黑白电视的交替上,感兴趣历史的同学可以点击YUV查看相关资料。YUV颜色空间在数字媒体中广泛使用是由于人类对亮度的的敏感度高于对色度的敏感度,但是在数字媒体中使用的是个变体,叫YCbCr,其中Cb是指蓝色色度,Cr就是红色色度。像JPEG、MPEG都是采用此格式,后续一般讲的YUV都是指YCbCr。(Y的值是根据RGB值计算出来的,所以Cg的值可以通过CbCr计算出来,所以在传输的时候只需要传输YCbCr)

1
2
3
4
5
计算公式: 
Y = KrR+KgG+Kb*B(k为权重值,ITU-R推荐的BT.601中定义Kb=0.114,Kr=0.299,Kb+Kr+Kg=1)
Cb=B-Y
Cr=R-Y
Cg=G-Y

YCbCr会按照一定的采样比对色度数据进行采样,采样比通常表示为 J​ : a : ​b,表示在一个宽为J像素、高为2像素的采样区域中进行采样,J表示采样的宽度,通常是4,a表示第一行色度采样数,b表示第二行色度采样数与第一行色度采样的不同样点数。

YCbCr采样比

最常见的采样比是 4:2:0 ,表示宽为4,第一行有两个色度采样点,第二行与第一行完全相同。所以总共有八个亮度采样点和两个色度采样点,每个亮度采样点需要记录一个值Y,而每个色度采样点都需要记录两个值Cb和Cr。最终需要记录的值为 8Y+2Cr+2Cb = 12,而如果使用RGB来记录则会需要3x8=24。

YCbCr的优势就在于,在达到最大压缩率的情况下,能够保证对人眼感知的失真度最小。拿4:2:0来说足足压缩了一半。

FFmpeg深入之ffmpeg_parse_options

简述

1
ffmpeg [global_options] {[input_file_options] -i input_url} ... {[output_file_options] output_url} ...

根据FFmpeg官方文档定义,FFmpeg命令由 全局参数+{输入参数+输入源}+{输出参数+输出流} 组成。其中全局参数可放在任何地方,输入参数必须放在输入源之前。

例:

1
ffmpeg -i video.mp4 -i 1.png -i 1.mp3 -filter_complex "[0:v]scale=750:1334,pad=750:1334:0:0:black[source];[source][1:v]overlay=x=0:y=0[result]" -map "[result]" -map 2:a -s 540x960 -t 7 -shortest -movflags faststart -y output.mp4

那么当FFmpeg接收到上面这样一串字符命令时,是如何解析和处理的?

1
2
3
4
5
6
int main(int argc, char **argv)
{
...
/* parse options and open all input/output files */
ret = ffmpeg_parse_options(argc, argv);
}

在FFmpeg的main方法中可以看到,所有的解析逻辑都在ffmpeg_parse_options函数中,下面我们来一起分析下该函数

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
int ffmpeg_parse_options(int argc, char **argv)
{
OptionParseContext octx;
uint8_t error[128];
int ret;

memset(&octx, 0, sizeof(octx));

/* 解析输入的字符命令 */
ret = split_commandline(&octx, argc, argv, options, groups,
FF_ARRAY_ELEMS(groups));
if (ret < 0) {
av_log(NULL, AV_LOG_FATAL, "Error splitting the argument list: ");
goto fail;
}

/* 应用全局参数 */
ret = parse_optgroup(NULL, &octx.global_opts);
if (ret < 0) {
av_log(NULL, AV_LOG_FATAL, "Error parsing global options: ");
goto fail;
}

/* configure terminal and setup signal handlers */
term_init();

/* 打开输入文件流 */
ret = open_files(&octx.groups[GROUP_INFILE], "input", open_input_file);
if (ret < 0) {
av_log(NULL, AV_LOG_FATAL, "Error opening input files: ");
goto fail;
}

/* 初始化滤镜图 */
ret = init_complex_filters();
if (ret < 0) {
av_log(NULL, AV_LOG_FATAL, "Error initializing complex filters.\n");
goto fail;
}

/* 打开输出文件流 */
ret = open_files(&octx.groups[GROUP_OUTFILE], "output", open_output_file);
if (ret < 0) {
av_log(NULL, AV_LOG_FATAL, "Error opening output files: ");
goto fail;
}

check_filter_outputs();

fail:
uninit_parse_context(&octx);
if (ret < 0) {
av_strerror(ret, error, sizeof(error));
av_log(NULL, AV_LOG_FATAL, "%s\n", error);
}
return ret;
}

FFmpeg深入之avformat_open_input

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# libavformat/avformat.h

/**
* 读取多媒体文件头信息
* 此时并未开始解码,开启的流通过avformat_close_input()关闭。
*
* @param ps AVFormatContext (通过avformat_alloc_context创建),可以传NULL。
* @param url 多媒体URL
* @param fmt 指定解封装的格式,传NULL时将自动检测.
* @param options 解封装参数自定义,可以传NULL
*
* @return 0 成功
*/
int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);

方法调用流程图

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×