webentwicklung-frage-antwort-db.com.de

Machen Sie einen Screenshot mit MediaProjection

Mit den in Android L verfügbaren MediaProjection-APIs ist dies möglich 

erfassen Sie den Inhalt des Hauptbildschirms (die Standardanzeige) in einem Oberflächenobjekt, das Ihre App dann über das Netzwerk senden kann

Es ist mir gelungen, VirtualDisplay zum Laufen zu bringen, und meine SurfaceView zeigt den Bildschirminhalt korrekt an. 

Was ich tun möchte, ist, ein in Surface angezeigtes Bild aufzunehmen und in eine Datei zu drucken. Ich habe folgendes versucht, aber ich bekomme nur eine schwarze Datei:

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

Haben Sie eine Idee, wie Sie die angezeigten Daten aus der Surface-Datei abrufen können?

BEARBEITEN

Als @j__m vorgeschlagen, setze ich nun VirtualDisplay mit Surface eines ImageReader ein:

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

Dann erstelle ich die virtuelle Anzeige, indem ich Surface an MediaProjection übergebe:

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

Um einen "Screenshot" zu erhalten, erwerbe ich ein Image vom ImageReader und lese die Daten daraus:

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

Das Problem ist, dass die resultierende Bitmap null ist.

Dies ist die getDataFromImage-Methode:

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

Die von Image zurückgegebene acquireLatestImage enthält immer Daten mit der Standardgröße 7672320 und die Dekodierung gibt null zurück.

Wenn ImageReader versucht, ein Bild abzurufen, wird der Status ACQUIRE_NO_BUFS zurückgegeben.

17

Nachdem ich etwas Zeit verbracht hatte und etwas mehr als wünschenswert über Android-Grafikarchitektur gelernt hatte, habe ich es geschafft. Alle notwendigen Teile sind gut dokumentiert, können jedoch zu Kopfschmerzen führen, wenn Sie sich mit OpenGL nicht auskennen. Hier eine schöne Zusammenfassung für Dummies.

Ich gehe davon aus, dass du

  • Wissen über Grafika , eine inoffizielle Android Media API-Testsuite, die von Googles arbeitsliebenden Mitarbeitern in ihrer Freizeit geschrieben wurde;
  • Kann durch Khronos GL ES docs lesen, um ggf. Lücken im OpenGL ES-Wissen zu schließen;
  • Habe dieses Dokument gelesen und die meisten dort (zumindest Teile über Hardware-Komponisten und BufferQueue) geschrieben.

Die BufferQueue ist das, worum es bei ImageReader geht. Diese Klasse war anfangs schlecht benannt - besser wäre es "ImageReceiver" - ein dummer Wrapper um das Empfangsende von BufferQueue (nicht über eine andere öffentliche API zugänglich). Lassen Sie sich nicht täuschen: Es werden keine Konvertierungen durchgeführt. Es erlaubt keine Abfrage von Formaten, die vom Produzenten unterstützt werden, auch wenn C++ BufferQueue diese Informationen intern verfügbar macht. In einfachen Situationen kann es zum Beispiel fehlschlagen, wenn der Produzent ein benutzerdefiniertes, obskures Format verwendet (z. B. BGRA).

Die oben aufgeführten Probleme sind der Grund, warum ich empfehle, OpenGL ES glReadPixels als generischen Fallback zu verwenden, aber trotzdem versuchen, ImageReader zu verwenden, sofern verfügbar, da dies das Abrufen des Bildes mit minimalen Kopien/Transformationen ermöglicht.


Um eine bessere Vorstellung davon zu bekommen, wie OpenGL für die Aufgabe verwendet wird, sehen wir uns Surface an, das von ImageReader/MediaCodec zurückgegeben wird. Es ist nichts Besonderes, nur normale Oberfläche auf SurfaceTexture mit zwei gotchas: OES_EGL_image_external und EGL_Android_recordable.

OES_EGL_image_external

Einfach ausgedrückt: OES_EGL_image_external ist ein Flag , das an glBindTexture übergeben werden muss, damit die Textur mit BufferQueue funktioniert. Anstatt ein bestimmtes Farbformat usw. zu definieren, ist es ein undurchsichtiger Behälter für alles, was vom Hersteller erhalten wird. Der tatsächliche Inhalt kann im YUV-Farbraum (obligatorisch für Camera API), RGBA/BGRA (häufig von Videotreibern verwendet) oder einem anderen, möglicherweise herstellerspezifischen Format vorliegen. Der Produzent bietet zwar einige Kleinigkeiten wie JPEG- oder RGB565-Darstellungen an, hegt jedoch keine großen Hoffnungen.

Der einzige Hersteller, der von CTS-Tests ab Android 6.0 abgedeckt wird, ist eine Kamera-API (AFAIK ist nur Java-Fassade). Der Grund, warum viele MediaProjection + RGBA8888 ImageReader-Beispiele herumfliegen, ist, dass es eine häufig anzutreffende gebräuchliche Bezeichnung ist und das einzige Format, das von OpenGL ES für glReadPixels vorgeschrieben wird. Seien Sie nicht überrascht, wenn sich Display Composer für ein vollständig nicht lesbares Format oder einfach für das Format entscheidet, das nicht von der ImageReader-Klasse (z. B. BGRA8888) unterstützt wird, und Sie müssen damit umgehen.

EGL_Android_recordable

Wie aus dem Lesen von der Spezifikation hervorgeht, handelt es sich um ein Flag, das an eglChooseConfig übergeben wird, um den Producer sanft in Richtung Erzeugung von YUV-Bildern zu verschieben. Oder optimieren Sie die Pipeline zum Lesen aus dem Videospeicher. Oder so. Mir sind keine CTS-Tests bekannt, die sicherstellen, dass die Behandlung korrekt ist (und selbst die Spezifikation legt nahe, dass einzelne Hersteller für eine spezielle Behandlung hartcodiert sein könnten). Seien Sie also nicht überrascht, wenn sie nicht unterstützt wird (siehe Android) 5.0 Emulator) oder still ignoriert. In Java-Klassen gibt es keine Definition. Definieren Sie die Konstante einfach selbst, wie Grafika es tut.

Zum harten Teil kommen

Was soll man also tun, um VirtualDisplay im Hintergrund "auf die richtige Art" zu lesen?

  1. Erstellen Sie einen EGL-Kontext und eine EGL-Anzeige, möglicherweise mit dem Flag "Recordable", jedoch nicht unbedingt.
  2. Erstellen Sie einen Offscreen-Puffer zum Speichern von Bilddaten, bevor diese aus dem Videospeicher gelesen werden.
  3. Erstellen Sie eine GL_TEXTURE_EXTERNAL_OES-Textur.
  4. Erstellen Sie einen GL - Shader, um die Textur von Schritt 3 in den Puffer von Schritt 2 zu zeichnen. Der Grafiktreiber stellt (hoffentlich) sicher, dass alles, was in der "externen" Textur enthalten ist, sicher in konventionelles RGBA konvertiert wird (siehe Spezifikation) ).
  5. Erstellen Sie Surface + SurfaceTexture mit "externer" Textur.
  6. Installieren Sie OnFrameAvailableListener in der genannten SurfaceTexture (dieses muss muss vor dem nächsten Schritt ausgeführt werden, sonst wird die BufferQueue geschraubt!)
  7. Geben Sie die Oberfläche von Schritt 5 an VirtualDisplay an

Ihr OnFrameAvailableListener Callback enthält die folgenden Schritte:

  • Den Kontext aktuell machen (z. B. indem Sie den Offscreen-Puffer aktuell machen);
  • updateTexImage, um ein Bild vom Produzenten anzufordern;
  • getTransformMatrix, um die Transformationsmatrix der Textur abzurufen und zu beheben, was auch immer der Wahnsinn an den Erzeugern leidet. Beachten Sie, dass diese Matrix das OpenGL-Koordinatensystem auf dem Kopf stehend fixiert, wir werden jedoch den Upside-Downness im nächsten Schritt wieder einführen.
  • Zeichnen Sie die "externe" Textur mit dem zuvor erstellten Shader in unseren Offscreen-Puffer. Der Shader muss zusätzlich die Y-Koordinate drehen, es sei denn, Sie möchten ein gespiegeltes Bild erhalten.
  • Verwenden Sie glReadPixels, um aus Ihrem Offscreen-Videopuffer in einen ByteBuffer zu lesen.Die meisten der obigen Schritte werden intern ausgeführt, wenn der Videospeicher mit ImageReader gelesen wird. Einige davon unterscheiden sich jedoch. Die Ausrichtung der Zeilen im erstellten Puffer kann durch glPixelStore definiert werden (Standardeinstellung ist 4, so dass Sie dies bei der Verwendung von 4-Byte-RGBA8888 nicht berücksichtigen müssen).

Beachten Sie, dass neben der Verarbeitung einer Textur mit Shadern GL ES keine automatische Konvertierung zwischen Formaten erfolgt (im Gegensatz zum Desktop-OpenGL). Wenn Sie RGBA8888-Daten wünschen, müssen Sie den Offscreen-Puffer in diesem Format zuordnen und ihn von glReadPixels anfordern.

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 des Frame-Rückrufs:.

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 unterstützt wird (was wahrscheinlich der Fall wäre), weisen Sie Offscreen-Puffer zu und rufen Sie das Bild in diesem Format mit glReadPixels ab.

Wenn Sie eine vollständige Nullkopie erstellen oder Formate verwenden möchten, die von OpenGL nicht unterstützt werden (z. B. JPEG), können Sie ImageReader besser verwenden.

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

Die verschiedenen "Wie mache ich einen Screenshot eines SurfaceView"? Die Antworten ( zum Beispiel dieses ) gelten immer noch: Sie können das nicht.

Die Oberfläche von SurfaceView ist eine separate Ebene, die vom System unabhängig von der View-based UI-Ebene zusammengestellt wird. Oberflächen sind keine Puffer von Pixeln, sondern Warteschlangen von Puffern mit einer Erzeuger-Verbraucher-Anordnung. Ihre App ist auf der Produzenten-Seite. Um einen Screenshot zu erhalten, müssen Sie auf der Verbraucherseite sein.

Wenn Sie die Ausgabe an eine SurfaceTexture leiten, haben Sie in Ihrem App-Prozess statt einer SurfaceView beide Seiten der Pufferwarteschlange. Sie können die Ausgabe mit GLES rendern und mit glReadPixels() in ein Array einlesen. Grafika hat einige Beispiele dafür, wie man das mit der Kameravorschau macht.

Um den Bildschirm als Video aufzunehmen oder über ein Netzwerk zu senden, möchten Sie ihn an die Eingabeoberfläche eines MediaCodec-Encoders senden.

Weitere Details zur Android-Grafikarchitektur finden Sie hier hier .

6
fadden

Ich habe diesen Funktionscode:

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

Ich glaube, dass der Rücklauf () auf dem Bytebuffer den Trick gemacht hat, nicht wirklich sicher, warum. Ich teste es gegen einen Android-Emulator 21, da ich momentan kein Android-5.0-Gerät zur Hand habe. 

Ich hoffe es hilft!

5
mtsahakis
5
j__m

Ich habe diesen Funktionscode: -für Tablet und Mobilgerät: -

 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-Aufruf: -

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

Ich hoffe, dies hilft, wer beim Aufnehmen durch MediaProjection Api verzerrte Bilder auf dem Gerät erhält.

0
user3076769