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 信号) 和音频时间戳,实现完美的音视频同步。
相关推荐
IT_陈寒2 小时前
JavaScript开发者必知的5个性能杀手,你踩了几个坑?
前端·人工智能·后端
跟着珅聪学java2 小时前
Electron 精美菜单设计
运维·前端·数据库
日光倾2 小时前
【Vue.js 入门笔记】闭包和对象引用
前端·vue.js·笔记
一只程序熊2 小时前
UniappX 未找到 “video“ 组件,已自动当做 “view“ 组件处理。请确保代码正确,或重新生成自定义基座后再试。
前端
林小帅2 小时前
【笔记】xxx 技术分享文档模板
前端
雾岛心情2 小时前
【HTML&CSS】HTML为文字添加格式和内容
前端·css·html
心.c2 小时前
如何在项目中减少 XSS 攻击
前端·xss
Rsun045513 小时前
Vue相关面试题
前端·javascript·vue.js
TON_G-T3 小时前
前端包管理器(npm、yarn、pnpm)
前端