Ffmpeg之native渲染YUV视频

Android 平台 不能直接渲染YUV格式视频,需要做YUV 转换 RGB,再去渲染,转换的方式有两种:

1、使用ffmpeg的转换api,将解码后的原数据转换为RGB格式数据,再渲染

2、使用opengl GPU 做转换 渲染

本文章主要介绍第二种方式基于NDK来做渲染(使用GPU做渲染,可以提高效率)

使用GPU 来做渲染 就需要 接触到 EGL 和 OPENGL 这两个词,这里摘录一些介绍和使用:

通俗上讲,OpenGL是一个操作GPU的API,它通过驱动向GPU发送相关指令,控制图形渲染管线状态机的运行状态。但OpenGL需要本地视窗系统进行交互,这就需要一个中间控制层,最好与平台无关。

EGL——因此被独立的设计出来,它作为OpenGL ES和本地窗口的桥梁。

EGL 是 OpenGL ES(嵌入式)和底层 Native 平台视窗系统之间的接口。EGL API 是独立于OpenGL ES各版本标准的独立API,其主要作用是为OpenGL指令创建 Context 、绘制目标Surface 、配置Framebuffer属性、Swap提交绘制结果等。此外,EGL为GPU厂商和OS窗口系统之间提供了一个标准配置接口。

一般来说,OpenGL ES 图形管线的状态被存储于 EGL 管理的一个Context中。而Frame Buffers 和其他绘制 Surfaces 通过 EGL API进行创建、管理和销毁。 EGL 同时也控制和提供了对设备显示和可能的设备渲染配置的访问。

EGL标准是C的,在Android系统Java层封装了相关API。

摘录自https://gameinstitute.qq.com/community/detail/103360

了解了EGL 和 OPENGL 就需要知道怎使用了,上面的连接已经 详细介绍了,API的调用流程,这里直接跳出代码

int openEglYuv(ANativeWindow *window) {
    
    EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
    if (display == EGL_NO_DISPLAY) {
        LOGE("YUV : get Display failure")
        return -1;
    }
    EGLint major;
    EGLint minor;
    EGLBoolean success = eglInitialize(display, &major, &minor);
    if (!success) {
        eglTerminate(display);
        LOGE("YUV : init Display failure")
        return -1;
    }
    global_Context.eglDisplay = display;
    EGLConfig config;
    EGLint configNum;
    EGLint configSpec[] = {
            EGL_RED_SIZE, 8,
            EGL_GREEN_SIZE, 8,
            EGL_BLUE_SIZE, 8,
            EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
//            EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
            EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
            EGL_STENCIL_SIZE, 8,
            EGL_NONE
    };


    success = eglChooseConfig(display, configSpec, &config, 1, &configNum);
    if (!success) {
        eglTerminate(display);
        LOGE("YUV : ChooseConfig failure")
        return -1;
    }


    EGLSurface surface = eglCreateWindowSurface(display, config, window, 0);
    if (surface == EGL_NO_SURFACE) {
        LOGE("YUV : CreateWindowSurface failure")
        eglTerminate(display);
        return -1;
    }
    global_Context.eglSurface = surface;
    EGLSurface psurface = eglCreatePbufferSurface(display, config, 0);
    if (psurface == EGL_NO_SURFACE) {
        LOGE("YUV : CreateWindowSurface failure")
        eglTerminate(display);
        return -1;
    }
    global_Context.eglPsurface = psurface;


    EGLint ctxAttr[] = {
            EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE
    };
    EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, ctxAttr);
    if (context == EGL_NO_CONTEXT) {
        eglDestroySurface(display, psurface);
        eglTerminate(display);
        LOGE("YUV : CreateContext failure")
        return -1;
    }

    global_Context.eglContext = context;


    return 0;

}

这一段代码只是做了相关的初始化工作,现在来编写 渲染相关的逻辑,这里写下步骤:

1、编写 顶点,片元 着色器 字符串。

2、加载 顶点,片元着色器

3、创建  渲染工程,管理着色器

4、提取着色器 参数,初始化 纹理(配置渲染逻辑)

5、渲染YUV

顶点 ,片元 着色器的语法,没怎么接触,不做详细分析,这里贴出代码:

const char *vertexShader_ = GET_STR(
        attribute
        vec4 aPosition;//输入的顶点座标,会在程序指定将数据输入到该字段
        attribute
        vec2 aTextCoord;//输入的纹理座标,会在程序指定将数据输入到该字段
        varying
        vec2 vTextCoord;//输出的纹理座标
        void main() {
            //这里其实是将上下翻转过来(因为安卓图片会自动上下翻转,所以转回来)
            vTextCoord = vec2(aTextCoord.x, 1.0 - aTextCoord.y);
            //直接把传入的座标值作为传入渲染管线。gl_Position是OpenGL内置的
            gl_Position = aPosition;
        }
);

const char *fragYUV420P_ = GET_STR(
        precision
        mediump float;
        varying
        vec2 vTextCoord;
        //输入的yuv三个纹理
        uniform
        sampler2D yTexture;//采样器
        uniform
        sampler2D uTexture;//采样器
        uniform
        sampler2D vTexture;//采样器
        void main() {
            vec3 yuv;
            vec3 rgb;
            //分别取yuv各个分量的采样纹理(r表示?)
            //
            yuv.x = texture2D(yTexture, vTextCoord).g;
            yuv.y = texture2D(uTexture, vTextCoord).g - 0.5;
            yuv.z = texture2D(vTexture, vTextCoord).g - 0.5;
            rgb = mat3(
                    1.0, 1.0, 1.0,
                    0.0, -0.39465, 2.03211,
                    1.13983, -0.5806, 0.0
            ) * yuv;
            //gl_FragColor是OpenGL内置的
            gl_FragColor = vec4(rgb, 1.0);
        }

);

2,装载着色器:

/**
 * 加载  着色器
 * @param type     着色器类型
 * @param shaderSrc  着色源码
 * @return
 */
GLuint LoadShader(GLenum type, const char *shaderSrc) {
    GLuint shader;
    GLint compiled;
    shader = glCreateShader(type);  //  创建 着色器 句柄
    if (shader == 0) {
        LOGE("create shader error");
        return 0;
    }

    // 装载 着色器源码
    glShaderSource(shader, 1, &shaderSrc, nullptr);

    // 编译着色器
    glCompileShader(shader);

    // 检测编译状态
    glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);

    if (!compiled) {
        GLint infoLen = 0;

        // 获取日志长度
        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLen);
        if (infoLen > 1) {
            GLchar *infoLog = static_cast<GLchar *>(malloc(sizeof(GLchar) * infoLen));
            // 获取日志 信息
            glGetShaderInfoLog(shader, infoLen, nullptr, infoLog);
            LOGE("%s", infoLog);
            free(infoLog);
        }
        glDeleteShader(shader);
        return 0;
    }
    return shader;
}

3,创建 渲染工程

GLuint LoadProgramYUV( ){
    GLint vertexShader = LoadShader(GL_VERTEX_SHADER,vertexShader_);
    GLint fragShader = LoadShader(GL_FRAGMENT_SHADER,fragYUV420P_);
    // 创建渲染程序
    GLint program = glCreateProgram();
    if (program == 0){
        LOGE("YUV : CreateProgram failure")
        glDeleteShader(vertexShader);
        glDeleteShader(fragShader);
        return 0;
    }

    // 先渲染程序中 加入着色器
    glAttachShader(program,vertexShader);
    glAttachShader(program,fragShader);


    // 链接 程序
    glLinkProgram(program);
    GLint  status = 0;
    glGetProgramiv(program,GL_LINK_STATUS,&status);
    if (status == 0){
        LOGE("glLinkProgram failure")
        glDeleteProgram(program);
        return 0;
    }
    global_Context.glProgram = program;
    glUseProgram(program);
    return 1;

}

4、配置渲染逻辑

void RenderYuvConfig(uint width, uint height) {

    /* 加入三维顶点数据*/
    static float ver[] = {
            1.0f, -1.0f, 0.0f,
            -1.0f, -1.0f, 0.0f,
            1.0f, 1.0f, 0.0f,
            -1.0f, 1.0f, 0.0f
    };
//
    GLuint apos = static_cast<GLuint>(glGetAttribLocation(global_Context.glProgram, "aPosition"));
    glEnableVertexAttribArray(apos);
    glVertexAttribPointer(apos, 3, GL_FLOAT, GL_FALSE, 0, ver);
//
//    //加入纹理座标数据
    static float fragment[] = {
            1.0f, 0.0f,
            0.0f, 0.0f,
            1.0f, 1.0f,
            0.0f, 1.0f
    };
    GLuint aTex = static_cast<GLuint>(glGetAttribLocation(global_Context.glProgram, "aTextCoord"));
    glEnableVertexAttribArray(aTex);
    glVertexAttribPointer(aTex, 2, GL_FLOAT, GL_FALSE, 0, fragment);



    //纹理初始化
    //设置纹理层对应的对应采样器?

    /**
     *  //获取一致变量的存储位置
    GLint textureUniformY = glGetUniformLocation(program, "SamplerY");
    GLint textureUniformU = glGetUniformLocation(program, "SamplerU");
    GLint textureUniformV = glGetUniformLocation(program, "SamplerV");
    //对几个纹理采样器变量进行设置
    glUniform1i(textureUniformY, 0);
    glUniform1i(textureUniformU, 1);
    glUniform1i(textureUniformV, 2);
     */
//     //对sampler变量,使用函数glUniform1i和glUniform1iv进行设置
    glUniform1i(glGetUniformLocation(global_Context.glProgram, "yTexture"), 0);
    glUniform1i(glGetUniformLocation(global_Context.glProgram, "uTexture"), 1);
    glUniform1i(glGetUniformLocation(global_Context.glProgram, "vTexture"), 2);






    //纹理ID
//    GLuint texts[3] = {0};
//    //创建若干个纹理对象,并且得到纹理ID
    glGenTextures(3, global_Context.mTextures);
//
//    //绑定纹理。后面的的设置和加载全部作用于当前绑定的纹理对象
//    //GL_TEXTURE0、GL_TEXTURE1、GL_TEXTURE2 的就是纹理单元,GL_TEXTURE_1D、GL_TEXTURE_2D、CUBE_MAP为纹理目标
//    //通过 glBindTexture 函数将纹理目标和纹理绑定后,对纹理目标所进行的操作都反映到对纹理上
    glBindTexture(GL_TEXTURE_2D, global_Context.mTextures[0]);
//    //缩小的过滤器
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
//    //放大的过滤器
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//    //设置纹理的格式和大小
//    // 加载纹理到 OpenGL,读入 buffer 定义的位图数据,并把它复制到当前绑定的纹理对象
//    // 当前绑定的纹理对象就会被附加上纹理图像。
//    //width,height表示每几个像素公用一个yuv元素?比如width / 2表示横向每两个像素使用一个元素?
    glTexImage2D(GL_TEXTURE_2D,
                 0,//细节基本 默认0
                 GL_LUMINANCE,//gpu内部格式 亮度,灰度图(这里就是只取一个亮度的颜色通道的意思)
                 width,//加载的纹理宽度。最好为2的次幂(这里对y分量数据当做指定尺寸算,但显示尺寸会拉伸到全屏?)
                 height,//加载的纹理高度。最好为2的次幂
                 0,//纹理边框
                 GL_LUMINANCE,//数据的像素格式 亮度,灰度图
                 GL_UNSIGNED_BYTE,//像素点存储的数据类型
                 NULL //纹理的数据(先不传)
    );
//
//    //绑定纹理
    glBindTexture(GL_TEXTURE_2D, global_Context.mTextures[1]);
//    //缩小的过滤器
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//    //设置纹理的格式和大小
    glTexImage2D(GL_TEXTURE_2D,
                 0,//细节基本 默认0
                 GL_LUMINANCE,//gpu内部格式 亮度,灰度图(这里就是只取一个颜色通道的意思)
                 width / 2,//u数据数量为屏幕的4分之1
                 height / 2,
                 0,//边框
                 GL_LUMINANCE,//数据的像素格式 亮度,灰度图
                 GL_UNSIGNED_BYTE,//像素点存储的数据类型
                 NULL //纹理的数据(先不传)
    );
//
//    //绑定纹理
    glBindTexture(GL_TEXTURE_2D, global_Context.mTextures[2]);
//    //缩小的过滤器
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//    //设置纹理的格式和大小
    glTexImage2D(GL_TEXTURE_2D,
                 0,//细节基本 默认0
                 GL_LUMINANCE,//gpu内部格式 亮度,灰度图(这里就是只取一个颜色通道的意思)
                 width / 2,
                 height / 2,//v数据数量为屏幕的4分之1
                 0,//边框
                 GL_LUMINANCE,//数据的像素格式 亮度,灰度图
                 GL_UNSIGNED_BYTE,//像素点存储的数据类型
                 NULL //纹理的数据(先不传)
    );


}

5,、渲染

void RenderYUV(GLuint width, GLuint height, AVFrame * frame) {

    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, global_Context.mTextures[0]);
    glTexSubImage2D(GL_TEXTURE_2D, 0,
                    0, 0,
                    width, height,
                    GL_LUMINANCE, GL_UNSIGNED_BYTE,
                    frame->data[0]);


    // 数据 U
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, global_Context.mTextures[1]);
    glTexSubImage2D(GL_TEXTURE_2D, 0,
                    0, 0,
                    width / 2, height / 2,
                    GL_LUMINANCE, GL_UNSIGNED_BYTE,
                    frame->data[1]);


    // 数据 V
    glActiveTexture(GL_TEXTURE2);
    glBindTexture(GL_TEXTURE_2D, global_Context.mTextures[2]);
    glTexSubImage2D(GL_TEXTURE_2D, 0,
                    0, 0,
                    width / 2, height / 2,
                    GL_LUMINANCE, GL_UNSIGNED_BYTE,
                    frame->data[2]);

    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    eglSwapBuffers(global_Context.eglDisplay, global_Context.eglSurface);

}

上面的代码实现了YUV420P 格式的视频,渲染NV21 的 格式视频,的着色器 和 配置是有所不同的这里也贴出来:

渲染 NV21 着色器:

const char *vertexNV21Shader_ = GET_STR(
        attribute
        vec4 aPosition;//输入的顶点座标,会在程序指定将数据输入到该字段
        attribute
        vec2 aTextCoord;//输入的纹理座标,会在程序指定将数据输入到该字段
        varying
        vec2 vTextCoord;//输出的纹理座标
        void main() {
            //这里其实是将上下翻转过来(因为安卓图片会自动上下翻转,所以转回来)
            vTextCoord = vec2(aTextCoord.x, 1.0 - aTextCoord.y);
//                vTextCoord = aTextCoord;
            //直接把传入的座标值作为传入渲染管线。gl_Position是OpenGL内置的
            gl_Position = aPosition;
        }
);

const char *fragYUV420SP_ = GET_STR(
        precision
        mediump float;
        varying
        vec2 vTextCoord;
        //输入的yuv三个纹理
        uniform
        sampler2D yTexture;//采样器
        uniform
        sampler2D uvTexture;//采样器
        const mat3 converMat = mat3(
                1.0, 1.0, 1.0,
                0.0, -0.39456, 2.03211,
                1.13983, -0.58060, 0.0
        );

        void main() {
            vec3 yuv;
            vec3 rgb;
            //分别取yuv各个分量的采样纹理(r表示?)
            //
            yuv.x = texture2D(yTexture, vTextCoord).r;
            yuv.y = texture2D(uvTexture, vTextCoord).r - 0.5;
            yuv.z = texture2D(uvTexture, vTextCoord).a - 0.5;
            //gl_FragColor是OpenGL内置的

            gl_FragColor = vec4(converMat * yuv, 1.0);
        }

);

渲染NV21 配置:

void RenderYuvNV21Config(uint width, uint height) {

    static float ver[] = {
            1.0f, -1.0f, 0.0f,
            -1.0f, -1.0f, 0.0f,
            1.0f, 1.0f, 0.0f,
            -1.0f, 1.0f, 0.0f
    };
    /* 加入三维顶点数据*/
//
    GLuint apos = static_cast<GLuint>(glGetAttribLocation(global_Context.glProgram, "aPosition"));
    glEnableVertexAttribArray(apos);
    glVertexAttribPointer(apos, 3, GL_FLOAT, GL_FALSE, 0, ver);
//
//    //加入纹理座标数据
    static float fragment[] = {
            1.0f, 0.0f,
            0.0f, 0.0f,
            1.0f, 1.0f,
            0.0f, 1.0f
    };
    GLuint aTex = static_cast<GLuint>(glGetAttribLocation(global_Context.glProgram, "aTextCoord"));
    glEnableVertexAttribArray(aTex);
    glVertexAttribPointer(aTex, 2, GL_FLOAT, GL_FALSE, 0, fragment);



    //纹理初始化
    //设置纹理层对应的对应采样器?

    /**
     *  //获取一致变量的存储位置
    GLint textureUniformY = glGetUniformLocation(program, "SamplerY");
    GLint textureUniformU = glGetUniformLocation(program, "SamplerU");
    GLint textureUniformV = glGetUniformLocation(program, "SamplerV");
    //对几个纹理采样器变量进行设置
    glUniform1i(textureUniformY, 0);
    glUniform1i(textureUniformU, 1);
    glUniform1i(textureUniformV, 2);
     */
//     //对sampler变量,使用函数glUniform1i和glUniform1iv进行设置
    glUniform1i(glGetUniformLocation(global_Context.glProgram, "yTexture"), 0);
    glUniform1i(glGetUniformLocation(global_Context.glProgram, "uvTexture"), 1);






    //纹理ID
//    GLuint texts[3] = {0};
//    //创建若干个纹理对象,并且得到纹理ID
    glGenTextures(3, global_Context.mTextures);
//
//    //绑定纹理。后面的的设置和加载全部作用于当前绑定的纹理对象
//    //GL_TEXTURE0、GL_TEXTURE1、GL_TEXTURE2 的就是纹理单元,GL_TEXTURE_1D、GL_TEXTURE_2D、CUBE_MAP为纹理目标
//    //通过 glBindTexture 函数将纹理目标和纹理绑定后,对纹理目标所进行的操作都反映到对纹理上
    glBindTexture(GL_TEXTURE_2D, global_Context.mTextures[0]);
//    //缩小的过滤器
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
//    //放大的过滤器
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//    //设置纹理的格式和大小
//    // 加载纹理到 OpenGL,读入 buffer 定义的位图数据,并把它复制到当前绑定的纹理对象
//    // 当前绑定的纹理对象就会被附加上纹理图像。
//    //width,height表示每几个像素公用一个yuv元素?比如width / 2表示横向每两个像素使用一个元素?
    glTexImage2D(GL_TEXTURE_2D,
                 0,//细节基本 默认0
                 GL_LUMINANCE,//gpu内部格式 亮度,灰度图(这里就是只取一个亮度的颜色通道的意思)
                 width,//加载的纹理宽度。最好为2的次幂(这里对y分量数据当做指定尺寸算,但显示尺寸会拉伸到全屏?)
                 height,//加载的纹理高度。最好为2的次幂
                 0,//纹理边框
                 GL_LUMINANCE,//数据的像素格式 亮度,灰度图
                 GL_UNSIGNED_BYTE,//像素点存储的数据类型
                 NULL //纹理的数据(先不传)
    );
//
//    //绑定纹理
    glBindTexture(GL_TEXTURE_2D, global_Context.mTextures[1]);
//    //缩小的过滤器
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//    //设置纹理的格式和大小
    glTexImage2D(GL_TEXTURE_2D,
                 0,//细节基本 默认0
                 GL_LUMINANCE_ALPHA,//gpu内部格式 亮度,灰度图(这里就是只取一个颜色通道的意思)
                 width / 2,//u数据数量为屏幕的4分之1
                 height / 2,
                 0,//边框
                 GL_LUMINANCE_ALPHA,//数据的像素格式 亮度,灰度图
                 GL_UNSIGNED_BYTE,//像素点存储的数据类型
                 NULL //纹理的数据(先不传)
    );



}

这里 其实只需要两个纹理,我这偷懒,没有去掉

渲染 NV21:

void RenderYUVNV21(GLuint width, GLuint height, AVFrame *frame) {

    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, global_Context.mTextures[0]);
    glTexSubImage2D(GL_TEXTURE_2D, 0,
                    0, 0,
                    width, height,
                    GL_LUMINANCE,
                    GL_UNSIGNED_BYTE,
                    frame->data[0]);


    // 数据 U
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, global_Context.mTextures[1]);
    glTexSubImage2D(GL_TEXTURE_2D, 0,
                    0, 0,
                    width / 2, height / 2,
                    GL_LUMINANCE_ALPHA,
                    GL_UNSIGNED_BYTE,
                    frame->data[1]);




    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    eglSwapBuffers(global_Context.eglDisplay, global_Context.eglSurface);

}

绑定山下文环境:

int makeContext(uint width,uint height,int type) {
    EGLBoolean success = eglMakeCurrent(global_Context.eglDisplay, global_Context.eglSurface,
                                        global_Context.eglPsurface, global_Context.eglContext);
    if (!success) {
        eglDestroySurface(global_Context.eglDisplay, global_Context.eglSurface);
        eglDestroyContext(global_Context.eglDisplay, global_Context.eglContext);
        eglTerminate(global_Context.eglDisplay);
        LOGE("YUV : MakeCurrent failure")
        return -1;
    }
    if (type == 12) {  //yuvj420
        LoadProgramYUV();
        RenderYuvConfig(width,height);
    } else if (type == 23) {
        LoadProgramYUVnv21();  // nv21
        RenderYuvNV21Config(width,height);
    }
    return 0;
}

调用这个方法需要注意:必须和渲染的线程在同一线程里面,否则会没有显示,黑屏,

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