直接通过 MediaCodec 转码 h 264 视频,无需纹理渲染

Transcode h264 video by MediaCodec directly withou texture render

我正在使用 MediaCodec 进行转码。

我创建了两个 mediacodec 实例,一个用于解码,另一个用于编码。我正在尝试将解码器 outputBuffer 直接发送到编码器 inputBuffer。

编译的时候好像没问题,executing.And很快就[=3​​1=]了。 但是输出视频文件有一些东西 wrong.I 检查了输出视频的元数据,它们都是正确的:比特率,帧率,分辨率......只有视频中的图像是这样的错误:screen shot

我以为它有问题,但我想不通...

我搜索了库和文档,找到了一些使用 Texture 表面渲染解码器输出数据并将数据传输到编码器的示例代码。但我认为这对我来说不应该是必要的。因为我不需要编辑 video.What 的图像,所以我只需要更改比特率和分辨率以使文件的大小更小。

这是我项目中的核心代码:

private void decodeCore() {
    MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
    int frameCount = 0;
    while (mDecodeRunning) {

        int inputBufferId = mDecoder.dequeueInputBuffer(50);
        if (inputBufferId >= 0) {
            // fill inputBuffers[inputBufferId] with valid data
            int sampleSize = mExtractor.readSampleData(mDecodeInputBuffers[inputBufferId], 0);
            if (sampleSize >= 0) {
                long time = mExtractor.getSampleTime();
                mDecoder.queueInputBuffer(inputBufferId, 0, sampleSize, time, 0);
            } else {
                mDecoder.queueInputBuffer(inputBufferId, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
            }

            mExtractor.advance();
        }

        int outputBufferId = mDecoder.dequeueOutputBuffer(bufferInfo, 50);
        if (outputBufferId >= 0) {

            FrameData data = mFrameDataQueue.obtain();
            //wait until queue has space to push data
            while (data == null) {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                data = mFrameDataQueue.obtain();
            }

            data.data.clear();
            data.size = 0;
            data.offset = 0;
            data.flag = 0;
            data.frameTimeInUs = bufferInfo.presentationTimeUs;

            // outputBuffers[outputBufferId] is ready to be processed or rendered.
            if (bufferInfo.size > 0) {
                ByteBuffer buffer = mDecodeOutputBuffers[outputBufferId];

                buffer.position(bufferInfo.offset);
                buffer.limit(bufferInfo.offset + bufferInfo.size);

                data.data.put(buffer);
                data.data.flip();

                data.size = bufferInfo.size;
                data.frameIndex = frameCount++;

            }

            data.flag = bufferInfo.flags;

            if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
                Log.d("bingbing_transcode", "decode over! frames:" + (frameCount - 1));
                mDecodeRunning = false;
            }

            mFrameDataQueue.pushToQueue(data);
            mDecoder.releaseOutputBuffer(outputBufferId, false);
            Log.d("bingbing_transcode", "decode output:\n frame:" + (frameCount - 1) + "\n" + "size:" + bufferInfo.size);
        } else if (outputBufferId == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
            mDecodeOutputBuffers = mDecoder.getOutputBuffers();
        } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
            // Subsequent data will conform to new format.
            mDecodeOutputVideoFormat = mDecoder.getOutputFormat();
            configureAndStartEncoder();
        }
    }

    mDecoder.stop();
    mDecoder.release();
}

private void encodeCore() {
    int trackIndex = 0;
    boolean muxerStarted = false;
    MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
    int frameCount = 0;
    while (mEncodeRunning) {
        int inputBufferId = mEncoder.dequeueInputBuffer(50);
        if (inputBufferId >= 0) {
            FrameData data = mFrameDataQueue.pollFromQueue();
            //wait until queue has space to push data
            while (data == null) {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                data = mFrameDataQueue.obtain();
            }

            if (data.size > 0) {
                ByteBuffer inputBuffer = mEncodeInputBuffers[inputBufferId];
                inputBuffer.clear();
                inputBuffer.put(data.data);
                inputBuffer.flip();
            }
            mEncoder.queueInputBuffer(inputBufferId, 0, data.size, data.frameTimeInUs, data.flag);
            mFrameDataQueue.recycle(data);
        }

        int outputBufferId = mEncoder.dequeueOutputBuffer(bufferInfo, 50);
        if (outputBufferId >= 0) {
            // outputBuffers[outputBufferId] is ready to be processed or rendered.
            ByteBuffer encodedData = mEncodeOutputBuffers[outputBufferId];

            if (bufferInfo.size > 0) {
                if (encodedData == null) {
                    throw new RuntimeException("encoderOutputBuffer " + outputBufferId + " was null");
                }

                if (!muxerStarted) {
                    throw new RuntimeException("muxer hasn't started");
                }

                frameCount++;
            }
            // adjust the ByteBuffer values to match BufferInfo (not needed?)
            encodedData.position(bufferInfo.offset);
            encodedData.limit(bufferInfo.offset + bufferInfo.size);

            mMuxer.writeSampleData(trackIndex, encodedData, bufferInfo);

            if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
                Log.d("bingbing_transcode", "encode over! frames:" + (frameCount - 1));
                mEncodeRunning = false;
            }

            mEncoder.releaseOutputBuffer(outputBufferId, false);
            Log.d("bingbing_transcode", "encode output:\n frame:" + (frameCount - 1) + "\n" + "size:" + bufferInfo.size);

        } else if (outputBufferId == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
            mEncodeOutputBuffers = mEncoder.getOutputBuffers();
        } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
            // should happen before receiving buffers, and should only happen once
            if (muxerStarted) {
                throw new RuntimeException("format changed twice");
            }

            MediaFormat newFormat = mEncoder.getOutputFormat();
            Log.d("bingbing_transcode", "encoder output format changed: " + newFormat);

            // now that we have the Magic Goodies, start the muxer
            trackIndex = mMuxer.addTrack(newFormat);
            mMuxer.start();
            muxerStarted = true;
            mEncodeOutputVideoFormat = newFormat;
        }
    }


    mEncoder.stop();
    mEncoder.release();
    if (muxerStarted) {
        mMuxer.stop();
        mMuxer.release();
    }
}

这两个函数 运行 在两个不同的线程中。

FrameData 是帧字节缓冲区和帧当前时间的简单存储以及一些需要的东西

使用字节缓冲区输入时,有一些关于输入数据布局的细节未定义。当宽度不是 16 的倍数时,一些编码器希望将输入数据行长度填充为 16 的倍数,而其他编码器将假定行长度等于宽度,没有额外填充。

AndroidCTS 测试(它定义了人们在所有设备上可以预期的行为)从字节缓冲区输入编码,有意只测试 16 的倍数的分辨率,因为他们知道不同的硬件供应商做这件事的方式不同,并且他们不想强制执行任何特定处理。

您通常不能假设解码器输出使用与编码器消耗的行大小相似的行大小。解码器可以自由地(有些人确实这样做了)return 比实际内容大小大得多的宽度,并使用 crop_left/crop_right 字段来指示它的哪些部分实际上是可见的。因此,如果解码器这样做,则不能将数据直接从解码器传递到编码器,除非逐行复制数据,同时考虑解码器和编码器使用的实际行大小。

此外,您甚至不能假设解码器使用与编码器相似的像素格式。许多高通设备对解码器输出使用特殊的平铺像素格式,而编码器输入是正常的平面数据。在这些情况下,在将数据输入编码器之前,您必须实施非常复杂的逻辑来解开数据。

使用纹理表面作为中间层隐藏了所有这些细节。对于您的用例来说,这听起来可能不是完全必要的,但它确实隐藏了解码器和编码器之间缓冲区格式的所有变化。