使用 MediaProjection 截取屏幕截图

2024-02-14

随着MediaProjectionAndroid L 中提供的 API 可以

将主屏幕(默认显示)的内容捕获到 Surface 对象中,然后您的应用程序可以通过网络发送该对象

我已经设法得到VirtualDisplay工作,以及我的SurfaceView正确显示屏幕内容。

我想做的是捕获显示在Surface,并将其打印到文件中。我已经尝试过以下操作,但我得到的只是一个黑色文件:

Bitmap bitmap = Bitmap.createBitmap
    (surfaceView.getWidth(), surfaceView.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
surfaceView.draw(canvas);
printBitmapToFile(bitmap);

关于如何检索显示的数据的任何想法Surface?

EDIT

正如 @j__m 建议的那样,我现在正在设置VirtualDisplay使用Surface of an ImageReader:

Display display = getWindowManager().getDefaultDisplay();
Point size = new Point();
display.getSize(size);
displayWidth = size.x;
displayHeight = size.y;

imageReader = ImageReader.newInstance(displayWidth, displayHeight, ImageFormat.JPEG, 5);

然后我创建虚拟显示并传递Surface to the MediaProjection:

int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY | DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC;

DisplayMetrics metrics = getResources().getDisplayMetrics();
int density = metrics.densityDpi;

mediaProjection.createVirtualDisplay("test", displayWidth, displayHeight, density, flags, 
      imageReader.getSurface(), null, projectionHandler);

最后,为了获得“屏幕截图”,我获得了Image来自ImageReader并从中读取数据:

Image image = imageReader.acquireLatestImage();
byte[] data = getDataFromImage(image);
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);

问题是生成的位图是null.

这是getDataFromImage method:

public static byte[] getDataFromImage(Image image) {
   Image.Plane[] planes = image.getPlanes();
   ByteBuffer buffer = planes[0].getBuffer();
   byte[] data = new byte[buffer.capacity()];
   buffer.get(data);

   return data;
}

The Image从返回的acquireLatestImage始终有默认大小为 7672320 的数据并且解码返回null.

更具体地说,当ImageReader试图获取图像、状态ACQUIRE_NO_BUFS被返回。


在花了一些时间并了解了一些超出预期的 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 读取数据,应该怎么做呢?

  1. 创建 EGL 上下文和 EGL 显示,可能带有“可记录”标志,但不一定。
  2. 创建一个离屏缓冲区,用于在从视频内存读取图像数据之前存储图像数据。
  3. 创建 GL_TEXTURE_EXTERNAL_OES 纹理。
  4. 创建一个 GL 着色器,用于将步骤 3 中的纹理绘制到步骤 2 中的缓冲区。视频驱动程序将(希望)确保“外部”纹理中包含的任何内容都将安全地转换为传统的 RGBA(请参阅规范)。
  5. 使用“外部”纹理创建 Surface + SurfaceTexture。
  6. 将OnFrameAvailableListener安装到所说的SurfaceTexture(这个must在下一步之前完成,否则 BufferQueue 将被搞砸!)
  7. 将步骤 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。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

使用 MediaProjection 截取屏幕截图 的相关文章

随机推荐

  • Ember 中的详细日志记录

    我现在正试图把注意力集中在 Ember 身上 但所有的魔力都让这一切变得困难 我已经设置了LOG TRANSITIONS true and Ember LOG BINDINGS true 这为我提供了一些最少的控制台日志记录 但我确实需要更
  • 使用 Raven DB 的数据访问架构

    我可以将哪些数据访问架构与 Raven DB 结合使用 基本上 我想通过接口分离持久性 因此我不会将底层存储暴露给上层 IE 我不想让别人看到我的域名文档存储库 or 文档会话它们来自 Raven DB 我已经实现了通用存储库模式 这似乎有
  • 如何使用 file_get_contents() 检索 Windows NT Auth 背后的文件

    我有一个设置 其中 LAMP 服务器需要从位于 Windows NT 身份验证后面的另一台服务器 IIS 的 javascript 文件检索输出 如果没有适当的身份验证 我可以使用file get contents 检索我需要的 javas
  • 通过 Apache Knox 网关访问 Apache NIFI REST API (jwt)

    我正在寻找配置 Apache 的资源KNOXTOKEN访问 Apache NIFI REST API 的服务 我已经有了KNOXSSO已配置 并且能够通过它访问 NIFI UI 但是 我找不到资源来通过 Curl 和 JWT 安全地访问 N
  • Apache Cordova - 不构建 i386 架构

    我在构建中遇到错误 使用 cordova 3 4 Undefined symbols for architecture i386 iconv referenced from zxing qrcode DecodedBitStreamPars
  • tox.ini 是否需要对 URL 中的锚点(哈希#)进行转义?

    我有一个像这样的tox ini tox skipsdist True envlist begin py35 py36 end testenv commands pip install e git ssh email protected cd
  • 向不同标头中定义的类中的函数授予友谊

    首先 这不是 家庭作业 它是 Thinking in C Vol 1 Chapter 5 ex 5 中的一个问题 我需要创建 3 个类 第一个类将其内部的友谊授予整个第二类 而仅授予第三类的一个函数友谊 我对向整个第二类授予友谊没有问题 但
  • 使用扩展 ArrayList 的 T 类中的方法

    不知道标题是否有意义 我会尝试解释一下 我有一个扩展 ArrayList 的 CustomList T 是 A1 类和 A2 类 两者都扩展了 A 类 这是一个自定义类 我需要这样做 public class CustomList
  • T-SQL 中的 TRY 和 RAISERROR

    有一个小问题 想知道我是否正确使用了这些 在我的 SQL 脚本中有 BEGIN TRY check some information and if there are certains errors RAISERROR Errors fou
  • Centos 7 中 mysqld.service 作业失败

    OS Centos 7 Linux 3 10 0 229 el7 x86 64 MySQL mysql57 community release el7 7 noarch rpm 我通过安装MySQL服务器yum 当我跑步时systemctl
  • 如何在 Backbone Marionette 中显示具有多个子视图的 CompositeView

    起始问题 我有一个 CompositeView 一个表 集合中的每个模型都表示为两个表行 模板如下 tr class row parent td parent info here td tr tr class row child td ch
  • 没有操作系统直接运行的程序叫什么名字?

    当我试图提出有关该主题的其他问题时 我很难正确表达我的问题 那么直接在相关计算机上运行的程序的正确名称是什么 一个可以描述内核和引导加载程序的术语 因为它们是在没有操作系统的情况下直接执行的 C 标准称之为 独立环境 我觉得这个术语和我见过
  • onclick="location.href='link.html'" 无法在 Safari 中加载页面

    我不明白onclick location href link html 在 Safari 5 0 4 中加载新页面 我正在使用以下方法构建下拉导航菜单
  • 如何将jhipster应用程序生成到不同的目录中?

    当我在 jhipster generator 的 cli 目录中运行以下命令时 cd cli node jhipster js 我正在同一目录 cli 中生成应用程序 我如何将此目录更改到其他位置 例如 将所有生成的文件导出到特定目录中 我
  • 使用 zip.js 在phonegap中解压缩文件

    我正在使用 PhoneGap Cordova 3 3 0 和最新版本zip js http gildas lormeau github io zip js core api html 该脚本能够获取存档内的文件列表 但无法获取任何二进制数据
  • 检查会话是否已设置,如果没有则创建一个?

    我想检查当前是否设置了会话 如果是 则允许页面正常运行 不执行任何操作 如果不创建会话 我看了另一个SO问题 其中发布了以下代码 if empty SESSION login else 最简单的方法是设置类似 SESSION a 对于每个会
  • Jasper Reports 中的 isPDFEmbedded 标签

    Jasper Reports 中 isPDFEmbedded 标签的用途是什么 您可以指定是否需要在报告的 pdf 导出中嵌入字体 将字体嵌入到 pdf 中会增加 pdf 的大小 但即使客户端计算机上未安装该字体 pdf 查看器也会显示正确
  • 使用 pscp 或其他工具将文件从 Linux 传输到 Windows

    问题陈述 我想将一些文件从远程计算机 Linux 复制到我的Windows计算机 我知道我可以使用 pscp 来做到这一点 我尝试在互联网上查找 找到了几篇文章 但在这些文章中我无法理解 并且在将文件从 Linx box 复制到 Windo
  • 如何在mathematica中应用涉及一百个变量的规则

    我有一个涉及 x1 x2 x100 的表达式 我还有一个列表lst有 100 个元素 如何将规则应用于此表达式以实现如下所示的效果 exp x1 gt lst 1 x2 gt lst 2 x100 gt lst 100 Thanks exp
  • 使用 MediaProjection 截取屏幕截图

    随着MediaProjectionAndroid L 中提供的 API 可以 将主屏幕 默认显示 的内容捕获到 Surface 对象中 然后您的应用程序可以通过网络发送该对象 我已经设法得到VirtualDisplay工作 以及我的Surf