1. 前言
看到该标题后,相信大家都会产生一个疑问,为什么要上传YUV到OES纹理(OES Texture)?
在解答这个疑问前,我们先了解一下MediaCodec。MediaCodec是Android提供的用于对音视频进行编解码的类,它通过访问底层的codec来实现编解码的功能。
MediaCodec编码器支持两种类型的输入数据,分别是ByteBuffer和Texuture(OES Texutre是Texture的一种)。
- Texture:Texture又叫纹理,表示视频数据存储在显存中,在编码前将纹理绘制到编码的SurfaceTexture中从而触发编码,整个编码流程不需要占用过多的CPU资源,由GPU和VPU协作完成编码任务。
- ByteBuffer:ByteBuffer将视频数据存储在内存中,编码前通过内存拷贝的方式传递给编码器。
两种方式各有优劣。使用 Texture 进行编码利用了GPU的图像处理能力,可以减少 CPU 的工作量,提高编码效率,而使用 ByteBuffer 进行编码则更加灵活,数据可以从文件中读取,也可从摄像头采集。
在安卓上,摄像头采集是由驱动和系统完成的,上层在获取视频数据时已经存储在Texture或者ByteBuffer中了,且摄像头采集帧率有限。如果要测试出极限的编码帧率,就要寻找一种方法,在应用层也能够将视频数据存储到Texture中传给MediaCodec编码。这里就要介绍今天的主角HardwareBuffer。
Android HardwareBuffer 是 Android 操作系统提供的一种用于共享和传递图形和图像数据的高性能、低延迟的跨进程内存缓冲区。它在 Android 8.0(API 级别 26)中首次引入,并在后续版本中不断改进和扩展。安卓支持将HardwareBuffer与纹理绑定,通过GPU将纹理绘制到编码的Surface中,减少内存的拷贝。
本文介绍如何使用HardwareBuffer上传YUV到OES纹理,主要内容有:
- 介绍HardwareBuffer及其实现原理
- 使用OpenGL上传YUV数据
- 渲染纹理验证上传结果
2. HardwareBuffer
Android HardwareBuffer 主要用于优化图形渲染、视频编解码、相机捕捉等场景,提供了一种高效的内存传输机制,可以加速多个应用和系统组件之间的数据传输。
以下是 Android HardwareBuffer 的一些关键特性和用途:
- 跨进程共享:Android HardwareBuffer 允许多个应用程序和系统组件之间共享图像和图形数据,而无需昂贵的数据复制操作。这对于多应用协作和优化性能非常有用。
- 性能优化:HardwareBuffer 提供了直接内存访问的机制,减少了数据复制和转换的开销,从而提高了性能并降低了延迟。这在图形渲染、视频编解码等性能敏感的应用中尤其有用。
- 多格式支持:HardwareBuffer 支持多种图像格式,包括 RGBA、YUV 等,使其适用于各种应用场景。
- 硬件加速:由于 HardwareBuffer 直接与硬件 GPU 和图像处理器集成,因此可以在硬件层面上进行优化,提供更高效的渲染和图像处理。
- 安全性:HardwareBuffer 具有内置的安全性,可确保数据传输和访问在 Android 系统中受到保护,防止恶意应用程序的滥用。
- API 支持:Android 提供了一组 API,开发者可以使用这些 API 创建、修改、共享和销毁 HardwareBuffer。一些与 OpenGL、Vulkan 等图形 API 集成的扩展也可用于更方便地与 HardwareBuffer 进行交互。
Android HardwareBuffer 是一个在 Android 平台上提供高性能、跨进程图像和图形数据传输的关键组件,对于提高多媒体应用程序的性能和效率非常有帮助。它可以用于图形渲染、相机捕捉、视频处理等各种应用场景,减少了数据复制和转换的开销,提供更流畅的用户体验。
2.1 实现原理
HardwareBuffer实质上是一块内存,该内存如何在GPU和CPU之间传递是提升编码效率的关键。为了避免概念混淆,本文将内存分为系统内存和显示内存。
- 系统内存和显示内存的定义:系统内存是指操作系统和应用程序可以直接访问的主机内存,而显示内存是指显卡(GPU)专用的内存,用于存储图形数据和执行图形计算。系统内存通常由CPU管理,而显示内存由GPU管理。
- 共享系统内存:在某些情况下,GPU和CPU可以共享一部分内存,以实现更高效的数据传输和共享数据。这种共享内存通常称为共享系统内存。共享系统内存可以通过特定的技术(如共享缓冲区或映射内存)实现,以便GPU和CPU可以直接访问相同的内存区域,从而避免数据复制和传输的开销。
- 物理存储器上的位置:系统内存和显示内存可以位于相同的物理存储器上,例如在集成显卡中,系统内存和显示内存可能是统一的物理内存。然而,在独立显卡中,显示内存通常是独立的显存,与系统内存分开。
那么HardwareBuffer存储在哪里呢?为什么使用HardwareBuffer可以提高编码效率?
前面说到输入Texture经由GPU绘制到编码Texture,MediaCodec编码器从纹理中读取数据进行编码,为了GPU能够访问到输入Texture的视频数据,意味着要将视频数据存放在显示内存中,同时为了提升效率,不让CPU参与数据的拷贝,有如下方法:
- 内存映射:将显示内存映射到程序的地址空间,程序往该地址写入视频数据(内存映射后,不需要CPU再把系统内存中的视频数据拷贝到显示内存中)。
- DMA控制器:由DMA控制器将系统内存中的视频数据拷贝到显示内存中,让CPU处理其它事情,拷贝完成后再重新获取CPU调度。
早期,所有设备之间的通信都需要经过CPU,结果严重影响了整个系统的性能。为了解决这个问题,有些设备加入了直接内存访问(DMA)的能力。DMA允许设备在北桥的帮助下,无需CPU的干涉,直接读写RAM。到了今天,所有高性能的设备都可以使用DMA。虽然DMA大大降低了CPU的负担,却占用了北桥的带宽,与CPU形成了争用。
在安卓手机中,图形处理单元(GPU)通常与主处理器(CPU)集成在同一芯片上,共享系统的内存(RAM)。这意味着显存并没有单独的物理存储器,而是使用系统内存来存储图形数据。
所以安卓HardwareBuffer通常采用的是内存映射的方式。HardwareBuffer是NDK提供的API,其底层由GraphicBuffer实现。
安卓手机通过使用帧缓冲区(Frame Buffer)来存储图形数据。帧缓冲区是一块显示内存区域,用于存储每个像素的颜色和其他相关信息。当图形数据需要在屏幕上显示时,GPU将从帧缓冲区中读取数据,并将其发送到显示屏上。
用户空间中的应用程序可以申请分配一块图形缓冲区(Graphic Buffer),并且将这块图形缓冲区映射到应用程序的地址空间来,以便可以向里面写入要绘制的画面的内容。
图形缓冲区(Graphic Buffer)是Android的术语,帧缓冲区(Frame Buffer)是Linux Kernel的术语。帧缓冲区是对显示内存的抽象,图形缓冲区可以在帧缓冲区上分配,即显示内存上分配,也可以在系统内存上分配,或者其它地方分配,这完全是由厂商提供的gralloc驱动决定。
Q: MediaCodec为什么要创建自己的Texture,而不直接使用输入Texture?
A: 类似于双缓冲技术,当编码器在访问Texture时,如果外部正在往里面写数据,就会导致画面异常。使用两个Texture,先把视频数据绑定到输入Texture,GPU将输入Texture绘制到帧缓冲区后,再交换输入Texture和编码Texture的缓冲区指针。
3. 上传
上传指将YUV数据和纹理进行绑定,需要使用EGL扩展。
EGL扩展:EGL扩展是对EGL标准的扩展,用于提供额外的功能或特性。这些扩展可以由硬件厂商、操作系统或图形库开发者提供,以满足特定的需求或支持特定的硬件功能。
int WrapYuvToTexture(EGLDisplay eglDisplay, int& textureId, const PlaneBuffer& planeBuffer,
uint32_t width, uint32_t height) {
EGLDisplay native_egl_display = eglDisplay;
AHardwareBuffer hardwareBuffer;
AHardwareBuffer_Desc desc = {};
desc.width = width;
desc.height = height;
desc.stride = width;
desc.layers = 1;
desc.usage = AHARDWAREBUFFER_USAGE_CPU_READ_NEVER | AHARDWAREBUFFER_USAGE_CPU_WRITE_NEVER |
AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT;
desc.format = AHARDWAREBUFFER_FORMAT_Y8Cb8Cr8_420;
int result = AHardwareBuffer_allocate(&desc, &hardwareBuffer);
if (result != 0) {
return -1;
}
AHardwareBuffer_Planes planes_info;
result = AHardwareBuffer_lockPlanes(hardwareBuffer, AHARDWAREBUFFER_USAGE_CPU_WRITE_RARELY, -1,
nullptr, &planes_info);
if (result != 0) {
AHardwareBuffer_release(hardwareBuffer);
return -1;
}
if (planes_info.planeCount != 3) {
AHardwareBuffer_release(hardwareBuffer);
return -1;
}
memcpy(planes_info.planes[0].data, planeBuffer.dataY, planes_info.planes[0].rowStride * height);
for (int i = 0; i < planes_info.planes[1].rowStride * height / 2; i++) {
uint8_t* uBufferPtr = reinterpret_cast<uint8_t*>(planes_info.planes[1].data) + i;
*uBufferPtr = *(planeBuffer.dataU + i / 2);
}
int32_t fence = -1;
result = AHardwareBuffer_unlock(hardwareBuffer, &fence);
if (result != 0) {
AHardwareBuffer_release(hardwareBuffer);
return -1;
}
EGLClientBuffer clientBuffer = g_eglGetNativeClientBufferANDROID(hardwareBuffer);
EGLImageKHR img = g_eglCreateImageKHR(native_egl_display, EGL_NO_CONTEXT,
EGL_NATIVE_BUFFER_ANDROID,
clientBuffer, 0);
if (img == EGL_NO_IMAGE_KHR) {
AHardwareBuffer_release(hardwareBuffer);
return -1;
}
glGenTextures(1, reinterpret_cast<GLuint*>(&textureId));
glBindTexture(GL_TEXTURE_EXTERNAL_OES, textureId);
g_glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES, (GLeglImageOES) img);
return 0;
}
3.1 输入
- eglDisplay,显示设备句柄,使用默认的即可
- textureId,用于保存纹理ID
- planeBuffer,生成的YUV数据,可以自定义数据结构,能访问到三个分量即可
- width,宽
- height,高
3.2 流程
- 申请HardwareBuffer内存,驱动会根据usage选择在系统内存还是显示内存申请
- 锁定HardwareBuffer,写入YUV三个分量,这里使用I420格式存储,但是是Y8Cb8Cr8,每个像素占用3字节,拷贝方式参考https://android.googlesource.com/platform/cts/+/master/tests/tests/nativehardware/jni/AHardwareBufferGLTest.cpp
- 使用EGL扩展函数将HardwareBuffer和OES纹理绑定,g_前缀的为扩展函数,使用方法请自行搜索
3.3 注意事项
- 申请失败或不再使用HardwareBuffer请使用AHardwareBuffer_release释放资源
- Y8Cb8Cr8格式存储I420,UV数据分别占用一个字节,所以是间隔一个字节存储一个分量
4. 渲染
将YUV数据与OES纹理进行绑定后,需要将OES纹理渲染到屏幕上来验证是否绑定成功。
可以自己搭建OpenGL ES的环境进行测试,也可以基于已有的项目进行验证。
参考GitHub - githubhaohao/NDK_OpenGLES_3_0: Android OpenGL ES 3.0 从入门到精通系统性学习教程,将YUV上传到OES纹理请参考上传一节。
注意OpenGL ES的上下文在多个线程间共享的问题,请搜索相关资料进行了解。
5. 参考文档
Android帧缓冲区(Frame Buffer)硬件抽象层(HAL)模块Gralloc的实现原理分析:Android帧缓冲区(Frame Buffer)硬件抽象层(HAL)模块Gralloc的实现原理分析_gralloc 能指定 stride吗-CSDN博客
GPU存储体系:https://www.cnblogs.com/hellobb/p/11023873.html
每个程序员都应该了解的内存知识:https://lrita.github.io/2018/06/10/programmer-should-know-about-memory-0/
HardwareBuffer单元测试:https://android.googlesource.com/platform/cts/+/master/tests/tests/nativehardware/jni/AHardwareBufferGLTest.cpp