Android利用硬解码和OpenGL ES来高效处理MP4视频

来源:互联网 发布:丹麦域名后缀 编辑:程序博客网 时间:2024/05/16 05:19


今日科技快讯


昨日,一则关于彭于晏出柜的谣言突然引发关注,让一向低调的高瓴资本和它的创始人张磊走入公众视野,他是腾讯和京东等公司的早期投资人。对于传出的谣言,彭于晏工作室深夜微博辟谣,称一切纯属捏造,谣言止于智者,已经交给律师在处理。也有一位接近张磊的高瓴资本人士声称,这是无稽谣言,清者自清。


作者简介


本篇来自 湖广午王 的投稿,分享了  Android利用硬解硬编和OpenGLES来高效的处理MP4视频

湖广午王 的博客地址:

http://blog.csdn.net/junzia/article/details/77924629


开始


最近工作中遇到一个问题,就是要对视频增加视频特效,实现类似于抖音的效果,抖音的效果由其他同事实现,我的工作重心在视频的处理,特效的集成。按照之前的思路很快就实现了这个功能,但是实际应用到项目中时却遇到各种问题。于是就有了这篇博客。

说是各种问题,特效方便的不管,我所遇到的视频处理的问题主要为以下两个方面:

  1. 处理过程耗时较长。因为处理的时候是按照之前的思路,用 MediaCodec 解码,取出ByteBuffer,然后用 OpenGLES 处理,处理完毕后 readPixels,得到图像数据,然后将图像数据推入 MediaCodec 编码。 在这里readPixels非常耗时。480*840的视频,一帧耗时基本是40ms+。

  2. 手机兼容性很成问题。虽然不需要考虑低版本兼容,只需要考虑4.4+的手机。但是Android手机市场的情况,开发者朋友们应该也都知道,各家有各家的小动作,混乱不堪。解码出来的视频数据,并不是固定的格式,虽然大多数手机都支持YUV420P或者YUV420SP,但是也有些奇葩手机,只能解码出OMX_QCOM_COLOR_FormatYUV420PackedSemiPlanar32m 这类的格式,总不能都去判断然后根据格式去转换吧。

之前看官方文档的时候,有看到 MediaCodec 解码视频支持直接解码到Surface上,编码也可以直接从Surface采集数据,这样的话,视频数据可以直接解码到Surface上,然后通过OpenGLES 处理,再又通过 Surface 进行编码,就无需关注解码出来的数据的格式了,而且应用层也不必自己去将原始数据导入 GPU 以及将处理后的数据导出GPU了,这些工作可以都丢给 Android SDK 去做。理论上就能一举解决上面的两个问题。那么具体应该如何做呢?


处理流程


有了理论,剩下的就是实现了。不卖关子,根据以上的方案,直接列出处理的流程:

  1. 利用MediaExtractor获取Mp4的音轨和视轨,获取音频视频的 MediaFormat.

  2. 根据音视频信息,创建视频解码器,视频编码器,音频暂时不处理就不创建编解码器了。其中视频解码器的Surface是通过先创建一个 SurfaceTexture,然后将这个SurfaceTexture 作为参数创建的,这样的话,视频流就可以通过这个 SurfaceTexture 提供给OpenGL环境作为输出。视频编码器的 Surface 可直接调用 createInputSurface()方法创建,这个 Surface 后续传递给OpenGL环境作为输出

  3. 创建 MediaMuxer,用于后面合成处理后的视频和音频。

  4. 创建 OpenGL 环境,用于处理视频图像,这个 OpenGL 环境由 EGL 创建,EGLSurface 为 WindowSurface,并以编码器创建的 Surface 作为参数。

  5. MediaExtractor 读取原始 Mp4 中的视频流,交由解码器解码到Surface上。

  6. SurfaceTexture 监听有视频帧时,通知 OpenGL 线程工作,处理视频图像,并渲染。

  7. OpenGL 线程每次渲染完毕,通知编码线程进行编码,编码后的数据通过 MediaMuxer 混合。

  8. 视频流处理完毕后,利用 MediaExtractor 读取音频流,并利用 MediaMuxer 混合到新的视频文件中。

  9. 处理完毕后调用 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 上,地址是:

https://github.com/doggycoder/AAVT

有需要的朋友可自行下载,如有帮助欢迎 fock 和 start。如果对于硬编硬解不太理解的,可以查阅官方文档,我的另外一篇博客也有编码的示例,可以参考——Android硬编码——音频编码、视频编码及音视频混合,地址为:

http://blog.csdn.net/junzia/article/details/54018671

对于 OpenGLES 不太熟悉的朋友,可以参考我前面的 OpenGLES 系列的博客。


欢迎长按下图 -> 识别图中二维码

或者 扫一扫 关注我的公众号