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

我试图让它在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(); } } 

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

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

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

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

提前致谢。

Solutions Collecting From Web of "使用Android MediaCodec从摄像头编码H.264"

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

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

 ffplay -fps 30 in.264 

你可以在PC上控制播放。

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

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

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

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

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

如果已将预览颜色空间设置为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; } 

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

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

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

Google文档还声称,对于API> = 12的所有设备,必须支持YV12平面YUV作为相机预览格式。因此,尝试它可能很有用(MediaCodec等效于您在代码段中使用的COLOR_FormatYUV420Planar )。

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