Android 视频分屏性能优化——GLContext共享

场景

在Android平台。在做分屏应用时,特别是低端设备上,为了实现极致的性能优化,就需要利用GLContext共享来实现。

假设有2个Surface组件,一个是GLSurfaceView,另一个也是GLSurfaceView,前一个我们叫它A,后一个我们称它B,为了实现后台渲染,我们利用EGL实现了一个EGLContext和EGL Texture。

我们的需求是,在视频解码后,能够实现A、B极致的性能渲染。

这是一个非常经典且高级的应用场景!利用"星形"共享架构来实现视频分屏渲染,是性能最高、内存占用最低的做法。

在视频播放场景中,核心链路是: 视频播放器 (解码)

→ Surface → SurfaceTexture → OES 纹理 (Root Context) → 共享给 A 和 B 渲染。

这里有三个极其重要的 OpenGL 机制需要处理:

  • OES 纹理:Android 视频解码出来的数据通常是 YUV 格式,不能直接用普通的 GL_TEXTURE_2D 渲染,必须使用 GL_TEXTURE_EXTERNAL_OES。
  • SurfaceTexture 的线程限制:SurfaceTexture.updateTexImage() 必须在创建它的 EGLContext 所在的线程调用。因此,我们需要为 Root Context 开启一个后台线程。
  • 纹理矩阵(Transform Matrix):视频解码到纹理后,方向通常是颠倒或错位的,必须使用 SurfaceTexture 提供的矩阵进行坐标变换。

OES纹理

要实现分屏渲染,首先是离屏能力必须具备

java 复制代码
public class RootEGLManager {
    private static RootEGLManager instance;
    private EGL10 mEgl;
    private EGLDisplay mEglDisplay;
    private EGLContext mRootContext;

    private RootEGLManager() {
        initRootContext();
    }

    public static synchronized RootEGLManager getInstance() {
        if (instance == null) {
            instance = new RootEGLManager();
        }
        return instance;
    }

    private void initRootContext() {
        mEgl = (EGL10) EGLContext.getEGL();
        mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);

        int[] version = new int[2];
        mEgl.eglInitialize(mEglDisplay, version);

        // 配置属性:支持 OpenGL ES 2.0/3.0
        int[] configAttribs = {
                EGL10.EGL_RENDERABLE_TYPE, 4, // EGL_OPENGL_ES2_BIT
                EGL10.EGL_RED_SIZE, 8,
                EGL10.EGL_GREEN_SIZE, 8,
                EGL10.EGL_BLUE_SIZE, 8,
                EGL10.EGL_ALPHA_SIZE, 8,
                EGL10.EGL_DEPTH_SIZE, 16,
                EGL10.EGL_NONE
        };

        EGLConfig[] configs = new EGLConfig[1];
        int[] numConfigs = new int[1];
        mEgl.eglChooseConfig(mEglDisplay, configAttribs, configs, 1, numConfigs);
        EGLConfig config = configs[0];

        // 指定 OpenGL ES 2.0 (如果用3.0,改为3)
        int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
        int[] contextAttribs = {
                EGL_CONTEXT_CLIENT_VERSION, 2,
                EGL10.EGL_NONE
        };

        // 创建 Root Context,第三个参数 EGL10.EGL_NO_CONTEXT 表示它不与其他Context共享
        mRootContext = mEgl.eglCreateContext(mEglDisplay, config, EGL10.EGL_NO_CONTEXT, contextAttribs);
    }

    public EGLContext getRootContext() {
        return mRootContext;
    }

    public void release() {
        if (mEglDisplay != EGL10.EGL_NO_DISPLAY && mRootContext != EGL10.EGL_NO_CONTEXT) {
            mEgl.eglDestroyContext(mEglDisplay, mRootContext);
            mRootContext = EGL10.EGL_NO_CONTEXT;
        }
    }
}

共享方案

下面是完整的代码实现方案:

第一步:创建后台视频纹理管理器 (VideoTextureManager) 这个类负责在后台线程绑定 Root Context,生成 OES 纹理,并提供 Surface 给播放器。

java 复制代码
public class VideoTextureManager {
  private HandlerThread mGLThread;
  private Handler mGLHandler;
  
  private SurfaceTexture mSurfaceTexture;
  private Surface mSurface;
  private int mOesTextureId;
  private float[] mTransformMatrix = new float[16];

  // 回调接口,通知 View A 和 B 刷新
  public interface OnFrameUpdatedListener {
      void onFrameUpdated(int textureId, float[] matrix);
  }
  private OnFrameUpdatedListener mListener;

  public VideoTextureManager(OnFrameUpdatedListener listener) {
      this.mListener = listener;
      // 1. 启动一个后台 GL 线程
      mGLThread = new HandlerThread("RootGLThread");
      mGLThread.start();
      mGLHandler = new Handler(mGLThread.getLooper());

      // 2. 在后台线程初始化 EGL 环境并创建纹理
      mGLHandler.post(this::initGLAndSurface);
  }

  private void initGLAndSurface() {
      EGL10 egl = (EGL10) EGLContext.getEGL();
      EGLDisplay display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
      EGLContext rootContext = RootEGLManager.getInstance().getRootContext();

      // 为了让 Root Context 能在这个线程 makeCurrent,我们需要创建一个 1x1 的离屏 Pbuffer Surface
      int[] pbufferAttribs = {
              EGL10.EGL_WIDTH, 1,
              EGL10.EGL_HEIGHT, 1,
              EGL10.EGL_NONE
      };
      // 注意:这里需要获取 RootEGLManager 中的 EGLConfig,为了简便,已经在 RootEGLManager 中暴露了 getConfig()
      EGLConfig config = RootEGLManager.getInstance().getConfig(); 
      EGLSurface pbufferSurface = egl.eglCreatePbufferSurface(display, config, pbufferAttribs);

      // 绑定 Root Context 到当前后台线程
      egl.eglMakeCurrent(display, pbufferSurface, pbufferSurface, rootContext);

      // 3. 创建 OES 纹理
      int[] textures = new int[1];
      GLES20.glGenTextures(1, textures, 0);
      mOesTextureId = textures[0];
      GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mOesTextureId);
      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);

      // 4. 创建 SurfaceTexture 和 Surface
      mSurfaceTexture = new SurfaceTexture(mOesTextureId);
      mSurface = new Surface(mSurfaceTexture);

      // 5. 监听视频帧到达
      mSurfaceTexture.setOnFrameAvailableListener(surfaceTexture -> {
          // 必须在当前 GL 线程调用 updateTexImage
          mGLHandler.post(() -> {
              try {
                  surfaceTexture.updateTexImage();
                  surfaceTexture.getTransformMatrix(mTransformMatrix);
                  GLES20.glFlush();
                  // 通知 A 和 B 渲染
                  if (mListener != null) {
                      mListener.onFrameUpdated(mOesTextureId, mTransformMatrix);
                  }
              } catch (Exception e) {
                  e.printStackTrace();
              }
          });
      });
  }

  // 提供给播放器 (如 MediaPlayer / ExoPlayer)
  public Surface getSurface() {
      return mSurface;
  }

  public void release() {
      mGLHandler.post(() -> {
          if (mSurface != null) mSurface.release();
          if (mSurfaceTexture != null) mSurfaceTexture.release();
          mGLThread.quitSafely();
      });
  }
}

第二步:编写支持 OES 的 Renderer (A 和 B 可以共用同一个类) 因为是视频渲染,着色器必须使用 #extension GL_OES_EGL_image_external : require。

kotlin 复制代码
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

public class VideoRenderer implements GLSurfaceView.Renderer {

    // OES 专属顶点着色器 (接收矩阵变换)
    private static final String VERTEX_SHADER =
            "attribute vec4 aPosition;\n" +
            "attribute vec4 aTexCoord;\n" +
            "uniform mat4 uTexMatrix;\n" + // 接收 SurfaceTexture 的矩阵
            "varying vec2 vTexCoord;\n" +
            "void main() {\n" +
            "  gl_Position = aPosition;\n" +
            "  vTexCoord = (uTexMatrix * aTexCoord).xy;\n" +
            "}";

    // OES 专属片段着色器
    private static final String FRAGMENT_SHADER =
            "#extension GL_OES_EGL_image_external : require\n" +
            "precision mediump float;\n" +
            "varying vec2 vTexCoord;\n" +
            "uniform samplerExternalOES uTexture;\n" + // 注意类型是 samplerExternalOES
            "void main() {\n" +
            "  gl_FragColor = texture2D(uTexture, vTexCoord);\n" +
            "}";

    private int mProgram;
    private FloatBuffer mVertexBuffer;
    
    // 由 VideoTextureManager 传递过来的数据
    private int mTextureId = 0;
    private float[] mTransformMatrix = new float[16];

    public VideoRenderer() {
        float[] vertices = {
            -1f, -1f,  0f, 0f,
             1f, -1f,  1f, 0f,
            -1f,  1f,  0f, 1f,
             1f,  1f,  1f, 1f
        };
        mVertexBuffer = ByteBuffer.allocateDirect(vertices.length * 4)
                .order(ByteOrder.nativeOrder()).asFloatBuffer().put(vertices);
    }

    // 供外部更新纹理状态
    public void updateFrame(int textureId, float[] matrix) {
        this.mTextureId = textureId;
        System.arraycopy(matrix, 0, this.mTransformMatrix, 0, 16);
    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        mProgram = buildProgram(VERTEX_SHADER, FRAGMENT_SHADER);
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        GLES20.glViewport(0, 0, width, height);
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

        if (mTextureId == 0) return; // 视频还没准备好

        GLES20.glUseProgram(mProgram);

        // 绑定 OES 纹理
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureId);
        GLES20.glUniform1i(GLES20.glGetUniformLocation(mProgram, "uTexture"), 0);

        // 传递矩阵
        int matrixHandle = GLES20.glGetUniformLocation(mProgram, "uTexMatrix");
        GLES20.glUniformMatrix4fv(matrixHandle, 1, false, mTransformMatrix, 0);

        // 绘制顶点
        int posHandle = GLES20.glGetAttribLocation(mProgram, "aPosition");
        int texHandle = GLES20.glGetAttribLocation(mProgram, "aTexCoord");

        mVertexBuffer.position(0);
        GLES20.glVertexAttribPointer(posHandle, 2, GLES20.GL_FLOAT, false, 16, mVertexBuffer);
        GLES20.glEnableVertexAttribArray(posHandle);

        mVertexBuffer.position(2);
        GLES20.glVertexAttribPointer(texHandle, 2, GLES20.GL_FLOAT, false, 16, mVertexBuffer);
        GLES20.glEnableVertexAttribArray(texHandle);

        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
    }

    private int buildProgram(String vertex, String fragment) {
        // 编译着色器代码 (同上一个回答的 GLUtils)
        // ...
        return program;
    }
}

第三步:在 Activity 中组装 (播放器 + 共享渲染) 在这里,我们将 GLSurfaceView 设置为 按需渲染 (RENDERMODE_WHEN_DIRTY)。只有当视频解码出新的一帧时,才通知 A 和 B 刷新,这样极其省电。

kotlin 复制代码
public class VideoSplitActivity extends AppCompatActivity {

    private GLSurfaceView glViewA;
    private GLSurfaceView glViewB;
    private VideoRenderer rendererA;
    private VideoRenderer rendererB;
    
    private VideoTextureManager mVideoTextureManager;
    private MediaPlayer mMediaPlayer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_video_split);

        glViewA = findViewById(R.id.gl_view_a);
        glViewB = findViewById(R.id.gl_view_b);

        // 1. 初始化 Root Context (确保在 View 初始化前调用)
        EGLContext rootContext = RootEGLManager.getInstance().getRootContext();

        // 2. 配置 View A
        rendererA = new VideoRenderer();
        glViewA.setEGLContextClientVersion(2);
        glViewA.setEGLContextFactory(new SharedEGLContextFactory(rootContext));
        glViewA.setRenderer(rendererA);
        glViewA.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); // 按需渲染

        // 3. 配置 View B
        rendererB = new VideoRenderer();
        glViewB.setEGLContextClientVersion(2);
        glViewB.setEGLContextFactory(new SharedEGLContextFactory(rootContext));
        glViewB.setRenderer(rendererB);
        glViewB.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); // 按需渲染

        // 4. 初始化视频纹理管理器
        mVideoTextureManager = new VideoTextureManager((textureId, matrix) -> {
            // 当视频有一帧新数据到达时:
            // 更新 A 和 B 的数据
            rendererA.updateFrame(textureId, matrix);
            rendererB.updateFrame(textureId, matrix);
            
            // 唤醒 A 和 B 进行绘制
            glViewA.requestRender();
            glViewB.requestRender();
        });

        // 5. 延迟一点时间等待 Surface 创建完成,然后初始化播放器
        glViewA.postDelayed(this::setupPlayer, 500);
    }

    private void setupPlayer() {
        mMediaPlayer = new MediaPlayer();
        try {
            // 设置视频源 (替换为你的视频路径)
            mMediaPlayer.setDataSource("http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4");
            
            // 将 Root Context 生成的 Surface 交给播放器!
            mMediaPlayer.setSurface(mVideoTextureManager.getSurface());
            
            mMediaPlayer.setLooping(true);
            mMediaPlayer.prepareAsync();
            mMediaPlayer.setOnPreparedListener(MediaPlayer::start);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mMediaPlayer != null) {
            mMediaPlayer.release();
        }
        if (mVideoTextureManager != null) {
            mVideoTextureManager.release();
        }
        RootEGLManager.getInstance().release();
    }
}

架构优势总结

  • 零拷贝 (Zero-Copy):视频解码器直接将数据写入显存(OES纹理),View A 和 View B 直接读取同一块显存进行渲染。整个过程 CPU 完全不参与像素搬运,性能极高。
  • 完美的同步:利用 SurfaceTexture.setOnFrameAvailableListener 配合 GLSurfaceView.requestRender(),实现了视频帧驱动渲染。视频每解码一帧,屏幕才刷新一次,避免了无效的 GPU 绘制。
  • 各自独立的后处理:虽然 A 和 B 共享了同一个视频纹理,但它们有各自的 VideoRenderer。这意味着我们可以给 A 加一个黑白滤镜(修改 A 的 Fragment Shader),给 B 加一个镜像翻转(修改 B 的 Vertex Shader),两者互不干扰!

SurfaceView和TextureView适配

当前,我们仅仅实现了GLSurfaceView的渲染性能优化,那SurfaceView和TextureView是否也可以优化呢?

其实是支持的,在很多音视频应用中,几乎全都是用普通的 SurfaceView 或 TextureView,很少是 GLSurfaceView。

之所以会有这个疑问,是因为前面的代码使用了 GLSurfaceView 提供的便捷 API (setEGLContextFactory) 来实现共享。普通的 SurfaceView 和 TextureView 确实没有这个 API。

但是,GLSurfaceView 本质上只是 SurfaceView + 一个自带的后台 EGL 线程。 只要我们自己接管 EGL 的初始化和线程管理,就可以把任何共享的 OpenGL 画面渲染到普通的 SurfaceView 或 TextureView 上。

方案

在普通的 View 上实现星形共享架构。

核心原理:万物皆可 eglCreateWindowSurface 在 OpenGL ES 的世界里,它根本不知道什么是 SurfaceView 或 TextureView。它只认识一个东西:NativeWindow(原生窗口)。

在 Android 中,无论是 SurfaceView 提供的 Surface,还是 TextureView 提供的 SurfaceTexture,都可以作为 NativeWindow 传给 EGL,用来创建一个 EGLSurface(OpenGL 的画布)。

SurfaceView:通过 surfaceHolder.getSurface() 获取。 TextureView:通过 onSurfaceTextureAvailable 回调拿到 SurfaceTexture,然后 new Surface(surfaceTexture) 获取。 如何实现?(自定义 EGL 渲染线程) 既然不用 GLSurfaceView,我们需要为 View A 和 View B 各自写一个简单的后台渲染线程。这个线程接收 Root Context 和目标 View 的 Surface。

  1. 编写一个通用的自定义 GL 渲染线程 这个线程的核心任务是:拿着 Root Context 作为共享源,拿着 View 的 Surface 作为画布,建立自己的 EGL 环境并循环渲染。
kotlin 复制代码
public class CustomGLRenderThread extends Thread {
    private Surface mSurface;           // 来自 SurfaceView 或 TextureView
    private EGLContext mRootContext;    // 我们的星形架构中心节点
    private VideoRenderer mRenderer;    // 之前写的渲染逻辑
    
    private boolean isRunning = true;
    private final Object mLock = new Object();
    private boolean mHasNewFrame = false;

    public CustomGLRenderThread(Surface surface, EGLContext rootContext) {
        this.mSurface = surface;
        this.mRootContext = rootContext;
        this.mRenderer = new VideoRenderer();
    }

    @Override
    public void run() {
        // 1. 初始化 EGL 环境 (核心:共享 RootContext,绑定目标 Surface)
        EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
        EGL14.eglInitialize(display, null, 0, null, 0);

        // 获取配置 (省略具体的 chooseConfig 代码,需与 RootContext 保持一致)
        EGLConfig config = getConfig(display); 

        // 【关键点 1】:创建 Context 时,传入 mRootContext 作为共享上下文!
        int[] ctxAttribs = { EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE };
        EGLContext myContext = EGL14.eglCreateContext(display, config, mRootContext, ctxAttribs, 0);

        // 【关键点 2】:将 Android 的 Surface 变成 OpenGL 的画布
        int[] surfaceAttribs = { EGL14.EGL_NONE };
        EGLSurface eglSurface = EGL14.eglCreateWindowSurface(display, config, mSurface, surfaceAttribs, 0);

        // 绑定当前线程
        EGL14.eglMakeCurrent(display, eglSurface, eglSurface, myContext);

        // 2. 初始化渲染器 (编译着色器等)
        mRenderer.onSurfaceCreated(null, null);
        // 假设宽高已知,或者通过外部传入
        mRenderer.onSurfaceChanged(null, 1080, 1920); 

        // 3. 进入渲染循环
        while (isRunning) {
            synchronized (mLock) {
                while (!mHasNewFrame && isRunning) {
                    try {
                        mLock.wait(); // 等待视频帧到来的唤醒信号
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                mHasNewFrame = false;
            }

            if (!isRunning) break;

            // 绘制画面 (直接读取共享的 OES 纹理)
            mRenderer.onDrawFrame(null);

            // 【关键点 3】:将画好的内容提交到屏幕显示!
            // 这相当于 GLSurfaceView 内部自动调用的方法
            EGL14.eglSwapBuffers(display, eglSurface); 
        }

        // 4. 退出清理
        EGL14.eglMakeCurrent(display, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT);
        EGL14.eglDestroySurface(display, eglSurface);
        EGL14.eglDestroyContext(display, myContext);
        mSurface.release();
    }

    // 供外部 (VideoTextureManager) 调用,通知有新画面了
    public void requestRender(int textureId, float[] matrix) {
        mRenderer.updateFrame(textureId, matrix);
        synchronized (mLock) {
            mHasNewFrame = true;
            mLock.notifyAll(); // 唤醒渲染线程
        }
    }

    public void release() {
        isRunning = false;
        synchronized (mLock) {
            mLock.notifyAll();
        }
    }
}
  1. 在 Activity 中结合普通的 SurfaceView / TextureView 使用 现在,我们可以完全抛弃 GLSurfaceView 了。
java 复制代码
public class VideoSplitActivity extends AppCompatActivity {
    private SurfaceView surfaceViewA;
    private TextureView textureViewB;
    
    private CustomGLRenderThread threadA;
    private CustomGLRenderThread threadB;
    private VideoTextureManager mVideoTextureManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_video_split);

        surfaceViewA = findViewById(R.id.surface_view_a);
        textureViewB = findViewById(R.id.texture_view_b);

        EGLContext rootContext = RootEGLManager.getInstance().getRootContext();

        // 监听 SurfaceView A 的创建
        surfaceViewA.getHolder().addCallback(new SurfaceHolder.Callback() {
            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                // 拿到 Surface,启动 A 的专属渲染线程
                threadA = new CustomGLRenderThread(holder.getSurface(), rootContext);
                threadA.start();
            }
            // ... 省略 changed 和 destroyed
        });

        // 监听 TextureView B 的创建
        textureViewB.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
            @Override
            public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
                // 拿到 SurfaceTexture 包装成 Surface,启动 B 的专属渲染线程
                Surface bSurface = new Surface(surface);
                threadB = new CustomGLRenderThread(bSurface, rootContext);
                threadB.start();
            }
            // ... 省略其他回调
        });

        // 初始化视频纹理管理器 (和之前一样)
        mVideoTextureManager = new VideoTextureManager((textureId, matrix) -> {
            // 视频解码出新帧了,通知 A 和 B 的线程去画!
            if (threadA != null) threadA.requestRender(textureId, matrix);
            if (threadB != null) threadB.requestRender(textureId, matrix);
        });
        
        // ... 初始化 MediaPlayer 绑定 mVideoTextureManager.getSurface()
    }
}

虽然写 CustomGLRenderThread 看起来代码变多了,但它带来了巨大的好处:

  • 生命周期解耦:GLSurfaceView 的 EGL Context 生命周期死死绑定在 View 上。View 销毁(比如切后台、屏幕旋转),Context 就没了,纹理全得重新加载。自己管理 EGL 线程,可以让 EGL Context 活在后台,View 销毁时只销毁 EGLSurface,View 重建时重新 eglCreateWindowSurface 即可,实现真正的无缝黑屏切换。
  • 支持 TextureView:GLSurfaceView 只能基于 SurfaceView。如果需要对视频画面做复杂的 View 动画(平移、缩放、透明度、放进 ScrollView 里滑动),SurfaceView 表现很差(会穿透、黑边),而 TextureView 表现完美。自己管理 EGL,就可以轻松把画面渲染到 TextureView 上。
  • 精准的音视频同步: 在while(isRunning) 循环中,可以极其精确地控制 eglSwapBuffers 的调用时机,配合 Choreographer (VSync 信号) 和音频时间戳,实现完美的音视频同步。
相关推荐
小码哥_常1 小时前
安卓开发秘籍:解锁10大性能优化秘诀
前端
try2find2 小时前
打印ascii码报错问题
java·linux·前端
郑州光合科技余经理3 小时前
同城O2O海外版二次开发实战:从支付网关到配送算法
开发语言·前端·后端·算法·架构·uni-app·php
冰暮流星3 小时前
javascript事件案例-全选框案例
服务器·前端·javascript
Csvn4 小时前
前端性能优化实战指南
前端
Moment4 小时前
2026 年,AI 全栈时代到了,前端简历别再只写前端技术了 🫠🫠🫠
前端·后端·面试
糯米团子7494 小时前
Web Worker
开发语言·前端·javascript
freewlt4 小时前
React Server Components 深度解析
前端·react.js·前端框架
wordbaby5 小时前
一次跨端 Loading 卡死复盘:把请求计数从 Axios 拦截器迁到 try/catch/finally
前端·axios
我命由我123455 小时前
JavaScript 开发 - 获取函数名称、获取函数参数数量、获取函数参数名称
开发语言·前端·javascript·css·html·html5·js