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

写在前面的话

<p>
上面一篇介绍了直播服务器的搭建,并且也使用直播服务器实现了数据流的转发功能,那么这一篇主要介绍下关于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的回调机制。

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

mCamera.setPreviewCallbackWithBuffer(new Camera.PreviewCallback() {
        @Override
        public void onPreviewFrame(byte[] data, Camera camera) {
            
        }
    });

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

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

  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) 停止采集,释放资源

代码如下

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这几个参数的含义吧

 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可以重复使用,在处理完数据之后再放回去即可。

代码如下

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,区别在于配置的不同,流程是一致的,这里就不做说明了,直接上代码

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~~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,513评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,312评论 1 305
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,124评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,529评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,937评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,913评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,084评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,816评论 0 205
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,593评论 1 249
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,788评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,267评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,601评论 3 261
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,265评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,158评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,953评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,066评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,852评论 2 277

推荐阅读更多精彩内容

  • 视频编码与封装方式详解 1.编码方式和封装格式 2.视频编码标准两大系统 MPEG-1 MPEG-2 MPEG-3...
    latthias阅读 6,222评论 0 22
  • 前言 说到视频,大家自己脑子里基本都会想起电影、电视剧、在线视频等等,也会想起一些视频格式 AVI、MP4、RMV...
    ForestSen阅读 22,452评论 10 203
  • 前言:每个成功者多是站在巨人的肩膀上!在做直播开发时 碰到了很多问题,在收集了许多人博客的基础上做出来了成功的直播...
    _方丈阅读 23,112评论 35 330
  • “随着技术的不断进步,视频技术的制作加工门槛逐渐降低,信息资源的不断增长,同时由于视频信息内容更加丰富完整的先天优...
    陈墨啊阅读 2,836评论 2 53
  • 或许是心情不好,和朋友一起出去散散心,就无方向地走到了一个公园。傍晚了,天空渐暗,消了刺眼的明亮,却多了几分静谧的...
    RR90后阅读 203评论 0 0