使用MediaProjection截取屏幕截图

Ale*_*aro 18 android bitmap surfaceview

借助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);
Run Code Online (Sandbox Code Playgroud)

有关如何从中检索显示数据的任何想法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);
Run Code Online (Sandbox Code Playgroud)

然后我创建虚拟显示传递SurfaceMediaProjection:

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);
Run Code Online (Sandbox Code Playgroud)

最后,为了获得"截图",我Image从中获取并从中ImageReader读取数据:

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

问题是生成的位图是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;
}
Run Code Online (Sandbox Code Playgroud)

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

更具体地,当ImageReader尝试获取图像时,ACQUIRE_NO_BUFS返回状态.

use*_*723 14

在花了一些时间学习Android图形架构之后,我得到了它的工作.所有必要的组件都充分证明,但可引起头痛,如果你不熟悉的OpenGL,所以这里是一个很好的总结"傻瓜".

我假设你

  • 了解Grafika,这是一款非官方的Android媒体API测试套件,由业余爱好的员工在业余时间撰写;
  • 可以阅读Khronos GL ES文档,以便在必要时填补OpenGL ES知识的空白;
  • 已阅读此文档并了解其中大部分内容(至少有关于硬件作曲家和BufferQueue的部分内容).

BufferQueue就是这样ImageReader的.这班被评为较差开始用-这将是更好地称之为"ImageReceiver" -各地(通过任何其他公共API无法访问)接收BufferQueue结束一个愚蠢的包装.不要被愚弄:它不会执行任何转换.它不允许生成器支持的查询格式,即使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_external是一个标志,必须传递给glBindTexture才能使纹理与BufferQueue一起工作.它不是定义特定的颜色格式等,而是从生产者那里收到的任何不透明的容器.实际内容可以是在YUV色彩空间(强制性相机API),RGBA/BGRA(经常使用的视频驱动器)或其他可能特定于厂商的格式.制作人可能会提供一些细节,例如JPEG或RGB565表示,但不会让你的希望高涨.

从Android 6.0开始,CTS测试覆盖的唯一生产者是Camera API(仅AFAIK的Java外观).究其原因,为什么有很多MediaProjection + RGBA8888 ImageReader的例子到处飞,是因为它是一个经常遇到的常用的单位和唯一的格式,通过OpenGL ES的规格为glReadPixels授权.不过,如果显示的作曲家决定使用完全不可读格式或简单的一个,通过的ImageReader类(如BGRA8888)不支持,你将不得不面对它不要感到惊讶.

EGL_ANDROID_recordable

从阅读规范可以看出,它是一个标志,传递给eglChooseConfig,以便轻轻地推动制作人生成YUV图像.或优化管道以便从视频内存中读取.或者其他的东西.我不知道任何CTS测试,确保它是正确的治疗(甚至规范本身表明,个别生产者可能硬编码给予特殊处理),所以如果它不恰当的话不要感到惊讶(参见Android 5.0模拟器)或默默忽略.Java类中没有定义,只需自己定义常量,就像Grafika一样.

努力工作

那么在背景中"正确的方式"从VirtualDisplay读取应该做些什么呢?

  1. 创建EGL上下文和EGL显示,可能带有"可记录"标志,但不一定.
  2. 创建一个屏幕外缓冲区,用于在从视频内存中读取图像数据之前存储它们.
  3. 创建GL_TEXTURE_EXTERNAL_OES纹理.
  4. 从步骤3绘制纹理从步骤2所述的视频驱动程序将(希望)确保缓冲创建GL着色器,任何事情,包含在"外部"纹理将被安全地转换成常规RGBA(参见规范).
  5. 使用"外部"纹理创建Surface + SurfaceTexture.
  6. 将OnFrameAvailableListener安装到所述SurfaceTexture(这必须在下一步之前完成,否则BufferQueue将被拧紧!)
  7. 将步骤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);
Run Code Online (Sandbox Code Playgroud)

只有完成上述所有操作后,才能使用producerSideSurface 初始化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
}
Run Code Online (Sandbox Code Playgroud)

上面的代码是一个大大简化的方法版本,可以在这个Github项目中找到,但是所有引用的类都直接来自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";
Run Code Online (Sandbox Code Playgroud)

分手

上述方法可以用来当ImageReader的不适合你,或者如果你想从GPU运动图像之前执行的图面内容有些着色器处理.

通过对屏幕外缓冲区执行额外复制可能会损害速度,但如果您知道接收缓冲区的确切格式(例如,来自ImageReader)并使用相同格式的glReadPixels,则运行着色器的影响将会很小.

例如,如果您的视频驱动程序使用BGRA内部格式,你会检查是否EXT_texture_format_BGRA8888支持(很可能会),分配离屏缓冲区,并以这种格式与glReadPixels retrive图像.

如果要执行一个完整的零拷贝或使用的格式,而不是OpenGL的(如JPEG)的支持,你还是最好使用ImageReader的.


fad*_*den 6

各种"我如何捕捉SurfaceView的屏幕截图"答案(例如这一个)仍然适用:你不能这样做.

SurfaceView的表面是一个单独的图层,由系统合成,独立于基于视图的UI图层.曲面不是像素的缓冲区,而是缓冲区的队列,具有生产者 - 消费者安排.你的应用程序是在制作人一方.获得屏幕截图需要您在消费者方面.

如果将输出定向到SurfaceTexture而不是SurfaceView,则应用程序进程中将包含缓冲区队列的两侧.您可以使用GLES渲染输出并将其读入数组glReadPixels(). Grafika有一些使用Camera预览做这样的事情的例子.

要将屏幕捕获为视频,或通过网络发送,您可能希望将其发送到MediaCodec编码器的输入表面.

有关Android图形架构的更多详细信息,请点击此处.


mts*_*kis 5

我有这个工作代码:

mImageReader = ImageReader.newInstance(width, height, ImageFormat.JPEG, 5);
mProjection.createVirtualDisplay("test", width, height, density, flags, mImageReader.getSurface(), new VirtualDisplayCallback(), mHandler);
mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {

        @Override
        public void onImageAvailable(ImageReader reader) {
            Image image = null;
            FileOutputStream fos = null;
            Bitmap bitmap = null;

            try {
                image = mImageReader.acquireLatestImage();
                fos = new FileOutputStream(getFilesDir() + "/myscreen.jpg");
                final Image.Plane[] planes = image.getPlanes();
                final Buffer buffer = planes[0].getBuffer().rewind();
                bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
                bitmap.copyPixelsFromBuffer(buffer);
                bitmap.compress(CompressFormat.JPEG, 100, fos);

            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                        if (fos!=null) {
                            try {
                                fos.close();
                            } catch (IOException ioe) { 
                                ioe.printStackTrace();
                            }
                        }

                        if (bitmap!=null)
                            bitmap.recycle();

                        if (image!=null)
                           image.close();
            }
          }

    }, mHandler);
Run Code Online (Sandbox Code Playgroud)

我相信Bytebuffer上的rewind()做了伎俩,但并不确定为什么.我正在针对Android模拟器21进行测试,因为目前我还没有安装Android-5.0设备.

希望能帮助到你!