使用Android MediaCodec从摄像头编码H.264

gle*_*man 30 android video-encoding video-streaming h.264 android-camera

我试图让它在Android 4.1上运行(使用升级的Asus Transformer平板电脑).感谢Alex对我之前的问题的回答,我已经能够将一些原始的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();
    }

}
Run Code Online (Sandbox Code Playgroud)

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

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

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

我觉得这更像是与H.264相关的问题,而不是Android/MediaCodec主题.或者我还没有正确使用MediaCodec API?

提前致谢.

av5*_*501 7

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

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

ffplay -fps 30 in.264
Run Code Online (Sandbox Code Playgroud)

您可以控制PC上的播放.

至于文件不可播放:它是否有SPS和PPS.您还应该启用NAL标头 - 附件b格式.我不太了解android,但是当任何h.264基本流不在任何容器中并且需要转储并稍后播放时,这是必需的.如果android默认为mp4,但默认的annexb标题将被关闭,所以也许有一个开关来启用它.或者,如果您逐帧获取数据,请自行添加.

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


fad*_*den 7

Android 4.3(API 18)提供了一个简单的解决方案.该MediaCodec班现在接受输入从表面上,这意味着你可以在相机的表面预览连接到编码器,并绕过所有古怪的YUV格式的问题.

还有一个新的MediaMuxer类,它将您的原始H.264流转换为.mp4文件(可选地在音频流中混合).

有关完成此操作的示例,请参阅CameraToMpegTest源.(它还演示了使用OpenGL ES片段着色器在录制视频时对其进行简单编辑.)


br1*_*br1 6

如果已将预览颜色空间设置为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;
    }
Run Code Online (Sandbox Code Playgroud)

要么

 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;
    }
Run Code Online (Sandbox Code Playgroud)