如何在Android硬解至OpenGL纹理
解读
面试官问“如何在Android硬解至OpenGL纹理”,并不是想听你背流程,而是验证三件事:
- 是否真正在国内Android真机环境(华为、小米、OPPO、三星)踩过硬解坑;
- 是否能把MediaCodec零拷贝(SurfaceTexture / AHardwareBuffer)与Unity的OpenGL上下文打通;
- 是否理解Unity主线程-渲染线程-硬件解码线程三线程同步,以及国产ROM纹理格式对齐、YUV→RGB转换、热切换Surface的适配细节。
一句话:给出一条零拷贝、低延迟、兼容90%以上国内机型的完整链路,并说明如何封装成Unity C#可调用的插件。
知识点
- Android MediaCodec的
CONFIGURE_FLAG_ENCODE反向使用,解码端配置COLOR_FormatSurface; - SurfaceTexture作为解码输出目标,内部是
GL_TEXTURE_EXTERNAL_OES,不是标准GL_TEXTURE_2D; - **Unity 2019.3+
UnityPlayer.currentActivity**获取主Activity,再getSurfaceTexture()绑定到Surface; - Unity渲染线程与MediaCodec回调线程必须用
eglGetCurrentContext()+eglMakeCurrent()二次绑定,否则glEGLImageTargetTexture2DOES会GL_INVALID_OPERATION; - 国产ROM兼容:华为GPU Turbo强制
4096×4096对齐,小米部分机型glTexParameteri必须加GL_TEXTURE_MAX_LEVEL=0; - 零拷贝核心:
AHardwareBuffer→EGLClientBuffer→eglCreateImageKHR→glEGLImageTargetTexture2DOES,Android 8.0+可用,7.0以下回退到SurfaceTexture; - Unity C#层只拿到
IntPtr texturePtr,在GL.IssuePluginEvent里把更新逻辑抛到渲染线程,防止glFinish阻塞主线程; - 生命周期:
OnApplicationPause(true)时必须MediaCodec.releaseSurface(),否则国产机后台会触发SIGSEGV; - 性能指标:硬解1080p@30fps,纹理上传延迟<16 ms,GPU占用增加<3 %,内存零增长。
答案
-
插件入口
在AndroidStudio新建unityplugin模块,继承UnityPlayerActivity,声明:public static native int createDecoder(int width, int height, long unityTexPtr);其中
unityTexPtr是C#层Texture2D.GetNativeTexturePtr()的值。 -
创建解码器
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()。 -
零拷贝绑定
在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纹理,用glEGLImageTargetTexture2DOES把img绑上去; - 返回该
OES texture的ID给Unity,C#层用Material.SetTexture("_MainTex", new Texture2D(...))直接采样即可。
- 通过
-
帧同步
MediaCodec每输出一帧,回调onOutputBufferAvailable→surfaceTexture.updateTexImage()→UnityPlayer.UnitySendMessage("HardVideoPlayer", "OnFrame", "");
Unity侧在渲染线程GL.IssuePluginEvent调用UpdateSurfaceTexture(),保证updateTexImage与glDrawArrays在同一线程,避免国产机闪屏。 -
释放
OnApplicationPause(true)→codec.stop()→codec.release()→eglDestroyImageKHR→glDeleteTextures,否则**华为Logcat会报“Surface abandoned”**导致崩溃。 -
最终接口
C#层只暴露:int Create(string path, Texture2D targetTex); void Play(); void Pause(); void Release();调用者无感知底层OpenGL,符合Unity跨平台原则。
拓展思考
- 如果项目同时要支持WebGL,硬解链路无法直接移植,需在Android端回退到软解+Shader YUV→RGB,并用
CommandBuffer.Blit把RenderTexture拷贝到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 texture先Blit到RenderTexture,再让Unity的CanvasRenderer引用该RenderTexture,这样UI和粒子都在同一GPU上下文,零额外拷贝,帧率可再提升8 %。