Ответ 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.