Подтвердить что ты не робот

Сделайте снимок экрана с помощью MediaProjection

С API MediaProjection, доступными в Android L, можно

захватить содержимое главного экрана (дисплей по умолчанию) в объект Surface, который ваше приложение затем может отправить по сети.

Мне удалось заставить работать VirtualDisplay, а мой SurfaceView правильно отображает содержимое экрана.

Что я хочу сделать, так это захватить фрейм, отображаемый в Surface, и распечатать его в файл. Я пробовал следующее, но все, что я получаю, это черный файл:

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

Любая идея о том, как извлечь отображаемые данные из Surface?

ИЗМЕНИТЬ

Так как @j__m предположил, что я теперь настраиваю VirtualDisplay, используя Surface для 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);

Затем я создаю виртуальный дисплей, передающий Surface в 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);

Наконец, чтобы получить "скриншот", я получаю Image из ImageReader и считываю данные из него:

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

Проблема заключается в том, что результирующая растровая карта null.

Это метод 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;
}

Image, возвращаемый с acquireLatestImage, всегда имеет данные с размером по умолчанию 7672320, а декодирование возвращает null.

Более конкретно, когда ImageReader пытается получить изображение, возвращается статус ACQUIRE_NO_BUFS.

4b9b3361

Ответ 1

Проведя некоторое время и узнав об архитектуре Android-графики немного более чем желательно, у меня есть это, чтобы работать. Все необходимые фрагменты хорошо документированы, но могут вызывать головные боли, если вы еще не знакомы с OpenGL, так что вот хорошее резюме "для чайников".

Я предполагаю, что вы

  • Знайте о Grafika, неофициальном тестовом наборе API для Android, написанном сотрудниками Google, любящими работу в свободное от работы время;
  • Можно прочитать Khronos GL ES docs, чтобы заполнить пробелы в знаниях OpenGL ES, когда это необходимо;
  • Прочитали этот документ и поняли большую часть написанного там (по крайней мере, части о аппаратных композиторах и BufferQueue).

BufferQueue - это то, о чем ImageReader. Этот класс был плохо назван для начала - было бы лучше назвать его "ImageReceiver" - немой оберткой вокруг получающего конца BufferQueue (недоступным через любой другой публичный API). Не обманывайте себя: он не выполняет никаких преобразований. Он не позволяет запрашивать форматы, поддерживаемые производителем, даже если С++ BufferQueue предоставляет эту информацию внутренне. Он может не работать в простых ситуациях, например, если производитель использует настраиваемый, неясный формат (например, BGRA).

Вышеупомянутые проблемы - вот почему я рекомендую использовать OpenGL ES glReadPixels как общий резерв, но все же пытаюсь использовать ImageReader, если он доступен, поскольку он потенциально позволяет получить изображение с минимальными копиями/преобразованиями.


Чтобы лучше понять, как использовать OpenGL для задачи, просмотрите Surface, возвращенный ImageReader/MediaCodec. Это ничего особенного, просто нормальная поверхность поверх SurfaceTexture с двумя gotchas: OES_EGL_image_external и EGL_ANDROID_recordable.

OES_EGL_image_external

Проще говоря, OES_EGL_image_external является aa flag, который должен быть передан glBindTexture, чтобы заставить текстуру работать с BufferQueue. Вместо определения определенного цветового формата и т.д., Это непрозрачный контейнер для получения от производителя. Фактическое содержимое может быть в цветовом пространстве YUV (обязательно для Camera API), RGBA/BGRA (часто используется видеодрайверами) или в другом, возможно, специфичном для поставщика формате. Производитель может предложить некоторые тонкости, такие как JPEG или RGB565, но не возлагайте большие надежды.

Единственный производитель, на который распространяются тесты CTS с Android 6.0, - это API-интерфейс камеры (AFAIK - это только Java-фасад). Причина, по которой многие примеры MediaProjection + RGBA8888 ImageReader летают, состоит в том, что это часто встречающееся общее достоинство и единственный формат, предусмотренный спецификацией OpenGL ES для glReadPixels. Не удивляйтесь, если композитор-композитор решает использовать полностью нечитаемый формат или просто тот, который не поддерживается классом ImageReader (например, BGRA8888), и вам придется иметь дело с ним.

EGL_ANDROID_recordable

Как видно из чтения спецификации, это флаг, переданный eglChooseConfig, чтобы мягко подтолкнуть производителя к созданию изображений YUV. Или оптимизируйте конвейер для чтения из видеопамяти. Или что-то. Я не осведомлен о каких-либо тестах CTS, обеспечивая его правильное лечение (и даже сама спецификация предполагает, что отдельные производители могут быть жестко закодированы, чтобы придать ему особое отношение), поэтому не удивляйтесь, если это не поддерживается (см. Android 5.0) или молча игнорируется. В классах Java нет определения, просто определите константу самостоятельно, как это делает Grafika.

Получение жесткой части

Итак, что нужно делать, чтобы читать из VirtualDisplay в фоновом режиме "правильно"?

  • Создайте контекст EGL и EGL-дисплей, возможно, с "записываемым" флагом, но не обязательно.
  • Создайте внеэкранный буфер для хранения данных изображения, прежде чем он будет считан из видеопамяти.
  • Создайте текстуру GL_TEXTURE_EXTERNAL_OES.
  • Создайте GL-шейдер для рисования текстуры с шага 3 до буфера с шага 2. Драйвер видео (надеюсь) гарантирует, что что-либо, содержащееся во "внешней" текстуре, будет безопасно преобразовано в обычный RGBA (см. спецификацию).
  • Создайте поверхность + SurfaceTexture, используя "внешнюю" текстуру.
  • Установите OnFrameAvailableListener в указанную SurfaceTexture (этот должен быть выполнен до следующего шага, иначе BufferQueue будет вкручен!)
  • Поместите поверхность с шага 5 в VirtualDisplay

Обратный вызов OnFrameAvailableListener будет содержать следующие шаги:

  • Сделать контекст текущим (например, заставляя ваш буферный поток выключен);
  • updateTexImage для запроса изображения от производителя;
  • getTransformMatrix, чтобы получить матрицу преобразования текстуры, фиксируя любое безумие, которое может преследовать выход производителя. Обратите внимание, что эта матрица установит перевернутую систему координат OpenGL, но на следующем шаге мы вновь представим перевернутость.
  • Нарисуйте "внешнюю" текстуру в нашем офшорном буфере, используя ранее созданный шейдер. Шейдеру необходимо дополнительно перевернуть координату Y, если вы не хотите, чтобы в итоге получилось перевернутое изображение.
  • Используйте glReadPixels для чтения из вашего автономного видео-буфера в ByteBuffer.

Большинство вышеперечисленных шагов выполняются внутренне при чтении видеопамяти с помощью ImageReader, но некоторые отличаются. Выравнивание строк в созданном буфере может быть определено glPixelStore (и по умолчанию - 4, поэтому вам не нужно учитывать его при использовании 4-байтного RGBA8888).

Обратите внимание, что кроме обработки текстуры с шейдерами GL ES не выполняет автоматического преобразования между форматами (в отличие от настольного OpenGL). Если вы хотите получить данные RGBA8888, не забудьте выделить внеэкранный буфер в этом формате и запросить его у 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);

Только после выполнения всего выше вы можете инициализировать свой VirtualDisplay с помощью producerSide Surface.

Код обратного вызова кадра:

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 video recorder you are writing
}

Приведенный выше код представляет собой значительно упрощенную версию подхода, найденную в этот проект Github, но все ссылочные классы приходят непосредственно из Grafika.

В зависимости от вашего оборудования вам может потребоваться несколько дополнительных обручей, чтобы все было сделано: с помощью setSwapInterval, вызывая glFlush перед тем, как сделать снимок экрана и т.д. Большинство из них можно понять самостоятельно из содержимого LogCat.

Чтобы избежать обратного преобразования Y, замените вершинный шейдер, используемый Grafika, со следующим:

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

Разделительные слова

Вышеописанный подход может использоваться, когда ImageReader не работает для вас, или если вы хотите выполнить некоторую шейдерную обработку содержимого Surface перед перемещением изображений с графического процессора.

На скорость может быть нанесен ущерб, добавив дополнительную копию в буфер вне экрана, но влияние запуска шейдера было бы минимальным, если вы знаете точный формат принимаемого буфера (например, из ImageReader) и используете тот же формат для glReadPixels.

Например, если ваш видеодрайвер использует BGRA в качестве внутреннего формата, вы должны проверить, поддерживается ли EXT_texture_format_BGRA8888 (вероятно, это будет), выделять внеэкранный буфер и извлекать изображение в этом формате с помощью glReadPixels.

Если вы хотите выполнить полную нуль-копию или использовать форматы, не поддерживаемые OpenGL (например, JPEG), вам все же лучше использовать ImageReader.

Ответ 2

Различные "как я могу сделать снимок экрана SurfaceView" (например, этот) все еще применяются: вы не можете этого сделать.

Поверхность SurfaceView представляет собой отдельный слой, скомпонованный системой, независимо от уровня пользовательского интерфейса на основе View. Поверхности не являются буферами пикселей, а скорее очередями буферов с устройством производителя-потребителя. Ваше приложение находится на стороне производителя. Получение снимка экрана требует, чтобы вы были на стороне потребителя.

Если вы направляете вывод в SurfaceTexture, вместо SurfaceView, у вас будут обе стороны очереди буферов в вашем приложении. Вы можете отобразить результат с помощью GLES и прочитать его в массиве с помощью glReadPixels(). Grafika есть примеры того, как делать это с предварительным просмотром камеры.

Чтобы захватить экран в виде видео или отправить его по сети, вы хотите отправить его на входную поверхность кодировщика MediaCodec.

Подробнее о графической архитектуре Android доступны здесь.

Ответ 4

У меня есть этот рабочий код:

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

Я считаю, что перемотка назад() на Bytebuffer сделала трюк, но не совсем уверен, почему. Я тестирую его против эмулятора Android 21, поскольку на данный момент у меня нет устройства Android-5.0.

Надеюсь, что это поможет!

Ответ 5

У меня есть этот рабочий код: -for планшет и мобильное устройство: -

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

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

Надеюсь, это поможет тем, кто получает искаженные изображения на устройстве во время захвата через MediaProjection Api.

Ответ 6

Я бы сделал что-то вроде этого:

  • Прежде всего, включите кеш чертежа в экземпляре SurfaceView

    surfaceView.setDrawingCacheEnabled(true);
    
  • Загрузите растровое изображение в SurfaceView

  • Затем в printBitmapToFile():

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

Не забудьте закрыть поток. Кроме того, для формата PNG параметр качества игнорируется.