美文网首页
Android OpenGL ES 十三.MediaCodec录

Android OpenGL ES 十三.MediaCodec录

作者: 有心人2021 | 来源:发表于2021-10-01 16:32 被阅读0次

    参照辉哥的博客,也是一位大牛,以前动脑学院的培训老师,讲的许多挺有深度。

    视频录制涉及的流程还是挺多,首先我们来罗列一下大致的流程:

    1. OpenGL ES 预览相机(Fbo)
    2. MediaCodec 编码相机数据
    3. MediaMuxer 合成输出视频文件

    1. OpenGL 预览相机

    我们需要用到 OpenGL 来渲染相机和采集数据,当然我们也可以直接用 SurfaceView 来预览 Camera,可查看来自stuff的例子,但直接用 SufaceView 并不方便美颜滤镜和加水印贴图。

    为了方便共享渲染同一个纹理,基于GLSurfaceView做了局部修改,前提是需要对 GLSurfaceView 的源码以及渲染流程了如指掌。能在不修改源码的情况下能解决的问题,尽量不要去动源码,因此我们尽量用扩展的方式去实现。

    /**
     * 扩展 GLSurfaceView ,暴露 EGLContext,需要拿到该EGLContext
     * 参照GLSurfaceView中相关源码
     */
    public class BaseGLSurfaceView extends GLSurfaceView {
        /**
         * EGL环境上下文
         */
        protected EGLContext mEglContext;
    
        public BaseGLSurfaceView(Context context) {
            this(context, null);
        }
    
        public BaseGLSurfaceView(Context context, AttributeSet attrs) {
            super(context, attrs);
            // 利用 setEGLContextFactory 这种扩展方式把 EGLContext 暴露出去
            setEGLContextFactory(new EGLContextFactory() {
                @Override
                public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) {
                    int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE};
                    mEglContext = egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list);
                    return mEglContext;
                }
    
                @Override
                public void destroyContext(EGL10 egl, EGLDisplay display, EGLContext context) {
                    if (!egl.eglDestroyContext(display, context)) {
                        Log.e("BaseGLSurfaceView", "display:" + display + " context: " + context);
                    }
                }
            });
        }
    
        /**
         * 通过此方法可以获取 EGL环境上下文,可用于共享渲染同一个纹理
         * @return EGLContext
         */
        public EGLContext getEglContext() {
            return mEglContext;
        }
    }
    

    顺便提醒一下,我们需要用扩展纹理属性,否则相机画面无法渲染出来,同时采用 FBO 离屏渲染来绘制,因为有些实际开发场景需要加一些水印或者是贴纸等等。
    FBO整体代码

        @Override
        public void onDrawFrame(GL10 gl) {
            // 绑定 fbo
            mFboRender.onBindFbo();
            GLES20.glUseProgram(mProgram);
            mCameraSt.updateTexImage();
    
            // 设置正交投影参数
            GLES20.glUniformMatrix4fv(uMatrix, 1, false, matrix, 0);
    
            GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVboId);
            /**
             * 设置坐标
             * 2:2个为一个点
             * GLES20.GL_FLOAT:float 类型
             * false:不做归一化
             * 8:步长是 8
             */
            GLES20.glEnableVertexAttribArray(vPosition);
            GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 8,
                    0);
            GLES20.glEnableVertexAttribArray(fPosition);
            GLES20.glVertexAttribPointer(fPosition, 2, GLES20.GL_FLOAT, false, 8,
                    mVertexCoordinate.length * 4);
    
            // 绘制到 fbo
            GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
            // 解绑
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
            GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
            mFboRender.onUnbindFbo();
            // 再把 fbo 绘制到屏幕
            mFboRender.onDrawFrame();
        }
    

    2. MediaCodec 编码相机数据

    相机渲染显示后,接下来我们开一个线程去共享渲染相机的纹理,并且把数据绘制到 MediaCodec 的 InputSurface 上。

        /**
         * 视频录制的渲染线程
         */
        public static final class VideoRenderThread extends Thread {
            private WeakReference<BaseVideoRecorder> mVideoRecorderWr;
            private boolean mShouldExit = false;
            private boolean mHashCreateContext = false;
            private boolean mHashSurfaceChanged = false;
            private boolean mHashSurfaceCreated = false;
            private EglHelper mEGlHelper;
            private int mWidth;
            private int mHeight;
    
            public VideoRenderThread(WeakReference<BaseVideoRecorder> videoRecorderWr) {
                this.mVideoRecorderWr = videoRecorderWr;
                mEGlHelper = new EglHelper();
            }
    
            public void setSize(int width, int height) {
                this.mWidth = width;
                this.mHeight = height;
            }
    
            @Override
            public void run() {
                while (true) {
                    if (mShouldExit) {
                        onDestroy();
                        return;
                    }
    
                    BaseVideoRecorder videoRecorder = mVideoRecorderWr.get();
                    if (videoRecorder == null) {
                        mShouldExit = true;
                        continue;
                    }
    
                    if (!mHashCreateContext) {
                        // 初始化创建 EGL 环境
                        mEGlHelper.initCreateEgl(videoRecorder.mSurface, videoRecorder.mEglContext);
                        mHashCreateContext = true;
                    }
    
                    GL10 gl = (GL10) mEGlHelper.getEglContext().getGL();
    
                    if (!mHashSurfaceCreated) {
                        // 回调 onSurfaceCreated
                        videoRecorder.mRenderer.onSurfaceCreated(gl, mEGlHelper.getEGLConfig());
                        mHashSurfaceCreated = true;
                    }
    
                    if (!mHashSurfaceChanged) {
                        // 回调 onSurfaceChanged
                        videoRecorder.mRenderer.onSurfaceChanged(gl, mWidth, mHeight);
                        mHashSurfaceChanged = true;
                    }
    
                    // 回调 onDrawFrame
                    videoRecorder.mRenderer.onDrawFrame(gl);
    
                    // 绘制到 MediaCodec 的 Surface 上面去
                    mEGlHelper.swapBuffers();
    
                    try {
                        // 60 fps
                        Thread.sleep(16);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
    
            private void onDestroy() {
                mEGlHelper.destroy();
            }
    
            public void requestExit() {
                mShouldExit = true;
            }
        }
    复制代码
    

    Thread.sleep(16);这地方注意,Thread.sleep是毫秒,辉哥写的是Thread.sleep(16/1000),会导致在按下录制按钮视频录制完成后,存在花屏的现象,查看视频的帧率,从10跳到1010,比对了和stuff的代码设定的参数等,也没发现任何异常,至于为什么,有待后续研究。可能绘制的输入数据过多,丢掉了部分帧信息。

    3. MediaMuxer 合成输出视频文件

    目前已有两个线程,一个线程是相机渲染到屏幕显示,一个线程是共享相机渲染纹理绘制到 MediaCodec 的 InputSurface 上。那么我们还需要一个线程用 MediaCodec 编码合成视频文件。

        /**
         * 视频的编码线程
         */
        public static final class VideoEncoderThread extends Thread {
            private WeakReference<BaseVideoRecorder> mVideoRecorderWr;
    
            private volatile boolean mShouldExit;
    
            private MediaCodec mVideoCodec;
            private MediaCodec.BufferInfo mBufferInfo;
            private MediaMuxer mMediaMuxer;
    
            /**
             * 视频轨道
             */
            private int mVideoTrackIndex = -1;
    
            private long mVideoPts = 0;
    
            public VideoEncoderThread(WeakReference<BaseVideoRecorder> videoRecorderWr) {
                this.mVideoRecorderWr = videoRecorderWr;
                mVideoCodec = videoRecorderWr.get().mVideoCodec;
                mBufferInfo = new MediaCodec.BufferInfo();
                mMediaMuxer = videoRecorderWr.get().mMediaMuxer;
            }
    
            @Override
            public void run() {
                mShouldExit = false;
                mVideoCodec.start();
    
                while (true) {
                    if (mShouldExit) {
                        onDestroy();
                        return;
                    }
    
                    int outputBufferIndex = mVideoCodec.dequeueOutputBuffer(mBufferInfo, 0);
    
                    if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                        mVideoTrackIndex = mMediaMuxer.addTrack(mVideoCodec.getOutputFormat());
                        mMediaMuxer.start();
                    } else {
                        while (outputBufferIndex >= 0) {
                            // 获取数据
                            ByteBuffer outBuffer = mVideoCodec.getOutputBuffers()[outputBufferIndex];
                            outBuffer.position(mBufferInfo.offset);
                            outBuffer.limit(mBufferInfo.offset + mBufferInfo.size);
    
                            // 修改视频的 pts
                            if (mVideoPts == 0) {
                                mVideoPts = mBufferInfo.presentationTimeUs;
                            }
                            mBufferInfo.presentationTimeUs -= mVideoPts;
    
                            // 写入数据
                            mMediaMuxer.writeSampleData(mVideoTrackIndex, outBuffer, mBufferInfo);
    
                            // 回调当前录制时间
                            if (mVideoRecorderWr.get().mRecordInfoListener != null) {
                                mVideoRecorderWr.get().mRecordInfoListener.onTime(mBufferInfo.presentationTimeUs / 1000);
                            }
    
                            // 释放 OutputBuffer
                            mVideoCodec.releaseOutputBuffer(outputBufferIndex, false);
                            outputBufferIndex = mVideoCodec.dequeueOutputBuffer(mBufferInfo, 0);
                        }
                    }
                }
            }
    
            private void onDestroy() {
                // 先释放 MediaCodec
                mVideoCodec.stop();
                mVideoCodec.release();
                // 后释放 MediaMuxer
                mMediaMuxer.stop();
                mMediaMuxer.release();
            }
    
            public void requestExit() {
                mShouldExit = true;
            }
        }
    复制代码
    

    在不深究解编码协议的前提下,只是把效果写出来还是很简单的,但一出现问题往往就无法下手了,因此还是有必要去深究一些原理。

    参考

    1. Android MediaCodec 退坑指南
    2. Android MediaCodec stuff (bigflake.com)

    转自作者:红橙Darren
    转自链接:https://juejin.cn/post/6844903904736460813

    具体使用代码如下,包含了音频录制,下一节对音频进行讲解https://github.com/445979241/opengles

    相关文章

      网友评论

          本文标题:Android OpenGL ES 十三.MediaCodec录

          本文链接:https://www.haomeiwen.com/subject/vlkrnltx.html