接上一篇最简单的MediaExtractor和MediaCodec播放视频,这一篇我们来实现最简单的视频的硬编码,继续使用上一篇的内容。
我们期望输入一个mp4文件,我们将这个文件解封装
-> 解码
-> 再编码
-> 封装
再重新获得一个mp4文件,这个过程中我们需要总共四个角色:
-
MediaExtractor
用来解封装mp4获得h264流 - 用来解码的
MediaCodec
,将h264解码获得yuv数据 - 用来编码的
MediaCodec
,将yuv数据编码获得h264数据 -
MediaMuxer
用来封装h264获得一个mp4文件
需要源码的可以直接点击Encoder查看,上一篇文章中已经使用了1和2来获取到了yuv数据,所以我们现在只关注3. 编码
和4. 封装
。
思路很简单,创建好编码器和混合器,解码得到的每一帧yuv数据都先通过编码器编成h264数据,再通过混合器写入mp4文件。这样解码完成编码也就完成了。
使用MediaCodec实现yuv数据编码成h264
如同MediaCodec解码的用法类似,解码的用法是一样的,不同的地方:
如何获得MediaFormat
如果使用MediaExtractor
来解封装mp4文件,我们可以得到一个MediaFormat
对象,我们用这个MediaFormat
可以直接创建MediaCodec并且configure。
而编码的时候我们则需要自己创建这个MediaFormat
对象。configure的时候
encodeCodec.configure(encodeMediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
最后一个参数需要传入一个MediaCodec.CONFIGURE_FLAG_ENCODE
,而解码的时候则不需要。
如何创建MediaCodec,先列一段代码:
void initMediaCodec() {
String codecName = MediaFormat.MIMETYPE_VIDEO_AVC;
int colorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar;
MediaFormat encodeMediaFormat = MediaFormat.createVideoFormat(codecName, width, height);
encodeMediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);
encodeMediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 2500000);
encodeMediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
encodeMediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
MediaCodec encodeCodec = MediaCodec.createByCodecName(codecName);
encodeCodec.configure(encodeMediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
encodeCodec.start();
}
先根据类型创建一个MediaFormat,再给此format设置一系列的参数。
通过codec类型创建MediaCodec,再用创建好的MediaFormat来配置codec,调用start进入等待编码状态。
需要关注的点有下面几个:
-
KEY_COLOR_FORMAT
,KEY_BIT_RATE
,KEY_FRAME_RATE
,KEY_I_FRAME_INTERVAL
这四个值是至少需要设置的。这几个参数其实顾名思义,帧率,比特率都是设置的越小,生成的视频就越小。如果没有设置或者设置错误,会报android.media.MediaCodec$CodecException
错误。 - 可以看出,我单独把colorFormat和codecName抽成了单独的属性,这俩有一定的关联性。
因为MediaCodec是硬编码,是依赖硬件厂商的支持的,比如上面写的MIMETYPE_VIDEO_AVC
和COLOR_FormatYUV420Planar
,也就是h264
和I420
,我们要做的其实就是找到一个支持将I420编码成h264的编码器,我们需要给MediaCodec提供的就是这个信息。手机厂商支持的不尽相同,如果我们写的手机并不支持,同样会报android.media.MediaCodec$CodecException
错误。
当然要注意输入的yuv格式需要跟配置的colorFormat对的上,如果对不上就需要转换成对应的YUV格式。比如COLOR_FormatYUV420Planar = I420 = YV21 = 19
,而COLOR_FormatYUV420SemiPlanar = NV12 = 21
。如果我们编码器设置的是COLOR_FormatYUV420Planar
,但是我们的源数据却是NV21,那我们就需要先将数据转成I420再丢给编码器去编码。
关于异常的问题,会在后面展开说,这里先介绍MediaCodec编码的基本流程:
private void encodeData(byte[] yuvBytes, long presentationTimeUs) {
MediaCodec.BufferInfo encodeOutputBufferInfo = new MediaCodec.BufferInfo();
// 得到可用的InputBuffer的index
int inputBufferIndex = encodeCodec.dequeueInputBuffer(-1);
if (inputBufferIndex >= 0) {
ByteBuffer inputBuffer = encodeCodec.getInputBuffer(inputBufferIndex);
// 得到InputBuffer并填充数据
inputBuffer.put(yuvBytes);
// 交给MediaCodec处理
encodeCodec.queueInputBuffer(inputBufferIndex, 0, yuvBytes.length, presentationTimeUs, 0);
}
// 得到编好的h264数据的ByteBuffer的位置
int outputBufferIndex = encodeCodec.dequeueOutputBuffer(encodeOutputBufferInfo, -1);
// do something
}
可以看出,MediaCodec编码的基本流程跟解码的基本是一样的。
使用MediaMuxer将h264封装成mp4
MediaMuxer
就类似于MediaExtractor
,用法相对MediaCodec也简单了不少。
MediaMuxer mMediaMuxer = new MediaMuxer(outputMp4Path, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
// 获得trackIndex
int encodeVideoTrackIndex = mMediaMuxer.addTrack(newFormat);
mMediaMuxer.start();
// 写入数据帧
mMediaMuxer.writeSampleData(encodeVideoTrackIndex, outputBuffer, encodeOutputBufferInfo);
// 停止和销毁
mMediaMuxer.stop();
mMediaMuxer.release();
上面其实就是MediaMuxer
的大致使用方法了,其实也是它的大部分公开方法了。
需要注意的点就是addTrack()
方法需要设置一个MediaFormat,如何正确的得到这个MediaFormat就是关键。我之前一度以为我可以使用上面在设置编码器MediaCodec
时我自己创建的MediaFormat,发现会报java.lang.IllegalStateException: Failed to stop the muxer
的错误,同样文章后面展开说这个错误。
这里来说https://bigflake.com/mediacodec/确实是一个入门的好参考。
int outputBufferIndex = encodeCodec.dequeueOutputBuffer(encodeOutputBufferInfo, -1);
switch (outputBufferIndex) {
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
MediaFormat newFormat = encodeCodec.getOutputFormat();
encodeVideoTrackIndex = mMediaMuxer.addTrack(newFormat);
mMediaMuxer.start();
break;
...
default:
ByteBuffer outputBuffer = encodeCodec.getOutputBuffer(outputBufferIndex);
byte[] outData = new byte[encodeOutputBufferInfo.size];
outputBuffer.get(outData);
muxerOutputBufferInfo.offset = 0;
muxerOutputBufferInfo.size = encodeOutputBufferInfo.size;
muxerOutputBufferInfo.flags = isVideoEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : MediaCodec.BUFFER_FLAG_KEY_FRAME;
muxerOutputBufferInfo.presentationTimeUs = presentationTimeUs;
mMediaMuxer.writeSampleData(encodeVideoTrackIndex, outputBuffer, encodeOutputBufferInfo);
encodeCodec.releaseOutputBuffer(outputBufferIndex, false);
break;
}
可以在编码结束后在INFO_OUTPUT_FORMAT_CHANGED
的时候来获取编码器返回的MediaFormat来构造MediaMuxer。
而且它会在真正的数据帧来临之前返回且只返回一次,我们可以放心的等MediaMuxer初始化之后开始调用writeSampleData
来写入数据。
可能遇到的问题及思考
1. android.media.MediaCodec$CodecException报错
android.media.MediaCodec$CodecException: Error 0xfffffc0e
at android.media.MediaCodec.native_configure(Native Method)
at android.media.MediaCodec.configure(MediaCodec.java:1960)
at android.media.MediaCodec.configure(MediaCodec.java:1889)
1.1. 某一个必要的key没有设置
KEY_COLOR_FORMAT
, KEY_BIT_RATE
, KEY_FRAME_RATE
, KEY_I_FRAME_INTERVAL
这四个值中的某一个没有设置。
1.2. 解码器对设置的ColorFormat不支持。
设备中的解码器可以使用下面的代码找到:
public static void getSupportTypes() {
MediaCodecList allMediaCodecLists = new MediaCodecList(-1);
MediaCodecInfo avcCodecInfo = null;
for (MediaCodecInfo mediaCodecInfo : allMediaCodecLists.getCodecInfos()) {
if (mediaCodecInfo.isEncoder()) {
String[] supportTypes = mediaCodecInfo.getSupportedTypes();
for (String supportType : supportTypes) {
if (supportType.equals(MediaFormat.MIMETYPE_VIDEO_AVC)) {
avcCodecInfo = mediaCodecInfo;
LogUtil.d(TAG, "编码器名称:" + mediaCodecInfo.getName() + " " + supportType);
MediaCodecInfo.CodecCapabilities codecCapabilities = avcCodecInfo.getCapabilitiesForType(MediaFormat.MIMETYPE_VIDEO_AVC);
int[] colorFormats = codecCapabilities.colorFormats;
for (int colorFormat : colorFormats) {
switch (colorFormat) {
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV411Planar:
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV411PackedPlanar:
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar:
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar:
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar:
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar:
LogUtil.d(MediaCodecUtil.TAG, "支持的格式::" + colorFormat);
break;
}
}
}
}
}
}
}
我这边设备的打印信息为:
D/LogUtil: MediaCodec 编码器名称:OMX.qcom.video.encoder.avc video/avc
D/LogUtil: MediaCodec 支持的格式::21
D/LogUtil: MediaCodec 编码器名称:OMX.google.h264.encoder video/avc
D/LogUtil: MediaCodec 支持的格式::19
D/LogUtil: MediaCodec 支持的格式::21
关于19和21代表了什么,定义在:
// MediaCodecInfo.java
/** @deprecated Use {@link #COLOR_FormatYUV420Flexible}. */
public static final int COLOR_FormatYUV411Planar = 17;
/** @deprecated Use {@link #COLOR_FormatYUV420Flexible}. */
public static final int COLOR_FormatYUV411PackedPlanar = 18;
/** @deprecated Use {@link #COLOR_FormatYUV420Flexible}. */
public static final int COLOR_FormatYUV420Planar = 19;
/** @deprecated Use {@link #COLOR_FormatYUV420Flexible}. */
public static final int COLOR_FormatYUV420PackedPlanar = 20;
/** @deprecated Use {@link #COLOR_FormatYUV420Flexible}. */
public static final int COLOR_FormatYUV420SemiPlanar = 21;
发现我的机器里,video/avc
的编码器有两个:OMX.qcom.video.encoder.avc
和OMX.google.h264.encoder
,分别支持[19]和[19, 21]。
如果我们使用MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)
来创建MediaCodec,那么会使用第一个匹配的OMX.qcom.video.encoder.avc
,如果我们使用了COLOR_FormatYUV420Planar
,那么就会报错。所以我们可以使用MediaCodec.createByCodecName("OMX.google.h264.encoder")
来创建MediaCodec,就会制定使用第二个编码器,那么使用19或者21的ColorFormat都可以。
可以看到在API23中已经弃用了COLOR_FormatYUV420Planar
转而推荐使用COLOR_FormatYUV420Flexible
,YUV420Flexible并不是一种确定的YUV420格式,而是包含COLOR_FormatYUV411Planar, COLOR_FormatYUV411PackedPlanar, COLOR_FormatYUV420Planar, COLOR_FormatYUV420PackedPlanar, COLOR_FormatYUV420SemiPlanar和COLOR_FormatYUV420PackedSemiPlanar。
在API 21引入YUV420Flexible的同时,它所包含的这些格式都deprecated掉了。需要注意的是几乎所有的厂商都支持COLOR_FormatYUV420Flexible
,但是是其中的哪一种就不一定了。
2. Failed to stop the muxer
在调用mediaMuxer.stop()
的时候会报下面Failed to stop the muxer
错误,直接Crash。
Process: com.yocn.media, PID: 32401
java.lang.IllegalStateException: Failed to stop the muxer
at android.media.MediaMuxer.nativeStop(Native Method)
at android.media.MediaMuxer.stop(MediaMuxer.java:454)
上面说的MediaMuxer.addTrack(mediaFormat)
的MediaFormat配置错误,可以在MediaCodec.INFO_OUTPUT_FORMAT_CHANGED
之后用encodeMediaCodec.getOutputFormat();
来使用。
那到底为什么不一样呢?如果我就是想自己来创建MediaFormat呢?
其实我们打印一下自己创建的MediaFormat跟encodeMediaCodec.getOutputFormat();
的比较一下就可以了。
D/LogUtil: encodeMediaFormat::{color-format=19, i-frame-interval=1, mime=video/avc, width=544, bitrate=2500000, frame-rate=20, height=960}
D/LogUtil: outputFormat::{max-bitrate=2500000, csd-1=java.nio.HeapByteBuffer[pos=0 lim=10 cap=10], mime=video/avc, width=544, bitrate=2500000, frame-rate=20, height=960, csd-0=java.nio.HeapByteBuffer[pos=0 lim=18 cap=18]}
我们发现其实就是少了csd-0
和csd-1
两个KV值。那其实可以试一下,使用下面代码将存在的取出来再设置到我们自己创建的MediaFormat里面去就不会报错了。
ByteBuffer csd_0 = decodeMediaFormat.getByteBuffer("csd-0");
ByteBuffer csd_1 = decodeMediaFormat.getByteBuffer("csd-1");
这是所谓的Codec-specific数据。可以查看MediaCodec API.
有些格式,特别是ACC音频和MPEG4、H.264和H.265视频格式要求实际数据以若干个包含配置数据或编解码器指定数据的缓存为前缀。
Codec-specific数据也可以被包含在传递给configure方法的格式信息(MediaFormat)中,在ByteBuffer条目中以"csd-0", "csd-1"等key标记。这些keys一直包含在通过MediaExtractor获得的Audio Track or Video Track的MediaFormat中。一旦调用start()方法,MediaFormat中的Codec-specific数据会自动提交给编解码器;你不能显示的提交这些数据。如果MediaFormat中不包含编解码器指定的数据,你可以根据格式要求,按照正确的顺序使用指定数目的缓存来提交codec-specific数据。在H264 AVC编码格式下,你也可以连接所有的codec-specific数据并作为一个单独的codec-config buffer提交。
从Android官网截取下来的图片可以看到在H264格式里csd-0
和csd-1
分别代表了sps
和pps
。
Missing codec specific data
D/MPEG4Writer: Video track source stopping
D/MPEG4Writer: Video track source stopped
E/MPEG4Writer: Missing codec specific data
E/MPEG4Writer: Dumping Video track's last 10 frames timestamp and frame type
E/MPEG4Writer: (9166667us, 9166667us Non-Key frame) ...
I/MPEG4Writer: Received total/0-length (286/0) buffers and encoded 285 frames. - Video
D/MPEG4Writer: Video track stopped. Stop source
因为上面在mediaMuxer.stop()
的时候会crash,我尝试不调用stop的情况下写mp4文件,不会crash,能生成mp4,大小跟生成的h264文件一样大,但是无法播放,在log中会发现上述报错日志。
尝试使用ffplay播放mp4文件的时候会报moov atom not found
错误。
解决方法就是上面的解决方法。
不贴大段代码了,具体代码查阅:SimpleVideoDecoderAndEncoder.java,如果文件位置修改查看不了可以看AndroidMediaCodec项目。
参考链接: