【问题标题】:Encoding H.264 from camera with Android MediaCodec使用 Android MediaCodec 从摄像头编码 H.264
【发布时间】:2012-11-07 15:21:22
【问题描述】:

我正试图让它在 Android 4.1 上运行(使用升级的 Asus Transformer 平板电脑)。感谢Alex's response to my previous question,我已经能够将一些原始 H.264 数据写入文件,但这个文件只能用ffplay -f h264 播放,而且它似乎丢失了有关帧速率的所有信息(播放速度极快)。颜色空间看起来也不正确(atm 在编码器端使用相机的默认值)。

public class AvcEncoder {

private MediaCodec mediaCodec;
private BufferedOutputStream outputStream;

public AvcEncoder() { 
    File f = new File(Environment.getExternalStorageDirectory(), "Download/video_encoded.264");
    touch (f);
    try {
        outputStream = new BufferedOutputStream(new FileOutputStream(f));
        Log.i("AvcEncoder", "outputStream initialized");
    } catch (Exception e){ 
        e.printStackTrace();
    }

    mediaCodec = MediaCodec.createEncoderByType("video/avc");
    MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", 320, 240);
    mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 125000);
    mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
    mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar);
    mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5);
    mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
    mediaCodec.start();
}

public void close() {
    try {
        mediaCodec.stop();
        mediaCodec.release();
        outputStream.flush();
        outputStream.close();
    } catch (Exception e){ 
        e.printStackTrace();
    }
}

// called from Camera.setPreviewCallbackWithBuffer(...) in other class
public void offerEncoder(byte[] input) {
    try {
        ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();
        ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers();
        int inputBufferIndex = mediaCodec.dequeueInputBuffer(-1);
        if (inputBufferIndex >= 0) {
            ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
            inputBuffer.clear();
            inputBuffer.put(input);
            mediaCodec.queueInputBuffer(inputBufferIndex, 0, input.length, 0, 0);
        }

        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
        int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo,0);
        while (outputBufferIndex >= 0) {
            ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
            byte[] outData = new byte[bufferInfo.size];
            outputBuffer.get(outData);
            outputStream.write(outData, 0, outData.length);
            Log.i("AvcEncoder", outData.length + " bytes written");

            mediaCodec.releaseOutputBuffer(outputBufferIndex, false);
            outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);

        }
    } catch (Throwable t) {
        t.printStackTrace();
    }

}

将编码器类型更改为“video/mp4”显然解决了帧率问题,但由于主要目标是制作流媒体服务,这不是一个好的解决方案。

我知道考虑到 SPS 和 PPS NALU,我删除了一些 Alex 的代码,但我希望这不是必需的,因为该信息也来自 outData,我认为编码器会正确格式化.如果不是这种情况,我应该如何在我的文件/流中安排不同类型的 NALU?

那么,为了制作一个有效的、工作的 H.264 流,我在这里缺少什么?我应该使用哪些设置来匹配相机的色彩空间和编码器的色彩空间?

我感觉这更像是一个与 H.264 相关的问题,而不是 Android/MediaCodec 主题。还是我仍然没有正确使用 MediaCodec API?

提前致谢。

【问题讨论】:

  • 我在使用 Android 的媒体播放器时遇到了很多问题,而且不同的 Android 手机的行为方式也不尽相同。你可以做转换服务器端吗?
  • 我正在开发类似的功能,但目前在从 Camera.setPreviewCallbackWithBuffer(...) 调用 offerEncoder(...) 时遇到 java.nio.BufferOverflowException,您能分享一下吗?您在 Camera.setPreviewCallbackWithBuffer(...) 中采用的方法。非常感谢。
  • 除了在 inputBuffer.put(input); 上发生的 java.nio.BufferOverflowException 之外,代码似乎没问题。陈述。尝试将字节拆分为 buffer.capacity() 块,最终在 MediaCodec.queueInputBuffer 调用时出现 IllegalStateException 错误。知道如何纠正这种情况吗?
  • 您解决了速度问题吗?如果有,怎么做?

标签: android video-streaming android-camera h.264 video-encoding


【解决方案1】:

对于您的快速播放 - 帧速率问题,您无需在此处执行任何操作。由于它是一种流式解决方案,因此必须提前告知另一方帧速率或每帧的时间戳。这两者都不是基本流的一部分。要么选择预先确定的帧速率,要么传递一些 sdp 或类似的东西,或者使用现有的协议,如 rtsp。在第二种情况下,时间戳是以 rtp 之类的形式发送的流的一部分。然后客户端必须depay rtp流并播放它。这就是基本流的工作方式。 [如果你有固定速率的编码器,要么修正你的帧速率,要么给出时间戳]

本地 PC 播放会很快,因为它不知道 fps。通过在输入之前给出 fps 参数,例如

ffplay -fps 30 in.264

您可以在 PC 上控制播放。

关于无法播放的文件:是否有 SPS 和 PPS。此外,您应该启用 NAL 标头 - 附件 b 格式。我对 android 了解不多,但这是任何 h.264 基本流在它们不在任何容器中并且需要在以后转储和播放时都可以播放的要求。 如果android默认是mp4,但是默认的附件b头会被关闭,所以也许有一个开关可以启用它。或者,如果您逐帧获取数据,只需自己添加即可。

至于颜色格式:我猜默认应该可以。所以尽量不要设置它。 如果没有尝试 422 Planar 或 UVYV / VYUY 交错格式。通常相机就是其中之一。 (但不是必须的,这些可能是我经常遇到的)。

【讨论】:

    【解决方案2】:

    Android 4.3 (API 18) 提供了一个简单的解决方案。 MediaCodec 类现在接受来自 Surfaces 的输入,这意味着您可以将相机的 Surface 预览连接到编码器并绕过所有奇怪的 YUV 格式问题。

    还有一个新的MediaMuxer class 可以将您的原始 H.264 流转换为 .mp4 文件(可以选择混合到音频流中)。

    请参阅CameraToMpegTest source 以获取执行此操作的示例。 (它还演示了使用 OpenGL ES 片段着色器对正在录制的视频进行简单的编辑。)

    【讨论】:

    【解决方案3】:

    如果您将预览颜色空间设置为 YV12,您可以像这样转换颜色空间:

    public static byte[] YV12toYUV420PackedSemiPlanar(final byte[] input, final byte[] output, final int width, final int height) {
            /* 
             * COLOR_TI_FormatYUV420PackedSemiPlanar is NV12
             * We convert by putting the corresponding U and V bytes together (interleaved).
             */
            final int frameSize = width * height;
            final int qFrameSize = frameSize/4;
    
            System.arraycopy(input, 0, output, 0, frameSize); // Y
    
            for (int i = 0; i < qFrameSize; i++) {
                output[frameSize + i*2] = input[frameSize + i + qFrameSize]; // Cb (U)
                output[frameSize + i*2 + 1] = input[frameSize + i]; // Cr (V)
            }
            return output;
        }
    

    或者

     public static byte[] YV12toYUV420Planar(byte[] input, byte[] output, int width, int height) {
            /* 
             * COLOR_FormatYUV420Planar is I420 which is like YV12, but with U and V reversed.
             * So we just have to reverse U and V.
             */
            final int frameSize = width * height;
            final int qFrameSize = frameSize/4;
    
            System.arraycopy(input, 0, output, 0, frameSize); // Y
            System.arraycopy(input, frameSize, output, frameSize + qFrameSize, qFrameSize); // Cr (V)
            System.arraycopy(input, frameSize + qFrameSize, output, frameSize, qFrameSize); // Cb (U)
    
            return output;
        }
    

    【讨论】:

    • 非常有帮助。你的第二个功能正是我需要的。能够抓取 YV12 预览数据并将其转换为 YUV420Planar 以输入到 MediaCodec 编码器。
    • 这个答案听起来不太对劲。在第一个代码中,方法名称是“YV12toYUV420PackedSemiPlanar”。但是,在下面的行中,您提到“COLOR_TI_FormatYUV420PackedSemiPlanar 是 NV12”。
    • @UtkarshSinha fourcc.org/yuv.php 不错的读物,NV12 YUV420。我的问题是保存的帧太少。现在我需要看太阳 2moro 来检查颜色。
    【解决方案4】:

    您可以查询 MediaCodec 以了解其支持的位图格式并查询您的预览。 问题是,一些 MediaCodecs 仅支持您无法从预览中获得的专有打包 YUV 格式。 特别是 2130706688 = 0x7F000100 = COLOR_TI_FormatYUV420PackedSemiPlanar 。 预览的默认格式是 17 = NV21 = MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV411Planar = YCbCr 420 Semi Planar

    【讨论】:

    • 此外,其中一些声称支持一种格式但提供另一种格式(例如 NV12/NV21 在很多三星设备上都被搞砸了,当您要求提供一种 YV 格式时,一些三星设备会提供一种NV 格式)。这很令人沮丧。
    【解决方案5】:

    如果您没有明确请求其他像素格式,相机预览缓冲区将以称为 NV21 的 YUV 420 格式到达,COLOR_FormatYCrYCb 是等效的 MediaCodec。

    不幸的是,正如此页面上的其他答案所提到的,不能保证在您的设备上,AVC 编码器支持这种格式。请注意,存在一些不支持 NV21 的奇怪设备,但我不知道任何可以升级到 API 16 的设备(因此,有 MediaCodec)。

    Google 文档还声称 YV12 平面 YUV 必须支持作为 API >= 12 的所有设备的相机预览格式。因此,尝试它可能很有用(MediaCodec 等价物是 COLOR_FormatYUV420Planar,您在你的代码 sn-p)。

    更新:正如 Andrew Cottrell 提醒我的,YV12 仍然需要色度交换才能变为 COLOR_FormatYUV420Planar。

    【讨论】:

    • 根据我的经验,YV12 预览数据和 YUV420Planar 格式是不等价的。我必须使用此页面上另一个答案中的 YV12toYUV420Planar() 函数才能从一个转换为另一个。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-12-04
    • 2013-04-07
    • 2015-08-21
    • 1970-01-01
    • 2012-02-01
    • 2016-06-05
    相关资源
    最近更新 更多