Android播放视频

图像的显示最终都是由显示器完成的,显示器通过接收到的颜色矩阵来进行对应的显示。而颜色矩阵的产生一般有两种,一种是通过GPU来进行渲染生成,另一种是通过CPU来进行渲染生成。其中GPU比较适合来处理这件事情,所以其效率高。(硬件加速也就是指使用GPU来进行渲染加速)

在Android平台上,GPU渲染的API有两套,一套就是OpenGL-ES,另一套就是7.0后推出的Vulkan。目前使用最多的还是OpenGL-ES。

整个渲染流程中主要节点如下:
SurfaceFlinger <- SurfaceView <- Surface <- EGLSurface <- EGLContext <- OpenGL-ES

接入原生渲染体系

Android原生平台上封装了一整套View体系用来进行图像的渲染和显示,所以任何的渲染都必须基于这个体系才能正确的显示出来。

View系统中提供了两个View可以用来进行自定义渲染,一个是SurfaceView,另一个是TextureView。(关于SurfaceView和TextureView可以看我之前写的Android图形架构总览

只要将图形数据写入到SurfaceView或TextureView的Surface中,那么最终SurfaceFlinger服务就会将图形内容显示到显卡上。

所以对于播放视频来说,就是要将视频每一帧的图像数据写入到SurfaceView或TextureView的Surface中即可。

SurfaceView可以通过addCallback来接收来自原生渲染的生命周期回调,通过getSurface来获取内部的Surface对象。

1
2
this.getHolder().addCallback();
this.getHolder().getSurface()

TextureView可以通过setSurfaceTextureListener来接收生命周期回调,通过getSurfaceTexture可以获取到SurfaceTexture。

1
2
setSurfaceTextureListener();
Surface surface = new Surface(getSurfaceTexture());

Surface与EGLSurface

一个Surface对象,可以关联一个EGLSurface对象(window_surface),window_surface可以通过swap方法将其内部图像缓冲数据传入到Surface中(其它类型的EGLSurface是不可以的)。

1
2
3
4
5
6
7
8
// 创建一个提供给opengl-es绘制的surface(display,配置,原生window,指定属性)
if (!(eglSurface = eglCreateWindowSurface(display, config, aNativeWindow, 0))) {
return;
}

// android中创建NativeWindow
ANativeWindow *pNativeWindow = ANativeWindow_fromSurface(jenv, surface);

EGLSurface与EGLContext

EGLContext是与线程相关的,一个线程中只能激活一个EGLContext,激活的EGLContext关联一个EGLSurface,激活后,在当前线程调用OpenGL-ES的API都将作用到EGLSurface的缓冲区中。

1
2
3
4
5
// 创建context,在context中保存了opengl-es的状态信息 (display,配置,共享context的handle 一般设为null,属性)
// 一个display可以创建多个context
if (!(context = eglCreateContext(display, config, 0, context_attribs))) {
return;
}
1
2
3
4
5
6
7
8
// 将上面创建的context绑定到当前线程上,并将context与surface进行关联。当makeCurrent执行后,就可以调用opengl-es的api对context中的状态集进行设定,
// 然后进而向surface中绘制内容,再把surface中的内容读取出来。
// 一个线程中enable状态的context只能有一个,如果当前线程已经有了一个enable状态的context,那么会先执行其flush操作,将没有执行完成的命令全部执行完成,然后将其改为disable状态,将新传入的context改为enable状态。
// 如果想要释放当前的context,也就是将当前的context disable,那么将第二个和第三个参数设置为 EGL_NO_SURFACE,第四个参数设置为 EGL_NO_CONTEXT即可。
// context enable后,视口大小和裁剪大小都会被设置为surface的尺寸
if (!eglMakeCurrent(display, eglSurface, eglSurface, context)) {
return;
}

OpenGL-ES渲染纹理

当我们准备好了EGLSurface和EGLContext后,那么接下来就是创建OpenGL—ES的Program进行纹理的绘制了。

创建Program

创建Vertex着色器

1
2
3
4
5
6
7
8
precision mediump float;
attribute highp vec4 aPosition;
attribute highp vec2 aTextureCoord;
varying vec2 textureCoordinate;
void main() {
gl_Position = aPosition;
textureCoordinate = aTextureCoord.xy;
}

创建Fragment着色器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
precision mediump float;
varying highp vec2 textureCoordinate;
uniform lowp sampler2D inputTextureY;
uniform lowp sampler2D inputTextureU;
uniform lowp sampler2D inputTextureV;

void main() {
vec3 yuv;
vec3 rgb;
yuv.r = texture2D(inputTextureY, textureCoordinate).r - (16.0 / 255.0);
yuv.g = texture2D(inputTextureU, textureCoordinate).r - 0.5;
yuv.b = texture2D(inputTextureV, textureCoordinate).r - 0.5;
rgb = mat3(1.164, 1.164, 1.164,
0.0, -0.213, 2.112,
1.793, -0.533, 0.0) * yuv;
gl_FragColor = vec4(rgb, 1.0);
}

创建Program

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 创建Program
programHandle = OpenGLUtils::createProgram(vertexShader, fragmentShader);
// 获取顶点坐标变量
positionHandle = glGetAttribLocation(programHandle, "aPosition");
// 获取纹理坐标变量
texCoordinateHandle = glGetAttribLocation(programHandle, "aTextureCoord");
// 获取YUV纹理变量
inputTextureHandle[0] = glGetUniformLocation(programHandle, "inputTextureY");
inputTextureHandle[1] = glGetUniformLocation(programHandle, "inputTextureU");
inputTextureHandle[2] = glGetUniformLocation(programHandle, "inputTextureV");

// 使用Program
glUseProgram(programHandle);
// 创建三个纹理,用来接收YUV数据
if (textures[0] == 0) {
glGenTextures(3, textures);
}
for (int i = 0; i < 3; ++i) {
glActiveTexture(GL_TEXTURE0 + i);
glBindTexture(GL_TEXTURE_2D, textures[i]);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glUniform1i(inputTextureHandle[i], i);
}

设置坐标

对于图像来说,首先创建一个平面矩形,然后在其上贴上纹理即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 矩形坐标,原点在屏幕中心,x、y轴为(-1,1)
static const float vertices[] = {
-1.0f, -1.0f, // left, bottom
1.0f, -1.0f, // right, bottom
-1.0f, 1.0f, // left, top
1.0f, 1.0f, // right, top
};
// 纹理坐标,原点在屏幕左上角,x、y轴为(0,1)
static const float texture_vertices[] = {
0.0f, 1.0f, // left, top
1.0f, 1.0f, // right, top
0.0f, 0.0f, // left, bottom
1.0f, 0.0f, // right, bottom
};

// 绑定顶点坐标
glVertexAttribPointer(positionHandle, 2, GL_FLOAT, GL_FALSE, 0, vertices);
glEnableVertexAttribArray(positionHandle);

// 绑定纹理坐标
glVertexAttribPointer(texCoordinateHandle, 2, GL_FLOAT, GL_FALSE, 0, texture_vertices);
glEnableVertexAttribArray(texCoordinateHandle);

设置纹理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 更新绑定纹理的数据
// 这里处理的是YUV420p格式的数据,所以UV是Y的一半
const GLsizei heights[3] = { texture->height, texture->height / 2, texture->height / 2};
for (int i = 0; i < 3; ++i) {
// 激活前面创建的YUV纹理
glActiveTexture(GL_TEXTURE0 + i);
// 绑定纹理
glBindTexture(GL_TEXTURE_2D, textures[i]);
// 将对应的YUV数据设置到纹理上
glTexImage2D(GL_TEXTURE_2D,
0,
GL_LUMINANCE,
texture->pitches[i],
heights[i],
0,
GL_LUMINANCE,
GL_UNSIGNED_BYTE,
// 具体数据
texture->pixels[i]);
// 设置纹理变量对应的纹理单元层
glUniform1i(inputTextureHandle[i], i);
}

绘制

1
2
// 绘制平面矩形
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

将OpenGL-ES生成的图像数据加入到EGLSurface绑定的Surface中

1
2
3
4
// 当opengl-es将内容绘制完成,调用该方法将该缓冲区加入到Surface的BufferQueue中
if (!eglSwapBuffers(display, eglSurface)) {
return;
}