Сделайте снимок экрана с помощью 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 .

Solutions Collecting From Web of "Сделайте снимок экрана с помощью MediaProjection"

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

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

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

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

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


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

OES_EGL_image_external

Проще говоря, OES_EGL_image_external – это флаг aa, который должен быть передан 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 в фоновом режиме «правильно»?

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

OnFrameAvailableListener вызов 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's 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 не работает для вас или если вы хотите выполнить некоторую обработку шейдера на содержимом поверхности, прежде чем перемещать изображения с графического процессора.

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

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

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

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

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

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

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

Более подробную информацию о графической архитектуре Android можно найти здесь .

ImageReader – это класс, который вы хотите.

https://developer.android.com/reference/android/media/ImageReader.html

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

 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.

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

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

  • Прежде всего, включите кеш SurfaceView экземпляре 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 параметр качества игнорируется.