在花了一些时间并了解了一些超出预期的 Android 图形架构之后,我已经让它可以工作了。所有必要的部分都有详细的文档记录,但如果您还不熟悉 OpenGL,可能会引起头痛,所以这里有一个“给傻瓜”的很好的总结。
我假设你
- 知道关于Grafika https://github.com/google/grafika,一个非官方的 Android 媒体 API 测试套件,由 Google 热爱工作的员工在业余时间编写;
- 可以通读Khronos GL ES 文档 https://www.khronos.org/registry/gles/必要时填补 OpenGL ES 知识的空白;
- 已读过这个文件 https://source.android.com/devices/graphics/architecture.html并理解那里写的大部分内容(至少是关于硬件作曲家和 BufferQueue 的部分)。
BufferQueue是什么ImageReader
是关于。该类一开始的命名就很糟糕——最好将其称为“ImageReceiver”——一个围绕 BufferQueue 接收端的愚蠢包装器(无法通过任何其他公共 API 访问)。不要被愚弄:它不执行任何转换。它不允许查询生产者支持的格式,即使 C++ BufferQueue 在内部公开该信息。在简单的情况下,它可能会失败,例如,如果制作者使用自定义的模糊格式(例如 BGRA)。
上述问题就是我推荐使用 OpenGL ES 的原因glReadPixels作为通用后备,但仍然尝试使用 ImageReader(如果可用),因为它可能允许以最少的副本/转换检索图像。
为了更好地了解如何使用 OpenGL 来完成任务,让我们看看Surface
,由 ImageReader/MediaCodec 返回。它没什么特别的,只是 SurfaceTexture 之上的普通 Surface,有两个问题:OES_EGL_image_external
and EGL_ANDROID_recordable
.
OES_EGL_image_external
简单的说,OES_EGL_image_external https://www.khronos.org/registry/gles/extensions/OES/OES_EGL_image_external.txt is a a flag http://developer.android.com/reference/android/opengl/GLES11Ext.html#GL_TEXTURE_EXTERNAL_OES,必须传递给gl绑定纹理使纹理与 BufferQueue 一起工作。它不是定义特定的颜色格式等,而是一个不透明的容器,用于容纳从生产者收到的任何内容。实际内容可能采用 YUV 色彩空间(相机 API 必需)、RGBA/BGRA(通常由视频驱动程序使用)或其他可能是供应商特定的格式。制作者可能会提供一些细节,例如 JPEG 或 RGB565 表示,但不要抱太大希望。
从 Android 6.0 开始,CTS 测试涵盖的唯一生产者是相机 API(据我所知,它只是 Java 外观)。之所以有很多 MediaProjection + RGBA8888 ImageReader 示例,是因为它是一种经常遇到的通用名称,也是 OpenGL ES 规范针对 glReadPixels 规定的唯一格式。如果显示编辑器决定使用完全不可读的格式或仅使用 ImageReader 类(例如 BGRA8888)不支持的格式,并且您将不得不处理它,请不要感到惊讶。
EGL_ANDROID_可记录
从阅读中可以看出规格 https://www.khronos.org/registry/gles/extensions/OES/OES_EGL_image_external.txt,它是一个标志,传递给例如选择配置为了温和地推动制作者生成 YUV 图像。或者优化从视频内存读取的管道。或者其他的东西。我不知道任何 CTS 测试,确保它是正确的处理(甚至规范本身表明,个别生产者可能会硬编码以给予它特殊处理),所以如果它碰巧不受支持,请不要感到惊讶(请参阅 Android 5.0模拟器)或默默地忽略。 Java类中没有定义,只需自己定义常量,就像Grafika那样。
进入困难部分
那么,要以“正确的方式”在后台从 VirtualDisplay 读取数据,应该怎么做呢?
- 创建 EGL 上下文和 EGL 显示,可能带有“可记录”标志,但不一定。
- 创建一个离屏缓冲区,用于在从视频内存读取图像数据之前存储图像数据。
- 创建 GL_TEXTURE_EXTERNAL_OES 纹理。
- 创建一个 GL 着色器,用于将步骤 3 中的纹理绘制到步骤 2 中的缓冲区。视频驱动程序将(希望)确保“外部”纹理中包含的任何内容都将安全地转换为传统的 RGBA(请参阅规范)。
- 使用“外部”纹理创建 Surface + SurfaceTexture。
- 将OnFrameAvailableListener安装到所说的SurfaceTexture(这个must在下一步之前完成,否则 BufferQueue 将被搞砸!)
- 将步骤 5 中的表面提供给 VirtualDisplay
Your OnFrameAvailableListener
回调将包含以下步骤:
- 使上下文成为当前上下文(例如,使屏幕外缓冲区成为当前上下文);
- updateTexImage 向生产者请求图像;
- getTransformMatrix 来检索纹理的变换矩阵,修复可能困扰生产者输出的任何疯狂问题。请注意,该矩阵将修复 OpenGL 颠倒坐标系,但我们将在下一步中重新引入颠倒性。
- 使用之前创建的着色器在屏幕外缓冲区上绘制“外部”纹理。着色器需要另外翻转它的 Y 坐标,除非您想最终得到翻转的图像。
- 使用 glReadPixels 将屏幕外视频缓冲区读取到 ByteBuffer 中。
上述大部分步骤都是在使用 ImageReader 读取视频内存时内部执行的,但也有一些不同。创建的缓冲区中的行对齐可以由 glPixelStore 定义(默认为 4,因此在使用 4 字节 RGBA8888 时不必考虑它)。
请注意,除了使用着色器处理纹理之外,GL ES 不会在格式之间进行自动转换(与桌面 OpenGL 不同)。如果您想要 RGBA8888 数据,请确保以该格式分配离屏缓冲区并从 glReadPixels 请求它。
EglCore eglCore;
Surface producerSide;
SurfaceTexture texture;
int textureId;
OffscreenSurface consumerSide;
ByteBuffer buf;
Texture2dProgram shader;
FullFrameRect screen;
...
// dimensions of the Display, or whatever you wanted to read from
int w, h = ...
// feel free to try FLAG_RECORDABLE if you want
eglCore = new EglCore(null, EglCore.FLAG_TRY_GLES3);
consumerSide = new OffscreenSurface(eglCore, w, h);
consumerSide.makeCurrent();
shader = new Texture2dProgram(Texture2dProgram.ProgramType.TEXTURE_EXT)
screen = new FullFrameRect(shader);
texture = new SurfaceTexture(textureId = screen.createTextureObject(), false);
texture.setDefaultBufferSize(reqWidth, reqHeight);
producerSide = new Surface(texture);
texture.setOnFrameAvailableListener(this);
buf = ByteBuffer.allocateDirect(w * h * 4);
buf.order(ByteOrder.nativeOrder());
currentBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
只有完成上述所有操作后,您才能使用以下命令初始化 VirtualDisplayproducerSide
表面。
帧回调代码:
float[] matrix = new float[16];
boolean closed;
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
// there may still be pending callbacks after shutting down EGL
if (closed) return;
consumerSide.makeCurrent();
texture.updateTexImage();
texture.getTransformMatrix(matrix);
consumerSide.makeCurrent();
// draw the image to framebuffer object
screen.drawFrame(textureId, matrix);
consumerSide.swapBuffers();
buffer.rewind();
GLES20.glReadPixels(0, 0, w, h, GLES10.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buf);
buffer.rewind();
currentBitmap.copyPixelsFromBuffer(buffer);
// congrats, you should have your image in the Bitmap
// you can release the resources or continue to obtain
// frames for whatever poor-man's video recorder you are writing
}
上面的代码是该方法的一个大大简化的版本,可以在这个 Github 项目 https://github.com/saki4510t/ScreenRecordingSample/,但所有引用的类都直接来自Grafika https://github.com/google/grafika/tree/master/src/com/android/grafika/gles.
根据您的硬件,您可能需要跳一些额外的圈才能完成工作:使用设置交换间隔, 呼叫glFlush在进行截图之前等等。其中大部分可以根据LogCat的内容自行计算出来。
为了避免 Y 坐标反转,请将 Grafika 使用的顶点着色器替换为以下之一:
String VERTEX_SHADER_FLIPPED =
"uniform mat4 uMVPMatrix;\n" +
"uniform mat4 uTexMatrix;\n" +
"attribute vec4 aPosition;\n" +
"attribute vec4 aTextureCoord;\n" +
"varying vec2 vTextureCoord;\n" +
"void main() {\n" +
" gl_Position = uMVPMatrix * aPosition;\n" +
" vec2 coordInterm = (uTexMatrix * aTextureCoord).xy;\n" +
// "OpenGL ES: how flip the Y-coordinate: 6542nd edition"
" vTextureCoord = vec2(coordInterm.x, 1.0 - coordInterm.y);\n" +
"}\n";
临别赠言
当 ImageReader 不适合您时,或者您想在从 GPU 移动图像之前对 Surface 内容执行一些着色器处理时,可以使用上述方法。
向离屏缓冲区进行额外的复制可能会损害它的速度,但如果您知道接收缓冲区的确切格式(例如来自 ImageReader)并为 glReadPixels 使用相同的格式,则运行着色器的影响将是最小的。
例如,如果您的视频驱动程序使用 BGRA 作为内部格式,您将检查是否EXT_texture_format_BGRA8888
支持(很可能会),分配屏幕外缓冲区并使用 glReadPixels 检索此格式的图像。
如果您想执行完整的零复制或使用 OpenGL 不支持的格式(例如 JPEG),那么最好使用 ImageReader。