MediaCodec:将图像转换为视频

MediaCodec: Convert image to video

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

我有一个 class 可以画画:

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是上述class的实例,presentationTimeNs我根据需要的帧率计算

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

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

在效率方面,输入帧的这种绘制可能是最高效的。每次向编码器提交帧时,您不能真正假设输入表面缓冲区内容的任何内容(我认为),因此您需要以某种方式将要编码的内容复制到其中,而这几乎可以做到。

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

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

但是,关于性能,绝对最大的问题和您(缺乏)性能的原因是您同步执行所有操作。你说

At the start of the processing, and then every time I successfully mux a frame thereafter, I call

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

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