ExternalTexture(外部纹理):在 Android SurfaceTexture 上实现视频零拷贝渲染

Android SurfaceTexture 上的视频零拷贝渲染:ExternalTexture 技术深度解析

大家好,今天我们来深入探讨一个在Android平台上实现视频高效渲染的关键技术:ExternalTexture。具体来说,我们将专注于如何利用ExternalTexture在SurfaceTexture上实现视频的零拷贝渲染。

1. 视频渲染的传统方式及其局限性

在深入ExternalTexture之前,我们先回顾一下传统的视频渲染方式及其固有的问题。 通常,在Android上渲染视频,我们需要经过以下步骤:

  1. 解码: 使用 MediaCodec 解码器将视频数据解码为原始的 YUV 或 RGB 帧。
  2. 数据传输: 将解码后的帧数据从 MediaCodec 的输出缓冲区复制到应用程序的内存空间。
  3. 格式转换(可选): 如果解码后的格式与渲染器所需的格式不同,则需要进行格式转换。例如,将 YUV420P 转换为 RGB565 或 RGBA8888。
  4. 纹理上传: 将转换后的像素数据上传到 OpenGL ES 的纹理对象。
  5. 渲染: 使用 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负载和功耗,是构建高性能视频应用的关键技术。理解其原理和应用,能够帮助开发者构建更流畅、高效的视频体验。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注