IBigerBiger的成长之路

移动端直播开发(二)数据源采集

上面一篇介绍了直播服务器的搭建,并且也使用直播服务器实现了数据流的转发功能,那么这一篇主要介绍下关于Android端的数据采集相关的内容,那么Android端的需要采集的数据有哪些?主要是Camera摄像头获取的数据,与麦克风获取的音频数据,当然现在的直播软件也会有针对于游戏直播相关的桌面屏幕视频数据采集功能,但是由于桌面屏幕视频数据采集相对来说存在信息泄露的风险,这里就不做介绍,主要还是以音视频数据采集为主

一.Camera视频数据源采集

对于直播来说,数据采集的同时我们要做到,视频数据同时显示在手机屏幕上,至于怎么将视频数据显示在手机屏幕上面,这个之前的博文有涉及到,大家可以看移动端滤镜开发(三)OpenGL实现预览播放效果,当然这篇文章也对于Camera相关API进行了讲解。

接下来就是对于数据的采集了

Android中的摄像头Camera提供了两个方式回调接口来获取每一帧数据:

第一种方式:setPreviewCallback方法,设置回调接口:PreviewCallback,在回调方法:onPreviewFrame(byte[] data, Camera camera) 中处理每一帧数据

第二种方式:setPreviewCallbackWithBuffer方法,同样设置回调接口:PreviewCallback,不过还需要一个方法配合使用:addCallbackBuffer,这个方法接受一个byte数组。

两者的区别在于:

第一种方式是onPreviewFrame回调方法会在每一帧数据准备好了就调用,但是第二种方式是在需要在前一帧的onPreviewFrame方法中调用addCallbackBuffer方法,下一帧的onPreviewFrame才会调用,同时addCallbackBuffer方法的参数的byte数据就是每一帧的原数据。所以这么一看就好理解了,就是第一种方法的onPreviewFrame调用是不可控制的,就是每一帧数据准备好了就回调,但是第二种方法是可控的,我们通过addCallbackBuffer的调用来控制onPreviewFrame的回调机制。

那么接下来就以第二种方法为例

1
2
3
4
5
6
mCamera.setPreviewCallbackWithBuffer(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
}
});

这里面的data就是获取到的视频数据,这里的预览数据是 ImageFormat.NV21 格式的,一般情况下我们搭建的直播服务器会支持 ImageFormat.NV21 的资源,所以我们其实是这里拿数据进行编码后上传给服务器,OpenGL负责显示,当然其实这里我们也可以拿到数据然后通过转换显示在屏幕上面,但是这么做的主要会有类型转换的问题,对于性能开销方面还是会有影响,所以选择上面的OpenGL方式来做,毕竟OpenGL是通过GPU来运算的。

这里也贴上上面说的视频数据转换的方法,供大家参考一下

1
2
3
4
YuvImage = image = new YuvImage(data, ImageFormat.NV21, size.width, size.height, null);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
image.compressToJpeg(new Rect(0, 0, size.width, size.height), 100, stream);
Bitmap bitmap = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size());

这样其实就是获取到了当前这一帧的bitmap数据

二.音频数据源采集

对于音频数据的采集,Android SDK 提供了两套音频采集的API,分别是:MediaRecorder 和 AudioRecord,前者是一个更加上层一点的API,它可以直接把手机麦克风录入的音频数据进行编码压缩(如AMR、MP3等)并存成文件,而后者则更接近底层,能够更加自由灵活地控制,可以得到原始的一帧帧PCM音频数据。

对于我们需要实时的数据上传来说,肯定后者才是我们选择的方式

接下来看一下AudioRecord采集音频资源的方式

AudioRecord 的工作流程如下

(1) 配置参数,初始化内部的音频缓冲区
(2) 开始采集
(3) 需要一个线程,不断地从 AudioRecord 的缓冲区将音频数据“读”出来,注意,这个过程一定要及时,否则就会出现“overrun”的错误,该错误在音频开发中比较常见,意味着应用层没有及时地“取走”音频数据,导致内部的音频缓冲区溢出。
(4) 停止采集,释放资源

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
int bufferSize = 2 * AudioRecord.getMinBufferSize(44100, AudioFormat.CHANNEL_IN_STEREO;, SrsEncoder.AFORMAT);
AudioRecord mic = new AudioRecord(MediaRecorder.AudioSource.MIC, 44100, AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT, bufferSize);
mic.startRecording();
byte pcmBuffer[] = new byte[4096];
while (aloop && !Thread.interrupted()) {
int size = mic.read(pcmBuffer, 0, pcmBuffer.length);
if (size <= 0) {
Log.e(TAG, "***** audio ignored, no data to read.");
break;
}
}

我们了解下AudioRecord这几个参数的含义吧

1
2
public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat,
int bufferSizeInBytes)
  • audioSource 该参数指的是音频采集的输入源,可选的值以常量的形式定义在 MediaRecorder.AudioSource 类中,常用的值包括:DEFAULT(默认),VOICE_RECOGNITION(用于语音识别,等同于DEFAULT),MIC(由手机麦克风输入),VOICE_COMMUNICATION(用于VoIP应用)等等

  • sampleRateInHz 采样率,注意,目前44100Hz是唯一可以保证兼容所有Android手机的采样率。

  • channelConfig 通道数的配置,可选的值以常量的形式定义在 AudioFormat 类中,常用的是 CHANNEL_IN_MONO(单通道),CHANNEL_IN_STEREO(双通道)

  • audioFormat 这个参数是用来配置“数据位宽”的,可选的值也是以常量的形式定义在 AudioFormat 类中,常用的是 ENCODING_PCM_16BIT(16bit),ENCODING_PCM_8BIT(8bit),注意,前者是可以保证兼容所有Android手机的

  • bufferSizeInBytes 这个参数配置的是 AudioRecord 内部的音频缓冲区的大小,该缓冲区的值不能低于一帧“音频帧”(Frame)的大小

这些配置一般情况下都用上面代码里面的不变就好了,pcmBuffer就是我们采集到的音频数据

音视频数据都采集完了,接下来就是要上传给我们的直播服务器了,但是在上传前我们需要做的工作就是进行编码

三.音视频编码

(1).视频编码

Android中视频编码有两种方式,主要是两个核心的类,一个是MediaCodec和MediaRecorder,这两个类有什么区别呢?其实很好理解,他们都可以对视频进行编码,但是MediaRecorder这个类相对于MediaCodec简单,因为他封装的很好,直接就是几个接口来完成视频录制,比如视频的编码格式,视频的保存路劲,视频来源等,用法简单,但是有一个问题就是不能接触到视频流数据了,处理不了原生的视频数据了,所以我们不能选择用MediaRecorder方式来进行编码。

MediaCodec编码流程图如下

图1 MediaCodec编码流程,来源于网络

对应的方法主要为:

  • getInputBuffers:获取需要编码数据的输入流队列,返回的是一个ByteBuffer数组

  • queueInputBuffer:输入流入队列

  • dequeueInputBuffer:从输入流队列中取数据进行编码操作

  • getOutputBuffers:获取编解码之后的数据输出流队列,返回的是一个ByteBuffer数组

  • dequeueOutputBuffer:从输出队列中取出编码操作之后的数据

  • releaseOutputBuffer:处理完成,释放ByteBuffer数据

接下来分析一下具体的流程:

视频流有一个输入队列,和输出队列,分别对应getInputBuffers和getOutputBuffers这两个方法获取这个队列,然后对于输入流这端有两个方法一个是queueInputBuffers是将视频流入队列,dequeueInputBuffer是从输入流队列中取出数据进行编解码操作,在输出端这边有一个dequeueOutputBuffer方法从输出队列中获取视频数据,releaseOutputBuffers方法将处理完的输出视频流数据ByteBuffer放回视频流输出队列中,再次循环使用。这样视频流输入端和输出端分别对应一个ByteBuffer队列,这些ByteBuffer可以重复使用,在处理完数据之后再放回去即可。

代码如下

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
MediaCodec vencoder;
MediaCodecInfo vmci;
// requires sdk level 16+, Android 4.1, 4.1.1, the JELLY_BEAN
try {
vencoder = MediaCodec.createByCodecName(vmci.getName());
} catch (IOException e) {
Log.e(TAG, "create vencoder failed.");
e.printStackTrace();
return -1;
}
MediaFormat videoFormat = MediaFormat.createVideoFormat(""video/avc"", vCropWidth, vCropHeight);
videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, mVideoColorFormat);
videoFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 0);
videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, 500 * 1000);
videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 24);
videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 48 / 24);
vencoder.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
ByteBuffer[] inBuffers = vencoder.getInputBuffers();
ByteBuffer[] outBuffers = vencoder.getOutputBuffers();
vencoder.start();
int inBufferIndex = vencoder.dequeueInputBuffer(-1);
if (inBufferIndex >= 0) {
ByteBuffer bb = inBuffers[inBufferIndex];
bb.clear();
bb.put(mFrameBuffer, 0, mFrameBuffer.length);
long pts = System.nanoTime() / 1000 - mPresentTimeUs;
vencoder.queueInputBuffer(inBufferIndex, 0, mFrameBuffer.length, pts, 0);
}
for (; ; ) {
int outBufferIndex = vencoder.dequeueOutputBuffer(vebi, 0);
if (outBufferIndex >= 0) {
ByteBuffer mEncoderBuffer = outBuffers[outBufferIndex];
vencoder.releaseOutputBuffer(outBufferIndex, false);
} else {
break;
}
}

这里首先要配置MediaCodec,配置了编码格式、视频大小、比特率、帧率等参数,然后就是通过vencoder来进行编码,最后编码得到mEncoderBuffer。

编码后的格式为H.264,对于H264格式说明如下:

H.264,MPEG-4,MPEG-2等这些都是压缩算法,毕竟带宽是有限的,为了获得更好的图像的传输和显示效果,就不断的想办法去掉一些信息,转换一些信息等等,这就是这些压缩算法的做的事情。H.264最大的优势是具有很高的数据压缩比率,在同等图像质量的条件下,H.264的压缩比是MPEG-2的2倍以上,是MPEG-4的1.5~2倍。举个例子,原始文件的大小如果为88GB,采用MPEG-2压缩标准压缩后变成3.5GB,压缩比为25∶1,而采用H.264压缩标准压缩后变为879MB,从88GB到879MB,H.264的压缩比达到惊人的102∶1!H.264为什么有那么高的压缩比?低码率(Low Bit Rate)起了重要的作用,和MPEG-2和MPEG-4 ASP等压缩技术相比,H.264压缩技术将大大节省用户的下载时间和数据流量收费。尤其值得一提的是,H.264在具有高压缩比的同时还拥有高质量流畅的图像。

(2).音频编码

音频编码其实也是采用上面的MediaCodec,区别在于配置的不同,流程是一致的,这里就不做说明了,直接上代码

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
MediaCodec aencoder;
aencoder = MediaCodec.createEncoderByType("audio/mp4a-latm");
MediaFormat audioFormat = MediaFormat.createAudioFormat("audio/mp4a-latm", 44100, 2);
audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, 32 * 1000;);
audioFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 0);
aencoder.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
ByteBuffer[] inBuffers = aencoder.getInputBuffers();
ByteBuffer[] outBuffers = aencoder.getOutputBuffers();
aencoder.start();
int inBufferIndex = aencoder.dequeueInputBuffer(-1);
if (inBufferIndex >= 0) {
ByteBuffer bb = inBuffers[inBufferIndex];
bb.clear();
bb.put(mAudioData, 0, size);
long pts = System.nanoTime() / 1000 - mPresentTimeUs;
aencoder.queueInputBuffer(inBufferIndex, 0, size, pts, 0);
}
for (; ; ) {
int outBufferIndex = aencoder.dequeueOutputBuffer(aebi, 0);
if (outBufferIndex >= 0) {
ByteBuffer mEncoderBuffer = outBuffers[outBufferIndex];
aencoder.releaseOutputBuffer(outBufferIndex, false);
} else {
break;
}
}

这里通过aencoder来进行编码,最后编码得到音频编码后的数据mEncoderBuffer。

音频编码后的格式为AAC格式,对于AAC格式说明如下:

[AAC]是由F[ra]unhofer IIS-A、杜比和AT&T共同开发的一种音频格式,它是MPEG-2规范的一部分。AAC所采用的运算法则与MP3的运算法则有所不同,AAC通过结合其他的功能 来提高编码效率。AAC的音频算法在压缩能力上远远超过了以前的一些压缩算法(比如MP3等)。它还同时支持多达48个音轨、15个低频音轨、更多种采样率和比特率、多种语言的兼容能力、更高的解码效率。总之,AAC可以在比MP3文件缩小30%的前提下提供更好的音质。

所以上面我们就实现了数据源的编码,编码其实分为两种,一种是利用系统的MediaCodec来编码的硬编,还有一种是软编,通常我们是用ffmpeg来软编,就不对软编进行说明了,有兴趣的可以自己去谷歌。

写在后面的话

我们通过采集与编码这一系列的操作拿到了可以推流的视频数据与音频数据,那么下一篇就是对推流相关介绍了,通过推流我们可以把这些数据传输到我们的视频服务器上,通过视频服务器的转发,从而可以实现移动端的直播,peace~~~