Tome una captura de pantalla usando MediaProjection

Con las API de MediaProjection disponibles en Android L, es posible

captura el contenido de la pantalla principal (la pantalla predeterminada) en un objeto Surface, que tu aplicación puede enviar a través de la red

SurfaceView funcionar VirtualDisplay y mi SurfaceView muestra correctamente el contenido de la pantalla.

Lo que quiero hacer es capturar un cuadro que se muestra en la Surface e imprimirlo en un archivo. He intentado lo siguiente, pero todo lo que obtengo es un archivo negro:

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

¿Alguna idea sobre cómo recuperar los datos mostrados desde la Surface ?

EDITAR

Entonces, como @j__m sugirió que ahora estoy configurando el VirtualDisplay usando Surface of a 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); 

Luego creo la pantalla virtual pasando Surface a 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); 

Finalmente, para obtener una “captura de pantalla”, obtengo una Image del ImageReader y leo sus datos:

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

El problema es que el bitmap resultante es null .

Este es el método 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; } 

La Image devuelta por el acquireLatestImage siempre tiene datos con un tamaño predeterminado de 7672320 y la deencoding devuelve null .

Más específicamente, cuando el ImageReader intenta adquirir una imagen, se devuelve el estado ACQUIRE_NO_BUFS .

Después de pasar algo de tiempo y aprender acerca de la architecture de gráficos de Android un poco más que deseable, tengo que funcionar. Todas las piezas necesarias están bien documentadas, pero pueden causar dolores de cabeza, si aún no está familiarizado con OpenGL, por lo que aquí hay un buen resumen “para los maniquíes”.

Estoy asumiendo que tú

  • Conozca acerca de Grafika , un conjunto de pruebas API de medios no oficiales de Android, escrito por los empleados que aman el trabajo de Google en su tiempo libre;
  • Puede leer documentos de Khronos GL ES para completar las lagunas en el conocimiento de OpenGL ES, cuando sea necesario;
  • He leído este documento y he entendido la mayor parte de lo escrito allí (al menos partes sobre compositores de hardware y BufferQueue).

El BufferQueue es de lo que trata ImageReader . Para empezar, esa clase fue mal nombrada, sería mejor llamarla “ImageReceiver”, una envoltura tonta para recibir el final de BufferQueue (inaccesible a través de cualquier otra API pública). No te dejes engañar: no realiza ninguna conversión. No permite consultar formatos, respaldados por el productor, incluso si C ++ BufferQueue expone esa información internamente. Puede fallar en situaciones simples, por ejemplo, si el productor utiliza un formato personalizado y oscuro (como BGRA).

Los problemas mencionados anteriormente son la razón por la que recomiendo utilizar OpenGL ES glReadPixels como respaldo genérico, pero aún así intente utilizar ImageReader si está disponible, ya que potencialmente permite recuperar la imagen con copias / transformaciones mínimas.


Para tener una mejor idea de cómo usar OpenGL para la tarea, veamos Surface , devuelto por ImageReader / MediaCodec. No es nada especial, solo Surface normal encima de SurfaceTexture con dos OES_EGL_image_external : OES_EGL_image_external y EGL_ANDROID_recordable .

OES_EGL_image_external

En pocas palabras, OES_EGL_image_external es una bandera , que se debe pasar a glBindTexture para que la textura funcione con BufferQueue. En lugar de definir un formato de color específico, etc., es un contenedor opaco para lo que se recibe del productor. Los contenidos reales pueden estar en el espacio de color YUV (obligatorio para la API de la cámara), RGBA / BGRA (a menudo utilizado por los controladores de video) u otro formato, posiblemente del proveedor específico. El productor puede ofrecer algunas sutilezas, como la representación JPEG o RGB565, pero no mantiene sus esperanzas en alto.

El único productor, cubierto por las pruebas CTS a partir de Android 6.0, es una API de cámara (AFAIK solo es fachada de Java). La razón por la que hay muchos ejemplos de ImageRejection + RGBA8888 ImageReader es porque es una denominación común que se encuentra con frecuencia y el único formato, exigido por las especificaciones de OpenGL ES para glReadPixels. Aún así, no se sorprenda si el compositor de la pantalla decide usar un formato completamente ilegible o simplemente el que no es compatible con la clase ImageReader (como BGRA8888) y tendrá que lidiar con él.

EGL_ANDROID_recordable

Como se desprende de la lectura de la especificación , es una bandera que se pasa a eglChooseConfig para empujar suavemente al productor hacia la generación de imágenes YUV. O optimice la tubería para leer desde la memoria de video. O algo. No conozco ninguna prueba de CTS, asegurándome de que sea el tratamiento correcto (e incluso la propia especificación sugiere que los productores individuales pueden estar codificados para darle un tratamiento especial), así que no se sorprenda si no es compatible (consulte Android 5.0 emulador) o ignorado silenciosamente. No hay definición en las clases de Java, solo defina la constante usted mismo, como lo hace Grafika.

Llegar a la parte difícil

Entonces, ¿qué se supone que uno debe hacer para leer desde VirtualDisplay en segundo plano “de la manera correcta”?

  1. Cree un contexto EGL y una visualización EGL, posiblemente con un indicador “grabable”, pero no necesariamente.
  2. Cree un búfer fuera de pantalla para almacenar datos de imagen antes de que se lea desde la memoria de video.
  3. Crea la textura GL_TEXTURE_EXTERNAL_OES.
  4. Cree un sombreador GL para dibujar la textura del paso 3 al búfer del paso 2. El controlador de video (con suerte) se asegurará de que cualquier cosa contenida en la textura “externa” se convierta de manera segura en RGBA convencional (consulte la especificación).
  5. Crea Surface + SurfaceTexture, usando una textura “externa”.
  6. Instale OnFrameAvailableListener en dicha SurfaceTexture (esto debe hacerse antes del siguiente paso, o de lo contrario, el BufferQueue se atornillará).
  7. Suministre la superficie desde el paso 5 a la pantalla virtual

Su OnFrameAvailableListener llamada OnFrameAvailableListener contendrá los siguientes pasos:

  • Haga que el contexto sea actual (por ejemplo, actualizando el búfer fuera de pantalla);
  • updateTexImage para solicitar una imagen del productor;
  • getTransformMatrix para recuperar la matriz de transformación de la textura, arreglando cualquier locura que pueda estar plagando la producción del productor. Tenga en cuenta que esta matriz fijará el sistema de coordenadas al revés de OpenGL, pero volveremos a introducir el revés en el próximo paso.
  • Dibuja la textura “externa” en nuestro buffer externo, usando el sombreador creado previamente. El sombreador también debe voltear su coordenada Y a menos que desee terminar con la imagen volteada.
  • Utilice glReadPixels para leer desde su buffer de video fuera de pantalla en un ByteBuffer.

La mayoría de los pasos anteriores se realizan internamente al leer la memoria de video con ImageReader, pero algunos difieren. La alineación de las filas en el búfer creado se puede definir mediante glPixelStore (y la predeterminada es 4, por lo que no es necesario que cuente con ella al usar 4-byte RGBA8888).

Tenga en cuenta que, aparte de procesar una textura con sombreadores, GL ES no realiza conversiones automáticas entre formatos (a diferencia del escritorio OpenGL). Si desea datos de RGBA8888, asegúrese de asignar el búfer fuera de pantalla en ese formato y solicítelo a 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); 

Solo después de hacer todo lo anterior puede inicializar su VirtualDisplay con producerSide Surface.

Código de callback de marco:

 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 } 

El código anterior es una versión de enfoque muy simplificada, que se encuentra en este proyecto de Github , pero todas las clases a las que se hace referencia provienen directamente de Grafika .

Dependiendo de su hardware, es posible que tenga que saltar algunos aros adicionales para hacer las cosas: usar setSwapInterval , llamar a glFlush antes de hacer la captura de pantalla, etc. La mayoría de estos se pueden determinar por su cuenta a partir de los contenidos de LogCat.

Para evitar la inversión de coordenadas Y, reemplace el sombreador de vértices, usado por Grafika, con el siguiente:

 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"; 

Palabras de despedida

El enfoque descrito anteriormente se puede usar cuando ImageReader no funciona para usted, o si desea realizar algún procesamiento de sombreado en el contenido de Surface antes de mover las imágenes de la GPU.

Su velocidad puede verse perjudicada al hacer una copia extra del buffer externo, pero el impacto del shader en ejecución sería mínimo si conoce el formato exacto del búfer recibido (por ejemplo, de ImageReader) y usa el mismo formato para glReadPixels.

Por ejemplo, si su controlador de video usa BGRA como formato interno, debería verificar si se admite EXT_texture_format_BGRA8888 (lo cual es probable), asignar buffer externo y recuperar la imagen en este formato con glReadPixels.

Si desea realizar una copia cero completa o emplear formatos, que OpenGL no admite (por ejemplo, JPEG), es mejor utilizar ImageReader.

Las diversas respuestas “¿Cómo capturo una captura de pantalla de un SurfaceView?” (Por ejemplo, esta ) se siguen aplicando: no se puede hacer eso.

La superficie de SurfaceView es una capa separada, compuesta por el sistema, independiente de la capa de interfaz de usuario basada en vista. Las superficies no son buffers de píxeles, sino más bien colas de buffers, con un arreglo productor-consumidor. Tu aplicación está en el lado del productor. Obtener una captura de pantalla requiere que esté del lado del consumidor.

Si dirige la salida a SurfaceTexture, en lugar de SurfaceView, tendrá ambos lados de la cola del buffer en el proceso de su aplicación. Puede renderizar la salida con GLES y leerla en una matriz con glReadPixels() . Grafika tiene algunos ejemplos de hacer cosas como esta con la vista previa de la cámara.

Para capturar la pantalla como video, o enviarla a través de una red, desearía enviarla a la superficie de entrada de un codificador MediaCodec.

Más detalles sobre la architecture de gráficos de Android están disponibles aquí .

Tengo este código de trabajo:

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

Creo que el rebobinado () en Bytebuffer hizo el truco, aunque no estoy seguro de por qué. Lo estoy probando contra un emulador de Android 21 ya que no tengo un dispositivo con Android 5.0 disponible en este momento.

¡Espero eso ayude!

Haría algo como esto:

  • En primer lugar, habilite la memoria caché de dibujo en la instancia de SurfaceView

     surfaceView.setDrawingCacheEnabled(true); 
  • Cargue el bitmap en la surfaceView

  • Luego, en printBitmapToFile() :

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

No te olvides de cerrar la secuencia. Además, para el formato PNG, el parámetro de calidad se ignora.