如何在不使用MediaExtractor的情况下使用MediaCodec解码H264视频?

24
我需要在不使用MediaExtractor的情况下使用MediaCodec,并且我正在使用FileInputStream读取文件。目前它无法正常工作,屏幕上显示出一张绿色的乱码图像。
以下是完整的源代码:
FileInputStream in = new FileInputStream("/sdcard/sample.ts");

String mimeType = "video/avc";
MediaCodec decoder = MediaCodec.createDecoderByType(mimeType);
MediaFormat format = MediaFormat.createVideoFormat(mimeType, 1920, 1080);

byte[] header_sps = { 0, 0, 0, 1, 103, 100, 0, 40, -84, 52, -59, 1, -32, 17, 31, 120, 11, 80, 16, 16, 31, 0, 0, 3, 3, -23, 0, 0, -22, 96, -108 };
byte[] header_pps = { 0, 0, 0, 1, 104, -18, 60, -128 };
format.setByteBuffer("csd-0", ByteBuffer.wrap(header_sps));
format.setByteBuffer("csd-1", ByteBuffer.wrap(header_pps));
format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 1920 * 1080);
format.setInteger("durationUs", 63446722);

decoder.configure(format, surface, null, 0);
decoder.start();

ByteBuffer[] inputBuffers = decoder.getInputBuffers();
ByteBuffer[] outputBuffers = decoder.getOutputBuffers();
BufferInfo info = new BufferInfo();
boolean isEOS = false;
long startMs = System.currentTimeMillis();

while (!Thread.interrupted()) {
    if (!isEOS) {
        int inIndex = decoder.dequeueInputBuffer(1000);
        if (inIndex >= 0) {
            byte buffer2[] = new byte[18800 * 8 * 8 * 8];
            ByteBuffer buffer = inputBuffers[inIndex];
            int sampleSize;

            sampleSize = in.read(buffer2, 0, 18800 * 4);

            buffer.clear();
            buffer.put(buffer2, 0, sampleSize);
            buffer.clear();

            if (sampleSize < 0) {
                decoder.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                isEOS = true;
            } else {
                decoder.queueInputBuffer(inIndex, 0, sampleSize, 0, 0);
            }
        }
    }

    int outIndex = decoder.dequeueOutputBuffer(info, 10000);
    switch (outIndex) {
    case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
        Log.d("DecodeActivity", "INFO_OUTPUT_BUFFERS_CHANGED");
        outputBuffers = decoder.getOutputBuffers();
        break;
    case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
        Log.d("DecodeActivity", "New format " + decoder.getOutputFormat());
        break;
    case MediaCodec.INFO_TRY_AGAIN_LATER:
        Log.d("DecodeActivity", "dequeueOutputBuffer timed out! " + info);
        break;
    default:
        ByteBuffer buffer = outputBuffers[outIndex];
        Log.v("DecodeActivity", "We can't use this buffer but render it due to the API limit, " + buffer);

        while (info.presentationTimeUs / 1000 > System.currentTimeMillis() - startMs) {
            try {
                sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
                break;
            }
        }
        decoder.releaseOutputBuffer(outIndex, true);
        break;
    }

    if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
        Log.d("DecodeActivity", "OutputBuffer BUFFER_FLAG_END_OF_STREAM");
        break;
    }
}

decoder.stop();
decoder.release();

如果我使用MediaExtractor,一切都很正常。当使用MediaExtractor时,通过查看MediaFormat,我得到了SPS/PPS值。如果我删除下面的部分,则屏幕上什么也不会显示。

byte[] header_sps = { 0, 0, 0, 1, 103, 100, 0, 40, -84, 52, -59, 1, -32, 17, 31, 120, 11, 80, 16, 16, 31, 0, 0, 3, 3, -23, 0, 0, -22, 96, -108 };
byte[] header_pps = { 0, 0, 0, 1, 104, -18, 60, -128 };
format.setByteBuffer("csd-0", ByteBuffer.wrap(header_sps));
format.setByteBuffer("csd-1", ByteBuffer.wrap(header_pps));
format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 1920 * 1080);
format.setInteger("durationUs", 63446722);

我错过了什么?如何在没有MediaExtractor的情况下以编程方式获取SPS/PPS值?

3
请问您能否分享一下已经修正过的代码? - dvrm
你使用的SPS和PPS值是标准值吗?在我的情况下,我以NAL单元的形式获取数据,因此我正在搜索具有NAL类型7和8的帧以获取PPS和SPS。我能否直接使用上述值?另外,我没有清楚地理解发布的答案。您能解释一下您代码中的问题是什么吗?我正在将此代码用作我的场景参考。 - Sandeep
不,每个流都有自己的SPS/PPS值,你应该像上面提到的那样获取它们。问题在于我向MediaCodec(queueInputBuffer)发送了一个固定大小的缓冲区(大小为18800 * 4),但实际上你应该只发送完整的视频帧。现在清楚了吗? - thiagolr
有一种方法可以排除MediaExtractor,并直接将byte[]帧(逐个)放入inputBuffer(ByteBuffer)中。这样你想要的是吗? - Volodymyr Kulyk
2个回答

11

我假设您正在阅读原始的H.264基本流,而不是MP4文件。

看起来您正在将固定大小的数据块提供给解码器。这是不行的。您需要将每个缓冲区放入单个访问单元。


那就是问题所在!现在我只向解码器提供视频包,它正在工作! - thiagolr
3
@thiagolr,你能发表一下解决方案吗?你是怎么解决的... 我在 decoder.dequeueOutputBuffer(info, 10000); 这里卡住了,始终得到 -1... - Patrick Leijser

7
对于你上一个问题,即“如何获取SPS和PPS值”,你需要有一种机制从文件中读取它们。
如果你有一个基本流文件,则需要解析该文件,识别NALU头并提取内容。
如果你有一个容器格式,则需要有一种机制来读取容器类型的文件格式并提取信息。
如果你有一个流输入,则可以从传入的SDP信息中接收内容。
至于你当前的代码,我建议将SPS和PPS连接成一个缓冲区,并将其提供给底层编解码器,如下所示。
byte[] csd_info = { 0, 0, 0, 1, 103, 100, 0, 40, -84, 52, -59, 1, -32, 17, 31, 120, 11, 80, 16, 16, 31, 0, 0, 3, 3, -23, 0, 0, -22, 96, -108, 0, 0, 0, 1, 104, -18, 60, -128 };
format.setByteBuffer("csd-0", ByteBuffer.wrap(csd_info));
format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 1920 * 1080);
format.setInteger("durationUs", 63446722);

1
主要问题已经通过其他(被接受的)答案得到解决。但是感谢您回答第二个问题!因此给您点赞! - thiagolr
1
MediaCodec编解码器在不同设备上对于NAL单元的定义似乎存在差异。有些设备需要在开头加上0001模式,而其他设备则不需要。还有一些设备对于非VCL单元的处理方式比较挑剔。 - Paul Steckler
@PaulSteckler.. 区别在于解码器的实现。一些编解码器坚持使用 RTP 标准,不处理起始码。例如,将 H.264 流式传输到 Quicktime 时,如果存在起始码,则不会显示视频,因为 RTP 标准规定第一个字节是 NALU 类型。理想情况下,解码器应该能够搜索起始码并具有足够的鲁棒性来处理变化。 - Ganesh

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接