直播行业的风潮,短视频的爆红,各种现象使得视频成为了现在内容生产的重要载体。这种现象导致各位产品都想在自己的app里多多少少的加点视频相关的功能,而视频相关的功能一般都避不开编码、解码这个话题。
MediaCodec 是android提供的用于进行硬件编解码的类,可以用于音频和视频,本文以视频的编码为主题。
先简单的介绍一下它的使用流程:
1、创建MediaCodec实体:
有四种方法 分别是MediaCodec 的构造方法,和其内含的三个静态方法:createDecoderByType/createEncoderByType/createByCodecName。
常用的是 createDecoderByType/createEncoderByType两个,顾名思义分别是创建用于解码和编码的实体,传入参数为编解码器的mime type名称。
下面是一些mine type的名称。
added in [API level 16](https://developer.android.com/guide/topics/manifest/uses-sdk-element.html#ApiLevels)
Instantiate the preferred decoder supporting input data of the given mime type.
The following is a partial list of defined mime types and their semantics:
* "video/x-vnd.on2.vp8" - VP8 video (i.e. video in .webm)
* "video/x-vnd.on2.vp9" - VP9 video (i.e. video in .webm)
* "video/avc" - H.264/AVC video
* "video/hevc" - H.265/HEVC video
* "video/mp4v-es" - MPEG4 video
* "video/3gpp" - H.263 video
* "audio/3gpp" - AMR narrowband audio
* "audio/amr-wb" - AMR wideband audio
* "audio/mpeg" - MPEG1/2 audio layer III
* "audio/mp4a-latm" - AAC audio (note, this is raw AAC packets, not packaged in LATM!)
* "audio/vorbis" - vorbis audio
* "audio/g711-alaw" - G.711 alaw audio
* "audio/g711-mlaw" - G.711 ulaw audio
最常用的当属 : "video/avc" - H.264/AVC video,现在网络上流传的短视频的视轨格式基本上都是H264。
于是我们踏出了坚实的第一步:
MediaCodec mediaCodec = MediaCodec.createEncoderByType(MIME);
2、配置MediaCodec:
就一件事:调用MediaCodec的configure方法:
public void configure(
MediaFormat format,
Surface surface,
MediaCrypto crypto,
int flags
);
- Surface surface:指定surface,一般用于解码器,设置后解码的内容会被渲染到所指定的surface上。无需要则传null
- MediaCrypto crypto:指定一个crypto对象,用于对媒体数据进行安全解密。对于非安全的编解码器,传null。
- int flags:当组件是编码器时,flags指定为常量CONFIGURE_FLAG_ENCODE。
- MediaFormat format:输入数据的格式(解码器)或输出数据的所需格式(编码器)。
前三个基本都是固定的,并没有什么变数。主要说一下MediaFormat:
解码的情况下,该实体大多通过MediaExtractor从视频中获取,这里(暂不展开谈MediaExtractor的内容)。
编码的情况实体需要由我们来进行创建,常用的方法是它的静态方法:createVideoFormat/createAudioFormat,两个顾名思义的方法。
以视频为例:
MediaFormat format = MediaFormat.createVideoFormat(MIME,width, height);
- MIME:第一步中的mine_type
- width,height:视频的分辨率/高宽
MediaFormat 可以为编码器设置一些特性参数,比如比特率,帧率,gop(关键帧 帧间 间隔)等等:
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE,2 * 1024 * 1024 ));
format.setInteger(MediaFormat.KEY_FRAME_RATE, fps);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, gop);
需要特別说明的是MediaFormat.KEY_COLOR_FORMAT :该属性用于指明video编码器的颜色格式,具体选择哪种颜色格式与输入的视频数据源颜色格式有关。
比如:Camera预览采集的图像流通常为NV21或YV12,那么编码器需要指定相应的颜色格式,否则编码得到的数据可能会出现花屏、叠影、颜色失真等现象。
MediaCodecInfo.CodecCapabilities.存储了编码器所有支持的颜色格式,常见颜色格式映射如下:
原始数据 编码器
NV12(YUV420sp) ———> COLOR_FormatYUV420PackedSemiPlanar
NV21 ———-> COLOR_FormatYUV420SemiPlanar
YV12(I420) ———-> COLOR_FormatYUV420Planar
然而我们的例子并没有使用上面的标识,而是使用了COLOR_FormatSurface,这是指对一个surface上的图像进行mediaCodec编码。
至于为什么不用上面的标识呢?当然是有坑啦,这个我们后面再进行详细的说明。
完成了MediaFormat 的构建,我们就成功的踏出了第二步
mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mMediaCodec.start();//启动!!
3、 数据处理:
大体步骤:
1、使用者从MediaCodec请求一个空的输入buffer(ByteBuffer),填充满数据后将它传递给MediaCodec处理。
int inputBufferIndex = mediaCodec.dequeueInputBuffer(0);//获取输入缓存区授权
ByteBuffer inputBuffer = codec.getInputBuffer(…);//获取实际buffer
// 填充
。。。
codec.queueInputBuffer(inputBufferId, …);//加入编码队列
2、MediaCodec处理完这些数据并将处理结果输出至一个空的输出buffer(ByteBuffer)中。
使用者从MediaCodec获取输出buffer的数据,消耗掉里面的数据,使用完输出buffer的数据之后,将其释放回编解码器。
int encoderStatus = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 0);//获取输出缓存区授权
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);//获取实际buffer取得编码后的数据
。。。
codec.releaseOutputBuffer(outputBufferId, …);//释放使缓冲buffer回到队列中
dequeueOutputBuffer的 mBufferInfo 为入参(传入一个实体,函数内会为这个实体的字段赋值),其中有buffer的size,offsize信息,帧的时间戳信息和用于表示帧属性的flag字段(最常用的就是用来判断是否是关键帧)。
3、循环上述两步,当编码完成时,在步骤1传入MediaCodec.BUFFER_FLAG_END_OF_STREAM标识,用以标志整个流程的结束。
codec.queueInputBuffer( videoInputBufferIndex, 0, 0, -1, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
在步骤2中读取到MediaCodec.BUFFER_FLAG_END_OF_STREAM时就可以停止整个编码任务啦。
就此我们完成了硬编码的最后一步。
4、坑坑坑
事情并没有那么简单,android硬编码中可谓是一步一坑。。。让我们从头开始迈步子:
1、坚实的第一步
——是真的很坚实 ^ > ^
2、第二步
MediaFormat 可以为编码器设置一些特性参数,然而不同的手机可以设置的参数不同(例如实际开发中我们可能需要设置
KEY_PROFILE,KEY_BITRATE_MODE等参数)如果设置了手机不支持的参数,在调用configure方法时可能会抛出异常,或者设置的参数不生效,导致编码出来的视频不能达到预期的效果等等情况。不过u1s1现在的手机对常用的参数的支持越来越好了。
我的方案:
- 是要对其进行适配,找寻手机适合的参数
- 对参数进行分级,反复尝试调用configure,失败则降级。例:
try{
if (encodeLevel > 1) {
format.setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileHigh);
format.setInteger("level", MediaCodecInfo.CodecProfileLevel.AVCLevel41);
} else if (encodeLevel > 0) {
format.setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileMain);
format.setInteger("level", MediaCodecInfo.CodecProfileLevel.AVCLevel32);
}
//encodeLevel=0的时候放弃该参数,不进行设置
mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
}catch (Exception e) {
if (level >= 1) {
encodeLevel --; // "level down"
prepareEncoder(encodeLevel );//重试
}
}
3、第三步:这是真的坑
看起来逻辑很顺,queueInputBuffer输入数据,然后再调用dequeueOutputBuffer获取输出。然而这个输入很成问题。
现在很多视频录制都要经过滤镜、美颜等等模块对图像进行加工,而这些模块往往输出的是RGB数据,我们要输入编码器首先要对颜色格式进行转换,不仅麻烦而且这个转换往往比较耗时,较差的手机编码速度跟不上,为了用户体验,我们被迫要削减录制视频的帧率。 然而当你转换完成后还有第二个坑在等着你。。。
还记得MediaFormat.KEY_COLOR_FORMAT 吗?这个就是要指定输入数据的颜色格式,上面的映射关系看着很美好, 实际上这个映射关系非常乱,同一个标识在不同手机上对应着不同的YUV格式,让人摸不着头脑,而且还没有什么好的判断方法。输入错误的yuv格式就会导致视频整体颜色错乱,你辛辛苦转换的颜色格式在一台手机上完美无缺,很有可能换台手机就乱套了。
所以上面使用了COLOR_FormatSurface,这是MediaCodec提供的另一种输入数据的方法。首先通过MediaCodec获取一个suface实体
Surface srface = mMediaCodec.createInputSurface();
然后通过Opengl ES将图像渲染至该Surface上(在下篇文章里再介绍Opengl ES相关api的使用),MediaCodec就会对图像进行编码,而我们就只需要调用dequeueOutputBuffer的部分获取编码后的数据,省去了耗时的颜色转换和queueInputBuffer的调用。需要注意的是这个解决方案是有版本限制的:Android 4.3, API18。
5、硬编码该崛起了
说了这么多坑,为什么还要说硬编码崛起呢?我们这就来说说硬编码的优点:
1、 硬编码是使用GPU来进行编码的。
这比起ffmpeg等软编码解决方案来说效率更高,而且可以腾出CPU的资源来供给给其他模块,例如ui、常常和视频关联的人脸识别模块等等。现在各种高清视频开始展露头角,例如4k,h265格式的视频,单位时间内编解码的运算量急剧上升,单靠cpu的软编码是很难维持良好的用户体验的。
2、 硬编码为原生代码,并不需要添加额外的编解码库。
对于轻量级app包的大小还是颇具影响力的,单单ffmpeg的so就不小了。虽然硬编码有版本限制(为了避坑在 API18 以上使用硬编码比较合适),然而时代在发展,18以下的手机的市场份额会越来越小,这个限制会渐渐的失去影响力。
3、Surface的输入方式,绘制即编码,非常方便。
如果你使用ffmpeg的软编码手段,对某视频进行加工,比如添加水印,预览的绘制流程使用opengl es, 编码的流程调用ffmpeg的api,都是加水印,两处代码两种形式,加大了代码整体的复杂度。而硬编码绘制渲染的代码可以同时用于预览和编码。