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;
}