目錄
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提供的測試功能
- 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崩潰的解決辦法。