Avec les API MediaProjection
disponibles dans Android L, il est possible de:
capturer le contenu de l'écran principal (affichage par défaut) dans un objet Surface, que votre application peut ensuite envoyer sur le réseau
J'ai réussi à faire fonctionner la VirtualDisplay
et ma SurfaceView
affiche correctement le contenu de l'écran.
Ce que je veux faire est de capturer une image affichée dans Surface
et de l’imprimer dans un fichier. J'ai essayé ce qui suit, mais tout ce que je reçois est un fichier noir:
Bitmap bitmap = Bitmap.createBitmap
(surfaceView.getWidth(), surfaceView.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
surfaceView.draw(canvas);
printBitmapToFile(bitmap);
Avez-vous une idée sur la façon de récupérer les données affichées à partir de la Surface
?
Donc, comme @j__m l'a suggéré, je configure maintenant la VirtualDisplay
en utilisant la Surface
d'une 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);
Ensuite, je crée l'affichage virtuel en passant la Surface
à 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);
Enfin, afin d’obtenir une "capture d’écran", j’obtiens une Image
de la ImageReader
et en lis les données:
Image image = imageReader.acquireLatestImage();
byte[] data = getDataFromImage(image);
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
Le problème est que le bitmap résultant est null
.
C'est la méthode 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
renvoyée à partir de la acquireLatestImage
a toujours des données de taille par défaut 7672320 et le décodage renvoie null
.
Plus précisément, lorsque la ImageReader
tente d'acquérir une image, le statut ACQUIRE_NO_BUFS
est renvoyé.
Après avoir passé un peu plus de temps que nécessaire et en savoir plus sur l'architecture graphique Android, je l'ai mise au travail. Toutes les pièces nécessaires sont bien documentées, mais peuvent causer des maux de tête, si vous n'êtes pas déjà familiarisé avec OpenGL, voici donc un résumé sympa "pour les nuls".
Je suppose que vous
La BufferQueue est ce à propos de ImageReader
. Cette classe était mal nommée pour commencer - il vaudrait mieux l'appeler "ImageReceiver" - un wrapper muet autour de la fin de réception de BufferQueue (inaccessible via une autre API publique). Ne soyez pas dupe: il n'effectue aucune conversion. Il n'autorise pas les formats de requête, pris en charge par le producteur, même si C++ BufferQueue expose ces informations en interne. Cela peut échouer dans des situations simples, par exemple si le producteur utilise un format obscur personnalisé (tel que BGRA).
Les problèmes énumérés ci-dessus expliquent pourquoi je recommande d'utiliser OpenGL ES glReadPixels comme solution de secours générique, tout en essayant d'utiliser ImageReader s'il est disponible, car il permet potentiellement de récupérer l'image avec un minimum de copies/transformations.
Pour avoir une meilleure idée de l'utilisation d'OpenGL pour cette tâche, examinons Surface
, renvoyé par ImageReader/MediaCodec. Ce n'est rien de spécial, juste une Surface normale au-dessus de SurfaceTexture avec deux pièges: OES_EGL_image_external
et EGL_Android_recordable
.
Pour résumer, OES_EGL_image_external est un indicateur , qui doit être passé à glBindTexture pour que la texture fonctionne avec BufferQueue. Plutôt que de définir un format de couleur spécifique, etc., il s'agit d'un conteneur opaque pour tout ce qui est reçu du producteur. Le contenu réel peut être au format YUV (obligatoire pour Camera API), RGBA/BGRA (souvent utilisé par les pilotes vidéo) ou dans un autre format, éventuellement spécifique au fournisseur. Le producteur peut offrir certaines subtilités, telles que la représentation JPEG ou RGB565, mais n'attendez pas votre espoir.
Le seul producteur, couvert par les tests CTS à partir d’Android 6.0, est une API de caméra (autant que je sache, c’est la façade Java). La raison pour laquelle il existe de nombreux exemples MediaProjection + RGBA8888 ImageReader survolant est qu’il s’agit d’une dénomination commune fréquemment rencontrée et du seul format requis par la spécification OpenGL ES pour glReadPixels. Ne soyez toujours pas surpris si le compositeur d’affichage décide d’utiliser un format complètement illisible ou tout simplement celui qui n’est pas pris en charge par la classe ImageReader (telle que BGRA8888) et vous devrez vous en occuper.
Comme le montre la lecture la spécification , il s’agit d’un indicateur passé à eglChooseConfig afin de pousser doucement le producteur à générer des images YUV. Ou optimisez le pipeline pour la lecture de la mémoire vidéo. Ou quelque chose. Je ne suis au courant d'aucun test CTS assurant que le traitement est correct (et même les spécifications suggèrent que certains producteurs peuvent être codés en dur pour lui donner un traitement spécial), alors ne soyez pas surpris s'il s'avère qu'il n'est pas pris en charge (voir Android 5.0 émulateur) ou silencieusement ignoré. Il n'y a pas de définition dans les classes Java, définissez simplement la constante vous-même, comme le fait Grafika.
Alors, que peut-on faire pour lire VirtualDisplay en arrière-plan "dans le bon sens"?
Votre rappel OnFrameAvailableListener
contiendra les étapes suivantes:
Notez qu'en dehors du traitement d'une texture avec des shaders GL, ES ne fait pas de conversion automatique entre les formats (contrairement à OpenGL sur le bureau). Si vous souhaitez obtenir des données RGBA8888, veillez à allouer le tampon hors écran dans ce format et à le demander à 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);
Code de rappel de trame:.
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
}
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";
EXT_texture_format_BGRA8888
est pris en charge (ce qui le sera probablement), allouer un tampon hors écran et récupérer l'image dans ce format avec glReadPixels.Si vous souhaitez effectuer une copie zéro complète ou utiliser des formats non pris en charge par OpenGL (JPEG, par exemple), vous aurez tout intérêt à utiliser ImageReader.
If you want to perform a complete zero-copy or employ formats, not supported by OpenGL (e.g. JPEG), you are still better off using ImageReader.
Les diverses réponses "comment capturer une capture d'écran d'une SurfaceView" ( par exemple celle-ci ) s'appliquent toujours: vous ne pouvez pas le faire.
La surface de SurfaceView est une couche séparée, composée par le système, indépendante de la couche d'interface utilisateur basée sur View. Les surfaces ne sont pas des tampons de pixels, mais plutôt des files d'attente de tampons, avec un arrangement producteur-consommateur. Votre application est du côté du producteur. Pour obtenir une capture d'écran, vous devez être du côté des consommateurs.
Si vous dirigez la sortie vers une SurfaceTexture, au lieu d'une SurfaceView, vous aurez les deux côtés de la file d'attente de mémoire tampon dans votre processus d'application. Vous pouvez rendre la sortie avec GLES et la lire dans un tableau avec glReadPixels()
. Grafika a quelques exemples de faire des choses comme celle-ci avec l'aperçu de l'appareil photo.
Pour capturer l'écran en tant que vidéo ou l'envoyer sur un réseau, vous souhaitez l'envoyer sur la surface d'entrée d'un encodeur MediaCodec.
Plus de détails sur l’architecture graphique Android sont disponibles ici .
J'ai ce code de travail:
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);
Je crois que le rewind () sur le Bytebuffer a fait l'affaire, je ne sais pas vraiment pourquoi. Je le teste par rapport à un émulateur Android 21 car je n’ai pas d’appareil Android-5.0 pour le moment.
J'espère que ça aide!
ImageReader
est la classe que vous voulez.
https://developer.Android.com/reference/Android/media/ImageReader.html
J'ai ce code de travail: -pour tablette et appareil mobile: -
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 call: -
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);
}
J'espère que cela aidera les personnes à obtenir des images déformées sur l'appareil lors de la capture via MediaProjection Api.