【问题标题】:MediaCodec: Convert image to videoMediaCodec:将图像转换为视频
【发布时间】:2018-07-16 11:04:47
【问题描述】:

我希望能够使用 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(),但随后在我所有的三星测试设备上输出的视频都会闪烁为黑色。

有没有更有效的方法可以将相同的位图一遍又一遍地绘制到我的编码器输入表面?

【问题讨论】:

    标签: android opengl-es surfaceview android-mediacodec


    【解决方案1】:

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

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

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

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

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

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

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

    【讨论】:

    • 信息量很大,谢谢!你在 StackOverflow 之外做任何咨询工作吗? ^_^
    • 我偶尔会做咨询工作,但我认为我目前无法接手任何新项目。很高兴听到您对答案表示赞赏!
    猜你喜欢
    • 2014-01-03
    • 1970-01-01
    • 2023-04-01
    • 2016-12-22
    • 2017-02-26
    • 2014-12-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多