Android OpenGL+Camera2渲染(5) —— 录制视频,实现快录慢录

Android OpenGL+Camera2渲染(1) —— OpenGL简单介绍

Android OpenGL+Camera2渲染(2) —— OpenGL实现Camera2图像预览

Android OpenGL+Camera2渲染(3) —— 大眼,贴纸功能实现

Android OpenGL+Camera2渲染(4) —— 美颜功能实现

Android OpenGL+Camera2渲染(5) —— 录制视频,实现快录慢录

 

这篇文章是在前面几篇的基础上实现的,把美颜,大眼,贴纸的效果,使用MediaCodec编码后录制成视频,支持快录和慢录的功能。

首先 我们进过处理之后的数据是存在纹理当中的,而MediaCodec 编码的输入队列支持把surface作为输入源,那么这两者如何关联呢? 方案是这样的。 自己配置一个EGL环境,把这个EGL环境中的EGLSurface和mediacodec的surface绑定,把当前的EGL环境和我们处理美颜滤镜的EGL绑定,这样就可以在当前的EGL环境中操作美颜滤镜的EGL环境中的纹理了。然后把数据输出的MediaCodec的surface中。

 

在开始录制的时候,取初始化编码器MediaCodec、MediaMuxer 和 配置EGL环境进行绑定。

public void start(float speed, String savePath) {
        mSavePath = savePath;
        mSpeed = speed;

        try {
            MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, mWidth, mHeight);

            mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);

            mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, mWidth * mHeight * fps / 5);

            mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, fps);

            mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, fps);

            mediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);

            mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

            inputSurface = mediaCodec.createInputSurface();

            mediaMuxer = new MediaMuxer(mSavePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);


            //-------------配置EGL环境----------------

            HandlerThread handlerThread = new HandlerThread("elgCodec");
            handlerThread.start();

            mHandler = new Handler(handlerThread.getLooper());
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    eglConfigBase = new EglConfigBase(mContext, mWidth, mHeight, inputSurface, eglContext);
                    mediaCodec.start();
                    isPlaying = true;
                }
            });

        } catch (IOException e) {
            e.printStackTrace();
        }


    }

这里传入一个speed,这个就和快录慢录有关系了,这个之后在讲。这里创建MediaCodec,通过

mediaCodec.createInputSurface()拿到了一个输入的Surface,使用 HandlerThread 创建了一个在子线程中处理的Handler,在这个线程中创建EGL环境。

eglConfigBase = new EglConfigBase(mContext, mWidth, mHeight, inputSurface, eglContext);

这里传入的 eglContext 其实就是美颜大眼的EGLContext,通过它产生关联。

    /**
     * @param context
     * @param width
     * @param height
     * @param surface    MediaCodec创建的surface 我们需要将其贴到我们的虚拟屏幕上去
     * @param eglContext GLThread的EGL上下文
     */
    public EglConfigBase(Context context, int width, int height, Surface surface, EGLContext eglContext) {
        //配置EGL环境
        createEGL(eglContext);
        //把Surface贴到  mEglDisplay ,发生关系
        int[] attrib_list = {
                EGL14.EGL_NONE
        };
        // 绘制线程中的图像 就是往这个mEglSurface 上面去画
        mEglSurface = EGL14.eglCreateWindowSurface(mEglDisplay, mEglConfig, surface, attrib_list, 0);
        // 绑定当前线程的显示设备及上下文, 之后操作opengl,就是在这个虚拟显示上操作
        if (!EGL14.eglMakeCurrent(mEglDisplay,mEglSurface,mEglSurface,mEglContext)) {
            throw  new RuntimeException("eglMakeCurrent 失败!");
        }
        //像虚拟屏幕画
        mScreenFilter = new ScreenFilter(context);
        mScreenFilter.onReady(width,height);
    }
private void createEGL(EGLContext eglContext) {
        //创建 虚拟显示器
        mEglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
        if (mEglDisplay == EGL14.EGL_NO_DISPLAY) {
            throw new RuntimeException("eglGetDisplay failed");
        }
        //初始化显示器
        int[] version = new int[2];
        // 12.1020203
        //major:主版本 记录在 version[0]
        //minor : 子版本 记录在 version[1]
        if (!EGL14.eglInitialize(mEglDisplay, version, 0, version, 1)) {
            throw new RuntimeException("eglInitialize failed");
        }
        // egl 根据我们配置的属性 选择一个配置
        int[] attrib_list = {
                EGL14.EGL_RED_SIZE, 8, // 缓冲区中 红分量 位数
                EGL14.EGL_GREEN_SIZE, 8,
                EGL14.EGL_BLUE_SIZE, 8,
                EGL14.EGL_ALPHA_SIZE, 8,
                EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, //egl版本 2
                EGL14.EGL_NONE
        };

        EGLConfig[] configs = new EGLConfig[1];
        int[] num_config = new int[1];
        // attrib_list:属性列表+属性列表的第几个开始
        // configs:获取的配置 (输出参数)
        //num_config: 长度和 configs 一样就行了
        if (!EGL14.eglChooseConfig(mEglDisplay, attrib_list, 0,
                configs, 0, configs.length, num_config, 0)) {
            throw new IllegalArgumentException("eglChooseConfig#2 failed");
        }
        mEglConfig = configs[0];
        int[] ctx_attrib_list = {
                EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, //egl版本 2
                EGL14.EGL_NONE
        };
        //创建EGL上下文
        // 3 share_context: 共享上下文 传绘制线程(GLThread)中的EGL上下文 达到共享资源的目的 发生关系
        mEglContext = EGL14.eglCreateContext(mEglDisplay, mEglConfig, eglContext, ctx_attrib_list
                , 0);
        // 创建失败
        if (mEglContext == EGL14.EGL_NO_CONTEXT) {
            throw new RuntimeException("EGL Context Error.");
        }
    }

这里的代码可以参考GlSurfaceView中的配置去写,发生关系就是在

mEglSurface = EGL14.eglCreateWindowSurface(mEglDisplay, mEglConfig, surface, attrib_list, 0);

这句产生的关联。

 

这里还创建了 ScreenFilter,用它 把外部的纹理信息(美颜滤镜后的纹理)写入到MediaCodec的Surface中。

看一下 EglConfigBase对外部提供的 draw 方法。

    public void draw(int textureId, long timestamp) {
        if (!EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, mCurrentEglContext)) {
            throw new RuntimeException("eglMakeCurrent 失败!");
        }


        screenFilter.onDrawFrame(textureId);

        EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, timestamp);
        //交换数据,输出到mediacodec InputSurface中
        EGL14.eglSwapBuffers(eglDisplay, eglSurface);

    }

 在写之前,需要绑定当前的EGL 环境,这里需要传入时间戳,在调用了screenFilter.onDrawFrame(textureId); 方法之后,需要刷新eglSurface的时间戳, eglSwapBuffers 交换数据, EGL的工作模式是双缓存模式, 内部有两个frame buffer (fb),当EGL将一个fb 显示屏幕上,另一个就在后台等待opengl进行交换。所以draw完之后,需要把后台的buffer显示到前台。

在 GlRenderWrapper 的 onDrawFrame

 @Override
    public void onDrawFrame(GL10 gl) {
        int textureId;
        // 配置屏幕
        //清理屏幕 :告诉opengl 需要把屏幕清理成什么颜色
        GLES20.glClearColor(0, 0, 0, 0);
        //执行上一个:glClearColor配置的屏幕颜色
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

        //更新获取一张图
        mSurfaceTexture.updateTexImage();

        mSurfaceTexture.getTransformMatrix(mtx);
        //cameraFiler需要一个矩阵,是Surface和我们手机屏幕的一个坐标之间的关系
        cameraFilter.setMatrix(mtx);

        textureId = cameraFilter.onDrawFrame(mTextures[0]);
        if (bigEyeEnable) {
            bigeyeFilter.setFace(tracker.mFace);
            textureId = bigeyeFilter.onDrawFrame(textureId);
        }

        if (beautyEnable) {
            textureId = beaytyFilter.onDrawFrame(textureId);
        }

        if (stickEnable) {
            stickerFilter.setFace(tracker.mFace);
            textureId = stickerFilter.onDrawFrame(textureId);
        }

        int id = screenFilter.onDrawFrame(textureId);
        //进行录制
        avcRecorder.encodeFrame(id, mSurfaceTexture.getTimestamp());

    }

最后就是把纹理ID,拿去录制。

encodeFrame

public void encodeFrame(final int textureId, final long timeStamp) {

        if (!isPlaying) return;

        mHandler.post(new Runnable() {
            @Override
            public void run() {
                eglConfigBase.draw(textureId, timeStamp);
                getCodec(false);
            }
        });

    }

这里的Handler 就是刚刚创建EGL环境的Handler,所有此EGL环境中的处理都需要在此Handler线程中进行。执行eglConfigBase.draw 去写入到MediaCodec的Surface中。

getCodec 就是负责去从MediaCodec的输出队列中,拿编码完成的数据,交给MediaMuxer进行保存。

 

private void getCodec(boolean endOfStream) {
        if (endOfStream) {
            mediaCodec.signalEndOfInputStream();
        }

        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();

        int status = mediaCodec.dequeueOutputBuffer(bufferInfo, 10_000);
        //表示请求超时,10_000毫秒内没有数据到来
        if (status == MediaCodec.INFO_TRY_AGAIN_LATER) {

        } else if (status == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
            //编码格式改变 ,第一次start 都会调用一次
            MediaFormat outputFormat = mediaCodec.getOutputFormat();
            //设置mediaMuxer 的视频轨
            avcIndex = mediaMuxer.addTrack(outputFormat);
            mediaMuxer.start();
        } else if (status == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
            //Outputbuffer  改变了
        } else {
            ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(status);
            if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
                bufferInfo.size = 0;
            }


            if (bufferInfo.size > 0) {
                bufferInfo.presentationTimeUs = (long) (bufferInfo.presentationTimeUs / mSpeed);
                outputBuffer.position(bufferInfo.offset);

                outputBuffer.limit(bufferInfo.size - bufferInfo.offset);
                //交给mediaMuxer 保存
                mediaMuxer.writeSampleData(avcIndex, outputBuffer, bufferInfo);

            }

            mediaCodec.releaseOutputBuffer(status, false);

            if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {

            }
        }

    }

在第28行, bufferInfo.presentationTimeUs = (long) (bufferInfo.presentationTimeUs / mSpeed); 把正常的pts时间戳做一个转换,这里的pts是一个时间段的相对时间戳,如果要实现快录,speed设置一个小于1的值,慢录就设置一个大于1的值。在这里上面提到startRecord的时候会传递进来一个mSpeed,是这样设置的。

 

 public void startRecord() {
        float speed = 1.f;
        switch (mSpeed) {
            case MODE_EXTRA_SLOW:
                speed = 0.3f;
                break;
            case MODE_SLOW:
                speed = 0.5f;
                break;
            case MODE_NORMAL:
                speed = 1.f;
                break;
            case MODE_FAST:
                speed = 1.5f;
                break;
            case MODE_EXTRA_FAST:
                speed = 3.f;
                break;
        }
        glRender.startRecord(speed, savePath);
    }

 

停止录制调用 stop()

    public void stop() {
        isPlaying = false;

        mHandler.post(new Runnable() {
            @Override
            public void run() {
                getCodec(true);
                mediaCodec.stop();
                mediaCodec.release();
                mediaCodec = null;
                mediaMuxer.stop();
                mediaMuxer.release();
                mediaMuxer = null;
                eglConfigBase.release();
                eglConfigBase = null;
                inputSurface.release();
                inputSurface = null;
                mHandler.getLooper().quitSafely();
                mHandler = null;
                if (onRecordListener != null) {
                    onRecordListener.recordFinish(mSavePath);
                }
            }
        });
    }

 

github项目地址:https://github.com/wangchao0837/OpenGlCameraRender

你可能感兴趣的:(Android OpenGL+Camera2渲染(5) —— 录制视频,实现快录慢录)