pantalla - take screenshot programmatically android studio
Tome una captura de pantalla usando MediaProjection (6)
Con las API de MediaProjection
disponibles en Android L es posible
capture el contenido de la pantalla principal (la pantalla predeterminada) en un objeto Surface, que su aplicación puede enviar a través de la red
Me las arreglé para hacer que VirtualDisplay
funcione, y mi SurfaceView
está mostrando correctamente el contenido de la pantalla.
Lo que quiero hacer es capturar un marco 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ó, ahora estoy configurando VirtualDisplay
utilizando la Surface
de un 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 la Surface
a la 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", adquiero una Image
de ImageReader
y leo los 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 desde acquireLatestImage
siempre tiene datos con un tamaño predeterminado de 7672320 y la decodificación devuelve null
.
Más específicamente, cuando el ImageReader
intenta adquirir una imagen, se devuelve el estado ACQUIRE_NO_BUFS
.
Después de pasar un tiempo y aprender un poco más de lo deseable sobre la arquitectura gráfica de Android, tengo que trabajar. 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 tu
- Conozca Grafika , una suite de prueba de API de medios de Android no oficial, escrita por los empleados de Google que aman el trabajo en su tiempo libre;
- Puede leer la documentación de Khronos GL ES para llenar los vacíos en el conocimiento de OpenGL ES, cuando sea necesario;
- He leído este documento y he entendido la mayoría de los escritos allí (al menos partes sobre compositores de hardware y BufferQueue).
El BufferQueue es de lo que se trata ImageReader
. Para empezar, a esa clase le faltaba un nombre, sería mejor llamarlo "ImageReceiver", un envoltorio tonto que recibe el final de BufferQueue (inaccesible a través de cualquier otra API pública). No te dejes engañar: no realiza conversiones. No permite los formatos de consulta, compatibles con el productor, incluso si C ++ BufferQueue expone esa información internamente. Puede fallar en situaciones simples, por ejemplo, si el productor utiliza un formato oscuro y personalizado (como BGRA).
Los problemas mencionados anteriormente son los motivos por los que recomiendo usar glleadPixels de OpenGL ES como respaldo genérico, pero aún intento usar ImageReader si está disponible, ya que potencialmente permite recuperar la imagen con un mínimo de copias / transformaciones.
Para tener una mejor idea de cómo usar OpenGL para la tarea, echemos un vistazo a Surface
, devuelto por ImageReader / MediaCodec. No es nada especial, solo la superficie normal sobre SurfaceTexture con dos errores: OES_EGL_image_external
y EGL_ANDROID_recordable
.
OES_EGL_image_external
En pocas palabras, OES_EGL_image_external es un flag que debe pasarse 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 todo lo que se recibe del productor. El contenido real puede estar en el espacio de color YUV (obligatorio para Camera API), RGBA / BGRA (a menudo utilizado por los controladores de video) u otro formato, posiblemente específico del proveedor. El productor puede ofrecer algunas sutilezas, como representación en JPEG o RGB565, pero no mantenga sus esperanzas en alto.
El único productor, cubierto por las pruebas de CTS a partir de Android 6.0, es una API de cámara (AFAIK solo es la fachada de Java). La razón por la cual hay muchos ejemplos de MediaProjection + RGBA8888 ImageReader volando es porque se trata de una denominación común frecuente y el único formato, exigido por la especificación de OpenGL ES para glReadPixels. Aún así, no se sorprenda si el compositor de pantalla decide utilizar un formato completamente ilegible o simplemente uno, no admitido por la clase ImageReader (como BGRA8888) y tendrá que lidiar con eso.
EGL_ANDROID_recordable
Como se desprende de la lectura de OES_EGL_image_external , es una bandera que se pasa a eglChooseConfig para empujar suavemente al productor hacia la generación de imágenes YUV. O bien, optimice la tubería para leer desde la memoria de video. O algo. No tengo conocimiento de ninguna prueba de CTS, asegurándome de que es el tratamiento correcto (e incluso la especificación misma sugiere que los productores individuales pueden tener un código duro para darle un tratamiento especial), así que no se sorprenda si no está respaldado (vea Android Emulador 5.0) o silenciosamente ignorado. No hay definición en las clases de Java, solo define la constante tú 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"?
- Cree el contexto de EGL y la pantalla de EGL, posiblemente con un indicador "grabable", pero no necesariamente.
- Cree un búfer fuera de la pantalla para almacenar datos de imágenes antes de que se lean de la memoria de video.
- Crear textura GL_TEXTURE_EXTERNAL_OES.
- Cree un sombreado GL para dibujar la textura del paso 3 al búfer del paso 2. El controlador de video asegurará (con suerte) que cualquier cosa contenida en la textura "externa" se convertirá de manera segura a RGBA convencional (consulte las especificaciones).
- Crea Surface + SurfaceTexture, usando textura "externa".
- Instale OnFrameAvailableListener en dicha SurfaceTexture (esto debe hacerse antes del siguiente paso, de lo contrario, ¡BufferQueue se atornillará!)
- Suministre la superficie desde el paso 5 a VirtualDisplay
Su OnFrameAvailableListener
llamada OnFrameAvailableListener
contendrá los siguientes pasos:
- Haga que el contexto sea actual (por ejemplo, al hacer que su búfer fuera de pantalla sea actual)
- updateTexImage para solicitar una imagen del productor;
- getTransformMatrix para recuperar la matriz de transformación de la textura, corrigiendo cualquier locura que pueda estar plagando la salida del productor. Tenga en cuenta que esta matriz solucionará el sistema de coordenadas OpenGL invertido, pero volveremos a introducir el estado invertido en el siguiente paso.
- Dibuje la textura "externa" en nuestro búfer fuera de pantalla, usando el sombreador creado anteriormente. El sombreador también debe voltear su coordenada Y a menos que desee terminar con la imagen volteada.
- Use glReadPixels para leer desde su búfer de video fuera de la pantalla a 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 puede definirse mediante glPixelStore (y su valor predeterminado es 4, por lo que no es necesario tenerlo en cuenta al usar RGBA8888 de 4 bytes).
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 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 devolución de llamada del 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 enormemente simplificada, que se encuentra en este proyecto Github , pero todas las clases a las que se hace referencia vienen directamente de Grafika .
Dependiendo de su hardware, es posible que tenga que saltar unos pocos aros adicionales para hacer las cosas: usar setSwapInterval , llamar a glFlush antes de hacer la captura de pantalla, etc. La mayoría de estos pueden resolverse por su cuenta a partir del contenido de LogCat.
Para evitar la inversión de la coordenada Y, reemplace el sombreado 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 un procesamiento de sombreado en el contenido de Surface antes de mover imágenes desde la GPU.
Es posible que su velocidad se vea afectada al realizar una copia adicional en el búfer fuera de pantalla, pero el impacto de ejecutar el sombreador sería mínimo si conoce el formato exacto del búfer recibido (por ejemplo, de ImageReader) y utiliza el mismo formato para glReadPixels.
Por ejemplo, si su controlador de video está utilizando BGRA como formato interno, usted verificará si EXT_texture_format_BGRA8888
es compatible (probablemente lo haría), asignará un búfer fuera de pantalla y recuperará la imagen en este formato con glReadPixels.
Si desea realizar una copia cero completa o emplear formatos, no compatibles con OpenGL (p. Ej., JPEG), es mejor que utilice ImageReader.
Haría algo como esto:
En primer lugar, habilite el caché de dibujo en la instancia de
SurfaceView
surfaceView.setDrawingCacheEnabled(true);
Cargue el mapa de bits en la
surfaceView
Luego, en el
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 el arroyo. Además, para el formato PNG, el parámetro de calidad se ignora.
Las diversas respuestas de "cómo capturar una captura de pantalla de un SurfaceView" ( por ejemplo, esta ) siguen aplicándose: 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 la vista. Las superficies no son buffers de píxeles, sino colas de buffers, con un arreglo productor-consumidor. Su aplicación está en el lado del productor. Obtener una captura de pantalla requiere que estés del lado del consumidor.
Si dirige la salida a un SurfaceTexture, en lugar de un SurfaceView, tendrá ambos lados de la cola del búfer en el proceso de su aplicación. Puede representar la salida con GLES y leerla en una matriz con glReadPixels()
. Grafika tiene algunos ejemplos de cómo hacer cosas como esta con la vista previa de la cámara.
Para capturar la pantalla como video, o enviarlo a través de una red, desearía enviarlo a la superficie de entrada de un codificador MediaCodec.
Más detalles sobre la arquitectura gráfica de Android están disponibles aquí .
Tengo este código de trabajo: -para tabletas y dispositivos móviles: -
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> llamada 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);
}
Espero que esto ayude a quienes están obteniendo imágenes distorsionadas en el dispositivo mientras capturan a través de MediaProjection Api.
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 el 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 en este momento.
¡Espero eso ayude!
ImageReader
es la clase que quieres.
https://developer.android.com/reference/android/media/ImageReader.html