Android MediaCodec简单总结
Posted 丞恤猿
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android MediaCodec简单总结相关的知识,希望对你有一定的参考价值。
#.MedaiCodec简介 MediaCodec是android中提供的音视频编码、解码工具。它主要是完成上层接口的封装,提供给开发者使用,编解码功能实际是在native底层服务中完成的。 #.MediaCodec工作的宏观流程: ##.包换两个缓冲区队列 一个输入缓冲区队列,包含一组输入缓冲区(格式ByteBuffer); 一个输出缓冲区队列,包含一组输出缓冲区(格式ByteBuffer); ##.使用中,需要不断重复以下过程:1.把原始数据放入输入缓冲区队列中一个空缓冲区上; 2.编/解码器从输入缓冲队列中获取缓冲区上数据,进行编码处理,结果存放到输出缓冲区上一个空缓冲区上, 处理完毕后,释放该输入缓冲区,它会被重新放回输入缓冲区队列,以便下次重复使用; 3.对输出缓冲区上数据做自己需要的业务处理处理,处理完毕后,释放该输出缓冲区, 它会被重新放回输出缓冲区队列,以便下次重复使用。
##.当对视频帧进行编/解码时,一般会用编码器创建一个输入Surface或为编码器设置一个输出Surface,在这里Surface充当着数据缓冲区的角色。 使用Surface可以提高编/解码器的性能,Surface直接使用native视频数据缓存,没有映射或复制它们到ByteBuffers,这种方式会更加高效。#.内部的状态机和处理流程 ##.状态机,三类状态:
1.停止态(Stopped): 1.1 未初始化状态(Uninitialized) 1.2 配置状态(Configured) 1.3 错误状态(Error) 2.执行态(Executing): 2.1 刷新状态(Flushed) 2.2 运行状态( Running) 2.3 流结束状态(End-of-Stream) 3.释放态(Released)##.操作流程:
1.一般流程为创建编/解码器,此时处于未初始化状态(Uninitialized); 2.调用configure(…)方法对编解码器进行配置,使编解码器转入配置状态(Configured); 3.调用start()方法,使其转入执行刷新状态(Flushed); 4.此时编解码器已经拥有其输入/输出缓存,当第一个输入缓存区被移出队列,编解码器转入运行状态( Running); 5.在运行状态( Running)中,编解码器不断对输入缓冲区中数据做编解码操作,结果存到输出缓冲区; 6.当一个带有end-of-stream标记的输入缓存入队列时,编解码器将转入流结束状态(End-of-Stream)。 在这种状态下,编解码器不再接收新的输入缓存,但它仍然产生输出缓存。直到 7.当输入缓存中所有数据都被处理完,带有end-of-stream标记的数据帧到达输出缓存后,转入释放态(Released)。 当我们处理完输出数据后,在此状态下可以用release()进行相关资源的释放。##.状态转化其它相关要点:
1.在执行状态(Executing)下的任何时候,通过调用flush()方法使编解码器重新返回到刷新子状态(Flushed); 2.通过调用stop()方法使编解码器返回到未初始化状态(Uninitialized),此时这个编解码器可以再次重新配置; 3.编解码器遇到错误时会进入错误状态(Error),此时应调用reset()方法使编解码器再次可用。 4.任何状态下调用reset()方法使编解码器返回到未初始化状态(Uninitialized)##.MediaCodec相关API介绍:
createDecoderByType:获取解码器对象 createEncoderBytype:获取编码器对象 configure:对编解码器进行配置,使编解码器转入配置状态 start:使编码器转入执行刷新状态 stop:结束并返回到未初始化状态 release:释放实例资源 createInputSurface:创建输入缓冲Surface setOutputSurface:设置输出缓冲Surface getInputBuffers:获取需要编码数据的输入流队列,返回的是一个ByteBuffer数组 queueInputBuffer:输入流入队列 dequeueInputBuffer:从输入流队列中取数据进行编码操作 getOutputBuffers:获取编解码之后的数据输出流队列,返回的是一个ByteBuffer数组 dequeueOutputBuffer:从输出队列中取出编码操作之后的数据 releaseOutputBuffer:处理完成,释放ByteBuffer数据##.MediaCodec的两种工作方式
1.同步方式:数据输入和数据输出依次进行。 要等待上一次数据输入后,才能数据输出; 等待上一次数据输出后,才能再次进行数据输入。 2.异步方式:数据输入和数据输出操作顺序相互独立。 是底层服务来判断何时输入/输出可以进行,然后进行相应回调, 开发者在回调中进行数据输入/输出处理。
//异步处理方式时,需要设置回调接口。
//在回调中进行数据处理
mEncoder.setCallback(new MediaCodec.Callback()
//有可用的输入缓冲区时回调
@Override
public void onInputBufferAvailable(@NonNull MediaCodec codec, int index)
//有可用的输出缓冲区时回调
@Override
public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info)
//输出格式变化时回调
@Override
public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format)
//出错时回调
@Override
public void onError(@NonNull MediaCodec codec, @NonNull MediaCodec.CodecException e)
);
Android MediaCodec+OpenGL视频编解码实践笔记
目录
Android MediaCodec+OpenGL视频编解码实践笔记
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提供的测试功能
- H264编码以及保存视频
- H264解码渲染
- Opengl绘制相机视频帧
- VP8解码渲染(工程根目录下out.vp8是一段使用libvpx中demo编码的vp8视频,ivf封装,可以使用IVFDataReader读取)
- H264码率控制模式设置
- 可以测试当前编码器设置vbr,cbr是否有效。
- H264码率设置(可以动态设置)
- H264帧率设置(可以动态设置)
- H264 IDR间隔设置
- H264插入关键帧
- MediaCodec解码后通过Opengl渲染视频
- 应用推后台测试,这里主要是需要监听Surface状态,通过一个消息队列控制是否需要重新初始化渲染,编解码使用的surface是通过纹理创建的,所以推后台不会影响编码和解码,只是停止渲染
- 相机分辨率选择
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简单总结的主要内容,如果未能解决你的问题,请参考以下文章
Android Multimedia框架总结(二十一)MediaCodec中创建到start过程(到jni部分)
Android MediaCodec+OpenGL视频编解码实践笔记
Android MediaCodec+OpenGL视频编解码实践笔记
Android Multimedia框架总结(二十)MediaCodec状态周期及Codec与输入/输出Buffer过程(附实例)