通过FFmpeg解码和OpenGL的YUV转RGB实现Android视频播放

前言

在我的博文 https://blog.csdn.net/ericbar/article/details/80506390 中,我们在Android平台上,实现了通过FFmpeg在native(C/C++)层进行视频解码,并通过OpenGL实现了硬件渲染工作,减少了CPU的消耗,提高整个视频播放的性能。
但是,FFmpeg解码后的YUV视频数据,仍然是通过软件方式转成RGB565的,效率极其低下,如果我们放开FFmepg的调试打印,应该可以看到类似的log打印:

05-30 14:47:35.231: D/FFmpeg(28248): No accelerated colorspace conversion found from yuv420p to rgb565le.

表明整个格式转换过程没有硬件加速,造成CPU负载较大,效率低,所以本文的目的就是要通过
OpenGL ES来实现YUV转RGB的硬件加速。

实现需求

  1. 只演示视频解码和显示,不进行音频解码,也不做音视频同步,每两帧视频之间通过固定延时来间隔,所以视频播放时存在偏慢和偏快的问题;
  2. 基于FFmpeg来进行解码,而不是基于Android自带的MediaPlayer播放器,也不基于Android的mediacodec硬件解码;
  3. 视频显示层,在JAVA层基于SurfaceView,在原生(本地C/C++)层通过OpenGL来实现渲染(通过EGL与ANativeWindow进行连接);
  4. 解码后的YUV视频数据,需要转换成rgb565le格式,通过OpenGL ES硬件加速的方法来实现;

实现架构

我们在整体上仍然沿用博文《基于FFmpeg和SurfaceView实现Android原生窗口(ANativeWindow)的视频播放》https://blog.csdn.net/ericbar/article/details/80416328 中的播放流程设计,所以这里不再进行累述,差异主要体现在如下三点:
1. 需要将视频最后显示的步骤,由“通过ANativeWindow窗口显示视频”变换成“通过OpenGL显示视频”。这点很好理解,因为我们要借用OpenGL来进行图像渲染;
2. 由于OpenGL的渲染,最终都依赖于平台的适配层与本地显示窗口进行连接,所以我们需要对Android平台的OpenGL适配层(EGL)进行一些初始化管理。有关于EGL的详细介绍,可以参考博文:
https://blog.csdn.net/ericbar/article/details/80506534
3. 视频解码后帧数据,从YUV格式转RGB格式,也通过OpenGL ES来完成,而不通过FFmpeg的软件转换。
下面是本文要实现的视频播放器主要架构图:
结构图3
主要的变化是YUV转RGB也是由OpenGL实现的。

代码结构

代码结构2
代码结构和《基于FFmpeg解码和OpenGL ES渲染的Android视频播放》中提到的一样,但是具体实现发生了变化。

主要代码

本文是在《基于FFmpeg解码和OpenGL ES渲染的Android视频播放》https://blog.csdn.net/ericbar/article/details/80506390 的基础上进一步完善的,所以大部分代码可以沿用,这里重点讲解FFmpeg解码后的视频帧数据如何通过OpenGL进行格式转换,并渲染到屏幕的。
首先看video_thread线程里,解码部分上下文代码,

avcodec_decode_video2(global_context.vcodec_ctx, pFrame, &frameFinished,
                packet);

        /*av_log(NULL, AV_LOG_ERROR,
         "packet_queue_get size is %d, format is %d\n", packet->size,
         pFrame->format);*/

        // Did we get a video frame?
        if (frameFinished) {
            renderSurface(pFrame);
            av_frame_unref(pFrame);
        }

看起来代码简单了,不再需要类似下面的YUV转RGB的函数调用:

av_image_alloc(pict.data, pict.linesize,
                    global_context.vcodec_ctx->width,
                    global_context.vcodec_ctx->height, AV_PIX_FMT_RGB565LE, 16);

// Convert the image into YUV format that SDL uses
img_convert(&pict, AV_PIX_FMT_RGB565LE, (AVPicture *) pFrame,
                    global_context.vcodec_ctx->pix_fmt,
                    global_context.vcodec_ctx->width,
                    global_context.vcodec_ctx->height);

下面是renderSurface()函数的实现,完成了对Render()函数的调用,并处理了退出播放时,纹理的销毁。

void renderSurface(uint8_t *pixel) {

    if (global_context.quit) {
        glDisable(GL_TEXTURE_2D);
        glDeleteTextures(1, &global_context.mTextureID);
        glDeleteProgram(global_context.glProgram);
        return;
    }

    if (global_context.pause) {
        return;
    }

    Render(pixel);
}

下面是Render()函数的源码,

void Render(AVFrame *frame) {
    GLfloat vVertices[] = { 0.0f, 0.5f, 0.0f, -0.5f, -0.5f, 0.0f, 0.5f, -0.5f,
            0.0f };
    // Clear the color buffer
    //glClear(GL_COLOR_BUFFER_BIT);

    // Use the program object
    glUseProgram(global_context.glProgram);

    //Get Uniform Variables Location
    GLint textureUniformY = glGetUniformLocation(global_context.glProgram,
            "tex_y");
    GLint textureUniformU = glGetUniformLocation(global_context.glProgram,
            "tex_u");
    GLint textureUniformV = glGetUniformLocation(global_context.glProgram,
            "tex_v");

    int w = global_context.vcodec_ctx->width;
    int h = global_context.vcodec_ctx->height;
    GLubyte* y = (GLubyte*) frame->data[0];
    GLubyte* u = (GLubyte*) frame->data[1];
    GLubyte* v = (GLubyte*) frame->data[2];
    GLint y_width = frame->linesize[0];
    GLint u_width = frame->linesize[1];
    GLint v_width = frame->linesize[2];

    // Set the viewport
    glViewport(0, 0, y_width, global_context.vcodec_ctx->height);

    //Y
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, global_context.mTextureID[0]);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, y_width, h, 0, GL_LUMINANCE,
            GL_UNSIGNED_BYTE, y);
    glUniform1i(textureUniformY, 0);
    //U
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, global_context.mTextureID[1]);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, u_width, h / 2, 0,
            GL_LUMINANCE, GL_UNSIGNED_BYTE, u);
    glUniform1i(textureUniformU, 1);
    //V
    glActiveTexture(GL_TEXTURE2);
    glBindTexture(GL_TEXTURE_2D, global_context.mTextureID[2]);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, v_width, h / 2, 0,
            GL_LUMINANCE, GL_UNSIGNED_BYTE, v);
    glUniform1i(textureUniformV, 2);

    // Retrieve attribute locations for the shader program.
    GLint aPositionLocation = glGetAttribLocation(global_context.glProgram,
            "a_Position");
    GLint aTextureCoordinatesLocation = glGetAttribLocation(
            global_context.glProgram, "a_TextureCoordinates");

    // Order of coordinates: X, Y, S, T
    // Triangle Fan
    GLfloat VERTEX_DATA[] = { 0.0f, 0.0f, 0.5f, 0.5f, -1.0f, -1.0f, 0.0f, 1.0f,
            1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, -1.0f, 1.0f, 0.0f,
            0.0f, -1.0f, -1.0f, 0.0f, 1.0f };

    glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GL_FLOAT,
            false, STRIDE, VERTEX_DATA);
    glEnableVertexAttribArray(aPositionLocation);

    glVertexAttribPointer(aTextureCoordinatesLocation, POSITION_COMPONENT_COUNT,
            GL_FLOAT, false, STRIDE, &VERTEX_DATA[POSITION_COMPONENT_COUNT]);
    glEnableVertexAttribArray(aTextureCoordinatesLocation);

    glDrawArrays(GL_TRIANGLE_FAN, 0, 6);

    eglSwapBuffers(global_context.eglDisplay, global_context.eglSurface);
}

这里需要注意一下,根据前述文章https://blog.csdn.net/ericbar/article/details/80505658 ,我们知道
FFmpeg解码后YUV数据的存储格式,所以我们在取Y,U,V分量时,按照data数组的索引分别取值即可。另外,解码后的数据存储宽度,并不一定等于像素宽度,因为CPU对齐的原因,存储数据的宽度可能会大于实际像素大小,因此这里要取linesize数组值,而不能采用图像的宽度。

    int w = global_context.vcodec_ctx->width;
    int h = global_context.vcodec_ctx->height;
    GLubyte* y = (GLubyte*) frame->data[0];
    GLubyte* u = (GLubyte*) frame->data[1];
    GLubyte* v = (GLubyte*) frame->data[2];
    GLint y_width = frame->linesize[0];
    GLint u_width = frame->linesize[1];
    GLint v_width = frame->linesize[2];

glViewport函数完成视窗大小的设置,有些码流在播放时可能会有绿屏的边框,通过窗口大小的设置,可以进行裁边去除,需要注意。

//Y
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, global_context.mTextureID[0]);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, y_width, h, 0, GL_LUMINANCE,
            GL_UNSIGNED_BYTE, y);
    glUniform1i(textureUniformY, 0);
    //U
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, global_context.mTextureID[1]);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, u_width, h / 2, 0,
            GL_LUMINANCE, GL_UNSIGNED_BYTE, u);
    glUniform1i(textureUniformU, 1);
    //V
    glActiveTexture(GL_TEXTURE2);
    glBindTexture(GL_TEXTURE_2D, global_context.mTextureID[2]);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, v_width, h / 2, 0,
            GL_LUMINANCE, GL_UNSIGNED_BYTE, v);
    glUniform1i(textureUniformV, 2);

Y,U,V需要三层独立的纹理,在GPU端合成,所以需要分别传入。这里需要注意width参数,取linesize值,而非实际图像宽度。
YUV转RGB的公式有很多,有标清标准BT.601和高清标准BT.709,我没有做深入研究,从网上参考过来的,其着色器编程代码GLSL如下,试验结果看起来效果差不多。

#if 1
            "{                                                                              \n"
            "  vec4 c = vec4((texture2D(tex_y, v_TextureCoordinates).r - 16./255.) * 1.164);\n"
            "  vec4 U = vec4(texture2D(tex_u, v_TextureCoordinates).r - 128./255.);         \n"
            "  vec4 V = vec4(texture2D(tex_v, v_TextureCoordinates).r - 128./255.);         \n"
            "  c += V * vec4(1.596, -0.813, 0, 0);                                          \n"
            "  c += U * vec4(0, -0.392, 2.017, 0);                                          \n"
            "  c.a = 1.0;                                                                   \n"
            "  gl_FragColor = c;                                                            \n"
            "}                                                                              \n";
#else
    "{                                                  \n"
    "       highp float y = texture2D(tex_y, v_TextureCoordinates).r;           \n"
    "       highp float u = texture2D(tex_u, v_TextureCoordinates).r - 0.5;     \n"
    "       highp float v = texture2D(tex_v, v_TextureCoordinates).r - 0.5;     \n"
    "       highp float r = y + 1.402 * v;                                      \n"
    "       highp float g = y - 0.344 * u - 0.714 * v;                          \n"
    "       highp float b = y + 1.772 * u;                                      \n"
    "       gl_FragColor = vec4(r, g, b, 1.0);                                  \n"
    "}                                                                          \n";
#endif

通过采用OpenGL硬件加速的方式后,视频渲染的帧率明显提高(视频播放快进),表明采用GPU进行YUV转RGB,并进行渲染,性能得到提升。

GitHub源码

请参考完整的源码路径:
https://github.com/ericbars/FFmpegYUVRGBOpenGL

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值