web-dev-qa-db-fra.com

Prendre une capture d'écran en utilisant MediaProjection

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?

MODIFIER

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é.

17
Alessandro Roaro

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

  • Connaître Grafika , une suite de tests non officielle pour les API multimédias Android, écrite par les employés professionnels de Google pendant leurs loisirs;
  • Peut lire les documents Khronos GL ES pour combler les lacunes dans les connaissances OpenGL ES, le cas échéant;
  • J'ai lu ce document et compris la plupart des écrits (au moins des parties concernant les compositeurs de matériel et BufferQueue).

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.

OES_EGL_image_external

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.

EGL_Android_recordable

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.

Arriver à la partie difficile

Alors, que peut-on faire pour lire VirtualDisplay en arrière-plan "dans le bon sens"?

  1. Créer un contexte EGL et un affichage EGL, éventuellement avec l'indicateur "enregistrable", mais pas nécessairement.
  2. Créez un tampon hors écran pour stocker les données d'image avant qu'elles ne soient lues dans la mémoire vidéo.
  3. Créez une texture GL_TEXTURE_EXTERNAL_OES.
  4. Créez un shader GL pour dessiner la texture de l'étape 3 dans la mémoire tampon de l'étape 2. Le pilote vidéo s'assurera (espérons-le) que tout élément contenu dans une texture "externe" sera converti en toute sécurité au format RGBA conventionnel (voir les spécifications ).
  5. Créez Surface + SurfaceTexture en utilisant une texture "externe".
  6. Installez OnFrameAvailableListener sur ladite SurfaceTexture (ceci doit être effectué avant l'étape suivante, sinon la BufferQueue sera vissée!)
  7. Fournissez la surface de l'étape 5 à VirtualDisplay

Votre rappel OnFrameAvailableListener contiendra les étapes suivantes:

  • Rendre le contexte actuel (par exemple, en rendant votre tampon hors écran courant);
  • updateTexImage pour demander une image au producteur;
  • getTransformMatrix pour récupérer la matrice de transformation de la texture, en corrigeant la folie qui peut nuire à la production du producteur. Notez que cette matrice corrigera le système de coordonnées à l'envers OpenGL, mais nous réintroduirons le retournement à la prochaine étape.
  • Dessinez la texture "externe" sur notre tampon hors écran, en utilisant le shader créé précédemment. En outre, le shader doit inverser sa coordonnée Y, sauf si vous souhaitez obtenir une image inversée.
  • Utilisez glReadPixels pour lire votre tampon vidéo hors écran 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 (la valeur par défaut est 4, vous n'avez donc pas à en tenir compte lorsque vous utilisez RGBA8888 à 4 octets).

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.

10
user1643723

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 .

6
fadden

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!

5
mtsahakis
5
j__m

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.

0
user3076769