如何在没有 MediaExtractor 的情况下为 H264 使用 MediaCodec

Posted

技术标签:

【中文标题】如何在没有 MediaExtractor 的情况下为 H264 使用 MediaCodec【英文标题】:How to use MediaCodec without MediaExtractor for H264 【发布时间】:2013-11-13 13:20:48 【问题描述】:

我需要在没有 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 值?

【问题讨论】:

您能与我们分享更正后的代码吗?? SPS和PPS值你用的是标准值吗?就我而言,我以 NAL 单元的形式获取数据,因此我正在为 PPS 和 SPS 搜索 NAL 类型为 7 和 8 的帧。相反,我可以直接使用上述值吗?另外,我没有清楚地理解发布的答案。你能解释一下你的代码到底有什么问题吗?我将此代码用作我的方案的参考。 不,每个流都有自己的 SPS/PPS 值,您应该像上面提到的那样获取它们。问题是我正在向 MediaCodec (queueInputBuffer) 发送一个固定大小的缓冲区(大小为 18800 * 4),但您应该只发送完整的视频帧。现在清楚了吗? 有一种方法可以排除MediaExtractor并将byte[]帧(一个接一个)直接放入inputBuffer(ByteBuffer)。这是你真正想要的吗? 【参考方案1】:

对于您的最后一个问题,即如何获得SPSPPS,您需要有一种机制从文件中读取相同的值。

如果您有一个elementary stream 文件,那么您需要解析该文件,识别NALU 标头并提取内容。

如果您有container 格式,则需要有一种机制来读取container 类型的file format 并提取信息。

如果您有streaming 输入,那么您可以从传入的SDP 信息中接收内容。

至于您当前的代码,我建议将SPSPPS 连接到一个缓冲区中,并将其提供给底层codec,如下所示

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! MediaCodec 编解码器似乎在设备之间对 NAL 单元的构成有所不同。有些人希望在开头使用 0001 模式,有些人则不希望。其他人对您使用非 VCL 设备做什么很挑剔。 @PaulSteckler.. 解码器的实现正在发挥作用。一些编解码器坚持RTP 标准并且不处理起始码。这方面的一个示例是H.264 流式传输到Quicktime,如果以RTP 标准规定的第一个字节为NALU 类型存在起始代码,则不显示视频。理想情况下,解码器应该能够搜索起始码并且足够健壮以处理变化。【参考方案2】:

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

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

【讨论】:

这就是问题所在!现在我只为解码器提供视频数据包,它正在工作! @thiagolr 你能发布解决方案吗?你是如何解决这个问题的。我被困在decoder.dequeueOutputBuffer(info, 10000); 我总是得到-1 ...

以上是关于如何在没有 MediaExtractor 的情况下为 H264 使用 MediaCodec的主要内容,如果未能解决你的问题,请参考以下文章

MediaExtractor、带有原始/资产文件的 MediaMetadataRetriever

音视频开发系列——全面了解Android MediaExtractor

[MediaExtractor与wav文件一起使用时抛出IllegalArgumentException

对于某些特定视频的音频,我使用 mediaExtractor 和 mediaFormat 获得一半的通道数和一半的采样率

如何在没有引用的情况下复制对象?

如何在没有循环的情况下将文件逐行读入变量