IJKPLAYER源码分析-OpenGL ES渲染

1 前言

  • IJKPLAYER在视频render之时,并非简单使用SDL渲染API,而是用了OpenGL ES,再分别在Android和iOS平台做视频的显示;
  • 一言以蔽之,OpenGL ES并不能做到直接在窗口上render并显示,而是需要一个中间媒介。这个中间媒介,将OpenGL ES的render结果,最终输出到窗口上显示;
  • 在Android端,这个中间媒介是EGL,是surface输出,而iOS端则是EAGL,具体到IJKPLAYER是EAGLContext + CAEAGLLayer;
  • 本文只介绍IJKPLAYER使用OpenGL ES跨Android和iOS平台的视频render,显示部分将另起文章介绍;

    提示:阅读本文需要有一定的OpenGL shader编程基础。

2 接口

2.1 SDL_Vout接口

  • 与SDL_Aout接口类似,SDL_Vout是IJKPLAYER对Android和iOS端视频输出的抽象;
  • 输入端是解码后的像素数据,输出端是OpenGL ES,由OpenGL ES做具体render工作;

    关于SDL_Vout实例的创建,将在Android和iOS平台显示一文再做介绍。此处略过。 

    以下是视频输出在render外围的几个重要结构体定义:

typedef struct SDL_Vout_Opaque {
    ANativeWindow   *native_window;
    SDL_AMediaCodec *acodec;
    int              null_native_window_warned; // reduce log for null window
    int              next_buffer_id;

    ISDL_Array       overlay_manager;
    ISDL_Array       overlay_pool;

    IJK_EGL         *egl;
} SDL_Vout_Opaque;

typedef struct SDL_VoutOverlay_Opaque SDL_VoutOverlay_Opaque;
typedef struct SDL_VoutOverlay SDL_VoutOverlay;
struct SDL_VoutOverlay {
    int w; /**< Read-only */
    int h; /**< Read-only */
    Uint32 format; /**< Read-only */
    int planes; /**< Read-only */
    Uint16 *pitches; /**< in bytes, Read-only */
    Uint8 **pixels; /**< Read-write */

    int is_private;

    int sar_num;
    int sar_den;

    SDL_Class               *opaque_class;
    SDL_VoutOverlay_Opaque  *opaque;

    void    (*free_l)(SDL_VoutOverlay *overlay);
    int     (*lock)(SDL_VoutOverlay *overlay);
    int     (*unlock)(SDL_VoutOverlay *overlay);
    void    (*unref)(SDL_VoutOverlay *overlay);

    int     (*func_fill_frame)(SDL_VoutOverlay *overlay, const AVFrame *frame);
};

typedef struct SDL_Vout_Opaque SDL_Vout_Opaque;
typedef struct SDL_Vout SDL_Vout;
struct SDL_Vout {
    SDL_mutex *mutex;

    SDL_Class       *opaque_class;
    SDL_Vout_Opaque *opaque;
    SDL_VoutOverlay *(*create_overlay)(int width, int height, int frame_format, SDL_Vout *vout);
    void (*free_l)(SDL_Vout *vout);
    int (*display_overlay)(SDL_Vout *vout, SDL_VoutOverlay *overlay);

    Uint32 overlay_format;
};

2.2 render接口

  • IJKPLAYER对像素格式的支持,无外乎yuv系列和rgb系列,以及iOS的videotoolbox的硬解像素格式;
  • 每种像素格式的render遵循共同的接口规范:IJK_GLES2_Renderer;
  • 值得一提的是,若视频源像素格式不在IJKPLAYER所支持的范围内,会提供一个选项,使得视频源的像素格式转换为IJKPLAYER所支持的目标格式,再render;

    像素格式render的接口定义: 

typedef struct IJK_GLES2_Renderer
{
    IJK_GLES2_Renderer_Opaque *opaque;

    GLuint program;

    GLuint vertex_shader;
    GLuint fragment_shader;
    GLuint plane_textures[IJK_GLES2_MAX_PLANE];

    GLuint av4_position;
    GLuint av2_texcoord;
    GLuint um4_mvp;

    GLuint us2_sampler[IJK_GLES2_MAX_PLANE];
    GLuint um3_color_conversion;

    GLboolean (*func_use)(IJK_GLES2_Renderer *renderer);
    GLsizei   (*func_getBufferWidth)(IJK_GLES2_Renderer *renderer, SDL_VoutOverlay *overlay);
    GLboolean (*func_uploadTexture)(IJK_GLES2_Renderer *renderer, SDL_VoutOverlay *overlay);
    GLvoid    (*func_destroy)(IJK_GLES2_Renderer *renderer);

    GLsizei buffer_width;
    GLsizei visible_width;

    GLfloat texcoords[8];

    GLfloat vertices[8];
    int     vertices_changed;

    int     format;
    int     gravity;
    GLsizei layer_width;
    GLsizei layer_height;
    int     frame_width;
    int     frame_height;
    int     frame_sar_num;
    int     frame_sar_den;

    GLsizei last_buffer_width;
} IJK_GLES2_Renderer;

3 render

    此处以常见的yuv420p进行介绍,其他类似,所不同的是视频像素格式排列差异。

3.1 shader

    所谓shader,其实是一段可以在GPU上执行的程序。为何视频render要用到shader?原因在于,可以利用GPU强大的浮点运算和并行计算能力,发挥硬件加速的作用,代替CPU做它不擅长的运算,进行图形render。

    视频的render需要关注的shader有2个,一是顶点shader,一是片段shader。

3.1.1 vertex shader

3.1.2 顶点坐标

typedef struct IJK_GLES2_Renderer
{
    // ......
    // 顶点坐标
    GLfloat vertices[8];
    // ......
} IJK_GLES2_Renderer;
  • 顶点坐标初始化,以确定图像显示区域,还可实现图片的平移、旋转和缩放;
  • iOS端图像缩放可以基于顶点坐标进行等比缩放或拉伸;
  • Android端图像缩放或拉伸是放在Java层实现的;

    顶点坐标的初始化: 

static void IJK_GLES2_Renderer_Vertices_apply(IJK_GLES2_Renderer *renderer)
{
    switch (renderer->gravity) {
        case IJK_GLES2_GRAVITY_RESIZE_ASPECT:
            break;
        case IJK_GLES2_GRAVITY_RESIZE_ASPECT_FILL:
            break;
        case IJK_GLES2_GRAVITY_RESIZE:
            IJK_GLES2_Renderer_Vertices_reset(renderer);
            return;
        default:
            ALOGE("[GLES2] unknown gravity %d\n", renderer->gravity);
            IJK_GLES2_Renderer_Vertices_reset(renderer);
            return;
    }

    if (renderer->layer_width <= 0 ||
        renderer->layer_height <= 0 ||
        renderer->frame_width <= 0 ||
        renderer->frame_height <= 0)
    {
        ALOGE("[GLES2] invalid width/height for gravity aspect\n");
        IJK_GLES2_Renderer_Vertices_reset(renderer);
        return;
    }

    float width     = renderer->frame_width;
    float height    = renderer->frame_height;

    if (renderer->frame_sar_num > 0 && renderer->frame_sar_den > 0) {
        width = width * renderer->frame_sar_num / renderer->frame_sar_den;
    }

    const float dW  = (float)renderer->layer_width	/ width;
    const float dH  = (float)renderer->layer_height / height;
    float dd        = 1.0f;
    float nW        = 1.0f;
    float nH        = 1.0f;

    // 2种等比缩放,以填充指定屏幕:iOS支持,Android则是在Java层处理缩放
    switch (renderer->gravity) {
        case IJK_GLES2_GRAVITY_RESIZE_ASPECT_FILL:  dd = FFMAX(dW, dH); break;
        case IJK_GLES2_GRAVITY_RESIZE_ASPECT:       dd = FFMIN(dW, dH); break;
    }

    nW = (width  * dd / (float)renderer->layer_width);
    nH = (height * dd / (float)renderer->layer_height);

    renderer->vertices[0] = - nW;
    renderer->vertices[1] = - nH;
    renderer->vertices[2] =   nW;
    renderer->vertices[3] = - nH;
    renderer->vertices[4] = - nW;
    renderer->vertices[5] =   nH;
    renderer->vertices[6] =   nW;
    renderer->vertices[7] =   nH;
}

    其实,仔细查看代码,Android和iOS在shader的顶点初始化时,均会调用到此函数,但Android略有不同,由于Android端图像的缩放在Android SDK之上层做,因此,Android端shader的顶点坐标初始化,其实是在以下函数:

static void IJK_GLES2_Renderer_Vertices_reset(IJK_GLES2_Renderer *renderer)
{
    renderer->vertices[0] = -1.0f;
    renderer->vertices[1] = -1.0f;
    renderer->vertices[2] =  1.0f;
    renderer->vertices[3] = -1.0f;
    renderer->vertices[4] = -1.0f;
    renderer->vertices[5] =  1.0f;
    renderer->vertices[6] =  1.0f;
    renderer->vertices[7] =  1.0f;
}

3.1.3 纹理坐标

    为何有了顶点坐标,还要有纹理坐标?个人浅见:

  • 顶点坐标可以确定图像的区域,宽高比,实现平移、旋转及缩放;
  • Sampler需要通过纹理UV坐标进行色彩的采样;
  • 纹理代表一张图片及其描述信息,但显示区域宽高比不一定和纹理图片一致,所以纹理UV坐标可实现按比例进行拉伸;
  • UV坐标是顶点的一个属性,顶点还有其他诸如NDC坐标、色彩等属性;
typedef struct IJK_GLES2_Renderer
{
    // ......

    // 纹理坐标
    GLfloat texcoords[8];
} IJK_GLES2_Renderer;

3.1.4 model view projection

  • model是模型矩阵,view是视图矩阵,projection是投影矩阵。在shader中,我们先将模型矩阵model与视图矩阵view相乘,得到模型视图矩阵mv;

  • 然后将顶点坐标position乘以mv矩阵,并最后乘以投影矩阵projection得到最终的屏幕坐标gl_Position;

  • 这个操作通常在渲染场景之前,在CPU上计算好相应的模型视图矩阵和投影矩阵,然后通过统一变量(uniform variable)传递到GLSL着色器中;

    // 通过model view projection矩阵,将顶点坐标转换为屏幕坐标
    IJK_GLES_Matrix modelViewProj;
    IJK_GLES2_loadOrtho(&modelViewProj, -1.0f, 1.0f, -1.0f, 1.0f, -1.0f, 1.0f);
    glUniformMatrix4fv(renderer->um4_mvp, 1, GL_FALSE, modelViewProj.m);                    IJK_GLES2_checkError_TRACE("glUniformMatrix4fv(um4_mvp)");

    在CPU上计算好model view projecttion矩阵,然后通过uniform variable传递给GLSL shader: 

void IJK_GLES2_loadOrtho(IJK_GLES_Matrix *matrix, GLfloat left, GLfloat right, GLfloat bottom, GLfloat top, GLfloat near, GLfloat far)
{
    GLfloat r_l = right - left;
    GLfloat t_b = top - bottom;
    GLfloat f_n = far - near;
    GLfloat tx = - (right + left) / (right - left);
    GLfloat ty = - (top + bottom) / (top - bottom);
    GLfloat tz = - (far + near) / (far - near);

    matrix->m[0] = 2.0f / r_l;
    matrix->m[1] = 0.0f;
    matrix->m[2] = 0.0f;
    matrix->m[3] = 0.0f;

    matrix->m[4] = 0.0f;
    matrix->m[5] = 2.0f / t_b;
    matrix->m[6] = 0.0f;
    matrix->m[7] = 0.0f;

    matrix->m[8] = 0.0f;
    matrix->m[9] = 0.0f;
    matrix->m[10] = -2.0f / f_n;
    matrix->m[11] = 0.0f;

    matrix->m[12] = tx;
    matrix->m[13] = ty;
    matrix->m[14] = tz;
    matrix->m[15] = 1.0f;
}

3.1.5 fragment shader

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包

    打赏作者

    老中医的博客

    你的鼓励将是我创作的最大动力

    ¥1 ¥2 ¥4 ¥6 ¥10 ¥20
    扫码支付:¥1
    获取中
    扫码支付

    您的余额不足,请更换扫码支付或充值

    打赏作者

    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

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

    余额充值