Android SurfaceTexture 上的视频零拷贝渲染:ExternalTexture 技术深度解析
大家好,今天我们来深入探讨一个在Android平台上实现视频高效渲染的关键技术:ExternalTexture。具体来说,我们将专注于如何利用ExternalTexture在SurfaceTexture上实现视频的零拷贝渲染。
1. 视频渲染的传统方式及其局限性
在深入ExternalTexture之前,我们先回顾一下传统的视频渲染方式及其固有的问题。 通常,在Android上渲染视频,我们需要经过以下步骤:
- 解码: 使用 MediaCodec 解码器将视频数据解码为原始的 YUV 或 RGB 帧。
- 数据传输: 将解码后的帧数据从 MediaCodec 的输出缓冲区复制到应用程序的内存空间。
- 格式转换(可选): 如果解码后的格式与渲染器所需的格式不同,则需要进行格式转换。例如,将 YUV420P 转换为 RGB565 或 RGBA8888。
- 纹理上传: 将转换后的像素数据上传到 OpenGL ES 的纹理对象。
- 渲染: 使用 OpenGL ES 着色器将纹理渲染到屏幕上。
这个过程存在几个明显的局限性:
- 内存拷贝: 数据从 MediaCodec 的输出缓冲区到应用程序内存,再到 OpenGL ES 纹理的上传,涉及多次内存拷贝。这些拷贝操作会消耗大量的 CPU 周期,并且会增加内存带宽的压力。
- 格式转换开销: 格式转换操作本身也是计算密集型的,尤其是在高分辨率视频的情况下,会显著影响性能。
- CPU负载: 整个过程依赖于 CPU 进行数据处理和传输,在高码率视频或低端设备上,CPU 容易成为瓶颈。
2. SurfaceTexture 和 ExternalTexture 的原理
为了解决上述问题,Android 引入了 SurfaceTexture 和 ExternalTexture。
- SurfaceTexture: SurfaceTexture 结合了 Surface 和 Texture 的特性。它是一个可以被 OpenGL ES 纹理使用的 Surface,允许直接从数据源(例如 MediaCodec)将数据写入纹理,而无需中间的内存拷贝。换句话说,SurfaceTexture充当了生产者(例如 MediaCodec)和消费者(例如 OpenGL ES 渲染器)之间的桥梁。
- ExternalTexture (GL_OES_EGL_image_external): ExternalTexture 是一种特殊的 OpenGL ES 纹理类型,专门用于与 SurfaceTexture 关联。它允许 OpenGL ES 着色器直接访问 SurfaceTexture 中存储的图像数据,而无需将数据复制到传统的 OpenGL ES 纹理对象。
简单来说,SurfaceTexture负责接收视频帧数据并将其存储在图形缓冲区中,而ExternalTexture则允许OpenGL ES程序直接读取这些数据,从而避免了CPU参与的数据拷贝。
3. 零拷贝渲染的实现步骤
下面我们通过代码示例来演示如何使用 SurfaceTexture 和 ExternalTexture 实现视频的零拷贝渲染。
3.1 创建 SurfaceTexture 和 OpenGL ES 上下文
// 初始化 OpenGL ES 环境
EGLDisplay mEglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
EGL14.eglInitialize(mEglDisplay, new int[1], new int[1]);
EGLConfig[] configs = new EGLConfig[1];
int[] numConfigs = new int[1];
int[] attribList = {
EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
EGL14.EGL_NONE
};
EGL14.eglChooseConfig(mEglDisplay, attribList, 0, configs, 0, 1, numConfigs, 0);
EGLContext mEglContext = EGL14.eglCreateContext(mEglDisplay, configs[0], EGL14.EGL_NO_CONTEXT, new int[]{
EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
EGL14.EGL_NONE
}, 0);
EGLSurface mEglSurface = EGL14.EglCreatePbufferSurface(mEglDisplay, configs[0], new int[]{
EGL14.EGL_WIDTH, 1,
EGL14.EGL_HEIGHT, 1,
EGL14.EGL_NONE
}, 0);
EGL14.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext);
// 创建 SurfaceTexture
int[] textures = new int[1];
GLES20.glGenTextures(1, textures, 0);
int mTextureId = textures[0];
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureId);
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
SurfaceTexture mSurfaceTexture = new SurfaceTexture(mTextureId);
代码解释:
- 首先,我们初始化 OpenGL ES 2.0 环境,包括 EGLDisplay、EGLConfig、EGLContext 和 EGLSurface。
- 然后,我们生成一个 OpenGL ES 纹理对象,并将其绑定到
GL_TEXTURE_EXTERNAL_OES目标。 这是关键的一步,因为它指定了这个纹理将用于 ExternalTexture。 - 设置纹理的过滤和环绕模式。
- 最后,我们创建一个 SurfaceTexture 对象,并将之前生成的纹理 ID 传递给它。这样,SurfaceTexture 就会将数据写入到这个纹理中。
3.2 配置 MediaCodec 并将其输出 Surface 设置为 SurfaceTexture
MediaCodec mMediaCodec = MediaCodec.createDecoderByType("video/avc"); // 例如,使用 H.264 解码器
MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);
format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, width * height);
mMediaCodec.configure(format, new Surface(mSurfaceTexture), null, 0);
mMediaCodec.start();
代码解释:
- 创建一个 MediaCodec 解码器,并指定要解码的视频格式(例如 H.264)。
- 创建一个 MediaFormat 对象,设置视频的宽度、高度和其他参数。
- 调用
mMediaCodec.configure()方法,将 SurfaceTexture 关联的 Surface 传递给 MediaCodec。 这告诉 MediaCodec 将解码后的帧数据直接输出到 SurfaceTexture 中。 - 启动 MediaCodec。
3.3 渲染视频帧
// 在渲染循环中
mSurfaceTexture.updateTexImage(); // 更新纹理图像
// 获取变换矩阵
float[] transformMatrix = new float[16];
mSurfaceTexture.getTransformMatrix(transformMatrix);
// 渲染
GLES20.glUseProgram(mProgram);
// 传递变换矩阵
int transformMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uTexMatrix");
GLES20.glUniformMatrix4fv(transformMatrixHandle, 1, false, transformMatrix, 0);
// 激活纹理单元
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureId);
GLES20.glUniform1i(GLES20.glGetUniformLocation(mProgram, "sTexture"), 0);
// 绘制
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
代码解释:
- 在渲染循环中,首先调用
mSurfaceTexture.updateTexImage()方法。 这个方法会从 MediaCodec 获取最新的视频帧数据,并将其更新到 SurfaceTexture 关联的纹理中。 - 调用
mSurfaceTexture.getTransformMatrix()方法获取变换矩阵。 这个矩阵用于处理视频的旋转和缩放。 这是因为MediaCodec输出的图像方向可能与屏幕方向不一致。 - 使用 OpenGL ES 着色器程序进行渲染。
- 将变换矩阵传递给着色器程序。
- 激活纹理单元 0,并将 ExternalTexture 绑定到该单元。
- 设置着色器程序的纹理采样器
sTexture为 0,表示使用纹理单元 0。 - 调用
GLES20.glDrawArrays()方法绘制纹理。
3.4 OpenGL ES 着色器代码
为了渲染 ExternalTexture,我们需要使用特殊的 OpenGL ES 着色器。 下面是一个简单的顶点着色器和片段着色器的示例:
顶点着色器 (vertex shader):
attribute vec4 aPosition;
attribute vec4 aTexCoord;
uniform mat4 uTexMatrix;
varying vec2 vTexCoord;
void main() {
gl_Position = aPosition;
vTexCoord = (uTexMatrix * aTexCoord).xy;
}
片段着色器 (fragment shader):
#extension GL_OES_EGL_image_external : require
precision mediump float;
varying vec2 vTexCoord;
uniform samplerExternalOES sTexture;
void main() {
gl_FragColor = texture2D(sTexture, vTexCoord);
}
代码解释:
- 顶点着色器:
aPosition: 顶点坐标。aTexCoord: 纹理坐标。uTexMatrix: 变换矩阵,用于处理视频的旋转和缩放。vTexCoord: 传递给片段着色器的纹理坐标。
- 片段着色器:
#extension GL_OES_EGL_image_external : require: 声明使用GL_OES_EGL_image_external扩展,这是使用 ExternalTexture 的必要条件。precision mediump float: 设置浮点数的精度。vTexCoord: 从顶点着色器接收的纹理坐标。samplerExternalOES sTexture: 声明一个samplerExternalOES类型的纹理采样器,用于采样 ExternalTexture。texture2D(sTexture, vTexCoord): 使用纹理采样器sTexture和纹理坐标vTexCoord对 ExternalTexture 进行采样,获取像素颜色。
4. ExternalTexture 的优势
使用 ExternalTexture 进行视频渲染的主要优势在于:
- 零拷贝: 避免了将视频数据从 MediaCodec 的输出缓冲区复制到应用程序内存,再到 OpenGL ES 纹理的多次内存拷贝,从而显著降低了 CPU 的负载和内存带宽的压力。
- 高性能: 减少了数据拷贝和格式转换的开销,提高了视频渲染的性能,尤其是在高分辨率视频和低端设备上。
- 低延迟: 减少了数据处理的延迟,提高了视频播放的流畅性。
- 降低功耗: 通过减少 CPU 的使用,降低了设备的功耗。
| 特性 | 传统渲染方式 | ExternalTexture 渲染方式 |
|---|---|---|
| 内存拷贝 | 多次 | 零次 |
| CPU 负载 | 高 | 低 |
| 性能 | 较低 | 较高 |
| 延迟 | 较高 | 较低 |
| 功耗 | 较高 | 较低 |
5. 注意事项和常见问题
- EGL 上下文: 确保 MediaCodec 和 OpenGL ES 使用的是同一个 EGL 上下文。否则,SurfaceTexture 无法正确地将数据传递给 OpenGL ES。
- 变换矩阵: 正确使用变换矩阵来处理视频的旋转和缩放,以确保视频以正确的方向显示。
- 纹理更新: 必须在每次渲染之前调用
mSurfaceTexture.updateTexImage()方法来更新纹理图像。 - GL_OES_EGL_image_external 扩展: 确保设备支持
GL_OES_EGL_image_external扩展。 可以通过GLES20.glGetString(GLES20.GL_EXTENSIONS)来检查是否支持该扩展。 - SurfaceTexture 生命周期: SurfaceTexture 的生命周期需要与 MediaCodec 的生命周期保持同步。 确保在 MediaCodec 停止之前释放 SurfaceTexture。
- 线程安全: SurfaceTexture 的
updateTexImage()方法需要在 OpenGL ES 上下文所在的线程中调用。
6. 实际应用场景
ExternalTexture 技术广泛应用于各种视频相关的应用场景,例如:
- 视频播放器: 用于实现高性能、低延迟的视频播放。
- 视频编辑器: 用于实现流畅的视频编辑和特效处理。
- 实时视频流: 用于实现低延迟的实时视频流传输和渲染。
- 相机预览: Android CameraX API 内部使用了 SurfaceTexture 来进行相机预览, 使用ExternalTexture能够更高效的处理相机预览数据。
- VR/AR 应用: 在 VR/AR 应用中,需要实时渲染大量的视频数据,ExternalTexture 可以显著提高渲染性能。
7. 代码示例:完整的渲染流程
public class ExternalTextureRenderer {
private int mTextureId;
private SurfaceTexture mSurfaceTexture;
private int mProgram;
private int aPositionHandle;
private int aTexCoordHandle;
private int uTexMatrixHandle;
private int sTextureHandle;
private float[] transformMatrix = new float[16];
private static final String VERTEX_SHADER =
"attribute vec4 aPosition;n" +
"attribute vec4 aTexCoord;n" +
"uniform mat4 uTexMatrix;n" +
"varying vec2 vTexCoord;n" +
"void main() {n" +
" gl_Position = aPosition;n" +
" vTexCoord = (uTexMatrix * aTexCoord).xy;n" +
"}";
private static final String FRAGMENT_SHADER =
"#extension GL_OES_EGL_image_external : requiren" +
"precision mediump float;n" +
"varying vec2 vTexCoord;n" +
"uniform samplerExternalOES sTexture;n" +
"void main() {n" +
" gl_FragColor = texture2D(sTexture, vTexCoord);n" +
"}";
public ExternalTextureRenderer() {
// 创建 OpenGL ES 程序
mProgram = createProgram(VERTEX_SHADER, FRAGMENT_SHADER);
aPositionHandle = GLES20.glGetAttribLocation(mProgram, "aPosition");
aTexCoordHandle = GLES20.glGetAttribLocation(mProgram, "aTexCoord");
uTexMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uTexMatrix");
sTextureHandle = GLES20.glGetUniformLocation(mProgram, "sTexture");
// 创建 ExternalTexture
int[] textures = new int[1];
GLES20.glGenTextures(1, textures, 0);
mTextureId = textures[0];
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureId);
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
mSurfaceTexture = new SurfaceTexture(mTextureId);
}
public SurfaceTexture getSurfaceTexture() {
return mSurfaceTexture;
}
public void drawFrame() {
// 更新纹理图像
mSurfaceTexture.updateTexImage();
mSurfaceTexture.getTransformMatrix(transformMatrix);
// 设置视口
GLES20.glViewport(0, 0, width, height);
// 清除颜色缓冲区
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
// 使用程序
GLES20.glUseProgram(mProgram);
// 设置顶点属性
GLES20.glVertexAttribPointer(aPositionHandle, 2, GLES20.GL_FLOAT, false, 0, vertexBuffer);
GLES20.glEnableVertexAttribArray(aPositionHandle);
// 设置纹理坐标属性
GLES20.glVertexAttribPointer(aTexCoordHandle, 2, GLES20.GL_FLOAT, false, 0, texCoordBuffer);
GLES20.glEnableVertexAttribArray(aTexCoordHandle);
// 传递变换矩阵
GLES20.glUniformMatrix4fv(uTexMatrixHandle, 1, false, transformMatrix, 0);
// 激活纹理单元 0
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureId);
GLES20.glUniform1i(sTextureHandle, 0);
// 绘制
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
}
private int createProgram(String vertexSource, String fragmentSource) {
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
int program = GLES20.glCreateProgram();
GLES20.glAttachShader(program, vertexShader);
GLES20.glAttachShader(program, fragmentShader);
GLES20.glLinkProgram(program);
int[] linkStatus = new int[1];
GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
if (linkStatus[0] != GLES20.GL_TRUE) {
String error = GLES20.glGetProgramInfoLog(program);
Log.e("ExternalTexture", "Could not link program: " + error);
GLES20.glDeleteProgram(program);
program = 0;
}
return program;
}
private int loadShader(int type, String shaderCode) {
int shader = GLES20.glCreateShader(type);
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
int[] compiled = new int[1];
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
if (compiled[0] == 0) {
String error = GLES20.glGetShaderInfoLog(shader);
Log.e("ExternalTexture", "Could not compile shader: " + error);
GLES20.glDeleteShader(shader);
shader = 0;
}
return shader;
}
}
这个完整的示例涵盖了 ExternalTexture 渲染的核心步骤,可以作为你实现零拷贝视频渲染的起点。
总结
ExternalTexture技术通过SurfaceTexture在Android平台上实现了视频的零拷贝渲染,有效避免了传统渲染方式中涉及的多次内存拷贝和格式转换,从而显著提高了渲染性能,降低了CPU负载和功耗,是构建高性能视频应用的关键技术。理解其原理和应用,能够帮助开发者构建更流畅、高效的视频体验。