2

I'm using JavaCV's FFmpegFrameGrabber to retrieve frames from a video file. This FFmpegFrameGrabber return a Frame which basically contain a Buffer[] to hold image pixels for a video frame.

Because performance is my top priority, I would like to use OpenGL ES to display this Buffer[] directly without converting it into Bitmap.

The view to be displayed is only taking less than half of the screen and following the OpenGL ES document:

Developers who want to incorporate OpenGL ES graphics in a small portion of their layouts should take a look at TextureView.

So I guess TextureViewis the right choice for this task. However I haven't found much resources about this (most of them is Camera Preview example).

I would like to ask how can I draw Buffer[] to a TextureView? And if this is not the most efficient way to do this, I'm willing to try your alternatives.


Update: So currently I have this set up like this:

In my VideoActivity where I repeatedly extract video's Frame which contain a ByteBuffer and then send this to my MyGLRenderer2 to be converted to OpenGLES's texture:

...
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 looks like this:

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

}

And FullFrameTexture looks like this:

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

}

For now I can display some frame for a very brief moment before the app crash (wrong color too).

vxh.viet
  • 388
  • 5
  • 21

1 Answers1

4

The most efficient way to do what you ask will be to convert your pixels to an OpenGL ES texture, and render that on the TextureView. The function to use is glTexImage2D().

You can find some examples in Grafika, which uses the function to upload some generated textures. Take a look at createImageTexture(). Grafika's gles package may be of use if you don't already have GLES code in your app.

FWIW, it would be more efficient to decode video frames directly to a Surface created from the TextureView's SurfaceTexture, but I don't know if JavaCV supports that.

Edit: Another approach, if you don't mind working with the NDK, is to use ANativeWindow. Create a Surface for the TextureView's SurfaceTexture, pass it to native code, then call ANativeWindow_fromSurface() to get the ANativeWindow. Use ANativeWindow_setBuffersGeometry() to set the size and color format. Lock the buffer, copy the pixels in, unlock the buffer to post it. I don't think this requires an extra data copy internally, and potentially has some advantages over the glTexImage2D() approach.

fadden
  • 51,356
  • 5
  • 116
  • 166
  • Thanks a lot for your guidance, it's very helpful, really. I will try the OpenGL ES first and return asap. – vxh.viet May 26 '16 at 02:21
  • would you mind elaborate more about "render the OpenGL ES texture on the TextureView"? Maybe I'm getting it wrong but even if I use the demo code `GeneratedTexture.createTestTexture(GeneratedTexture.Image.FINE);` the output is always 0. – vxh.viet May 26 '16 at 03:22
  • Make sure you're calling it from a thread with an active EGL context. See e.g. TextureViewGLActivity for an example of rendering onto a TextureView with GLES, using a dedicated renderer thread. – fadden May 26 '16 at 04:56
  • yes, I do follow the TextureViewGLActivity example. I've tried to replace all the `GL_SCISSOR_TEST` bit in `Renderer.doAnimation()` with a simple `GeneratedTexture.createTestTexture(GeneratedTexture.Image.FINE);` but I'm still getting black screen. – vxh.viet May 26 '16 at 05:06
  • Just to be clear: creating a texture just creates a texture in GPU memory. It doesn't actually draw anything. See e.g. "hardware scaler exerciser" for code that draws textured meshes. – fadden May 26 '16 at 14:37
  • Thank you @fadden, this is really helpful. After some testing with `GLSurfaceView`, it can be displayed as a portion of a layout so why would the Developer recommend using `TextureView` for this purpose? What kind of benefit would it gain? – vxh.viet May 26 '16 at 15:09
  • GLSurfaceView uses a separate layer, which can be in front of or behind the View layer, but not mixed in. TextureView is rendered on the View layer. This makes TextureView more flexible, but also less efficient. For more details, see https://source.android.com/devices/graphics/architecture.html – fadden May 26 '16 at 15:38
  • I think I got it wrong the whole time. After some more investigation, instead of just trying to "draw the texture", I would need to: 1. Draw 2 triangles. 2. Combine them into one big rectangle which fill the whole `GLSurfaceView`. 3. Create a big texture for this rectangle and apply it. Is that the correct sequence? – vxh.viet May 26 '16 at 16:37
  • Look for uses of gles/FullFrameRect. It fills the viewport with a single textured rect. It's usually used for things like video players rendering frames from a SurfaceTexture (which takes whatever is sent to its Surface and makes it available as a GLES external texture). As you've discovered, you can barely get out of bed with fewer than 200 lines of code when working with GLES 2.x+, but Grafika has the shaders and polygons you need for simple projects. – fadden May 26 '16 at 16:59
  • After reading your comment [here](http://stackoverflow.com/questions/32972457/how-to-load-android-camera-frame-into-opengl-es-texture-via-ndk), I would like to ask what kind of performance can I expect from `glTexImage2D()`? Currently, converting a `ByteBuffer` into a `Bitmap` taking 300-400 milliseconds for a 720p frame, 1080p frame easily double that. I'm developing a video scrubbing feature, so grabbing and converting will be repeatedly called. Grabbing takes about 5-17 milliseconds, so for acceptable performance, converting to opengl texture and render should be done in 5-30 milliseconds. – vxh.viet May 29 '16 at 07:13
  • You'll find a crude 512x512 RGBA glTexImage2D benchmark in Grafika ("glTexImage2D speed test"). Modifying to your desired size should be straightforward. Bear in mind that some devices require power-of-2 dimensions, so 1280x720 would be 2048x1024. (There's an ARB extension you can check for... how portable does your app need to be?) – fadden May 29 '16 at 15:21
  • Well I'm not sure what you mean by "portable", if "portable " means the ability to reuse this piece of code in other platform then not very much, I just need to focus on Android. The reason I go this FFmpeg + OpenGL ES route is none of the currently available media player on Android meet my demands on performance. Either the seeking is very inaccurate or very slow. – vxh.viet May 30 '16 at 02:02
  • 1
    Hi @fadden, I think I have to discard this method as it might not meet my performance requirement. I'm playing around with your [ExtractMpegFramesTest](http://bigflake.com/mediacodec/#ExtractMpegFramesTest). The most costly operation is the png conversion which I do not need. Is it possible to use `ExtractMpegFramesTest` to achieve video scrubbing? Let's say I decode 30 frames in advance when let the user scrub through it while I decode the next 30 frames. Another question does it need to be consecutive frame as I might not need that much frame. One decoded frame for every 3 frame is perfect. – vxh.viet May 31 '16 at 02:27
  • Are you using MediaExtractor / MediaCodec now? With that your pipeline will look more like Grafika's PlayMovieActivity. You can't avoid feeding frames to MediaCodec, as many frames are based on deltas from previous frames. You can decode ahead, but you need somewhere to store uncompressed frames, and that'll add up quickly. You can skip around, but you have to start decoding on sync frame boundaries. (It might be worth starting a new question if you've significantly changed your approach.) – fadden May 31 '16 at 04:17
  • I can change to MediaExtractor / MediaCodec if it can keep min sdk to 16. I've investigated the `PlayMovieActivity` but I'm not sure how to implement the seeking part. Basically what I'm trying to do is provide fast and accurate seeking ability for video. So much pain trying to achieve this on Android when literally it can be done with 1 line of code in iOS. – vxh.viet May 31 '16 at 04:22
  • Working with MediaCodec pre-API 18 can be annoying. Unfortunately API 16/17 still comprise 17% of the market. Seeking to a specific frame requires `seekTo(offset, SEEK_TO_PREVIOUS_SYNC)` and then feeding frames to the decoder until the target frame is reached; that's unavoidable with AVC's I/P/B layout. (Fast-forward on a TiVo is smooth, fast-reverse is not... same reason.) Different approach for rendering: if you want to delve into the NDK, you can get access to a Surface buffer through ANativeWindow. I added some info to my answer. Looks like vlc does this. – fadden May 31 '16 at 17:31
  • Hi fadden, I know this is a lot to ask but if you could quickly give my updated code snippet a look I would be really appreciated. The frame is retrieved from a 720p video. Thank you so much for your time. – vxh.viet Jun 02 '16 at 08:38