Prenez une capture d'écran en utilisant MediaProjection

MediaProjection APIs disponibles sur Android L il est possible de

capturez le contenu de l'écran principal (l'affichage par défaut) dans un objet de Surface, que votre application peut ensuite envoyer à travers le réseau

j'ai réussi à obtenir l' VirtualDisplay travail, et mon SurfaceView afficher correctement le contenu de l'écran.

ce que je veux faire c'est capturer un cadre affiché dans le Surface, et de les imprimer dans un fichier. J'ai essayé la suivante, 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);

N'importe quelle idée sur la façon de récupérer les données affichées à partir du <!--8?

EDIT

alors comme @j _ _ m a suggéré que je suis en train de mettre en place le VirtualDisplaySurface d'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);

puis je crée l'affichage virtuel en passant le 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);

enfin, pour obtenir une "screenshot" j'obtiens un ImageImageReader et lire les données à partir de:

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

Le problème est que l'image résultante est null.

C'est le getDataFromImage méthode:

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 retourné à partir de la acquireLatestImage a toujours des données avec une taille par défaut de 7672320 et le décodage retourne null.

Plus précisément, lorsque le ImageReader essaie d'acquérir une image, le statut ACQUIRE_NO_BUFS est renvoyé.

16
demandé sur Alessandro Roaro 2014-10-24 14:14:32

5 réponses

après avoir passé un peu de temps et d'apprendre sur L'architecture graphique Android un peu plus que souhaitable, Je l'ai fait fonctionner. 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à familier avec OpenGL, voici donc un résumé agréable "pour les nuls".

je suppose que vous

  • Grafika, une suite test non officielle de L'API médias Android, écrite par les employés de Google qui aiment le travail dans leur pièce de rechange temps;
  • Peux lire Khronos GL ES docs pour combler les lacunes dans les connaissances opérationnelles, au besoin;
  • lire ce document et a compris la plupart des écrits là (au moins des parties sur les compositeurs de matériel et BufferQueue).

Le BufferQueue est ce que ImageReader. Cette classe a été mal nommé pour commencer par – il serait mieux de l'appeler "ImageReceiver" - un emballage stupide autour de la fin de réception de BufferQueue (inaccessible via toute autre API publique). Ne vous y trompez pas: il n'effectue pas de conversions. Il n'autorise pas les formats d'interrogation, pris en charge par le producteur, même si c++ BufferQueue expose ces informations en interne. Elle peut échouer dans des situations simples, par exemple si le producteur utilise une coutume, un format obscur, (comme BGRA).

les questions ci-dessus énumérées sont pourquoi je recommande D'utiliser OpenGL ES glReadPixels comme repli générique, mais toujours essayer d'utiliser ImageReader si disponible, puisqu'il permet potentiellement de récupérer l'image avec un minimum de copies/transformations.


pour avoir une meilleure idée comment utiliser OpenGL pour la tâche, regardons Surface, renvoyé par ImageReader / MediaCodec. Il n'y a rien de spécial, juste une Surface normale sur le dessus de la surface avec deux gotchas: OES_EGL_image_external et EGL_ANDROID_recordable.

OES_EGL_image_external

il suffit de mettre, OES_EGL_image_external est un drapeau, qui doit être transmis à glBindTexture pour faire fonctionner la texture avec BufferQueue. Plutôt que de définir un format de couleur spécifique, etc., c'est un récipient opaque pour tout ce qui est reçu du producteur. Le contenu réel peut être dans L'espace coloré YUV (obligatoire pour L'API de la caméra), RGBA/BGRA (souvent utilisé par les pilotes vidéo) ou autre format, éventuellement spécifique au vendeur. Le producteur peut offrir quelques agréments, tels que la représentation JPEG ou RGB565, mais ne gardez pas vos espoirs haut.

le seul producteur, couvert par les tests CTS à partir D'Android 6.0, est une API de caméra (AFAIK seulement c'est Java façade). La raison pour laquelle il y a de nombreux exemples D'utilisation de Mediaprojection + Rgba8888 D'ImageReader est que C'est une dénomination commune fréquemment rencontrée et le seul format, mandaté par OpenGLES spec pour glReadPixels. Ne soyez toujours pas surpris si display composer décide d'utiliser un format complètement illisible ou simplement celui, non supporté par la classe ImageReader (tel que comme BGRA8888) et vous aurez à traiter avec elle.

EGL_ANDROID_recordable

ainsi Qu'il ressort de la lecture spécification, c'est un drapeau, passé à eglChooseConfig afin de pousser doucement le producteur vers la génération D'images YUV. Ou optimiser le pipeline pour la lecture de la mémoire vidéo. Ou quelque chose. Je ne suis pas au courant d'aucun test CTS, s'assurer qu'il est traitement correct (et même la spécification elle-même suggère, que les producteurs individuels peuvent soyez hard-codé pour lui donner un traitement spécial), alors ne soyez pas surpris si elle se trouve être non supportée (voir émulateur Android 5.0) ou silencieusement ignorée. Il n'y a pas de définition dans les classes Java, il suffit de définir la constante vous-même, comme le fait Grafika.

Arriver à dur

alors qu'est-ce qu'on est censé faire pour lire à partir de VirtualDisplay en arrière-plan "de la bonne façon"?

  1. créer le contexte EGL et l'affichage EGL, éventuellement avec le drapeau "enregistrable", mais pas nécessairement.
  2. créer un tampon hors écran pour stocker les données d'image avant qu'elles ne soient lues à partir de la mémoire vidéo.
  3. créer une texture GL_TEXTURE_EXTERNAL_OES.
  4. créer un shader GL pour dessiner la texture de l'étape 3 au tampon de l'étape 2. Le pilote vidéo s'assurera (avec un peu de chance) que tout ce qui est contenu dans la texture "externe" sera converti en toute sécurité en RGBA classique (voir la spécification).
  5. Créer une Surface de + SurfaceTexture, à l'aide de "externes" texture.
  6. Installer OnFrameAvailableListener le dit SurfaceTexture (ce soit fait avant la prochaine étape, sinon la BufferQueue sera vissée!)
  7. fournir la surface de l'étape 5 à la VirtualDisplay

OnFrameAvailableListener rappel contiendra les étapes suivantes:

  • Faire le contexte actuel (par exemple, en faisant de votre écran tampon courant);
  • updateTexImage pour demander une image du producteur;
  • getTransformMatrix pour récupérer la matrice de transformation de la texture, réparant toute folie qui pourrait affecter la production du producteur. Notez que cette matrice va corriger le système de coordonnées à l'envers D'OpenGL, mais nous allons réintroduire la mise à l'envers à l'étape suivante.
  • dessinez la texture" externe " sur notre tampon offscreen, en utilisant le shader précédemment créé. Le shader doit en plus retourner sa coordonnée Y à moins que vous ne vouliez finir avec image réfléchie.
  • utilisez glReadPixels pour lire à partir de votre tampon vidéo offscreen dans un ByteBuffer.

la plupart des étapes ci-dessus sont effectuées en interne lors de la lecture de la mémoire vidéo avec ImageReader, mais certaines diffèrent. L'alignement des lignes dans le tampon créé peut être défini par glPixelStore (et par défaut à 4, donc vous n'avez pas à en tenir compte en utilisant 4 octets RGBA8888).

notez qu'à part le traitement d'une texture avec des ombres, il n'y a pas d'automatique conversion entre les formats (contrairement à L'OpenGL de bureau). Si vous voulez des données RGBA8888, assurez-vous d'allouer le tampon offscreen dans ce format et de 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);

ce N'est qu'après avoir fait tout ce qui précède que vous pouvez initialiser votre jeu VirtualDisplay avec producerSide Surface.

Code de cadre rappel:

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
}

le code ci-dessus est une version grandement simplifiée de l'approche, trouvée dans ce projet Github, mais tous référencés les cours viennent directement de Grafika.

selon votre matériel, vous pourriez avoir à sauter quelques cerceaux supplémentaires pour obtenir des choses faites: en utilisant setSwapInterval, appelant glFlush avant de faire la capture d'écran etc. La plupart d'entre eux peuvent être résolus sur votre propre à partir du contenu de LogCat.

afin d'éviter l'inversion des coordonnées Y, remplacer le shader vertex, utilisé par Grafika, par le suivant un:

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

mots d'Adieu

L'approche décrite ci-dessus peut être utilisée lorsque ImageReader ne fonctionne pas pour vous, ou si vous souhaitez effectuer un traitement d'ombre sur le contenu de la Surface avant de déplacer des images depuis GPU.

sa vitesse peut être affectée en faisant une copie supplémentaire à la mémoire tampon offscreen, mais l'impact de l'exécution de shader serait minime si vous connaissez le format exact de la mémoire tampon reçue (par exemple de ImageReader) et utilisez le même format pour glReadPixels.

par exemple, si votre pilote vidéo utilise BGRA comme format interne, vous vérifierez si EXT_texture_format_BGRA8888 est pris en charge (probablement), affecte un tampon hors écran et récupère l'image dans ce format avec glReadPixels.

si vous voulez effectuer une copie zéro complète ou employer des formats, non supportés par OpenGL (par exemple JPEG), Vous êtes encore mieux à L'aide D'ImageReader.

9
répondu user1643723 2017-04-28 12:00:02

les différentes réponses "comment capturer une capture d'écran D'une vue de surface" (exemple) tous s'appliquent toujours: vous ne pouvez pas faire cela.

la surface de SurfaceView est une couche séparée, composée par le système, indépendante de la couche UI basée sur la vue. 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 sur le producteur. Obtenir une capture d'écran exige que vous soyez sur le consommateur côté.

si vous dirigez la sortie vers une sortie SurfaceTexture, au lieu d'une vue SurfaceView, vous aurez les deux côtés de la file d'attente buffer dans votre processus app. Vous pouvez rendre la sortie avec GLES et la lire dans un tableau avec glReadPixels(). Grafika a quelques exemples de faire des trucs comme ça avec la prévisualisation de la Caméra.

Pour capturer l'écran vidéo, ou de l'envoyer sur un réseau, vous souhaitez l'envoyer à l'entrée de la surface d'un MediaCodec encodeur.

Plus de détails sur L'architecture graphique Android sont disponibles ici.

6
répondu fadden 2017-05-23 12:18:25
5
répondu j__m 2014-10-24 23:17:07

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 le tour, pas vraiment sûr de savoir pourquoi. Je le teste contre un émulateur Android 21 car je n'ai pas D'appareil Android-5.0 sous la main pour le moment.

j'Espère que ça aide!

5
répondu mtsahakis 2014-11-03 17:36:19

je voudrais faire quelque chose comme ceci:

  • tout d'abord, activez le cache de dessin sur le SurfaceView exemple

    surfaceView.setDrawingCacheEnabled(true);
    
  • chargez le bitmap dans le surfaceView

  • puis, dans le printBitmapToFile():

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

n'oubliez pas de fermer le ruisseau. De plus, pour le format PNG, le paramètre qualité est ignoré.

-2
répondu overbet13 2014-10-24 11:16:21