音视频系列--OpenGL+FBO录制视频

上一篇讲了用OpenGL纹理渲染摄像头数据,这一篇打算利用上一篇的开发成果,录制视频。

一、FBO


前面讲的利用OpenGL纹理渲染的Camera数据是直接显示到屏幕的,但是在直播推流时候是不能这么做的,这时候得把用OpenGL渲染出来的数据放到一个容器中,然后用这个容器的数据去显示到屏幕,或者录制,或者去直播推流就行了。这个容器就是这里要介绍的FBO(Frame Buffer object),离屏缓存。

我们需要对纹理进行多次渲染采样时,而这些渲染采样是不需要展示给用户看的,所以我们就可以用一个单独的缓冲对象(离屏渲染)来存储我们的这几次渲染采样的结果,等处理完后才显示到窗口上。文章最后参考部分,有关于FBO的文章,可以参考学习下。

1.1、创建FBO


@Override
public void setSize(int width, int height) {
    super.setSize(width, height);
    releaseFrame();

    //frameBuffer 是图层的一个引用
    frameBuffer = new int[1];
//        实例化fbo,让摄像头的数据先渲染到fbo
    GLES20.glGenFramebuffers(1, frameBuffer, 0);

//        生成一个纹理,相当于图层
    frameTextures = new int[1];
    GLES20.glGenTextures(frameTextures.length, frameTextures, 0);

    // 配置纹理
    for (int i = 0; i < frameTextures.length; i++) {
        //绑定纹理,后续配置纹理,开始操作纹理,后续操作是原子操作
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, frameTextures[i]);
//            GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER,
//                    GLES20.GL_NEAREST);//放大过滤
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,
                GLES20.GL_LINEAR);//缩小过滤,一般建议这种
        //告诉,gpu操作完了
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
    }

//        开始做绑定操作
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, frameTextures[0]);

    /**
     * 指定一个二维的纹理图片显示方式
     * level
     *     指定细节级别,0级表示基本图像,n级则表示Mipmap缩小n级之后的图像(缩小2^n)
     * internalformat
     *     指定纹理内部格式,必须是下列符号常量之一:GL_ALPHA,GL_LUMINANCE,GL_LUMINANCE_ALPHA,GL_RGB,GL_RGBA。
     * width height
     *     指定纹理图像的宽高,所有实现都支持宽高至少为64 纹素的2D纹理图像和宽高至少为16 纹素的立方体贴图纹理图像 。
     * border
     *     指定边框的宽度。必须为0。
     * format
     *     指定纹理数据的格式。必须匹配internalformat。下面的符号值被接受:GL_ALPHA,GL_RGB,GL_RGBA,GL_LUMINANCE,和GL_LUMINANCE_ALPHA。
     * type
     *     指定纹理数据的数据类型。下面的符号值被接受:GL_UNSIGNED_BYTE,GL_UNSIGNED_SHORT_5_6_5,GL_UNSIGNED_SHORT_4_4_4_4,和GL_UNSIGNED_SHORT_5_5_5_1。
     * data
     *     指定一个指向内存中图像数据的指针。
     */

    GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE,
            null);

    //要开始使用 gpu的fbo,数据区域gpu
    GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBuffer[0]);  //綁定FBO

    //真正发生绑定fbo和纹理(图层)
    GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, frameTextures[0], 0);

    //释放纹理图层
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
    //释放fbo
    GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
}

1.2、绘制到FBO

@Override
public int onDraw(int texture) {
//      数据渲染到fbo中,输出设备就是fbo
    GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBuffer[0]);
    super.onDraw(texture);
    GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);

    return frameTextures[0] ;
}

在这个部分绘制的屏幕数据就是到了FBO所在图层。拿到绘制到FBO的图层索引frameTextures[0],然后不加glBindFramebuffer绑定,就可以绘制到屏幕上。

二、自定义EGL线程


上面数据渲染到了FBO,接着就可以从FBO拿到数据,然后录制成视频。因为需要调用OpenGL相关操作,所以这里需要自定义EGL线程,GLSurfaceView里面默认有一个GLThread线程,已经绑定了EGL。

OpenGL 是跨平台的、专业的图形编程接口,而接口的实现是由厂商来完成的。而当我们使用这组接口完成绘制之后,要把结果显示在屏幕上,就要用到 EGL 来完成这个转换工作。EGL是OpenGL ES 渲染API和本地窗口系统(native platform window system)之间的一个中间接口层,它也主要由厂商来实现。

关于EGL的相关使用,可以参考最后的文章。

public class EGLEnv {
    private EGLDisplay mEglDisplay;
    //    调用 oepngl 的函数
    private EGLContext mEglContext;
    private final EGLConfig mEglConfig;
    private final EGLSurface mEglSurface;

    private ScreenFilter screenFilter;

    //mediacodec提供的场地,变成egl线程
    public EGLEnv(Context context, EGLContext mGlContext, Surface surface, int width, int height) {
        // 获得显示窗口,作为OpenGL的绘制目标
        mEglDisplay= EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);

        if (mEglDisplay == EGL14.EGL_NO_DISPLAY) {
            throw new RuntimeException("eglGetDisplay failed");
        }

//        100%  固定代码
        // 初始化展示窗口
        int[] version = new int[2];
        if(!EGL14.eglInitialize(mEglDisplay, version,0,version,1)) {
            throw new RuntimeException("eglInitialize failed");
        }


        // 配置 属性选项
        int[] configAttribs = {
                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, //opengl es 2.0
                EGL14.EGL_NONE
        };
        int[] numConfigs = new int[1];
        EGLConfig[] configs = new EGLConfig[1];
        //EGL 根据属性选择一个配置
        if (!EGL14.eglChooseConfig(mEglDisplay, configAttribs, 0, configs, 0, configs.length,
                numConfigs, 0)) {
            throw new RuntimeException("EGL error " + EGL14.eglGetError());
        }
        mEglConfig = configs[0];
        int[] context_attrib_list = {
                EGL14.EGL_CONTEXT_CLIENT_VERSION,2,
                EGL14.EGL_NONE
        };
//      创建好了之后,生成上下文 你是可以读取到数据
        mEglContext=EGL14.eglCreateContext(mEglDisplay, mEglConfig, mGlContext, context_attrib_list,0);

        if (mEglContext == EGL14.EGL_NO_CONTEXT){
            throw new RuntimeException("EGL error " + EGL14.eglGetError());
        }
        /**
         * 创建EGLSurface
         */
        int[] surface_attrib_list = {
                EGL14.EGL_NONE
        };

//        录屏推流
        mEglSurface = EGL14.eglCreateWindowSurface(mEglDisplay, mEglConfig, surface, surface_attrib_list, 0);
        // mEglSurface == null
        if (mEglSurface == null){
            throw new RuntimeException("EGL error " + EGL14.eglGetError());
        }
        /**
         * 绑定当前线程的显示器display mEglDisplay  虚拟, 不是物理设备
         */
        if (!EGL14.eglMakeCurrent(mEglDisplay,mEglSurface,mEglSurface,mEglContext)){
            throw new RuntimeException("EGL error " + EGL14.eglGetError());
        }
        screenFilter = new ScreenFilter(context);
        screenFilter.setSize(width,height);

    }

    public void draw(int textureId, long timestamp) {
        screenFilter.onDraw(textureId);
//      给帧缓冲   时间戳
        EGLExt.eglPresentationTimeANDROID(mEglDisplay,mEglSurface,timestamp);
        //EGLSurface是双缓冲模式,交换缓冲区
        EGL14.eglSwapBuffers(mEglDisplay,mEglSurface);
    }

    public void release(){
        EGL14.eglDestroySurface(mEglDisplay,mEglSurface);
        EGL14.eglMakeCurrent(mEglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
                EGL14.EGL_NO_CONTEXT);
        EGL14.eglDestroyContext(mEglDisplay, mEglContext);
        EGL14.eglReleaseThread();
        EGL14.eglTerminate(mEglDisplay);
        screenFilter.release();
    }
}

后面在录制视频时候会向EGL提供一个Surface,当把FBO数据渲染到自定义的EGL显示设备时,后面就可以直接通过MediaCodec编码渲染数据。

三、录制视频


private void codec(boolean endOfStream) {

    //endOfStream为true录制完成
    if(endOfStream){
        mMediaCodec.signalEndOfInputStream();
        return;
    }

//      编码
    while (true) {

        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
        int index = mMediaCodec.dequeueOutputBuffer(bufferInfo, 10_000);
//            编码的地方
        //需要更多数据
        if (index == MediaCodec.INFO_TRY_AGAIN_LATER) {
            //如果是结束那直接退出,否则继续循环
            if (!endOfStream) {
                break;
            }
        } else if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
            //输出格式发生改变  第一次总会调用所以在这里开启混合器
            MediaFormat newFormat = mMediaCodec.getOutputFormat();
            track = mMuxer.addTrack(newFormat);
            mMuxer.start();
        } else if (index == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
            //可以忽略
        } else {
            //调整时间戳
            bufferInfo.presentationTimeUs = (long) (bufferInfo.presentationTimeUs / mSpeed);
            //有时候会出现异常 : timestampUs xxx < lastTimestampUs yyy for Video track
            if (bufferInfo.presentationTimeUs <= mLastTimeStamp) {
                bufferInfo.presentationTimeUs = (long) (mLastTimeStamp + 1_000_000 / 25 / mSpeed);
            }
            mLastTimeStamp = bufferInfo.presentationTimeUs;

            //正常则 index 获得缓冲区下标
            ByteBuffer encodedData = mMediaCodec.getOutputBuffer(index);
            //如果当前的buffer是配置信息,不管它 不用写出去
            if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
                bufferInfo.size = 0;
            }
            if (bufferInfo.size != 0) {
                //设置从哪里开始读数据(读出来就是编码后的数据)
                encodedData.position(bufferInfo.offset);
                //设置能读数据的总长度
                encodedData.limit(bufferInfo.offset + bufferInfo.size);
                //写出为mp4
                mMuxer.writeSampleData(track, encodedData, bufferInfo);
            }
            // 释放这个缓冲区,后续可以存放新的编码后的数据啦
            mMediaCodec.releaseOutputBuffer(index, false);
            // 如果给了结束信号 signalEndOfInputStream
            if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                break;
            }
        }

    }
}

详细代码位置

四、参考


1 .OpenGL 之 EGL 使用实践

2 .OpenGL 之 帧缓冲 使用实践

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值