音视频ijkplayer源码解析系列4--ijkplayer里面如何使用SDL渲染

还没有看过前置知识的同学可以先从下面的链接点过去:

音视频 ijkplayer 源码解析系列1--播放器介绍

音视频 ijkplayer源码解析系列2--如何解码图像

音视频ijkplayer源码解析系列3--解码流程

在前面的章节我们看过了分析过了ijkplayer的解码流程,那么他的视频流是如何配合SDL进行渲染的呢,本章会给出答案

1、初始化

首先SDL环境的初始化,ijkplayer屏蔽了渲染的api,对外暴露的方法都是SDL_Vou的对象

cpp 复制代码
SDL_Vout *SDL_VoutAndroid_CreateForANativeWindow()
{
    SDL_Vout *vout = SDL_Vout_CreateInternal(sizeof(SDL_Vout_Opaque));
    if (!vout)
        return NULL;

    SDL_Vout_Opaque *opaque = vout->opaque;
    opaque->native_window = NULL;
    if (ISDL_Array__init(&opaque->overlay_manager, 32))
        goto fail;
    if (ISDL_Array__init(&opaque->overlay_pool, 32))
        goto fail;

    opaque->egl = IJK_EGL_create();
    if (!opaque->egl)
        goto fail;

    vout->opaque_class    = &g_nativewindow_class;
    vout->create_overlay  = func_create_overlay;
    vout->free_l          = func_free_l;
    vout->display_overlay = func_display_overlay;

    return vout;
fail:
    func_free_l(vout);
    return NULL;
}

对应上述初始化代码,主要就是创建IJK_EGL对象,并保存对应的渲染函数。

那我们就不可不谈SDL_Vout里面到底存了哪些东西,我直接在函数定义的地方写注释帮助理解

cpp 复制代码
struct SDL_Vout {
    //显而易见这里就是个锁
    SDL_mutex *mutex;

    SDL_Class       *opaque_class;
    // 存储android窗口相关信息,主要就是窗口ANativeWindow,解码器以及EGL对象
    SDL_Vout_Opaque *opaque;
    // 创建SDL_VoutOverlay对象的函数
    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、渲染函数

sdl渲染函数为func_display_overlay,其关键逻辑为:前置判断window、overlay以及overlay的宽高是否有问题,然后图像格式,执行对象的渲染函数。

就以YUV420的格式为例,他就是执行IJK_EGL_display(opaque->egl, native_window, overlay)。这个函数的主要逻辑,就是借助EGL创建窗口。

IJK_EGL_display里面的实现可以分成如下几个部分:

  1. IJK_EGL_makeCurrent创建EGL环境,核心逻辑就是
    1. eglGetDisplay获取显示设备
    2. eglInitialize初始化
    3. eglChooseConfig配置设置
    4. eglCreateWindowSurface创建EGL的surface,可以理解为画布
    5. eglCreateContext创建EGL的上下文
    6. eglMakeCurrent上下文绑定到当前线程和表面 ,后续操作都是针对这个线程和surface的了
  2. IJK_EGL_display_internal渲染当前的surface,接下来我们拆解下IJK_EGL_display_internal内部干了什么事情

3、拆解IJK_EGL_display_internal

3.1 IJK_EGL_prepareRenderer函数逻辑

  1. 检查渲染器是否初始化:首先会先检查当前EGL的渲染函数egl->opaque->renderer是否初始化。如果发现egl->opaque->renderer失效或者渲染器和当前视频格式不一样,会重新创建一个新的renderer渲染器。

    这个renderer的作用是什么:renderer里封装了opengl的流程,包含了着色器、程序等正常的流程(这部分还是很有意思的,详情可看本文的"4、render渲染器流程")

  2. 然后IJK_GLES2_Renderer_use标记使用当前的渲染器

  3. 之后IJK_EGL_setSurfaceSize设置egl的宽高和ANativeWindow的buffer空间

  4. glViewport设置open gl画布空间

3.2 IJK_GLES2_Renderer_renderOverlay函数逻辑

首先就是检查宽高以及是否旋转屏幕,需要重新渲染。

核心渲染的逻辑其实就是renderer->func_uploadTexture,在"3.1 IJK_EGL_prepareRenderer函数逻辑"中初始化了renderer着色器的时候,会根据图像格式创建对应的渲染方法,func_uploadTexture会根据不同的格式去赋值的。

那我们就以RGB的格式去看整个renderer渲染器的流程。详情可看本文的"4、render渲染器流程"

3.3 eglSwapBuffers交换缓冲器触发渲染

4、renderer渲染器流程

首选初始化是所有流程的第一步,renderer渲染器是根据图片格式初始化的,我们就以常见的rgb888格式为例分析下面的整个流程

cpp 复制代码
IJK_GLES2_Renderer *IJK_GLES2_Renderer_create(SDL_VoutOverlay *overlay)
{
    if (!overlay)
        return NULL;

    IJK_GLES2_printString("Version", GL_VERSION);
    IJK_GLES2_printString("Vendor", GL_VENDOR);
    IJK_GLES2_printString("Renderer", GL_RENDERER);
    IJK_GLES2_printString("Extensions", GL_EXTENSIONS);

    IJK_GLES2_Renderer *renderer = NULL;
    switch (overlay->format) {
        case SDL_FCC_RV16:      renderer = IJK_GLES2_Renderer_create_rgb565(); break;
        case SDL_FCC_RV24:      renderer = IJK_GLES2_Renderer_create_rgb888(); break;
        case SDL_FCC_RV32:      renderer = IJK_GLES2_Renderer_create_rgbx8888(); break;
#ifdef __APPLE__
        case SDL_FCC_NV12:      renderer = IJK_GLES2_Renderer_create_yuv420sp(); break;
        case SDL_FCC__VTB:      renderer = IJK_GLES2_Renderer_create_yuv420sp_vtb(overlay); break;
#endif
        case SDL_FCC_YV12:      renderer = IJK_GLES2_Renderer_create_yuv420p(); break;
        case SDL_FCC_I420:      renderer = IJK_GLES2_Renderer_create_yuv420p(); break;
        case SDL_FCC_I444P10LE: renderer = IJK_GLES2_Renderer_create_yuv444p10le(); break;
        default:
            ALOGE("[GLES2] unknown format %4s(%d)\n", (char *)&overlay->format, overlay->format);
            return NULL;
    }

    renderer->format = overlay->format;
    return renderer;
}

首先就会执行IJK_GLES2_Renderer_create_rgbx8888这个函数。

4.1 创建着色器(顶点和片段)

创建顶点着色器和片段着色器的代码逻辑在IJK_GLES2_Renderer_create_base,它是被各个图像格式函数IJK_GLES2_Renderer_create_****直接调用的。

其实IJK_GLES2_Renderer_create_****这样格式的函数,在创建渲染器的时候,有一段共有逻辑是一致的,就是IJK_GLES2_Renderer_create_base这个函数,我们就从IJK_GLES2_Renderer_create_base开始入手讲:

cpp 复制代码
IJK_GLES2_Renderer *IJK_GLES2_Renderer_create_base(const char *fragment_shader_source)
{
    assert(fragment_shader_source);

    IJK_GLES2_Renderer *renderer = (IJK_GLES2_Renderer *)calloc(1, sizeof(IJK_GLES2_Renderer));
    if (!renderer)
        goto fail;

    renderer->vertex_shader = IJK_GLES2_loadShader(GL_VERTEX_SHADER, IJK_GLES2_getVertexShader_default());
    if (!renderer->vertex_shader)
        goto fail;

    renderer->fragment_shader = IJK_GLES2_loadShader(GL_FRAGMENT_SHADER, fragment_shader_source);
    if (!renderer->fragment_shader)
        goto fail;

    renderer->program = glCreateProgram();                          IJK_GLES2_checkError("glCreateProgram");
    if (!renderer->program)
        goto fail;

    glAttachShader(renderer->program, renderer->vertex_shader);     IJK_GLES2_checkError("glAttachShader(vertex)");
    glAttachShader(renderer->program, renderer->fragment_shader);   IJK_GLES2_checkError("glAttachShader(fragment)");
    glLinkProgram(renderer->program);                               IJK_GLES2_checkError("glLinkProgram");
    GLint link_status = GL_FALSE;
    glGetProgramiv(renderer->program, GL_LINK_STATUS, &link_status);
    if (!link_status)
        goto fail;


    renderer->av4_position = glGetAttribLocation(renderer->program, "av4_Position");                IJK_GLES2_checkError_TRACE("glGetAttribLocation(av4_Position)");
    renderer->av2_texcoord = glGetAttribLocation(renderer->program, "av2_Texcoord");                IJK_GLES2_checkError_TRACE("glGetAttribLocation(av2_Texcoord)");
    renderer->um4_mvp      = glGetUniformLocation(renderer->program, "um4_ModelViewProjection");    IJK_GLES2_checkError_TRACE("glGetUniformLocation(um4_ModelViewProjection)");

    return renderer;

fail:

    if (renderer && renderer->program)
        IJK_GLES2_printProgramInfo(renderer->program);

    IJK_GLES2_Renderer_free(renderer);
    return NULL;
}

对应上述的代码,其实和opengl的流程是相似的。

而创建顶点着色器,而且不管是哪种图像格式,顶点着色器函数都是一样的,都是下面的着色器代码

cpp 复制代码
static const char g_shader[] = IJK_GLES_STRING(
    precision highp float;
    varying   highp vec2 vv2_Texcoord;
    attribute highp vec4 av4_Position;
    attribute highp vec2 av2_Texcoord;
    uniform         mat4 um4_ModelViewProjection;

    void main()
    {
        gl_Position  = um4_ModelViewProjection * av4_Position;
        vv2_Texcoord = av2_Texcoord.xy;
    }
);

IJK_GLES2_loadShader将着色器的加载过程通用封装了起来,不管想加载什么着色器函数都可以直接调用,代码如下:

cpp 复制代码
GLuint IJK_GLES2_loadShader(GLenum shader_type, const char *shader_source)
{
    assert(shader_source);

    GLuint shader = glCreateShader(shader_type);        IJK_GLES2_checkError("glCreateShader");
    if (!shader)
        return 0;

    assert(shader_source);

    glShaderSource(shader, 1, &shader_source, NULL);    IJK_GLES2_checkError_TRACE("glShaderSource");
    glCompileShader(shader);                            IJK_GLES2_checkError_TRACE("glCompileShader");

    GLint compile_status = 0;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &compile_status);
    if (!compile_status)
        goto fail;

    return shader;

fail:

    if (shader) {
        IJK_GLES2_printShaderInfo(shader);
        glDeleteShader(shader);
    }

    return 0;
}

对应上述代码,我们可以发现其在创建着色器时的主要流程是:

  1. 根据传入的着色器类型,调用glCreateShader创建对应的着色器
  2. 根据传入的着色器代码,调用glShaderSource设置着色器的源代码
  3. 调用glCompileShader编译着色器
  4. 调用glGetShaderiv检查着色器编译是否正常

4.2 创建着色器程序

首先先通过glCreateProgram创建着色器程序。

然后使用glAttachShader把顶点着色器和片段着色器都绑定到着色器程序中。

接着通过glLinkProgram把着色器链接成最后的程序。

最后glGetProgramiv检查下程序创建是否正常,具体的代码实现可以参照下面的代码

cpp 复制代码
renderer->program = glCreateProgram();                          IJK_GLES2_checkError("glCreateProgram");
if (!renderer->program)
  goto fail;

glAttachShader(renderer->program, renderer->vertex_shader);     IJK_GLES2_checkError("glAttachShader(vertex)");
glAttachShader(renderer->program, renderer->fragment_shader);   IJK_GLES2_checkError("glAttachShader(fragment)");
glLinkProgram(renderer->program);                               IJK_GLES2_checkError("glLinkProgram");
GLint link_status = GL_FALSE;
glGetProgramiv(renderer->program, GL_LINK_STATUS, &link_status);
if (!link_status)
  goto fail;


renderer->av4_position = glGetAttribLocation(renderer->program, "av4_Position");                IJK_GLES2_checkError_TRACE("glGetAttribLocation(av4_Position)");
    renderer->av2_texcoord = glGetAttribLocation(renderer->program, "av2_Texcoord");                IJK_GLES2_checkError_TRACE("glGetAttribLocation(av2_Texcoord)");
    renderer->um4_mvp      = glGetUniformLocation(renderer->program, "um4_ModelViewProjection");    IJK_GLES2_checkError_TRACE("glGetUniformLocation(um4_ModelViewProjection)");

除了上述流程以外,还会通过glGetAttribLocation把顶点着色器的属性:av4_Positionav2_Texcoordum4_ModelViewProjection位置都存到渲染器里面。

4.3 指定渲染器的渲染相关函数

cpp 复制代码
renderer->us2_sampler[0] = glGetUniformLocation(renderer->program, "us2_SamplerX"); IJK_GLES2_checkError_TRACE("glGetUniformLocation(us2_SamplerX)");

renderer->func_use            = rgb_use;
renderer->func_getBufferWidth = rgbx8888_getBufferWidth;
renderer->func_uploadTexture  = rgbx8888_uploadTexture;

对照上述的代码,可以发现流程上就是获取片段着色器采样器属性位置,然后保存渲染相关的函数,其中:

  • func_use作用:主要就是指定着色器函数和创建纹理对象(glGenTextures)、绑定纹理单位(glActiveTexture、glBindTexture)

    纹理单元是由图像硬件提供的,数量是有限的。例如OpenGL要求实现至少提供16个纹理单元,但具体的数量取决于具体的硬件和驱动。你可以通过glGetIntegerv(GL_MAX_TEXTURE_UNITS, &num)来查询可用的纹理单元数量。

    纹理对象是由用户创建的,理论上数量是无限的,只受制于系统的内存。你可以通过glGenTextures函数来创建纹理对象,通过glDeleteTextures函数来删除纹理对象。

    你可以将纹理对象想象为一种资源,例如一张图片或一帧视频。你可以创建多个纹理对象,每个对象包含不同的资源。然后,你可以通过glBindTexture函数将这些资源绑定到纹理对象,然后在着色器中使用这些纹理单元来访问这些资源

  • func_getBufferWidth作用:计算缓存中图像宽度,比如rgb888,相当于每一个颜色是一个字节,那么他的计算方式就是

    cpp 复制代码
    static GLsizei rgbx8888_getBufferWidth(IJK_GLES2_Renderer *renderer, SDL_VoutOverlay *overlay)
    {
        if (!overlay)
            return 0;
    
        return overlay->pitches[0] / 4;
    }
  • rgbx8888_uploadTexture作用:用纹理来渲染一帧图像,其核心的函数就是glBindTexture和glTextImage2D

4.4 实际渲染

我们从一帧图像实际渲染的地方开始

video_refresh_thread线程while内循环执行,源码在最后,可以结合函数调用路径去看源码:

  1. video_image_display2函数内开始取图像流队列数据

  2. 取出的一帧数据为Frame *vp,其实就是做好音画同步以后,正常拿到当前的一帧图像

  3. 取出vp-bmp,执行SDL_VoutDisplayYUVOverlay去渲染,从这个函数命名可以得知这个方式是把YUV图像直接渲染在SDL的界面上。

    这里的vp->bmp的类型是SDL_VoutOverlay,是从什么实际创建的呢?

    答案是在SDL_VoutAndroid_CreateForANativeWindow函数里面SDL_Vout,而vout->display_overlay就是创建SDL_VoutOverlay方法、

cpp 复制代码
static void video_image_display2(FFPlayer *ffp)
{
    VideoState *is = ffp->is;
    Frame *vp;
    Frame *sp = NULL;

    vp = frame_queue_peek_last(&is->pictq);

    if (vp->bmp) {
        if (is->subtitle_st) {
            if (frame_queue_nb_remaining(&is->subpq) > 0) {
                sp = frame_queue_peek(&is->subpq);

                if (vp->pts >= sp->pts + ((float) sp->sub.start_display_time / 1000)) {
                    if (!sp->uploaded) {
                        if (sp->sub.num_rects > 0) {
                            char buffered_text[4096];
                            if (sp->sub.rects[0]->text) {
                                strncpy(buffered_text, sp->sub.rects[0]->text, 4096);
                            }
                            else if (sp->sub.rects[0]->ass) {
                                parse_ass_subtitle(sp->sub.rects[0]->ass, buffered_text);
                            }
                            ffp_notify_msg4(ffp, FFP_MSG_TIMED_TEXT, 0, 0, buffered_text, sizeof(buffered_text));
                        }
                        sp->uploaded = 1;
                    }
                }
            }
        }
        if (ffp->render_wait_start && !ffp->start_on_prepared && is->pause_req) {
            if (!ffp->first_video_frame_rendered) {
                ffp->first_video_frame_rendered = 1;
                ffp_notify_msg1(ffp, FFP_MSG_VIDEO_RENDERING_START);
            }
            while (is->pause_req && !is->abort_request) {
                SDL_Delay(20);
            }
        }
        SDL_VoutDisplayYUVOverlay(ffp->vout, vp->bmp);
        ffp->stat.vfps = SDL_SpeedSamplerAdd(&ffp->vfps_sampler, FFP_SHOW_VFPS_FFPLAY, "vfps[ffplay]");
        if (!ffp->first_video_frame_rendered) {
            ffp->first_video_frame_rendered = 1;
            ffp_notify_msg1(ffp, FFP_MSG_VIDEO_RENDERING_START);
        }

        if (is->latest_video_seek_load_serial == vp->serial) {
            int latest_video_seek_load_serial = __atomic_exchange_n(&(is->latest_video_seek_load_serial), -1, memory_order_seq_cst);
            if (latest_video_seek_load_serial == vp->serial) {
                ffp->stat.latest_seek_load_duration = (av_gettime() - is->latest_seek_load_start_at) / 1000;
                if (ffp->av_sync_type == AV_SYNC_VIDEO_MASTER) {
                    ffp_notify_msg2(ffp, FFP_MSG_VIDEO_SEEK_RENDERING_START, 1);
                } else {
                    ffp_notify_msg2(ffp, FFP_MSG_VIDEO_SEEK_RENDERING_START, 0);
                }
            }
        }
    }
}
相关推荐
x007xyz1 个月前
前端纯手工绘制音频波形图
前端·音视频开发·canvas
音视频牛哥1 个月前
Android摄像头采集选Camera1还是Camera2?
音视频开发·视频编码·直播
九酒2 个月前
【harmonyOS NEXT 下的前端开发者】WAV音频编码实现
前端·harmonyos·音视频开发
音视频牛哥2 个月前
结合GB/T28181规范探讨Android平台设备接入模块心跳实现
音视频开发·视频编码·直播
哔哩哔哩技术2 个月前
自研点直播转码核心
音视频开发
音视频牛哥2 个月前
Android平台轻量级RTSP服务模块二次封装版调用说明
音视频开发·视频编码·直播
音视频牛哥2 个月前
Android平台RTSP|RTMP直播播放器技术接入说明
音视频开发·视频编码·直播
山雨楼2 个月前
ExoPlayer架构详解与源码分析(15)——Renderer
android·架构·音视频开发
音视频牛哥2 个月前
Windows平台如何实现多路RTSP|RTMP流合成后录像或转发RTMP服务
音视频开发·视频编码·直播
音视频牛哥2 个月前
GB28181设备接入模块和轻量级RTSP服务有什么区别?
音视频开发·视频编码·直播