性能比肩美拍秒拍的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://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绘制帧率,和视频帧率是两回事,请酌情设置。
感谢一位读了这篇博客的网友,给我指出了其中可以优化的地方
1、如果使用ffmpeg开源方案处理音视频,那么AAC应该使用fdk_aac而不应该使用很久没更新的faac。
2、glReadPixels回读数据效率低下,笔者正在尝试升级到gles3.0看看能不能有什么办法快速获取渲染结果图像,如果您知道,请在后面留言,谢谢啦!
在Android上做音视频处理,如果还想要更快的编解码,如果是Java层则逃不开MediaCodec,如果是C++层,可以向下研究,比如OpenMAXIL等等。
后记:
经过半年努力,解决了其中部分有效率问题的地方
(1)编解码部分
编解码部分之前文章采用的X264+FFMPEG的开源方案,而继续学习之后,找到了android上特有的实现方案。
版本<4.4:x264+ffmpeg or 私有API(libstagefright.so)。
版本=4.4:jni反调android.media.MediaCodec or 或者在java层开发。
版本>4.4:NdkMediaCodec(android.media.MediaCodec 的 jni接口)。
(2)AAC更优开源方案
AAC开源方案FDKAAC一直在更新,效率有提升,而faac早就不更新了。so…你懂的。
AAC也可以使用MediaCodec或者NdkMediaCodec
(3)OpenGL之framebuffer数据的回读
GLES版本<3.0:使用glReadPixels 或者 EGLImageKHR(eglCreateImageKHR,glEGLImageTargetTexture2DOES)
GLES版本=3.0:Pixel Pack Buffer + glMapBufferRange。
Android版本>=4.2:还有一个android平台化的回读FrameBuffer的方案,那就是新建SurfaceTexture和Surface,然后新创建一个OpenGL Context,一比一再渲染一次,即可将FrameBuffer渲染到这个SurfaceTexture上面,surface还可以作为编码器的输入。这样不仅可以快速从渲染结果传递数据到编码器,还能实现跨线程传递纹理数据,属于android平台本身提供的功能,非opengl自带能力。之所以是4.2,是因为SurfaceTexture在4.2以后才基本完善,之前各种不稳定。
共同学习,写下你的评论
评论加载中...
作者其他优质文章