Как нарисовать Buffer[] в TextureView на Android?

Я использую JavaCV FFmpegFrameGrabber для извлечения кадров из видеофайла. . Этот FFmpegFrameGrabber возвращает Frame, который в основном содержит Buffer[] для хранения пикселей изображения для кадр видео.

Поскольку производительность является моим главным приоритетом, я хотел бы использовать OpenGL ES для непосредственного отображения этого Buffer[] без преобразования его в Bitmap.

Отображаемый вид занимает менее половины экрана и соответствует OpenGL ES документ:

Разработчики, которые хотят включить графику OpenGL ES в небольшую часть своих макетов, должны взглянуть на TextureView.

Так что я думаю, что TextureView это правильный выбор для этой задачи. Однако я не нашел много ресурсов по этому поводу (большинство из них - пример предварительного просмотра камеры).

Я хотел бы спросить, как я могу нарисовать Buffer[] в TextureView? И если это не самый эффективный способ сделать это, я готов попробовать ваши альтернативы.


Обновление: Итак, в настоящее время я настроил это следующим образом:

В моем VideoActivity, где я неоднократно извлекаю видео Frame, которые содержат ByteBuffer, а затем отправляю это в мой MyGLRenderer2 для преобразования в текстуру OpenGLES:

...
mGLSurfaceView = (GLSurfaceView)findViewById(R.id.gl_surface_view);
mGLSurfaceView.setEGLContextClientVersion(2);
mRenderer = new MyGLRenderer2(this);
mGLSurfaceView.setRenderer(mRenderer);
mGLSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
...

private void grabCurrentFrame(final long currentPosition){
    if(mCanSeek){
        new AsyncTask(){

            @Override
            protected void onPreExecute() {
                super.onPreExecute();
                mCanSeek = false;
            }

            @Override
            protected Object doInBackground(Object[] params) {
                try {
                    Frame frame = mGrabber.grabImage();
                    setCurrentFrame((ByteBuffer)frame.image[0]);
                }
                catch (Exception e) {
                    e.printStackTrace();
                }
                return null;
            }

            @Override
            protected void onPostExecute(Object o) {
                super.onPostExecute(o);
                mCanSeek = true;
                }
            }
        }.execute();
    }
}

private void setCurrentFrame(ByteBuffer buffer){
    mRenderer.setTexture(buffer);
}

MyGLRenderer2 выглядит так:

public class MyGLRenderer2 implements GLSurfaceView.Renderer {
private static final String TAG = "MyGLRenderer2";
private FullFrameTexture mFullFrameTexture;

public MyGLRenderer2(Context context){
    super();
}

@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
}

@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
    GLES20.glViewport(0,0,width, height);
    GLES20.glClearColor(0,0,0,1);
    mFullFrameTexture = new FullFrameTexture();
}

@Override
public void onDrawFrame(GL10 gl) {
    createFrameTexture(mCurrentBuffer, 1280, 720, GLES20.GL_RGB); //should not need to be a power of 2 since I use GL_CLAMP_TO_EDGE
    mFullFrameTexture.draw(textureHandle);
    if(mCurrentBuffer != null){
        mCurrentBuffer.clear();
    }
}

//test
private ByteBuffer mCurrentBuffer;

public void setTexture(ByteBuffer buffer){
    mCurrentBuffer = buffer.duplicate();
    mCurrentBuffer.position(0);
}

private int[] textureHandles = new int[1];
private int textureHandle;

public void createFrameTexture(ByteBuffer data, int width, int height, int format) {
    GLES20.glGenTextures(1, textureHandles, 0);
    textureHandle = textureHandles[0];
    GlUtil.checkGlError("glGenTextures");

    // Bind the texture handle to the 2D texture target.
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureHandle);

    // Configure min/mag filtering, i.e. what scaling method do we use if what we're rendering
    // is smaller or larger than the source image.
    GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
    GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
    GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
    GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
    GlUtil.checkGlError("loadImageTexture");

    // Load the data from the buffer into the texture handle.
    GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, /*level*/ 0, format,
            width, height, /*border*/ 0, format, GLES20.GL_UNSIGNED_BYTE, data);
    GlUtil.checkGlError("loadImageTexture");
}

}

А FullFrameTexture выглядит так:

public class FullFrameTexture {
private static final String VERTEXT_SHADER =
    "uniform mat4 uOrientationM;\n" +
        "uniform mat4 uTransformM;\n" +
        "attribute vec2 aPosition;\n" +
        "varying vec2 vTextureCoord;\n" +
        "void main() {\n" +
        "gl_Position = vec4(aPosition, 0.0, 1.0);\n" +
        "vTextureCoord = (uTransformM * ((uOrientationM * gl_Position + 1.0) * 0.5)).xy;" +
        "}";

private static final String FRAGMENT_SHADER =
    "precision mediump float;\n" +
        "uniform sampler2D sTexture;\n" +
        "varying vec2 vTextureCoord;\n" +
        "void main() {\n" +
        "gl_FragColor = texture2D(sTexture, vTextureCoord);\n" +
        "}";

private final byte[] FULL_QUAD_COORDINATES = {-1, 1, -1, -1, 1, 1, 1, -1};

private ShaderProgram shader;

private ByteBuffer fullQuadVertices;

private final float[] orientationMatrix = new float[16];
private final float[] transformMatrix = new float[16];

public FullFrameTexture() {
    if (shader != null) {
        shader = null;
    }

    shader = new ShaderProgram(EglUtil.getInstance());

    shader.create(VERTEXT_SHADER, FRAGMENT_SHADER);

    fullQuadVertices = ByteBuffer.allocateDirect(4 * 2);

    fullQuadVertices.put(FULL_QUAD_COORDINATES).position(0);

    Matrix.setRotateM(orientationMatrix, 0, 0, 0f, 0f, 1f);
    Matrix.setIdentityM(transformMatrix, 0);
}

public void release() {
    shader = null;
    fullQuadVertices = null;
}

public void draw(int textureId) {
    shader.use();

    GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);

    int uOrientationM = shader.getAttributeLocation("uOrientationM");
    int uTransformM = shader.getAttributeLocation("uTransformM");

    GLES20.glUniformMatrix4fv(uOrientationM, 1, false, orientationMatrix, 0);
    GLES20.glUniformMatrix4fv(uTransformM, 1, false, transformMatrix, 0);

    // Trigger actual rendering.
    renderQuad(shader.getAttributeLocation("aPosition"));

    shader.unUse();
}

private void renderQuad(int aPosition) {
    GLES20.glVertexAttribPointer(aPosition, 2, GLES20.GL_BYTE, false, 0, fullQuadVertices);
    GLES20.glEnableVertexAttribArray(aPosition);
    GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
}

}

На данный момент я могу отобразить какой-то кадр за очень короткий момент до сбоя приложения (тоже неправильный цвет).


person vxh.viet    schedule 25.05.2016    source источник


Ответы (1)


Самый эффективный способ сделать то, что вы просите, — преобразовать ваши пиксели в текстуру OpenGL ES и отобразить ее в TextureView. Используйте функцию glTexImage2D().

Вы можете найти несколько примеров в Grafika, которая использует эту функцию для загрузки некоторых сгенерированных текстур. Взгляните на createImageTexture(). Пакет Grafika gles может быть полезен, если в вашем приложении еще нет кода GLES.

FWIW, было бы более эффективно декодировать видеокадры непосредственно на поверхность, созданную из SurfaceTexture TextureView, но я не знаю, поддерживает ли это JavaCV.

Изменить. Другой подход, если вы не возражаете против работы с NDK, заключается в использовании ANativeWindow. Создайте поверхность для TextureView. SurfaceTexture, передайте его собственному коду, затем вызовите ANativeWindow_fromSurface(), чтобы получить ANativeWindow. Используйте ANativeWindow_setBuffersGeometry() для установки размера и формата цвета. Заблокируйте буфер, скопируйте пиксели, разблокируйте буфер, чтобы опубликовать его. Я не думаю, что для этого требуется дополнительная внутренняя копия данных, и потенциально у него есть некоторые преимущества по сравнению с подходом glTexImage2D().

person fadden    schedule 25.05.2016
comment
Большое спасибо за ваше руководство, это очень полезно, на самом деле. Сначала я попробую OpenGL ES и вернусь как можно скорее. - person vxh.viet; 26.05.2016
comment
не могли бы вы подробнее рассказать о рендеринге текстуры OpenGL ES в TextureView? Возможно, я ошибаюсь, но даже если я использую демонстрационный код GeneratedTexture.createTestTexture(GeneratedTexture.Image.FINE);, вывод всегда равен 0. - person vxh.viet; 26.05.2016
comment
Убедитесь, что вы вызываете его из потока с активным контекстом EGL. См., например. TextureViewGLActivity для примера рендеринга в TextureView с GLES с использованием выделенного потока рендерера. - person fadden; 26.05.2016
comment
да, я следую примеру TextureViewGLActivity. Я пытался заменить все биты GL_SCISSOR_TEST в Renderer.doAnimation() на простые GeneratedTexture.createTestTexture(GeneratedTexture.Image.FINE);, но все равно получаю черный экран. - person vxh.viet; 26.05.2016
comment
Просто для ясности: создание текстуры просто создает текстуру в памяти графического процессора. На самом деле ничего не рисует. См., например. Аппаратный скейлер-тренажер для кода, рисующего текстурированные сетки. - person fadden; 26.05.2016
comment
Спасибо @fadden, это действительно полезно. После некоторого тестирования с GLSurfaceView его можно отобразить как часть макета, так почему разработчик рекомендует использовать TextureView для этой цели? Какую пользу это принесет? - person vxh.viet; 26.05.2016
comment
GLSurfaceView использует отдельный слой, который может быть перед слоем View или позади него, но не смешиваться с ним. TextureView визуализируется на слое View. Это делает TextureView более гибким, но и менее эффективным. Дополнительные сведения см. на странице source.android.com/devices/graphics/architecture.html - person fadden; 26.05.2016
comment
Я думаю, что все время ошибался. После дополнительных исследований, вместо того, чтобы просто рисовать текстуру, мне нужно было: 1. Нарисовать 2 треугольника. 2. Объедините их в один большой прямоугольник, который заполнит весь GLSurfaceView. 3. Создайте большую текстуру для этого прямоугольника и примените ее. Это правильная последовательность? - person vxh.viet; 26.05.2016
comment
Ищите варианты использования gles/FullFrameRect. Он заполняет область просмотра одним текстурированным прямоугольником. Обычно он используется для таких вещей, как видеоплееры, отображающие кадры из SurfaceTexture (который берет все, что отправляется на его поверхность, и делает его доступным в качестве внешней текстуры GLES). Как вы уже поняли, при работе с GLES 2.x+ едва ли можно встать с постели, написав менее 200 строк кода, но в Grafika есть шейдеры и полигоны, необходимые для простых проектов. - person fadden; 26.05.2016
comment
Прочитав ваш комментарий здесь Я хотел бы спросить, какую производительность я могу ожидать от glTexImage2D()? В настоящее время преобразование ByteBuffer в Bitmap занимает 300-400 миллисекунд для кадра 720p, кадр 1080p легко удваивает это время. Я разрабатываю функцию очистки видео, поэтому захват и преобразование будут вызываться неоднократно. Захват занимает около 5-17 миллисекунд, поэтому для приемлемой производительности преобразование текстуры в opengl и рендеринг должны выполняться за 5-30 миллисекунд. - person vxh.viet; 29.05.2016
comment
Вы найдете сырой 512x512 RGBA тест glTexImage2D в Grafika (тест скорости glTexImage2D). Изменение желаемого размера должно быть простым. Имейте в виду, что для некоторых устройств требуется степень двойки, поэтому 1280x720 будет 2048x1024. (Есть расширение ARB, которое вы можете проверить... насколько переносимым должно быть ваше приложение?) - person fadden; 29.05.2016
comment
Ну, я не уверен, что вы подразумеваете под портативностью, если переносимость означает возможность повторного использования этого фрагмента кода на другой платформе, то не очень, мне просто нужно сосредоточиться на Android. Причина, по которой я иду по этому маршруту FFmpeg + OpenGL ES, заключается в том, что ни один из доступных в настоящее время медиаплееров на Android не соответствует моим требованиям к производительности. Либо поиск очень неточный, либо очень медленный. - person vxh.viet; 30.05.2016
comment
Привет @fadden, я думаю, что мне придется отказаться от этого метода, так как он может не соответствовать моим требованиям к производительности. Я играю с вашим ExtractMpegFramesTest. Самая затратная операция — конвертация в png, которая мне не нужна. Можно ли использовать ExtractMpegFramesTest для очистки видео? Допустим, я декодирую 30 кадров заранее, когда пользователь прокручивает их, пока я декодирую следующие 30 кадров. Другой вопрос, должен ли это быть последовательный кадр, поскольку мне может не понадобиться столько кадров. Один декодированный кадр на каждые 3 кадра идеален. - person vxh.viet; 31.05.2016
comment
Используете ли вы сейчас MediaExtractor/MediaCodec? С этим ваш конвейер будет больше похож на PlayMovieActivity от Grafika. Вы не можете избежать передачи кадров в MediaCodec, так как многие кадры основаны на дельтах от предыдущих кадров. Вы можете декодировать заранее, но вам нужно где-то хранить несжатые кадры, и это быстро складывается. Вы можете пропустить, но вы должны начать декодирование на границах кадров синхронизации. (Возможно, стоит начать новый вопрос, если вы значительно изменили свой подход.) - person fadden; 31.05.2016
comment
Я могу перейти на MediaExtractor/MediaCodec, если он может сохранить min sdk равным 16. Я исследовал PlayMovieActivity, но не уверен, как реализовать часть поиска. В основном то, что я пытаюсь сделать, это обеспечить возможность быстрого и точного поиска видео. Столько боли, пытаясь добиться этого на Android, когда буквально это можно сделать с помощью 1 строки кода в iOS. - person vxh.viet; 31.05.2016
comment
Работа с MediaCodec pre-API 18 может раздражать. К сожалению, API 16/17 по-прежнему составляют 17% рынка. Для поиска определенного кадра требуется seekTo(offset, SEEK_TO_PREVIOUS_SYNC), а затем передача кадров в декодер до тех пор, пока не будет достигнут целевой кадр; это неизбежно с компоновкой I/P/B AVC. (Перемотка вперед на TiVo выполняется плавно, а перемотка назад — нет... по той же причине.) Другой подход к рендерингу: если вы хотите углубиться в NDK, вы можете получить доступ к буферу Surface через ANativeWindow. Я добавил некоторую информацию в свой ответ. Похоже, vlc делает это. - person fadden; 31.05.2016
comment
Привет, Фадден, я знаю, что у меня много вопросов, но если бы вы могли быстро взглянуть на мой обновленный фрагмент кода, я был бы очень признателен. Кадр извлекается из видео 720p. Большое вам спасибо за ваше время. - person vxh.viet; 02.06.2016