Android利用硬解硬编和OpenGLES来高效的处理MP4视频
Posted 湖广午王
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android利用硬解硬编和OpenGLES来高效的处理MP4视频相关的知识,希望对你有一定的参考价值。
最近工作中遇到一个问题,就是要对视频增加视频特效,实现类似于抖音的效果,抖音的效果由其他同事实现,我的工作重心在视频的处理,特效的集成。按照之前的思路很快就实现了这个功能,但是实际应用到项目中时却遇到各种问题。于是就有了这篇博客。
遇到的问题
说是各种问题,特效方便的不管,我所遇到的视频处理的问题主要为以下两个方面:
- 处理过程耗时较长。因为处理的时候是按照之前的思路,用MediaCodec解码,取出ByteBuffer,然后用OpenGLES处理,处理完毕后readPixels,得到图像数据,然后将图像数据推入MediaCodec编码。 在这里readPixels非常耗时。480*840的视频,一帧耗时基本是40ms+。
- 手机兼容性很成问题。虽然不需要考虑低版本兼容,只需要考虑4.4+的手机。但是android手机市场的情况,开发者朋友们应该也都知道,各家有各家的小动作,混乱不堪。解码出来的视频数据,并不是固定的格式,虽然大多数手机都支持YUV420P或者YUV420SP,但是也有些奇葩手机,只能解码出
OMX_QCOM_COLOR_FormatYUV420PackedSemiPlanar32m
这类的格式,总不能都去判断然后根据格式去转换吧。
之前看官方文档的时候,有看到MediaCodec解码视频支持直接解码到Surface上,编码也可以直接从Surface采集数据,这样的话,视频数据可以直接解码到Surface上,然后通过OpenGLES处理,再又通过Surface进行编码,就无需关注解码出来的数据的格式了,而且应用层也不必自己去将原始数据导入GPU以及将处理后的数据导出GPU了,这些工作可以都丢给Android SDK去做。理论上就能一举解决上面的两个问题。那么具体应该如何做呢?
处理流程
有了理论,剩下的就是实现了。不卖关子,根据以上的方案,直接列出处理的流程:
- 利用MediaExtractor获取Mp4的音轨和视轨,获取音频视频的MediaFormat.
- 根据音视频信息,创建视频解码器,视频编码器,音频暂时不处理就不创建编解码器了。其中视频解码器的Surface是通过先创建一个SurfaceTexture,然后将这个SurfaceTexture作为参数创建的,这样的话,视频流就可以通过这个SurfaceTexture提供给OpenGL环境作为输出。视频编码器的Surface可直接调用createInputSurface()方法创建,这个Surface后续传递给OpenGL环境作为输出
- 创建MediaMuxer,用于后面合成处理后的视频和音频。
- 创建OpenGL环境,用于处理视频图像,这个OpenGL环境由EGL创建,EGLSurface为WindowSurface,并以编码器创建的Surface作为参数。
- MediaExtractor读取原始Mp4中的视频流,交由解码器解码到Surface上。
- SurfaceTexture监听有视频帧时,通知OpenGL线程工作,处理视频图像,并渲染。
- OpenGL线程每次渲染完毕,通知编码线程进行编码,编码后的数据通过MediaMuxer混合。
- 视频流处理完毕后,利用MediaExtractor读取音频流,并利用MediaMuxer混合到新的视频文件中。
- 处理完毕后调用MediaMuxer的stop方法,处理后的视频就生成成功了。
具体实现
流程一捋,道理到家都懂,具体怎么实现呢。根据以上流程上代码了。
创建需要的编解码工具
这里是直接把1、2、3步的事情,在一个方法中完成了:
//todo 获取视频旋转信息,并做出相应处理
MediaMetadataRetriever mMetRet=new MediaMetadataRetriever();
mMetRet.setDataSource(mInputPath);
mExtractor=new MediaExtractor();
mExtractor.setDataSource(mInputPath);
int count=mExtractor.getTrackCount();
//解析Mp4
for (int i=0;i<count;i++)
MediaFormat format=mExtractor.getTrackFormat(i);
String mime=format.getString(MediaFormat.KEY_MIME);
if(mime.startsWith("audio"))
mAudioDecoderTrack=i;
else if(mime.startsWith("video"))
mVideoDecoderTrack=i;
mInputVideoWidth=format.getInteger(MediaFormat.KEY_WIDTH);
mInputVideoHeight=format.getInteger(MediaFormat.KEY_HEIGHT);
mVideoDecoder=MediaCodec.createDecoderByType(mime);
mVideoTextureId=mEGLHelper.createTextureID();
//注意这里,创建了一个SurfaceTexture
mVideoSurfaceTexture=new SurfaceTexture(mVideoTextureId);
mVideoSurfaceTexture.setOnFrameAvailableListener(mFrameAvaListener);
//将SurfaceTexture作为参数创建一个Surface,用来接收解码视频流
mVideoDecoder.configure(format,new Surface(mVideoSurfaceTexture),null,0);
if(!isRenderToWindowSurface)
if(mOutputVideoWidth==0||mOutputVideoHeight==0)
mOutputVideoWidth=mInputVideoWidth;
mOutputVideoHeight=mInputVideoHeight;
MediaFormat videoFormat=MediaFormat.createVideoFormat(mime,mOutputVideoWidth,mOutputVideoHeight);
videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
videoFormat.setInteger(MediaFormat.KEY_BIT_RATE,mOutputVideoHeight*mOutputVideoWidth*5);
videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 24);
videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
mVideoEncoder=MediaCodec.createEncoderByType(mime);
mVideoEncoder.configure(videoFormat,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE);
//注意这里,创建了一个Surface,这个Surface是编码器的输入,也是OpenGL环境的输出
mOutputSurface=mVideoEncoder.createInputSurface();
Bundle bundle=new Bundle();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
bundle.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE,mOutputVideoHeight*mOutputVideoWidth*5);
mVideoEncoder.setParameters(bundle);
//这里的if是测试时候,直接解码到屏幕上,外部设置了OutputSurface,用于测试,所以不必管
if(!isRenderToWindowSurface)
//如果用户没有设置渲染到指定Surface,就需要导出视频,暂时不对音频做处理
mMuxer=new MediaMuxer(mOutputPath,MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
MediaFormat format=mExtractor.getTrackFormat(mAudioDecoderTrack);
mAudioEncoderTrack=mMuxer.addTrack(format);
创建OpenGL环境
第4步,创建OpenGL环境,用来处理视频图像,先直接贴个工具类,用于创建OpenGL环境
public class EGLHelper
private EGLSurface mEGLSurface;
private EGLContext mEGLContext;
private EGLDisplay mEGLDisplay;
private EGLConfig mEGLConfig;
private EGLContext mShareEGLContext=EGL14.EGL_NO_CONTEXT;
private boolean isDebug=true;
private int mEglSurfaceType=EGL14.EGL_WINDOW_BIT;
private Object mSurface;
/**
* @param type one of @link EGL14#EGL_WINDOW_BIT、@link EGL14#EGL_PBUFFER_BIT、@link EGL14#EGL_PIXMAP_BIT
*/
public void setEGLSurfaceType(int type)
this.mEglSurfaceType=type;
public void setSurface(Object surface)
this.mSurface=surface;
/**
* create the environment for OpenGLES
* @param eglWidth width
* @param eglHeight height
*/
public boolean createGLES(int eglWidth, int eglHeight)
int[] attributes = new int[]
EGL14.EGL_SURFACE_TYPE, mEglSurfaceType, //渲染类型
EGL14.EGL_RED_SIZE, 8, //指定RGB中的R大小(bits)
EGL14.EGL_GREEN_SIZE, 8, //指定G大小
EGL14.EGL_BLUE_SIZE, 8, //指定B大小
EGL14.EGL_ALPHA_SIZE, 8, //指定Alpha大小,以上四项实际上指定了像素格式
EGL14.EGL_DEPTH_SIZE, 16, //指定深度缓存(Z Buffer)大小
EGL14.EGL_RENDERABLE_TYPE, 4, //指定渲染api类别, 如上一小节描述,这里或者是硬编码的4(EGL14.EGL_OPENGL_ES2_BIT)
EGL14.EGL_NONE ; //总是以EGL14.EGL_NONE结尾
int glAttrs[] =
EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, //0x3098是EGL14.EGL_CONTEXT_CLIENT_VERSION,但是4.2以前没有EGL14
EGL14.EGL_NONE
;
int bufferAttrs[]=
EGL14.EGL_WIDTH,eglWidth,
EGL14.EGL_HEIGHT,eglHeight,
EGL14.EGL_NONE
;
//获取默认显示设备,一般为设备主屏幕
mEGLDisplay= EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
//获取版本号,[0]为版本号,[1]为子版本号
int[] versions=new int[2];
EGL14.eglInitialize(mEGLDisplay,versions,0,versions,1);
log(EGL14.eglQueryString(mEGLDisplay, EGL14.EGL_VENDOR));
log(EGL14.eglQueryString(mEGLDisplay, EGL14.EGL_VERSION));
log(EGL14.eglQueryString(mEGLDisplay, EGL14.EGL_EXTENSIONS));
//获取EGL可用配置
EGLConfig[] configs = new EGLConfig[1];
int[] configNum = new int[1];
EGL14.eglChooseConfig(mEGLDisplay, attributes,0, configs,0, 1, configNum,0);
if(configs[0]==null)
log("eglChooseConfig Error:"+ EGL14.eglGetError());
return false;
mEGLConfig = configs[0];
//创建EGLContext
mEGLContext= EGL14.eglCreateContext(mEGLDisplay,mEGLConfig,mShareEGLContext, glAttrs,0);
if(mEGLContext==EGL14.EGL_NO_CONTEXT)
return false;
//获取创建后台绘制的Surface
switch (mEglSurfaceType)
case EGL14.EGL_WINDOW_BIT:
mEGLSurface=EGL14.eglCreateWindowSurface(mEGLDisplay,mEGLConfig,mSurface,new int[]EGL14.EGL_NONE,0);
break;
case EGL14.EGL_PIXMAP_BIT:
break;
case EGL14.EGL_PBUFFER_BIT:
mEGLSurface=EGL14.eglCreatePbufferSurface(mEGLDisplay,mEGLConfig,bufferAttrs,0);
break;
if(mEGLSurface==EGL14.EGL_NO_SURFACE)
log("eglCreateSurface Error:"+EGL14.eglGetError());
return false;
if(!EGL14.eglMakeCurrent(mEGLDisplay,mEGLSurface,mEGLSurface,mEGLContext))
log("eglMakeCurrent Error:"+EGL14.eglQueryString(mEGLDisplay,EGL14.eglGetError()));
return false;
log("gl environment create success");
return true;
public void setShareEGLContext(EGLContext context)
this.mShareEGLContext=context;
public EGLContext getEGLContext()
return mEGLContext;
public boolean makeCurrent()
return EGL14.eglMakeCurrent(mEGLDisplay,mEGLSurface,mEGLSurface,mEGLContext);
public boolean destroyGLES()
EGL14.eglMakeCurrent(mEGLDisplay,EGL14.EGL_NO_SURFACE,EGL14.EGL_NO_SURFACE,EGL14.EGL_NO_CONTEXT);
EGL14.eglDestroySurface(mEGLDisplay,mEGLSurface);
EGL14.eglDestroyContext(mEGLDisplay,mEGLContext);
EGL14.eglTerminate(mEGLDisplay);
log("gl destroy gles");
return true;
public void setPresentationTime(long time)
EGLExt.eglPresentationTimeANDROID(mEGLDisplay,mEGLSurface,time);
public boolean swapBuffers()
return EGL14.eglSwapBuffers(mEGLDisplay,mEGLSurface);
//创建视频数据流的OES TEXTURE
public int createTextureID()
int[] texture = new int[1];
GLES20.glGenTextures(1, texture, 0);
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture[0]);
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR);
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE);
return texture[0];
private void log(String log)
if(isDebug)
Log.e("EGLHelper",log);
借助上面的工具类创建OpenGL环境。可以看到里面使用了信号量,是用于当有新的视频图像时由SurfaceTexture的监听器通知GL线程执行渲染,没有的话就等待新的视频图像解码完后再执行处理工作。
mSem=new Semaphore(0);
//设置输出的Surface
mEGLHelper.setSurface(mOutputSurface);
//根据设置的输出视频的宽高创建OpenGL环境
boolean ret=mEGLHelper.createGLES(mOutputVideoWidth,mOutputVideoHeight);
if(!ret)return;
mRenderer.onCreate(mOutputVideoWidth,mOutputVideoHeight);
while (mGLThreadFlag)
try
mSem.acquire();
catch (InterruptedException e)
e.printStackTrace();
mVideoSurfaceTexture.updateTexImage();
//回调用户的处理函数
mRenderer.onDraw();
//设置时间点,用于输出视频图像的时间点,这里是填入输入视频的时间点
mEGLHelper.setPresentationTime(mVideoDecoderBufferInfo.presentationTimeUs*1000);
if(!isRenderToWindowSurface)
//调用编码函数进行编码
videoEncodeStep(false);
mEGLHelper.swapBuffers();
if(!isRenderToWindowSurface)
//编码视频,传入true表示视频结束
videoEncodeStep(true);
//销毁OpenGL环境
mEGLHelper.destroyGLES();
mRenderer.onDestroy();
第6步就是用于通知这个GL线程执行渲染工作,只需要在监听器中,发出信号就可以了。
private SurfaceTexture.OnFrameAvailableListener mFrameAvaListener=new SurfaceTexture.OnFrameAvailableListener()
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture)
mSem.release();
;
视频流解码
第5步,需要将视频解码,解码的方法如下。在解码的线程中循环调用此方法,其返回值为true时结束循环,也就是视频帧解码完毕。
//视频解码到SurfaceTexture上,以供后续处理。返回值为是否是最后一帧视频
private boolean videoDecodeStep()
int mInputIndex=mVideoDecoder.dequeueInputBuffer(TIME_OUT);
if(mInputIndex>=0)
ByteBuffer buffer=getInputBuffer(mVideoDecoder,mInputIndex);
buffer.clear();
synchronized (Extractor_LOCK)
mExtractor.selectTrack(mVideoDecoderTrack);
int ret = mExtractor.readSampleData(buffer, 0);
if (ret != -1)
mVideoDecoder.queueInputBuffer(mInputIndex, 0, ret, mExtractor.getSampleTime(), mExtractor.getSampleFlags());
isVideoExtractorEnd = !mExtractor.advance();
while (true)
int mOutputIndex=mVideoDecoder.dequeueOutputBuffer(mVideoDecoderBufferInfo,TIME_OUT);
if(mOutputIndex>=0)
mVideoDecoder.releaseOutputBuffer(mOutputIndex,true);
else if(mOutputIndex==MediaCodec.INFO_OUTPUT_FORMAT_CHANGED)
MediaFormat format=mVideoDecoder.getOutputFormat();
else if(mOutputIndex==MediaCodec.INFO_TRY_AGAIN_LATER)
break;
return isVideoExtractorEnd;
视频流编码并混合
在第四步的代码中,已经出现了视频流编码的方法了,也就是videoEncodeStep(boolean)
,其实现如下:
private boolean videoEncodeStep(boolean isEnd)
if(isEnd)
mVideoEncoder.signalEndOfInputStream();
while (true)
int mOutputIndex=mVideoEncoder.dequeueOutputBuffer(mVideoEncoderBufferInfo,TIME_OUT);
if(mOutputIndex>=0)
ByteBuffer buffer=getOutputBuffer(mVideoEncoder,mOutputIndex);
if(mVideoEncoderBufferInfo.size>0)
mMuxer.writeSampleData(mVideoEncoderTrack,buffer,mVideoEncoderBufferInfo);
mVideoEncoder.releaseOutputBuffer(mOutputIndex,false);
else if(mOutputIndex==MediaCodec.INFO_OUTPUT_FORMAT_CHANGED)
MediaFormat format=mVideoEncoder.getOutputFormat();
mVideoEncoderTrack=mMuxer.addTrack(format);
mMuxer.start();
synchronized (MUX_LOCK)
MUX_LOCK.notifyAll();
else if(mOutputIndex==MediaCodec.INFO_TRY_AGAIN_LATER)
break;
return false;
音频流处理
因为现在暂时不需要对视音频处理,所以直接从原始MP4中读取音频流混合到新的Mp4中即可,与解码相同,这个方法也是在线程中循环调用,返回true时终止循环,最后调用MediaMuxer的stop方法,新的视频就生成好了。
private boolean audioDecodeStep(ByteBuffer buffer)
buffer.clear();
synchronized (Extractor_LOCK)
mExtractor.selectTrack(mAudioDecoderTrack);
int length=mExtractor.readSampleData(buffer,0);
if(length!=-1)
int flags=mExtractor.getSampleFlags();
mAudioEncoderBufferInfo.size=length;
mAudioEncoderBufferInfo.flags=flags;
mAudioEncoderBufferInfo.presentationTimeUs=mExtractor.getSampleTime();
mAudioEncoderBufferInfo.offset=0;
mMuxer.writeSampleData(mAudioEncoderTrack,buffer,mAudioEncoderBufferInfo);
isAudioExtractorEnd=!mExtractor.advance();
return isAudioExtractorEnd;
为了不阻塞主线程,音视频的处理单独开一个线程处理为好。
mDecodeThread=new Thread(new Runnable()
@Override
public void run()
//视频处理
while (mCodecFlag&&!videoDecodeStep());
mGLThreadFlag=false;
try
mGLThread.join();
catch (InterruptedException e)
e.printStackTrace();
//将原视频中的音频复制到新视频中
ByteBuffer buffer=ByteBuffer.allocate(1024*32);
while (!audioDecodeStep(buffer));
buffer.clear();
mMuxer.stop();
if(mCompleteListener!=null)
mCompleteListener.onComplete(mOutputPath);
);
其他
源码在github上,有需要的朋友可自行下载,如有帮助欢迎fock和start。如果对于硬编硬解不太理解的,可以查阅官方文档,我的另外一篇博客也有编码的示例,可以参考——Android硬编码——音频编码、视频编码及音视频混合。对于OpenGLES不太熟悉的朋友,可以参考我前面的OpenGLES系列的博客。
欢迎转载,转载请保留文章出处。湖广午王的博客[http://blog.csdn.net/junzia/article/details/77924629]
以上是关于Android利用硬解硬编和OpenGLES来高效的处理MP4视频的主要内容,如果未能解决你的问题,请参考以下文章
Android音视频开发学习MediaCodec API,完成音频AAC硬编硬解
WebRTC[44] - WebRTC 在安卓端的视频硬编硬解策略详解