MediaCodec:将图像转换为视频

时间:2018-02-06 11:32:48

标签: android opengl-es surfaceview mediacodec

我希望能够使用MediaCodec将位图写入视频。我希望视频例如是3秒长,30 fps。我的目标是Android API 21。

我有一个绘图的课程:

public class ImageRenderer {
    private static final String NO_FILTER_VERTEX_SHADER = "" +
            "attribute vec4 position;\n" +
            "attribute vec4 inputTextureCoordinate;\n" +
            " \n" +
            "varying vec2 textureCoordinate;\n" +
            " \n" +
            "void main()\n" +
            "{\n" +
            "    gl_Position = position;\n" +
            "    textureCoordinate = inputTextureCoordinate.xy;\n" +
            "}";
    private static final String NO_FILTER_FRAGMENT_SHADER = "" +
            "varying highp vec2 textureCoordinate;\n" +
            " \n" +
            "uniform sampler2D inputImageTexture;\n" +
            " \n" +
            "void main()\n" +
            "{\n" +
            "     gl_FragColor = texture2D(inputImageTexture, textureCoordinate);\n" +
            "}";

    private int mGLProgId;
    private int mGLAttribPosition;
    private int mGLUniformTexture;
    private int mGLAttribTextureCoordinate;

    private static final int NO_IMAGE = -1;
    private static final float CUBE[] = {
            -1.0f, -1.0f,
            1.0f, -1.0f,
            -1.0f, 1.0f,
            1.0f, 1.0f,
    };

    private int mGLTextureId = NO_IMAGE;
    private final FloatBuffer mGLCubeBuffer;
    private final FloatBuffer mGLTextureBuffer;

    private Bitmap bitmap;

    private static final float TEXTURE_NO_ROTATION[] = {
            0.0f, 1.0f,
            1.0f, 1.0f,
            0.0f, 0.0f,
            1.0f, 0.0f,
    };

    public ImageRenderer(Bitmap bitmap) {
        this.bitmap = bitmap;

        mGLCubeBuffer = ByteBuffer.allocateDirect(CUBE.length * 4)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer();
        mGLCubeBuffer.put(CUBE).position(0);

        mGLTextureBuffer = ByteBuffer.allocateDirect(TEXTURE_NO_ROTATION.length * 4)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer();
        mGLTextureBuffer.put(TEXTURE_NO_ROTATION).position(0);

        GLES20.glClearColor(0, 0, 0, 1);
        GLES20.glDisable(GLES20.GL_DEPTH_TEST);

        mGLProgId = OpenGlUtils.loadProgram(NO_FILTER_VERTEX_SHADER, NO_FILTER_FRAGMENT_SHADER);
        mGLAttribPosition = GLES20.glGetAttribLocation(mGLProgId, "position");
        mGLUniformTexture = GLES20.glGetUniformLocation(mGLProgId, "inputImageTexture");
        mGLAttribTextureCoordinate = GLES20.glGetAttribLocation(mGLProgId,
                "inputTextureCoordinate");

        GLES20.glViewport(0, 0, bitmap.getWidth(), bitmap.getHeight());
        GLES20.glUseProgram(mGLProgId);
    }

    public void drawFrame() {
        GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);

        // Draw bitmap
        mGLTextureId = OpenGlUtils.loadTexture(bitmap, mGLTextureId, false);

        GLES20.glUseProgram(mGLProgId);

        mGLCubeBuffer.position(0);
        GLES20.glVertexAttribPointer(mGLAttribPosition, 2, GLES20.GL_FLOAT, false, 0, mGLCubeBuffer);
        GLES20.glEnableVertexAttribArray(mGLAttribPosition);
        mGLTextureBuffer.position(0);
        GLES20.glVertexAttribPointer(mGLAttribTextureCoordinate, 2, GLES20.GL_FLOAT, false, 0,
                mGLTextureBuffer);
        GLES20.glEnableVertexAttribArray(mGLAttribTextureCoordinate);
        if (mGLTextureId != OpenGlUtils.NO_TEXTURE) {
            GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
            GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mGLTextureId);
            GLES20.glUniform1i(mGLUniformTexture, 0);
        }
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
        GLES20.glDisableVertexAttribArray(mGLAttribPosition);
        GLES20.glDisableVertexAttribArray(mGLAttribTextureCoordinate);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
    }
}

我还有一个InputSurface连接到我的视频编码器和多路复用器。

在处理开始时,然后每次我成功复用一帧后,我都会打电话:

inputSurface.makeCurrent();
imageRenderer.drawFrame();
inputSurface.setPresentationTime(presentationTimeNs);
inputSurface.swapBuffers();
inputSurface.releaseEGLContext();

其中inputSurfaceimageRenderer是上述类的实例,presentationTimeNs我根据所需的帧速率计算。

这通常可行,但感觉非常低效。我觉得我不必重复地重新绘制位图,即使我知道它没有改变。我尝试在开始时只调用一次或两次drawFrame(),但输出的视频在我的所有三星测试设备上都闪烁为黑色。

是否有更有效的方法可以将相同的位图反复绘制到我的编码器输入表面?

1 个答案:

答案 0 :(得分:0)

效率方面,输入帧的这种绘制可能与它将获得的效率一样高效。每次向编码器提交一个帧时,你都无法真正假设输入表面缓冲区内容(我认为),所以你需要以某种方式将要编码的内容复制到其中,这就完全可以了。< / p>

如果你跳过绘图,你需要记住,你所绘制的表面不仅仅是一个缓冲区,而是一组缓冲区(通常是4-10个缓冲区)。当使用编码器的直接缓冲区访问模式时,编码器会告诉您它准确填充的池中哪一个缓冲区,在这种情况下,如果你在这种情况下跳过绘图,你可能会更好运之前已经填充了缓冲区(并希望编码器没有使内容无效)。

使用曲面输入,您无法知道要写入哪个缓冲区。在这种情况下,您可以例如尝试只做前N次绘图。我不认为你可以获得实际的缓冲区数量 - 你可以尝试调用不推荐使用的getInputBuffers()方法,但我认为不可能将它与表面输入结合使用。

然而,关于性能,你(缺乏)性能的绝对最大问题和原因是你正在同步地做所有事情。你说

  

在处理开始时,然后每次我成功复用一帧后,我都会调用

硬件编码器通常会有一些延迟,如果您一次开始编码多个,则从开始到结束对单个帧进行编码所需的时间比每帧的平均时间长。

假设您在异步模式下使用MediaCodec,我建议您只在一个线程中对所有90个帧进行串行编码,并在回调中将输出数据包写入多路复用器。这应该保持编码器管道繁忙。 (一旦编码器的输入缓冲区耗尽,inputSurface方法将阻塞,直到编码器完成一个帧并释放另一个输入缓冲区。)您可能还希望缓冲队列中的输出数据包并异步写入它们对于复用器(我记得有关MediaMuxer偶尔会阻塞的时间超过你想要的情况)。

相关问题