Сделайте скриншот с помощью MediaProjection
С MediaProjection
API, доступные в 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
возвращается
5 ответов
Потратив некоторое время и изучив графическую архитектуру Android немного больше, чем хотелось бы, я начал работать. Все необходимые части хорошо документированы, но могут вызвать головную боль, если вы еще не знакомы с OpenGL, так что вот хороший обзор "для чайников".
Я предполагаю, что вы
- Знайте о Grafika, неофициальном тестовом пакете Android Media API, написанном любящими работу сотрудниками 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
а также EGL_ANDROID_recordable
,
OES_EGL_image_external
Проще говоря, OES_EGL_image_external - это флаг, который должен быть передан glBindTexture, чтобы текстура работала с BufferQueue. Вместо того, чтобы определять конкретный цветовой формат и т. Д., Это непрозрачный контейнер для всего, что получено от производителя. Фактическое содержимое может быть в цветовом пространстве YUV (обязательно для API камеры), RGBA/BGRA (часто используется видеодрайверами) или в другом, возможно, специфическом для поставщика формате. Производитель может предложить некоторые тонкости, такие как представление в формате JPEG или RGB565, но не стоит надеяться.
Единственный производитель, охваченный тестами CTS для Android 6.0, - это Camera API (AFAIK только это Java фасад). Причина, по которой существует множество примеров MediaRejection + RGBA8888 ImageReader, заключается в том, что это часто встречающийся общий наименование и единственный формат, предписанный спецификацией OpenGL ES для glReadPixels. Тем не менее, не удивляйтесь, если display composer решит использовать полностью нечитаемый формат или просто тот, который не поддерживается классом ImageReader (например, BGRA8888), и вам придется иметь с ним дело.
EGL_ANDROID_recordable
Как видно из прочтения спецификации, это флаг, передаваемый eglChooseConfig для того, чтобы осторожно подтолкнуть производителя к созданию изображений YUV. Или оптимизируйте конвейер для чтения из видеопамяти. Или что-то. Мне не известны какие-либо тесты CTS, обеспечивающие их правильное обращение (и даже сама спецификация предполагает, что отдельные производители могут быть жестко запрограммированы для предоставления ему особого режима), поэтому не удивляйтесь, если это окажется неподдерживаемым (см. Android 5.0 эмулятор) или молча игнорируется. В Java-классах нет определения, просто определите константу самостоятельно, как это делает Графика.
Начало трудной части
Так что же нужно делать, чтобы читать из VirtualDisplay в фоновом режиме "правильный путь"?
- Создайте контекст EGL и отображение EGL, возможно, с флагом "доступный для записи", но не обязательно.
- Создайте внеэкранный буфер для хранения данных изображения перед его чтением из видеопамяти.
- Создайте текстуру GL_TEXTURE_EXTERNAL_OES.
- Создайте шейдер GL для отрисовки текстуры из шага 3 в буфер из шага 2. (надеюсь) будет гарантировать, что все, что содержится во "внешней" текстуре, будет безопасно преобразовано в обычную RGBA (см. Спецификацию).
- Создайте Surface + 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
Поверхность.
Код фрейма обратного вызова:
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 не работает для вас, или если вы хотите выполнить некоторую шейдерную обработку содержимого Surface перед перемещением изображений из графического процессора.
Его скорость может быть подорвана дополнительным копированием в внеэкранный буфер, но влияние запуска шейдера будет минимальным, если вы знаете точный формат полученного буфера (например, из ImageReader) и используете тот же формат для glReadPixels.
Например, если ваш видеодрайвер использует BGRA в качестве внутреннего формата, вы можете проверить, EXT_texture_format_BGRA8888
поддерживается (это, вероятно, будет), выделить буфер за пределами экрана и получить изображение в этом формате с помощью glReadPixels.
Если вы хотите выполнить полное обнуление или использовать форматы, не поддерживаемые OpenGL (например, JPEG), вам все же лучше использовать ImageReader.
Различные ответы "как сделать снимок экрана SurfaceView" ( например, этот) все еще применимы: вы не можете этого сделать.
Поверхность SurfaceView представляет собой отдельный слой, составленный системой, независимо от уровня пользовательского интерфейса на основе View. Поверхности - это не буферы пикселей, а очереди буферов с расположением производителя-потребителя. Ваше приложение на стороне производителя. Чтобы сделать снимок экрана, вы должны быть на стороне потребителя.
Если вы направите вывод в SurfaceTexture вместо SurfaceView, у вас будут обе стороны очереди буфера в процессе вашего приложения. Вы можете сделать вывод с помощью GLES и прочитать его в массив с glReadPixels()
, У Графика есть несколько примеров таких вещей с помощью предварительного просмотра камеры.
Чтобы захватить экран в виде видео или отправить его по сети, необходимо отправить его на входную поверхность кодировщика 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);
Я полагаю, что rewind() на Bytebuffer добился цели, хотя я не совсем уверен, почему. Я тестирую его на эмуляторе Android 21, так как на данный момент у меня под рукой нет устройства Android-5.0.
Надеюсь, поможет!
У меня есть этот рабочий код:-Для планшета и мобильного устройства:-
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.
Я бы сделал что-то вроде этого:
Прежде всего, включите кэш рисования на
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 параметр качества игнорируется.