如何在Android硬解至OpenGL纹理

解读

面试官问“如何在Android硬解至OpenGL纹理”,并不是想听你背流程,而是验证三件事:

  1. 是否真正在国内Android真机环境(华为、小米、OPPO、三星)踩过硬解坑;
  2. 是否能把MediaCodec零拷贝(SurfaceTexture / AHardwareBuffer)与Unity的OpenGL上下文打通;
  3. 是否理解Unity主线程-渲染线程-硬件解码线程三线程同步,以及国产ROM纹理格式对齐、YUV→RGB转换、热切换Surface的适配细节。
    一句话:给出一条零拷贝、低延迟、兼容90%以上国内机型的完整链路,并说明如何封装成Unity C#可调用的插件。

知识点

  • Android MediaCodecCONFIGURE_FLAG_ENCODE反向使用,解码端配置COLOR_FormatSurface
  • SurfaceTexture作为解码输出目标,内部是GL_TEXTURE_EXTERNAL_OES,不是标准GL_TEXTURE_2D
  • **Unity 2019.3+UnityPlayer.currentActivity**获取主Activity,再getSurfaceTexture()绑定到Surface
  • Unity渲染线程MediaCodec回调线程必须用eglGetCurrentContext()+eglMakeCurrent()二次绑定,否则glEGLImageTargetTexture2DOESGL_INVALID_OPERATION
  • 国产ROM兼容:华为GPU Turbo强制4096×4096对齐,小米部分机型glTexParameteri必须加GL_TEXTURE_MAX_LEVEL=0
  • 零拷贝核心:AHardwareBufferEGLClientBuffereglCreateImageKHRglEGLImageTargetTexture2DOES,Android 8.0+可用,7.0以下回退到SurfaceTexture
  • Unity C#层只拿到IntPtr texturePtr,在GL.IssuePluginEvent里把更新逻辑抛到渲染线程,防止glFinish阻塞主线程;
  • 生命周期OnApplicationPause(true)时必须MediaCodec.releaseSurface(),否则国产机后台会触发SIGSEGV
  • 性能指标:硬解1080p@30fps,纹理上传延迟<16 ms,GPU占用增加<3 %,内存零增长。

答案

  1. 插件入口
    AndroidStudio新建unityplugin模块,继承UnityPlayerActivity,声明:

    public static native int createDecoder(int width, int height, long unityTexPtr);
    

    其中unityTexPtr是C#层Texture2D.GetNativeTexturePtr()的值。

  2. 创建解码器
    Java侧MediaCodec配置:

    MediaFormat f = MediaFormat.createVideoFormat("video/avc", w, h);
    f.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaFormat.COLOR_FormatSurface);
    codec.configure(f, new Surface(surfaceTexture), null, 0);
    codec.start();
    

    surfaceTexture保存为全局变量,供后续updateTexImage()

  3. 零拷贝绑定
    在C++侧(jni.cpp)拿到unityTexPtr后:

    • 通过eglGetCurrentContext()确认当前是Unity渲染线程;
    • 创建EGLImageKHR
      EGLint attrs[] = { EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, EGL_NONE };
      EGLImageKHR img = eglCreateImageKHR(eglGetDisplay(EGL_DEFAULT_DISPLAY),
                                          eglGetCurrentContext(),
                                          EGL_GL_TEXTURE_2D_KHR,
                                          (EGLClientBuffer)(size_t)unityTexPtr, attrs);
      
    • 生成一个GL_TEXTURE_EXTERNAL_OES纹理,用glEGLImageTargetTexture2DOESimg绑上去;
    • 返回该OES texture的ID给Unity,C#层用Material.SetTexture("_MainTex", new Texture2D(...))直接采样即可。
  4. 帧同步
    MediaCodec每输出一帧,回调onOutputBufferAvailablesurfaceTexture.updateTexImage()UnityPlayer.UnitySendMessage("HardVideoPlayer", "OnFrame", "")
    Unity侧在渲染线程GL.IssuePluginEvent调用UpdateSurfaceTexture(),保证updateTexImageglDrawArrays在同一线程,避免国产机闪屏

  5. 释放
    OnApplicationPause(true)codec.stop()codec.release()eglDestroyImageKHRglDeleteTextures,否则**华为Logcat会报“Surface abandoned”**导致崩溃。

  6. 最终接口
    C#层只暴露:

    int Create(string path, Texture2D targetTex);
    void Play();
    void Pause();
    void Release();
    

    调用者无感知底层OpenGL,符合Unity跨平台原则。

拓展思考

  • 如果项目同时要支持WebGL,硬解链路无法直接移植,需在Android端回退到软解+Shader YUV→RGB,并用CommandBuffer.BlitRenderTexture拷贝到WebGL可读的RGBA32
  • 当需要多路硬解(例如大厅四路直播),MediaCodec实例数受限于codec.maxSupportedInstances,国产低端机(骁龙4系)只能跑2路,此时要用MediaCodecList动态检测并降级为H.264 baseline+软解
  • Unity 2022.3已实验性支持AndroidVideoExternalSurface,内部封装了SurfaceTexture,但国内Apk商店(华为应用市场)要求最低API 21,而官方实现最低API 26,仍需自研插件兼容;
  • 如果后续做数字孪生需要4K 60 fps硬解,必须开启MediaCodec.KEY_LOW_LATENCY+KEY_PRIORITY_REALTIME,并关闭KEY_REQUEST_SYNC_FRAME,否则小米13 Pro会出现首帧延迟>100 ms
  • 最后,字节跳动、米哈游、叠纸等一线厂在面试时还会追问:如何在硬解纹理上叠UI粒子而不触发GPU readback?答案是:把OES textureBlitRenderTexture,再让Unity的CanvasRenderer引用该RenderTexture,这样UI和粒子都在同一GPU上下文,零额外拷贝,帧率可再提升8 %。