使用MediaProjection截取屏幕截图

时间:2014-10-24 10:14:32

标签: android bitmap surfaceview

使用Android L中提供的MediaProjection 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

中检索显示的数据

修改

因为@j__m建议我现在使用VirtualDisplay的{​​{1}}设置Surface

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

的虚拟显示
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; } 返回的Image始终为默认大小为7672320的数据,解码返回acquireLatestImage

更具体地说,当null尝试获取图片时,会返回状态ImageReader

6 个答案:

答案 0 :(得分:11)

在花了一些时间学习Android图形架构之后,我得到了它的工作。所有必要的部分都有详细记录,但如果您还不熟悉OpenGL,可能会引起麻烦,所以这里有一个很好的总结"对于假人而言#34;。

我假设你

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

BufferQueue是ImageReader的意思。这个课程的名字很难开头 - 最好把它叫做#34; ImageReceiver" - 一个接收BufferQueue端的哑包装器(通过任何其他公共API无法访问)。不要被愚弄:它不会进行任何转换。即使C ++ BufferQueue在内部公开该信息,它也不允许生成器支持的查询格式。它可能在简单的情况下失败,例如,如果生产者使用自定义的,模糊的格式(例如BGRA)。

上面列出的问题是我建议使用OpenGL ES glReadPixels 作为通用回退的原因,但仍然尝试使用ImageReader(如果可用),因为它可能允许使用最少的副本/转换来检索图像。

为了更好地了解如何使用OpenGL完成任务,让我们看一下ImageReader / MediaCodec返回的Surface。没有什么特别的,只有SurfaceTexture顶部的普通Surface有两个陷阱:OES_EGL_image_externalEGL_ANDROID_recordable

OES_EGL_image_external

简单地说,OES_EGL_image_external是一个flag,必须传递给 glBindTexture 才能使纹理与BufferQueue一起工作。它不是定义特定的颜色格式等,而是从生产者那里收到的任何不透明的容器。实际内容可以是YUV颜色空间(对于Camera API是强制性的),RGBA / BGRA(通常由视频驱动程序使用)或其他可能的特定于供应商的格式。制作人可能会提供一些细节,例如JPEG或RGB565表示,但不要把你的希望寄托在高处。

从Android 6.0开始,CTS测试覆盖的唯一一个制作人是Camera API(仅AFAIK的Java外观)。原因是,为什么有很多MediaProjection + RGBA8888 ImageReader示例飞来飞去是因为它是一个经常遇到的共同面额和唯一的格式,由glReadPixels的OpenGL ES规范强制要求。如果显示作曲家决定使用完全不可读的格式或者仅仅是ImageReader类不支持的格式(例如BGRA8888),你仍然不会感到惊讶,你将不得不处理它。

EGL_ANDROID_recordable

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

努力工作

那么应该怎样做才能从背景中读取VirtualDisplay"正确的方式"?

  1. 创建EGL上下文和EGL显示,可能使用"可记录"国旗,但不一定。
  2. 在从视频内存中读取图像数据之前,创建一个用于存储图像数据的屏幕外缓冲区。
  3. 创建GL_TEXTURE_EXTERNAL_OES纹理。
  4. 创建一个GL着色器,用于将步骤3中的纹理绘制到步骤2中的缓冲区。视频驱动程序将(希望)确保包含在" external"中的任何内容。纹理将安全地转换为传统的RGBA(参见规范)。
  5. 创建Surface + SurfaceTexture,使用" external"质地。
  6. 将OnFrameAvailableListener安装到上述SurfaceTexture(此必须在下一步之前完成,否则BufferQueue将被搞砸!)
  7. 将步骤5中的曲面提供给VirtualDisplay
  8. 您的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 Surface初始化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。

答案 1 :(得分:6)

各种“我如何捕获SurfaceView的屏幕截图”答案(e.g. this one)仍然适用:你不能这样做。

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

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

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

有关Android图形架构的更多详细信息,请here

答案 2 :(得分:5)

答案 3 :(得分: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);

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

希望它有所帮助!

答案 4 :(得分:0)

我有以下工作代码:-适用于平板电脑和移动设备:-

 private void createVirtualDisplay() {
        // get width and height
        Point size = new Point();
        mDisplay.getSize(size);
        mWidth = size.x;
        mHeight = size.y;

        // start capture reader
        if (Util.isTablet(getApplicationContext())) {
            mImageReader = ImageReader.newInstance(metrics.widthPixels, metrics.heightPixels, PixelFormat.RGBA_8888, 2);
        }else{
            mImageReader = ImageReader.newInstance(mWidth, mHeight, PixelFormat.RGBA_8888, 2);
        }
        // mImageReader = ImageReader.newInstance(450, 450, PixelFormat.RGBA_8888, 2);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            mVirtualDisplay = sMediaProjection.createVirtualDisplay(SCREENCAP_NAME, mWidth, mHeight, mDensity, VIRTUAL_DISPLAY_FLAGS, mImageReader.getSurface(), null, mHandler);
        }
        mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
            int onImageCount = 0;

            @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
            @Override
            public void onImageAvailable(ImageReader reader) {

                Image image = null;
                FileOutputStream fos = null;
                Bitmap bitmap = null;

                try {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                        image = reader.acquireLatestImage();
                    }
                    if (image != null) {
                        Image.Plane[] planes = new Image.Plane[0];
                        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
                            planes = image.getPlanes();
                        }
                        ByteBuffer buffer = planes[0].getBuffer();
                        int pixelStride = planes[0].getPixelStride();
                        int rowStride = planes[0].getRowStride();
                        int rowPadding = rowStride - pixelStride * mWidth;

                        // create bitmap
                        //
                        if (Util.isTablet(getApplicationContext())) {
                            bitmap = Bitmap.createBitmap(metrics.widthPixels, metrics.heightPixels, Bitmap.Config.ARGB_8888);
                        }else{
                            bitmap = Bitmap.createBitmap(mWidth + rowPadding / pixelStride, mHeight, Bitmap.Config.ARGB_8888);
                        }
                        //  bitmap = Bitmap.createBitmap(mImageReader.getWidth() + rowPadding / pixelStride,
                        //    mImageReader.getHeight(), Bitmap.Config.ARGB_8888);
                        bitmap.copyPixelsFromBuffer(buffer);

                        // write bitmap to a file
                        SimpleDateFormat df = new SimpleDateFormat("dd-MM-yyyy_HH:mm:ss");
                        String formattedDate = df.format(Calendar.getInstance().getTime()).trim();
                        String finalDate = formattedDate.replace(":", "-");

                        String imgName = Util.SERVER_IP + "_" + SPbean.getCurrentImageName(getApplicationContext()) + "_" + finalDate + ".jpg";

                        String mPath = Util.SCREENSHOT_PATH + imgName;
                        File imageFile = new File(mPath);

                        fos = new FileOutputStream(imageFile);
                        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
                        Log.e(TAG, "captured image: " + IMAGES_PRODUCED);
                        IMAGES_PRODUCED++;
                        SPbean.setScreenshotCount(getApplicationContext(), ((SPbean.getScreenshotCount(getApplicationContext())) + 1));
                        if (imageFile.exists())
                            new DbAdapter(LegacyKioskModeActivity.this).insertScreenshotImageDetails(SPbean.getScreenshotTaskid(LegacyKioskModeActivity.this), imgName);
                        stopProjection();
                    }

                } 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);
    }

2> onActivityResult调用:-

 if (Util.isTablet(getApplicationContext())) {
                    metrics = Util.getScreenMetrics(getApplicationContext());
                } else {
                    metrics = getResources().getDisplayMetrics();
                }
                mDensity = metrics.densityDpi;
                mDisplay = getWindowManager().getDefaultDisplay();

3>

  public static DisplayMetrics getScreenMetrics(Context context) {
            WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

            Display display = wm.getDefaultDisplay();
            DisplayMetrics dm = new DisplayMetrics();
            display.getMetrics(dm);

            return dm;
        }
        public static boolean isTablet(Context context) {
            boolean xlarge = ((context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == 4);
            boolean large = ((context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_LARGE);
            return (xlarge || large);
        }

希望这可以帮助谁在通过MediaProjection Api捕获时在设备上获取失真的图像。

答案 5 :(得分:-4)

我会做这样的事情:

  • 首先,在SurfaceView实例

    上启用绘图缓存
    surfaceView.setDrawingCacheEnabled(true);
    
  • 将位图加载到surfaceView

  • 然后,在printBitmapToFile()

    Bitmap surfaceViewDrawingCache = surfaceView.getDrawingCache();
    FileOutputStream fos = new FileOutputStream("/path/to/target/file");
    surfaceViewDrawingCache.compress(Bitmap.CompressFormat.PNG, 100, fos);
    

不要忘记关闭流。此外,对于PNG格式,将忽略质量参数。