还没有看过前置知识的同学可以先从下面的链接点过去:
在前面的章节我们看过了分析过了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
里面的实现可以分成如下几个部分:
IJK_EGL_makeCurrent
创建EGL环境,核心逻辑就是eglGetDisplay
获取显示设备eglInitialize
初始化eglChooseConfig
配置设置eglCreateWindowSurface
创建EGL的surface,可以理解为画布eglCreateContext
创建EGL的上下文eglMakeCurrent
上下文绑定到当前线程和表面 ,后续操作都是针对这个线程和surface的了
IJK_EGL_display_internal
渲染当前的surface,接下来我们拆解下IJK_EGL_display_internal
内部干了什么事情
3、拆解IJK_EGL_display_internal
3.1 IJK_EGL_prepareRenderer函数逻辑
-
检查渲染器是否初始化:首先会先检查当前EGL的渲染函数
egl->opaque->renderer
是否初始化。如果发现egl->opaque->renderer
失效或者渲染器和当前视频格式不一样,会重新创建一个新的renderer渲染器。这个renderer的作用是什么:renderer里封装了opengl的流程,包含了着色器、程序等正常的流程(这部分还是很有意思的,详情可看本文的"4、render渲染器流程")
-
然后
IJK_GLES2_Renderer_use
标记使用当前的渲染器 -
之后
IJK_EGL_setSurfaceSize
设置egl的宽高和ANativeWindow的buffer空间 -
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;
}
对应上述代码,我们可以发现其在创建着色器时的主要流程是:
- 根据传入的着色器类型,调用
glCreateShader
创建对应的着色器 - 根据传入的着色器代码,调用
glShaderSource
设置着色器的源代码 - 调用
glCompileShader
编译着色器 - 调用
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_Position
、av2_Texcoord
和um4_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,相当于每一个颜色是一个字节,那么他的计算方式就是
cppstatic 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内循环执行,源码在最后,可以结合函数调用路径去看源码:
-
video_image_display2
函数内开始取图像流队列数据 -
取出的一帧数据为Frame *vp,其实就是做好音画同步以后,正常拿到当前的一帧图像
-
取出
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);
}
}
}
}
}