Skip to main content
 首页 » 编程设计

android之使用 MediaProjection 截取屏幕截图

2024年02月20日27xiaohuochai

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。