前言
本人接触Android的时间有限,如果您有更好的解决方案,欢迎吐槽。
众所周知,Android平台开发分为Java层和C++层,即Android SDK和Android NDK。常规产品功能只需要涉及到Java层即可,除非特殊需要是不需要引入NDK的。但如果是进行音视频开发呢?Android系统Java层API对音视频的支持在MediaCodec之前,还停留在非常抽象API的级别(即只提供简单的参数和方法,可以控制的行为少,得不到中间数据,不能进行复杂功能的开发,更谈不上扩展)。而在MediaCodec在推出之后,也未能彻底解决问题,原因有这些:1、MediaCodec出现的Android版本并不低,使用则无法兼容低版本机器和系统版本;2、由于Android的开源和定制特性,各大厂商实现的MediaCodec也不尽相同,也导致同一段代码A机器跑着是这个样,B机器跑着就是另一个样了。所以程序员童鞋们就把目光转向了NDK,但是NDK里面谷歌并没有提供什么关于音视频处理的API(比如解析生成文件,编解码帧),于是童鞋们又想着使用开源的C/C++框架,首当其冲的当然是最出名的ffmpeg、x264、mp3lame、faac这些了。问题又来了,ffmpeg最早对x86支持是最好的,arm平台或者mips平台支持就不这么好了(笔者调研ffmpeg2.0以后情况有所好转)。那就只能使用软解软编,速度跟不上是个什么体验亲们知道吗?举个栗子,假设要录制640x480的视频,音频视频全部使用软编码,x264如果纯软编码加上手机CPU的处理性能50毫秒甚至100毫秒一帧都说不定,总之就是慢,还要算上音频还要压缩编码。如果想录制25帧率的视频,一帧的编码时间是不能超过40毫秒的,否则速度就跟不上了,算上其他业务功能花的时间,这个时间起码要降到30毫秒以下,然后再使用多线程异步编码的方式优化一下应该勉强能达到边获取画面边生成视频文件。正是因为有这样那样的不方便,笔者才经过几个月的研究,找到了一个还不算太完美的解决方案供大家参考,本文将全面介绍各个环节的技术实现方案,最后并附上工程源码。顺便声明一下,笔者在进行这项工作之前Android开发经验基本上算是1(不是0是因为以前写过helloworld),但是C/C++,Java都已经掌握,还在ios上使用objc开发过项目,所以我想Android也差异不大,语言不一样,平台不一样,API不一样,系统机制不一样,其他应该就一样了。
NDK有哪些API可用?
先把NDK的include打开,普查一下到底NDK提供了哪些接口可以用。谷歌还算是有人性,其实除了linux系统级的API外,其实还是有一些音视频相关的API的。
OpenSL,可以直接在C++层操作音频采集和播放设备,进行录音和播放声音,从API9开始支持。
EGL,可以在C++层创建OpenGL的绘制环境,用于视频图像渲染,还可以用于一些图像处理如裁剪、拉伸、旋转,甚至更高级的滤镜特效处理也是可以的。另外不得不说在C++自己创建OpenGL的渲染环境比使用Java层的GLSurfaceView灵活性、可控性、扩展性方面直接提升好几个数量级。而EGL在API9也已经支持了。
OpenGL(ES), NDK在Java层提供了OpenGL接口,而在NDK层也提供了更原生的OpenGL头文件,而要使用GLSL那就必须要有OpenGLES2.0+了,还好NDK也很早就支持了,OpenGLES2.0在API5就开始支持了,万幸!!
OpenMAXAL,这是普查过程中发现的一个让人不爽的库,因为从它的接口定义来看它有例如以比较抽象接口方式提供的播放视频的功能和打开摄像头的功能。播放视频就用不到了,后面自己编解码自己渲染实现,看到这个打开摄像头的接口,心中当时是欣喜了一把的,结果是我的MX3居然告诉我该接口没实现。那结果就必须从Java层传摄像头的数据到C++层了。不过OpenMAXIL,前者的兄弟,倒是个好东西,可惜谷歌暂时没有开放接口。
这样一来,图像采集就必须从Java层打开Camera,然后获取到数据之后通过JNI传递到C++层了。渲染图像的View也要从java层创建SurfaceView然后传递句柄到C++层进而使用EGL来初始化OpenGL的渲染环境。声音的采集和播放就和Java没关系了,底层就可以直接处理完了。
选择开源框架
ffmpeg 文件解析,图像拉伸,像素格式转换,大多数解码器,笔者选用的2.7.5版本,有针对ARM的不少优化,解码速度还算好。
x264 H264的编码器,新的版本也对ARM有很多优化,如果使用多线程编码一帧640x480可以低至3-4毫秒。
mp3lame MP3的编码器,其实测试工程里面没用到(测试工程使用的MP4(H264+AAC)的组合),只是习惯性强迫症编译了加进编码器列表里
faac AAC的编码器,也是很久没更新了,编码速度上算是拖后腿的,所以后面才有个曲线救国的设计来解决音频编码的问题。
完整解决方案图
音频编码慢的问题
x264和ffmpeg都下载比较新的版本,然后开启asm,neon等优化选项编译之后,编解码速度还能接受。可是FAAC的编码速度着实还是有点慢。笔者于是乎想到个办法,就是存储临时文件,在录制的时候视频数据直接调用x264编码,不走ffmpeg中转(这样可以更灵活配置x264参数,达到更快的目的),而音频数据就直接写入文件。这样录制的临时文件其实和正儿八经的视频文件大小差距不大,不会造成磁卡写入速度慢的瓶颈问题,同时还可解决编辑播放的时候拖动进度条的准确度问题,同时解决关键帧抽帧的问题,因为临时文件都是自己写的,文件里什么内容都可以自己掌控。不得不说一个问题就是定义的抽象视频文件读取写入接口Reader和Writer,而读取写入正式MP4文件的实现和读取写入临时文件的实现都是实现这个Reader和Writer的,所以日后想改成直接录制的时候就生成MP4只需要初始化的时候new另一个对象即可。还有一招来解决速度慢的问题就是多线程异步写入,采集线程拿到数据之后丢给另一个线程来进行编码写入,只要编码写入的平均速度跟得上帧率就可以满足需求。
引入OpenGL2D/3D引擎
当在C++层使用EGL创建了OpenGL的渲染环境之后,就可以使用任何C/C++编写的基于OpenGL框架了。笔者这里引入了COCOS2D-X来给视频加一些特效,比如序列帧,粒子效果等。COCOS2D-X本身有自己的渲染线程和OpenGL渲染环境,需要把这些代码干掉之后,写一部分代码让COCOS2D-X渲染到你自己创建的EGL环境上。另外COCOS2D-X的对象回收机制是模拟的Objective-C的引用计数和自动回收池方式,工程源码中的COCOS2D-X回收机制笔者也进行了简化修改,说实话个人觉得它的引用计数模拟的还可以,和COM差不多的原理,统一基类就可以实现,但是自动回收池就不用完全照搬Objective-C了,没必要搞回收池压栈了,全局一个回收池就够用了嘛。(纯属个人观点)
主副线程模式
OpenGL的glMakeCurrent是线程敏感的,大家都知道。和OpenGL相关的所有操作都是线程敏感的,即文理加载,glsl脚本编译链接,context创建,glDraw操作都要求在同一个线程内。而Android平台没有类似iOS上自带的MainOperationQueue的方式,所以笔者自己设计了一个主副线程模式(我自己取的名字),即主线程就是Android的UI线程,负责UI绘制的响应按钮Action。然后其他所有操作都交给副线程来做。也就说每一种用户的操作的响应函数都不直接干事,而是学习MFC的方式,post一个消息和数据到副线程。那么副线程就必然要用单线程调度消息循环和多任务的方式了,消息循环不说了,MFC的模式。单线程调度多任务可能好多童鞋没接触过,其实就是将传统的单线程处理的任务,分成很多个时间片,让线程每次只处理一个时间片,然后缓存处理状态,到下一次轮到它的时候再继续处理。
比如任务接口是 IMission {bool onMissionStart(); bool onMissionStep(); void onMissionStop();} 调度线程先执行一次onMissionStart如果返回false则执行onMissionStop结束任务;如果前者返回true,则不断的调用onMissionStep,直到返回false,再执行onMissionStop,任务结束。具体的处理都要封装成任务接口的实现类,然后丢进任务列表。
试想,这样的设计架构下,是不是所有的操作都在同一个线程里了,OpenGL的调用也都在同一个线程里了,还有附带的效果就是妈妈再也不用担心多线程并发处理到处加锁导致的性能问题和bug问题了,不要怀疑它的性能,因为就算多线程到CPU那一级也变成了单线程了。redis不就是单线程的么,速度快的杠杠的。
总结一下
使用OpenSL录音和播音
使用EGL在C++层创建OpenGL环境
改造COCOS2D-X,使用自己创建的OpenGL环境
直接使用x264而非ffmpeg中转,按最快的编码方式配置参数,一定记得开启x264的多线程编码。
x264和ffmpeg都要下载比较新的,并且编译的时候使用asm,neon等选项。(笔者是在ubuntu上跨平台编译的)
如果录制的时候直接编码视频和音频速度跟不上就写入临时文件,图像编码,声音直接存PCM。
除了Android主线程外,另外只开一个副线程用于调度,具体小模块耗时的任务就单独开线程,框架主体上只存在两个线程,一主一副。
完整工程源码
使用的API15开发,其实是可以低到API9的。
源码地址:http://download.csdn.net/detail/yangyk125/9416064
操作演示:http://www.tudou.com/programs/view/PvY9MMugbRw/
渲染完生成视频的位置:/SD卡/e4fun/video/*.mp4
需要说明一下的是:
1、com.android.video.camera.EFCameraView类 最前面两个private字段定义当前选用的摄像头分辨率宽度和高度,要求当前摄像头支持这个分辨率。
2、jni/WORKER/EFRecordWorker.cpp的createRecordWorker函数内,定义当前录制视频的各种基本参数,请根据测试机器的性能自由配置。
3、jni/WORKER/EFRecordWorker.cpp的on_create_worker函数内,有个设置setAnimationInterval调用,设置OpenGL绘制帧率,和视频帧率是两回事,请酌情设置。
如果跑不起来,请加QQ 119508078 咨询。
本人工作地成都,欢迎志同道合的童鞋们骚扰。
感谢一位读了这篇博客的网友,给我指出了其中可以优化的地方
1、如果使用ffmpeg开源方案处理音视频,那么AAC应该使用fdk_aac而不应该使用很久没更新的faac。
2、glReadPixels回读数据效率低下,笔者正在尝试升级到gles3.0看看能不能有什么办法快速获取渲染结果图像,如果您知道,请在后面留言,谢谢啦!
在Android上做音视频处理,如果还想要更快的编解码,如果是Java层则逃不开MediaCodec,如果是C++层,可以向下研究,比如OpenMAXIL等等。
补充:最近大家都在问同一个问题,就是怎么编译不过?怎么跑不起来?
编译不过的问题: 因为历史原因需要在eclipse+NDK10上编译和运行,编译之后一定检查APK下面是否包含so。
跑不起来的问题:NXTimer是调度计时器的实现,计时器的反馈通知方式应该从SIGEV_SIGNAL设置成 SIGEV_THREAD(还有其他对应的修改,请自行查资料了解 Linux上POSIX timer的使用),因为5.0以后就会和系统冲突。
性能问题:因为这个设计的时候,主要为了兼容4.4以下android系统,所以主要是软编码。也就是现在如果你要开发一个可以发布的产品,这个架构已经不应该被采纳了,它的价值仅限于学习原理使用。4.4以上系统还是应该采用android.media* + opengles 来实现上述功能,ffmpeg可以作为辅助,完成一些不太要求性能的处理,比如说非MediaCodec支持的解码,音频的处理等等。
Android都更新到7.X了,而2.X的代码在新版本系统上运行到处都是坑,应大家的要求,我重新修改了一些地方,在5.X,6.X上面勉强可以运行了,但还是各种不稳定。还是建议大家学习使用android.media.* 。
https://github.com/yangyk125/AndroidVideo