Android MediaCodec+OpenGL视频编解码实践笔记

Posted vonchenchen1

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android MediaCodec+OpenGL视频编解码实践笔记相关的知识,希望对你有一定的参考价值。

目录

Android MediaCodec+OpenGL视频编解码实践笔记

1.Demo提供的测试功能

2.视频编码与相机本地预览渲染

2.1 初始化编码器与OpenGL环境

2.2 本地预览渲染与编码

3.视频编码与相机本地预览渲染

4.踩坑记录

5.总结


 

android MediaCodec+OpenGL视频编解码实践笔记

本文总结了Android MediaCodec配合OpenGL进行视频编解码以及渲染的相关流程。使用MediaCodec+OpenGL进行视频编解码可以省去数据拷贝的问题,同时可以利用Android自带的硬解码功能提高程序的性能。下文将提供并分析一个Demo,主要涉及调用Android MediaCodec进行编解码,以及渲染相关流程,针对实际工程中SurfaceView推后台等情况进行优化,渲染部分主要参考了Grafik,目前主要在rk3288平台验证。

Demo下载地址

https://download.csdn.net/download/lidec/12559380

1.Demo提供的测试功能

  1. H264编码以及保存视频
  2. H264解码渲染
  3. Opengl绘制相机视频帧
  4. VP8解码渲染(工程根目录下out.vp8是一段使用libvpx中demo编码的vp8视频,ivf封装,可以使用IVFDataReader读取)
  5. H264码率控制模式设置
  6. 可以测试当前编码器设置vbr,cbr是否有效。
  7. H264码率设置(可以动态设置)
  8. H264帧率设置(可以动态设置)
  9. H264 IDR间隔设置
  10. H264插入关键帧
  11. MediaCodec解码后通过Opengl渲染视频
  12. 应用推后台测试,这里主要是需要监听Surface状态,通过一个消息队列控制是否需要重新初始化渲染,编解码使用的surface是通过纹理创建的,所以推后台不会影响编码和解码,只是停止渲染
  13. 相机分辨率选择

2.视频编码与相机本地预览渲染

视频编码采用了自建SurfaceTexure的方式,直接使用自建纹理填入相机,主要实现流程在EncodeTask中。构造函数中传入相机需要渲染的SurfaceView,并监听其中SurfaceHolder的相应事件。这里还自建了一个MsgPipe,内部会开启一个线程,用于处理编码和渲染中的相关状态,包括资源的销毁和重新初始化,这里之所以开启线程还有一个考虑就是给Opengl提供线程,线程消息如下

public void onPipeRecv(CodecMsg msg) 

    if(msg.currentMsg == CodecMsg.MSG.MSG_ENCODE_CAPTURE_FRAME_READY)
        renderAndEncode();
    else if(msg.currentMsg == CodecMsg.MSG.MSG_ENCODE_RESUME_RENDER)
        initGL();
    else if(msg.currentMsg == CodecMsg.MSG.MSG_ENCODE_PAUSE_RENDER) 
        releaseRender();
    else if(msg.currentMsg == CodecMsg.MSG.MSG_ENCODE_STOP_TASK)
        //停止解码任务
        mMsgQueue.stopPipe();
        //发一条空消息 避免线程等待
        CodecMsg msgEmpty = new CodecMsg();
        mMsgQueue.addFirst(msgEmpty);
    else if(msg.currentMsg == CodecMsg.MSG.MSG_ENCODE_CHANGE_BITRATE)
        //改变编码码率
        resetEncodeBitrate(msg);
    else if(msg.currentMsg == CodecMsg.MSG.MSG_ENCODE_CHANGE_FRAMERATE)
        //改变编码帧率
        resetEncodeFramerate(msg);
        
    

下图是编码与本地视频渲染流程的示意图,其中绿色代表本地视频渲染相关功能,蓝色代表MediaCodec编码相关功能,紫色代表OpenGL相关功能。

2.1 初始化编码器与OpenGL环境

开启编码线程后,向编码线程发送一条自定义的MSG_ENCODE_RESUME_RENDER消息,首先在编码线程中创建EGL相关,将要渲染的外部SurfaceView中的Surface传入,并调用makeCurrent方法开启OpenGL环境,这样就可以在线程中进行OpenGL相关操作。借用这个环境完成OpenGL初始化,编译并生成Program,这里注意这个FragmentShader需要一个OES类型的纹理,用来与Camera交互。最终将生成的纹理Id包装成Android的SurfaceTexture,传递给Camera,当Camera开启之后,视频数据就会绘制到这个纹理Id上。

下一步是准备MediaCodec编码器相关,除了正规的初始化操作外,还必须调用MediaCodec的 createInputSurface()方法,拿出MediaCodec内部的Surface,这个Surface用于接收视频帧数据,具体操作就是将上文提到的给相机的纹理Id重新绘制到这个Surface上,这时就可以阻塞读取MediaCodec,读出的数据就是编码好的视频流。这样做的好处是避免了视频帧数据的拷贝,只需要OpenGL绘制就可以传递数据到编码器。

当Camera开启预览时,传入Camera的SurfaceTexture会回调onFrameAvaliable方法,这时相机数据已经就绪,我们向线程队列发送MSG_ENCODE_CAPTURE_FRAME_READY,进入渲染与编码环节的处理。

private void initGL() 

		if(mEglCore == null) 
			//创建egl环境
			mEglCore = new EglCore(null, EglCore.FLAG_RECORDABLE);
		

		if(!mRenderSurface.isValid())
			//如果mRenderSurface没有就绪 直接退出 surfaceCreated触发后会再次触发MSG_ENCODE_RESUME_RENDER事件 调用initGL()
			Logger.i(TAG, "mRenderSurface is not valid");
			return;
		

		try 
			//封装egl与对应的surface
			mRenderWindowSurface = new WindowSurface(mEglCore, mRenderSurface, false);
		catch (Exception e)
			Logger.printErrStackTrace(TAG, e, "create encode WindowSurface Exception:");
			return;
		
		mRenderWindowSurface.makeCurrent();

		if(mInternalTexDrawer == null) 
			//drawer封装opengl program相关
			mInternalTexDrawer = new FullFrameRect(new Texture2dProgram(Texture2dProgram.ProgramType.TEXTURE_EXT));
			//mInternalTexDrawer = new FullFrameRect(new Texture2dProgram(Texture2dProgram.ProgramType.TEXTURE_EXT_BW));
			//mInternalTexDrawer = new FullFrameRect(new Texture2dProgram(Texture2dProgram.ProgramType.TEXTURE_EXT_FILT));
			//绑定一个纹理 根据TEXTURE_EXT 内部绑定一个相机纹理
			mTextureId = mInternalTexDrawer.createTextureObject();
			//使用纹理创建SurfaceTexture 用来接收相机数据
			mCameraTexture = new SurfaceTexture(mTextureId);
			//监听接收数据
			mCameraTexture.setOnFrameAvailableListener(new OnFrameAvailableListener() 
				@Override
				public void onFrameAvailable(SurfaceTexture surfaceTexture) 
					//相机采集到一帧画面
					CodecMsg codecMsg = new CodecMsg();
					codecMsg.currentMsg = CodecMsg.MSG.MSG_ENCODE_CAPTURE_FRAME_READY;
					mMsgQueue.addLast(codecMsg);
				
			);

			if(mOnEncodeTaskEventListener != null)
				mOnEncodeTaskEventListener.onCameraTextureReady(mCameraTexture);
			

			mStreamWidth = mCaptureWidth;//2;
			mStreamHeight = mCaptureHeight;//2;

			mSimpleEncoder = new SimpleEncoder(mStreamWidth, mStreamHeight, mInitFrameRate, MediaFormat.MIMETYPE_VIDEO_AVC, true, mEncodeInfo);
			mSimpleEncoder.setOnCricularEncoderEventListener(mOnCricularEncoderEventListener);
			mSimpleEncoder.setOnInnerEventListener(new SimpleEncoder.OnInnerEventListener() 
				@Override
				public void onFrameRateReceive(int frameRate) 
					//返回当前真实帧率
					mRealFrameRate = frameRate;

					//计算丢帧间隔 如果给定帧率小于等于最大帧率 说明在帧率控制范围内 开始控制帧率
					if(mTargetFrameRate <= CAM_MAX_FRAME_RATE) 

						if(Math.abs(mTargetFrameRate - mRealFrameRate) <= 1)
							return;
						

						int delta = Math.abs(mTargetFrameRate - mRealFrameRate);
						if (mTargetFrameRate < mRealFrameRate) 
							//目标帧率 小于真实帧率 需要增加丢帧数 增加丢帧频率 减小丢帧间隔
							if(mFrameSkipFrameGap > 2) 
								if(delta >= 4)
									mFrameSkipFrameGap -= 2;
								else 
									mFrameSkipFrameGap--;
								
							

						else if(mTargetFrameRate > mRealFrameRate)
							//目标帧率 大于真实帧率 需要减少丢帧数 降低丢帧频率 增大丢帧间隔
							if(mFrameSkipFrameGap < CAM_MAX_FRAME_RATE) 
								if(delta >= 4)
									mFrameSkipFrameGap += 2;
								else 
									mFrameSkipFrameGap++;
								
							
						
					

				
			);
			//getInputSurface()最终获取的是MediaCodec调用createInputSurface()方法创建的Surface
			//这个Surface传入当前egl环境 作为egl的窗口参数(win) 通过eglCreateWindowSurface与egldisplay进行关联
			mEncodeWindowSurface = new WindowSurface(mEglCore, mSimpleEncoder.getInputSurface(), true);

			if (mHDEncoder != null) 
				mHDEncodeWindowSurface = new WindowSurface(mEglCore, mHDEncoder.getInputSurface(), true);
			
		
	

2.2 本地预览渲染与编码

线程收到消息后会进入本地视频预览画面的渲染和编码的环节。上文提到传给Camera的SurfaceTexture已经就绪,我们需要在当前OpenGL线程中调用updateTexImage(),将Camera中图像数据更新到SurfaceTexture的纹理中。注意这个方法必须在OpenGL环境的线程中调用,在上一步初始化的时候makeCurrent相当于开启了OpenGL环境。

下面就可以利用初始化好的Program和其他OpenGL相关变量绘制图片。首先将当前EGL的窗口切换到传入要渲染的SurfaceView的Surface,makeCurrent后进行OpenGL绘制,这样就渲染到窗口中了。之后将窗口切换到MediaCodec生成的Surface,再次调用OpenGL绘制,这次视频帧不渲染到窗口上,而是传递给MediaCodec,绘制完毕后MediaCodec就会对这帧视频进行编码。这个过程中可以对视频帧进行缩放,旋转,镜像和美颜的处理,同时也可以对编码数据进行丢帧从而控制Android帧率。下面代码是Demo中的实现,这里有三次OpenGl绘制,对应三个窗口,一个是预览窗口,另外两个分别编码两路分辨率不同的视频流,用来实现Simulcast。

private void renderAndEncode() 
        //Log.d(TAG, "drawFrame");
        if (mEglCore == null) 
            Log.d(TAG, "Skipping drawFrame after shutdown");
            return;
        

		mCameraTexture.updateTexImage();

        /********* draw to Capture Window **********/
        // Latch the next frame from the camera.
		if(mRenderWindowSurface != null) 
			mRenderWindowSurface.makeCurrent();

			//用于接收相机预览纹理id的SurfaceTexture
			//updateTexImage()方法在OpenGLES环境调用 将数据绑定给OpenGLES对应的纹理对象GL_OES_EGL_image_external 对应shader中samplerExternalOES
			//updateTexImage 完毕后接收下一帧
			//由于在OpenGL ES中,上传纹理(glTexImage2D(), glSubTexImage2D())是一个极为耗时的过程,在1080×1920的屏幕尺寸下传一张全屏的texture需要20~60ms。这样的话SurfaceFlinger就不可能在60fps下运行。
			//因此, Android采用了image native buffer,将graphic buffer直接作为纹理(direct texture)进行操作

			mCameraTexture.getTransformMatrix(mCameraMVPMatrix);
			//显示图像全部 glViewport 传入当前控件的宽高
			GLES20.glViewport(0, 0, mSurfaceWidth, mSurfaceHeight);

			//Matrix.rotateM(mCameraMVPMatrix, 0, 270, 0, 0, 1);

			//通过修改顶点坐标 将采集到的视频按比例缩放到窗口中
			float[] drawVertexMat = ScaleUtils.getScaleVertexMat(mSurfaceWidth, mSurfaceHeight, mCaptureWidth, mCaptureHeight);
			mInternalTexDrawer.rescaleDrawRect(drawVertexMat);
			mInternalTexDrawer.drawFrame(mTextureId, mCameraMVPMatrix);

			mRenderWindowSurface.swapBuffers();
		

		mFrameCount ++;

		mFrameSkipCnt++;

		//实际丢帧处
		if (mFrameSkipCnt != mFrameSkipFrameGap && mFrameSkipCnt % mFrameSkipFrameGap == 0) 
			return;
		

		if(mHDEncoder != null) 
			if(mFrameCount == 1) 
				//mHDEncoder.requestKeyFrame();
			

			mHDEncodeWindowSurface.makeCurrent();
			// 给编码器显示的区域
			GLES20.glViewport(0, 0, mHDEncoder.getWidth() , mHDEncoder.getHeight());
			// 如果是横屏 不需要设置
			Matrix.multiplyMM(mEncodeHDMatrix, 0, mCameraMVPMatrix, 0, mEncodeTextureMatrix, 0);
			// 恢复为基本scales
			mInternalTexDrawer.rescaleDrawRect(mBaseScaleVertexBuf);
			// 下面往编码器绘制数据
			mInternalTexDrawer.drawFrame(mTextureId, mEncodeHDMatrix);
			mHDEncoder.frameAvailableSoon();
			mHDEncodeWindowSurface.setPresentationTime(mCameraTexture.getTimestamp());
			mHDEncodeWindowSurface.swapBuffers();
		

		if(mSimpleEncoder != null) 
			if(mFrameCount == 1 /*|| mFrameCount%10 == 0*/) 
				//mSimpleEncoder.requestKeyFrame();
			

			// 切到当前egl环境
			mEncodeWindowSurface.makeCurrent();
			// 给编码器显示的区域
			GLES20.glViewport(0, 0, mSimpleEncoder.getWidth() , mSimpleEncoder.getHeight());
			// 如果是横屏 不需要设置
			Matrix.multiplyMM(mEncodeMatrix, 0, mCameraMVPMatrix, 0, mEncodeTextureMatrix, 0);
			// 恢复为基本scale
			mInternalTexDrawer.rescaleDrawRect(mBaseScaleVertexBuf);
			// 下面往编码器绘制数据  mEncoderSurface中维护的egl环境中的win就是 mEncoder中MediaCodec中的surface
			// 也就是说这一步其实是往编码器MediaCodec中放入了数据
			mInternalTexDrawer.drawFrame(mTextureId, mEncodeMatrix);
			//通知从MediaCodec中读取编码完毕的数据
			mSimpleEncoder.frameAvailableSoon();
			mEncodeWindowSurface.setPresentationTime(mCameraTexture.getTimestamp());
			mEncodeWindowSurface.swapBuffers();

			Logger.i("lidechen_test", "test3");
			//mEncodeWindowSurface.readImageTest();
		
	

为了保证编码器不阻塞视频帧采集和编码器设置的顺序,编码器在另外一个线程队列中维护。当准备给编码器绘制时先向这个线程发一条消息,线程开始阻塞读取编码器。读取到编码数据后根据不同的info,对于H264分别代表SPS/PPS,关键帧,非关键帧数据。这里SPS/PPS只在配置完编码器后生成,对于实时视频需要在第一次保存起来,手动补到每个关键帧之前。下面代码对应读取编码后的数据以及SPS/PPS的拼接。

@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
        void drainVideoEncoder() 
            final int TIMEOUT_USEC = 0;     // no timeout -- check for buffers, bail if none
            //mVideoEncoder.flush();
            ByteBuffer[] encoderOutputBuffers = mVideoEncoder.getOutputBuffers();
            byte[] outData;

            Logger.d(TAG, "drainVideoEncoder");

            while (true) 
                int encoderStatus = mVideoEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
                if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) 
                    // no output available yet
                    Logger.d(TAG, "drainVideoEncoder INFO_TRY_AGAIN_LATER");
                    break;
                 else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) 
                    // not expected for an encoder
                    Logger.d(TAG, "drainVideoEncoder INFO_OUTPUT_BUFFERS_CHANGED");
                    encoderOutputBuffers = mVideoEncoder.getOutputBuffers();
                 else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) 
                    // Should happen before receiving buffers, and should only happen once.
                    // The MediaFormat contains the csd-0 and csd-1 keys, which we'll need
                    // for MediaMuxer.  It's unclear what else MediaMuxer might want, so
                    // rather than extract the codec-specific data and reconstruct a new
                    // MediaFormat later, we just grab it here and keep it around.
                    mEncodedFormat = mVideoEncoder.getOutputFormat();
                    Logger.d(TAG, "drainVideoEncoder INFO_OUTPUT_FORMAT_CHANGED "+mEncodedFormat);
                    //Logger.d(TAG, "encoder output format changed: " + mEncodedFormat);
                 else if (encoderStatus < 0) 
                    Logger.w(TAG, "unexpected result from encoder.dequeueOutputBuffer: " + encoderStatus);
                    // let's ignore it
                 else 

                    Logger.d(TAG, "drainVideoEncoder mBufferInfo size: "+mBufferInfo.size+" offset: "+mBufferInfo.offset+" pts: "+mBufferInfo.presentationTimeUs);

                    ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
                    if (encodedData == null) 
                        throw new RuntimeException("encoderOutputBuffer " + encoderStatus + " was null");
                    
                    long pts = computePresentationTime(mCount);
                    mCount += 1;
                    // adjust the ByteBuffer values to match BufferInfo (not needed?)
                    outData = new byte[mBufferInfo.size];
                    encodedData.get(outData);

                    if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_CODEC_CONFIG) 

                        //SPS PPS
                        configbyte = new byte[outData.length];
                        System.arraycopy(outData, 0, configbyte, 0, configbyte.length);
                        if (VERBOSE)
                            Logger.v(TAG , "OnEncodedData BUFFER_FLAG_CODEC_CONFIG " + configbyte.length);
                        

                        Logger.d(TAG, "drainVideoEncoder CODEC_CONFIG: "+ toString(outData));

                        if(mOnCricularEncoderEventListener != null)
                            mOnCricularEncoderEventListener.onConfigFrameReceive(outData, mBufferInfo.size, mVideoWidth, mVideoHeight);
                        

                     else if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_SYNC_FRAME) 

                        byte[] keyframe;
                        if(((short)outData[4] & 0x001f) == 0x05)
                            //IDR帧前加入sps pps
                            keyframe = new byte[mBufferInfo.size + configbyte.length];
                            System.arraycopy(configbyte, 0, keyframe, 0, configbyte.length);
                            System.arraycopy(outData, 0, keyframe, configbyte.length, outData.length);
                        else 
                            keyframe = outData;
                        

                        if (VERBOSE) 
                            Logger.v(TAG , "OnEncodedData BUFFER_FLAG_SYNC_FRAME " + keyframe.length);
                        

                        //Logger.d(TAG, "drainVideoEncoder CODEC_SYNC_FRAME: "+ toString(keyframe));

                        if(mOnCricularEncoderEventListener != null)
                            mOnCricularEncoderEventListener.onKeyFrameReceive(keyframe, keyframe.length, mVideoWidth, mVideoHeight);
                        

                        mStatBitrate += keyframe.length * 8;

                        updateEncodeStatistics();

                     else 

                        //Logger.d(TAG, "drainVideoEncoder P_FRAME: "+ toString(outData));

                        if(mOnCricularEncoderEventListener != null)
                            mOnCricularEncoderEventListener.onOtherFrameReceive(outData, outData.length, mVideoWidth, mVideoHeight);
                        

                        mStatBitrate += outData.length * 8;

                        updateEncodeStatistics();
                    

                    mVideoEncoder.releaseOutputBuffer(encoderStatus, false);
                    if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) 
                        break;
                    
                
            
        

以上就是视频编码渲染的主要流程,这里要注意第一次初始化和收到SurfaceView变化的回调后都需要重新初始化编码器,也就是都要向线程发送一条初始化的消息。如果当前APP推后台渲染Surface被销毁,并不影响MediaCodec对应的Surface,视频采集和编码依然可以进行。

3.视频编码与相机本地预览渲染

解码流程和编码流程类似,可以理解为这时MediaCodec作为Decoder,和相机一样,都是向Surface中吐数据。我们创建这个Surface用的纹理Id是本地生成的,一旦解码出数据,就可以调用updateTexImage(),将图片帧传递到当前纹理,这样就可以在渲染OpenGL线程中直接绘制这个纹理了。为了保证顺序以及可以在应用推后台以及恢复等操作后依然可以正常使用OpenGL环境,这里依然使用一个线程队列控制解码渲染的流程。

public void initRender(int width, int height, SurfaceView surfaceView, String mediaFormatType)

        mCurrentFrameWidth = width;
        mCurrentFrameHeight = height;
        mRenderSurfaceView = surfaceView;
        mMediaFormatType = mediaFormatType;

        mRenderSurfaceHolder = mRenderSurfaceView.getHolder();

        mRenderSurfaceHolder.addCallback(mHolderCallback);

        //如果当前渲染surface就绪 则赋值  否则在就绪回调中赋值
        if(surfaceView.getHolder().getSurface().isValid())
            mRenderSurface = mRenderSurfaceHolder.getSurface();
            mSurfaceWidth = mRenderSurfaceView.getMeasuredWidth();
            mSurfaceHeight = mRenderSurfaceView.getMeasuredHeight();
        

        mMsgQueue.setOnPipeListener(new MsgPipe.OnPipeListener<CodecMsg>() 
            @Override
            public void onPipeStart() 

                Logger.i(TAG, "lidechen_test onPipeStart");
            

            @Override
            public void onPipeRecv(CodecMsg msg) 

                int ret = 0;

                if(msg.currentMsg == CodecMsg.MSG.MSG_RESUME_RENDER_TASK) 

                    Logger.d(TAG, "[onPipeRecv] MSG_RESUME_RENDER_TASK");

                    initGLEnv();
                    mIsRenderEnvReady = true;

                    if(mOnRenderEventListener != null)
                        mOnRenderEventListener.onTaskPrepare();
                    
                else if(msg.currentMsg == CodecMsg.MSG.MSG_PAUSE_RENDER_TASK)

                    mIsRenderEnvReady = false;

                    Logger.d(TAG, "[onPipeRecv] MSG_PAUSE_RENDER_TASK");

                    mRenderSurfaceHolder.addCallback(mHolderCallback);
                    releaseRender();

                    //lidechen_test 测试重建解码器 如果关键帧差距过大会导致黑屏
                    //mDecodeWrapper.release();
                    //mDecodeWrapper = null;

                else if(msg.currentMsg == CodecMsg.MSG.MSG_DECODE_FRAME_READY)

                    Logger.d(TAG, "[onPipeRecv] MSG_DECODE_FRAME_READY");


                    if(!mIsRenderEnvReady)
                        return;
                    

                    //解码成功 开始渲染
                    //try 
                        ret = renderToRenderSurface();
                    //catch (Exception e)
                    //    Logger.e(TAG, "lidechen_test onPipeRecv "+e.toString());
                    //
                    //Logger.i(TAG, "lidechen_test renderToRenderSurface ret="+ret);
                else if(msg.currentMsg == CodecMsg.MSG.MSG_STOP_RENDER_TASK)

                    Logger.d(TAG, "[onPipeRecv] MSG_STOP_RENDER_TASK");

                    //停止解码任务
                    mMsgQueue.stopPipe();

                    //发一条空消息 避免线程等待
                    CodecMsg msgEmpty = new CodecMsg();
                    mMsgQueue.addFirst(msgEmpty);
                
            

            @Override
            public void onPipeRelease() 

                //任务停止后清除资源
                release();

                if(mOnRenderEventListener != null)
                    mOnRenderEventListener.onTaskEnd();
                
            
        );

    

具体流程可以参考下图

首先依然要创建EGL相关对象和设置,然后传入当前要渲染的SurfaceView的Surface,这个Surface也承担了EGL开启环境的任务。现在用这个Surface绑定到EGL上,makeCurrent后就开启了OpenGL环境,开始创建OpenGL的Program,创建用于解码的OES纹理,将这个纹理包装为SurfaceTexture后再包装为Surface,传递给MediaCodec作为解码后数据的接收者。这里以后就可以开启解码器了。具体代码如下

private void initGLEnv()

        if(mEglCore == null)
            mEglCore = new EglCore(null, EglCore.FLAG_RECORDABLE);
        

        //初始化渲染窗口
        mRendererWindowSurface = new WindowSurface(mEglCore, mRenderSurface, false);
        mRendererWindowSurface.makeCurrent();

        if(mEXTTexDrawer == null) 
            //drawer封装opengl
            mEXTTexDrawer = new FullFrameRect(new Texture2dProgram(Texture2dProgram.ProgramType.TEXTURE_EXT));
            //绑定一个TEXTURE_2D纹理
            mTextureId = mEXTTexDrawer.createTextureObject();
            //创建一个SurfaceTexture用来接收MediaCodec的解码数据
            mDecodeSurfaceTexture = new SurfaceTexture(mTextureId);
            mDecodeSurfaceTexture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() 
                @Override
                public void onFrameAvailable(SurfaceTexture surfaceTexture) 
                    Logger.i("lidechen_test", "onFrameAvailable");
                
            );
            //监听MediaCodec解码数据到 mDecodeSurfaceTexture
            //使用SurfaceTexture创建一个解码Surface
            mDecodeSurface = new Surface(mDecodeSurfaceTexture);
        

        if(mDecodeWrapper == null) 
            mDecodeWrapper = new DecodeWrapper();
            mDecodeWrapper.init(mCurrentFrameWidth, mCurrentFrameHeight, mDecodeSurface, mMediaFormatType);
            mDecodeWrapper.setOnDecoderEnventLisetener(new SimpleDecoder.OnDecoderEnventLisetener() 
                @Override
                public void onFrameSizeInit(int frameWidth, int frameHeight) 

                

                @Override
                public void onFrameSizeChange(int frameWidth, int frameHeight) 

                
            );
        
    

下面就可以给解码器输入视频流数据了。输入视频流buffer后如果返回的长度大于等于0就说明解码成功,这时我们同样不去直接读取视频帧数据buffer,而是给渲染线程队列发送一个消息MSG_DECODE_FRAME_READY,线程队列收到之后就updateTexImage(),将视频帧绘制到渲染的窗口上。

public int decode(byte[] input, int offset, int count , long pts) 

        ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
        int inputBufferIndex = mMediaCodec.dequeueInputBuffer(DEFAULT_TIMEOUT_US);
        if (inputBufferIndex >= 0) 
            ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
            inputBuffer.clear();
            inputBuffer.put(input, offset, count);
            mMediaCodec.queueInputBuffer(inputBufferIndex, 0, count, pts, 0);
        

        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
        int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(bufferInfo, DEFAULT_TIMEOUT_US);
        Logger.d(TAG , "decode outputBufferIndex " + outputBufferIndex);

        MediaFormat format = mMediaCodec.getOutputFormat();
        mFrameWidth = format.getInteger(MediaFormat.KEY_WIDTH);
        mFrameHeight = format.getInteger(MediaFormat.KEY_HEIGHT);
        Logger.d(TAG , "mFrameWidth=" + mFrameWidth+ " mFrameHeight="+mFrameHeight);

        if (outputBufferIndex >= 0) 

            if(mFrameWidth<= 0||mFrameHeight<= 0)
                //首次解码
                mRecWidth = mFrameWidth;
                mRecHeight = mFrameHeight;

                Logger.d(TAG , "mFrameWidth=" + mFrameWidth+ " mFrameHeight="+mFrameHeight);

                if(mOnDecoderEnventLisetener != null)
                    mOnDecoderEnventLisetener.onFrameSizeInit(mFrameWidth, mFrameHeight);
                
            else

                if(mFrameWidth != mRecWidth || mFrameHeight != mRecHeight)
                    //码流分辨率改变
                    mRecWidth = mFrameWidth;
                    mRecHeight = mFrameHeight;

                    mOnDecoderEnventLisetener.onFrameSizeChange(mFrameWidth, mFrameHeight);
                
            

            mMediaCodec.releaseOutputBuffer(outputBufferIndex, true);
        else if(outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED)
            Logger.i(TAG, "decode info MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED");
        else if(outputBufferIndex == INFO_OUTPUT_FORMAT_CHANGED)
            Logger.i(TAG, "decode info MediaCodec.INFO_OUTPUT_FORMAT_CHANGED");
        else if(outputBufferIndex == INFO_TRY_AGAIN_LATER)
            Logger.i(TAG, "decode info MediaCodec.INFO_TRY_AGAIN_LATER");
        else 
            Logger.i(TAG, "decode info outputBufferIndex="+outputBufferIndex);
        

        return outputBufferIndex;
    

下面是视频帧渲染的代码流程,这里可以对当前帧做处理。

 /**
     * 渲染到外部SurfaceView对应的surface上
     */
    private int renderToRenderSurface()

        mDecodeSurfaceTexture.updateTexImage();

        mDecodeSurfaceTexture.getTransformMatrix(mDecodeMVPMatrix);

        Utils.printMat(mDecodeMVPMatrix, 4, 4);

        mRendererWindowSurface.makeCurrent();
        GLES20.glViewport(0, 0, mSurfaceWidth, mSurfaceHeight);

        float[] vertex = ScaleUtils.getScaleVertexMat(mSurfaceWidth, mSurfaceHeight, mCurrentFrameWidth, mCurrentFrameHeight);
        mEXTTexDrawer.rescaleDrawRect(vertex);

        mEXTTexDrawer.drawFrame(mTextureId, mDecodeMVPMatrix);
        mRendererWindowSurface.swapBuffers();
        return 0;
    

这里就将视频渲染到窗口上了。

4.踩坑记录

最后要记录一下开发过程中实际遇到的问题。由于使用OpenGL绘制必须在EGL环境下,而环境又需要窗口的依赖,一旦推后台窗口就会被销毁,导致时序问题。另外使用RK3288开发板测试时发现一旦远端视频流尺寸发生改变,如果不重新初始化MediaCodec也会导致崩溃。这里详细记录一下调用流程,一旦Surface失效或者尺寸改变,必须进行重新初始化。

首先初始化CircularDecoderToSurface,调用init方法。这里创建RenderTask,并开启阻塞线程,发送一条消息进行初始化。 由于底层首次会根据payloadType创建解码器,而目前初始化EGL环境是在另一个线程异步创建,目前发现如果异步初始化会导致环境没有创建完毕就直接开始解码,这样解码器标志位没有置位,导致有关键帧被跳过,所以这里手动阻塞线程,直到环境创建完毕。

/**
     * 初始化视频解码器
     */
    private void init() 

        mRenderSurfaceView = getSurfaceView(mAccount);

        if(mRenderTask != null)
            mRenderTask.stopRender();
        

        mRenderTask = new RenderTask();
        mRenderTask.setOnRenderEventListener(new RenderTask.OnRenderEventListener() 
            @Override
            public void onTaskPrepareReady() 

                //解码器与渲染环境准本就绪 才能开始解码和渲染
                Logger.i(TAG, "RenderTask prepare ready mAccount="+mAccount);

                mInitSem.release();
            

            @Override
            public void onTaskPrepareError() 

                mInitSem.release();
            

            @Override
            public void onTaskEnd() 

            
        );
        mRenderTask.initRender(mWidth, mHeight, mRenderSurfaceView, mMediaFormatType);
        mRenderTask.startRender();

        try 
            //超时等待1秒 如果释放发生异常 1秒后自动跳过
            mInitSem.tryAcquire(1000, TimeUnit.MILLISECONDS);
         catch (InterruptedException e) 
            Logger.printErrStackTrace(TAG, e, "Exception:");
        
    

无论环境创建成功与否,都不会一直阻塞。如果调用init时Surface没有就绪,则无法给当前EGL绑定Surface,也就无法进行OpenGL相关操作,由于现在手动创建解码用的Surface,必须在EGL环境下创建和编译program,所以如果当前环境没有就绪就会报错。此处在RenderTask中增加相关保护,必须初始化完环境后才允许解码,必须渲染surface就绪才允许渲染,这两点很重要,可以参照流程图中 mIsRenderEnvReady 与 mIsPauseDecode两个值的变化。

关于推后台

对于推后台而言,这里需要重置解码器。主要是监听渲染Surface的生命周期,一旦Surface挂掉,立刻禁止解码,Surface启动后发送MSG_RESUME_RENDER_TASK重置当前mRendererWindowSurface即可,这个对象包装了渲染Surface。

else if(msg.currentMsg == CodecMsg.MSG.MSG_RESET_DECODER)

                    Logger.d(TAG, "[onPipeRecv] MSG_RESET_DECODER");

                    long start = System.currentTimeMillis();

                    mCurrentFrameWidth = msg.currentFrameWidth;
                    mCurrentFrameHeight = msg.currentFrameHeight;

                    release();

                    ret = initRenderEnv();
                    if(ret != 0)
                        //如果reset编码器的时候推后台 会导致egl挂载surface时出现无效的surface的情况 抛出异常导致后续崩溃
                        //这种情况下直接返回 当切前台surface再次生效时 触发MSG_RESUME_RENDER_TASK 重新初始化解码渲染相关
                        return;
                    
                    initDecodeEnv();

                    ret = mDecodeWrapper.decode(msg.data, msg.offset, msg.length, msg.pts);
                    if (ret >= 0) 
                        //解码成功 立刻通知渲染线程渲染
                        CodecMsg msgDec = new CodecMsg();
                        msgDec.currentMsg = CodecMsg.MSG.MSG_DECODE_FRAME_READY;
                        msgDec.currentFrameWidth = msg.currentFrameWidth;
                        msgDec.currentFrameHeight = msg.currentFrameHeight;
                        mMsgQueue.addFirst(msgDec);
                    

                    long end = System.currentTimeMillis();
                    Logger.i(TAG, "[onPipeRecv] MSG_RESET_DECODER spend="+(end-start));

                    mIsPauseDecode = false;

解码SurfaceHolder中监听相关生命周期事件,一旦回调surfaceCreated方法则发送CodecMsg.MSG.MSG_RESET_DECODER消息重置解码器,如果分辨率改变如果崩溃也可以发这个消息进行重置。

class HolderCallback implements SurfaceHolder.Callback

        @Override
        public void surfaceCreated(SurfaceHolder surfaceHolder) 
            mRenderSurface = surfaceHolder.getSurface();

			...
            //发送 CodecMsg.MSG.MSG_RESET_DECODER 消息
            CodecMsg msg = getResumeRenderMsg();
            mMsgQueue.addFirst(msg);
        

        @Override
        public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) 

            ...
        

        @Override
        public void surfaceDestroyed(SurfaceHolder surfaceHolder) 

            CodecMsg msg = getPauseRenderMsg();
            mMsgQueue.addFirst(msg);
        
    

5.总结

本文记录了Android MediaCodec编解码以及OpenGL渲染的主要流程,使用OpenGL直接对纹理进行操作可以省去大量的数据拷贝,对于减少设备发热,提高程序运行效率有者关键的作用。同时也分析了使用线程队列控制OpenGL线程,处理推后台或者改变分辨率的情况下MediaCodec崩溃的解决办法。

以上是关于Android MediaCodec+OpenGL视频编解码实践笔记的主要内容,如果未能解决你的问题,请参考以下文章

Android OpenGL ES 学习 - MediaCodec + OpenGL 解析H264视频+滤镜

Android OpenGL ES 学习 - MediaCodec + OpenGL 解析H264视频+滤镜

Android MediaCodec在异步模式下比在同步模式下慢吗?

android 采集摄像头预览帧,使用opencv和MediaCodec直接录制水印滤镜视频

android 采集摄像头预览帧,使用opencv和MediaCodec直接录制水印滤镜视频

error about opengl