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崩潰的解決辦法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章