gpt4 book ai didi

android - 使用 MediaProjection 截取屏幕截图

转载 作者:行者123 更新时间:2023-12-03 03:26:24 26 4
gpt4 key购买 nike

MediaProjection Android L 中可用的 API

capture the contents of the main screen (the default display) into a Surface object, which your app can then send across the network



我设法获得了 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 中检索显示数据的任何想法?

编辑

所以正如@j__m 建议的那样,我现在正在设置 VirtualDisplay使用 SurfaceImageReader :
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 的虚拟显示到 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方法:
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;
}

ImageacquireLatestImage 返回总是有默认大小为 7672320 的数据,解码返回 null .

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

最佳答案

在花了一些时间并了解了一些超出预期的 Android 图形架构之后,我已经开始使用它了。所有必要的部分都有详细记录,但如果您还不熟悉 OpenGL,可能会引起头痛,所以这里有一个很好的总结“傻瓜”。
我假设你

  • 了解 Grafika ,一个非官方的Android媒体API测试套件,由谷歌热爱工作的员工在业余时间编写;
  • 可以通读Khronos GL ES docs必要时填补 OpenGL ES 知识的空白;
  • 已阅读 this document并理解那里写的大部分内容(至少是关于硬件 Composer 和 BufferQueue 的部分)。

  • BufferQueue 是什么 ImageReader是关于。该类一开始名字很糟糕——最好称它为“ImageReceiver”——一个围绕 BufferQueue 接收端的愚蠢包装器(无法通过任何其他公共(public) API 访问)。不要被愚弄:它不执行任何转换。它不允许查询格式,生产者支持,即使 C++ BufferQueue 在内部公开该信息。在简单的情况下它可能会失败,例如,如果生产者使用自定义的、晦涩的格式(例如 BGRA)。
    上面列出的问题是为什么我建议使用 OpenGL ES glReadPixels 作为通用后备,但仍然尝试使用 ImageReader(如果可用),因为它可能允许以最少的副本/转换检索图像。

    为了更好地了解如何使用 OpenGL 来完成任务,让我们看看 Surface ,由 ImageReader/MediaCodec 返回。没什么特别的,只是 SurfaceTexture 上的普通 Surface 有两个陷阱: OES_EGL_image_externalEGL_ANDROID_recordable .
    OES_EGL_image_external
    简单地说, OES_EGL_image_externalflag ,必须将其传递给 glBindTexture 以使纹理与 BufferQueue 一起使用。它不是定义特定的颜色格式等,而是从生产者那里收到的任何东西的不透明容器。实际内容可能是 YUV 色彩空间(Camera API 必需的)、RGBA/BGRA(通常由视频驱动程序使用)或其他可能是供应商特定的格式。制作人可能会提供一些细节,例如 JPEG 或 RGB565 表示,但不要抱太大希望。
    从 Android 6.0 开始,CTS 测试涵盖的唯一生产者是相机 API(AFAIK 仅是 Java 外观)。之所以有许多 MediaProjection + RGBA8888 ImageReader 示例,是因为它是一种经常遇到的常见面额,并且是 OpenGL ES 规范对 glReadPixels 强制要求的唯一格式。如果显示编辑器决定使用完全不可读的格式,或者只是使用 ImageReader 类(例如 BGRA8888)不支持的格式,那么您仍然不必感到惊讶,并且您将不得不处理它。
    EGL_ANDROID_recordable
    从阅读中可以明显看出 the specification ,它是一个标志,传递给 eglChooseConfig 以温和地插入生产者生成 YUV 图像。或者优化从视频内存读取的管道。或者其他的东西。我不知道有任何 CTS 测试,以确保它是正确的处理(甚至规范本身也表明,个别生产者可能被硬编码以给予特殊处理),所以如果它碰巧不受支持,请不要感到惊讶(请参阅 Android 5.0 模拟器)或静默忽略。 Java 类中没有定义,只需自己定义常量,就像 Grafika 所做的那样。
    进入困难部分
    那么在后台“以正确的方式”从 VirtualDisplay 中读取数据应该怎么做呢?
  • 创建 EGL 上下文和 EGL 显示,可能带有“可记录”标志,但不一定。
  • 在从视频内存中读取图像数据之前,创建一个屏幕外缓冲区来存储图像数据。
  • 创建 GL_TEXTURE_EXTERNAL_OES 纹理。
  • 创建一个 GL 着色器,用于将第 3 步中的纹理绘制到第 2 步中的缓冲区。视频驱动程序将(希望)确保“外部”纹理中包含的任何内容都将安全地转换为传统的 RGBA(请参阅规范)。
  • 创建 Surface + SurfaceTexture,使用“外部”纹理。
  • 将 OnFrameAvailableListener 安装到上述 SurfaceTexture(这个 必须在下一步之前完成 ,否则 BufferQueue 会被搞砸!)
  • 将第 5 步中的表面提供给 VirtualDisplay

  • 您的 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);
    只有在完成上述所有操作后,您才能使用 producerSide 初始化您的 VirtualDisplay。表面。
    帧回调代码:
    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
    }
    上面的代码是方法的大大简化版本,可在 this Github project 中找到,但所有引用的类都直接来自 Grafika .
    根据您的硬件,您可能需要跳过一些额外的环节才能完成任务:使用 setSwapInterval,在制作屏幕截图之前调用 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。

    关于android - 使用 MediaProjection 截取屏幕截图,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/26545970/

    26 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com