简体   繁体   English

MediaCodec:将图像转换为视频

[英]MediaCodec: Convert image to video

I want to be able to write a bitmap to a video using MediaCodec. 我希望能够使用MediaCodec将位图写入视频。 I want the video to be eg 3 seconds long and 30 fps. 我希望视频长3秒,每秒30帧。 I am targeting Android API 21. 我的目标是Android API 21。

I have a class that does the drawing: 我有一个做绘图的课:

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

I also have an InputSurface hooked up to my video encoder and muxer. 我还有一个InputSurface连接到我的视频编码器和多路复用器。

At the start of the processing, and then every time I successfully mux a frame thereafter, I call: 在处理开始时,此后每次成功复用帧时,我都会调用:

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

where inputSurface and imageRenderer are instances of the above classes, and presentationTimeNs I calculate based on the required frame rate. 其中inputSurfaceimageRenderer是上述类的实例,而presentationTimeNs我根据所需的帧速率进行计算。

This generally does work, but feels pretty inefficient. 这通常是可行的,但感觉效率很低。 I feel like I am unnecessarily redrawing the bitmap over and over, even though I know it hasn't changed. 我感觉我不必要一遍又一遍地重绘位图,即使我知道它没有改变。 I tried calling drawFrame() just once or twice at the beginning, but then the outputted video flickers to black on all my Samsung test devices. 我尝试在一开始只调用一次或两次drawFrame() ,但是随后在所有三星测试设备上输出的视频均闪烁为黑色。

Is there a more efficient way I can draw the same bitmap over and over to my encoder input surface? 有没有一种更有效的方法可以将相同的位图一遍又一遍地绘制到编码器输入表面?

Efficiency wise, this drawing of the input frame probably is as efficient as it will get. 在效率方面,输入框的绘制可能会达到所需要的效率。 Each time you submit a frame to the encoder, you can't really assume anything about the input surface buffer content (I think), so you need to copy the content to be encoded into it somehow, and this does pretty much it. 每次将帧提交给编码器时,您都无法真正假设有关输入表面缓冲区内容的任何信息(我认为),因此您需要以某种方式将要编码的内容复制到其中,这几乎可以做到。

If you skip drawing, you need to keep in mind that the surface you're drawing into isn't just a single buffer, but a set of a number of buffers (usually 4-10 buffers or so). 如果您跳过绘图,则需要记住,要绘图的表面不仅是单个缓冲区,而是一组许多缓冲区(通常为4-10个缓冲区)。 When using the direct buffer access mode of the encoder, the encoder will tell you exactly which one of the buffers out of the pool it gave you to fill, and in such cases, you might have better luck with skipping drawing in the case if you've already filled the buffer before (and hoping that the encoder hasn't invalidated the contents). 使用编码器的直接缓冲区访问模式时,编码器会准确告诉您池中哪个缓冲区可以填充您的缓冲区,在这种情况下,如果您愿意跳过绘图,可能会更好。之前已经填充了缓冲区(并希望编码器没有使内容无效)。

With surface input, you don't get to know which buffer you got to write into. 使用表面输入时,您无需知道要写入哪个缓冲区。 In that case, you could eg try just doing the drawing the first N times. 在这种情况下,您可以尝试只进行前N次绘制。 I don't think you can get the actual number of buffers though - you could try calling the deprecated getInputBuffers() method, but I don't think it's possible to use it in combination with surface input. 我认为您无法获得实际的缓冲区数量-您可以尝试调用不赞成使用的getInputBuffers()方法,但我认为无法将其与表面输入结合使用。

However, about performance, the absolutely biggest issue and reason for your (lack of) performance is that you're doing everything synchronously. 但是,关于性能,绝对最大的问题和(缺乏)性能的原因是,您正在同步进行所有操作。 You said 你说

At the start of the processing, and then every time I successfully mux a frame thereafter, I call 在处理开始时,然后每次我成功地对帧进行多路复用时,我都会调用

Hardware encoders generally have a bit of latency, and the time it takes to encode a single frame from start to finish is longer than the average time per frame, if you start encoding more than one at a time. 硬件编码器通常会有一点延迟,并且如果您一次开始编码多个帧,则从开始到结束对单个帧进行编码所花费的时间将比每帧的平均时间更长。

Assuming you're using MediaCodec in async mode, I would suggest to just serially do the encoding of all the 90 frames in one thread, and write output packets to the muxer when you get them in the callback. 假设您在异步模式下使用MediaCodec,我建议只在一个线程中串行地对所有90帧进行编码,并在回调中获取输出数据包时将这些数据包写入复用器。 That should keep the encoder pipeline busy. 那应该使编码器管道繁忙。 (Once the input buffers to the encoder are exhausted, the inputSurface methods will block until the encoder has completed a frame and freed up another one of the input buffers.) You might also want to buffer the output packets in a queue and write them asynchronously to the muxer (I remember reading about cases where MediaMuxer occasionally can block longer than you'd like). (一旦编码器的输入缓冲区已用尽,inputSurface方法将阻塞,直到编码器完成一帧并释放另一个输入缓冲区为止。)您可能还希望将输出数据包缓冲在队列中并异步写入到多路复用器(我记得读过一些有关MediaMuxer偶尔会阻塞更长的时间的案例)。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM