关键技术
处理EOS
当没有输入数据时,必须通过queueInputBuffer携带BUFFER_FLAG_END_OF_STREAM标志位通知Codec,BUFFER_FLAG_END_OF_STREAM可以与最后一个合法输入帧一起输入,也可以单独输入一个带BUFFER_FLAG_END_OF_STREAM的空帧,此时带BUFFER_FLAG_END_OF_STREAM的空帧的pts值会被忽略。
然后,Codec会继续输出数据,直到没有更多输出数据了。
当没有输出数据时,Codec通过dequeueOutputBuffer为BufferInfo设置BUFFER_FLAG_END_OF_STREAM,BUFFER_FLAG_END_OF_STREAM可以与最后一个合法输出帧一起输出,也可以与一个空帧一起输出,此时带BUFFER_FLAG_END_OF_STREAM的空帧的pts值应该被忽略。
但是实际用下来发现:非空EOS输出帧的pts有时也为0,此时这个输出帧也应该被丢弃。
控制关键帧数量
MediaCodec提供了KEY_I_FRAME_INTERVAL控制关键帧频率,表示多少秒一个I帧。实际测试下来发现,必须通过OpenGL向MediaCodec.createInputSurface创建的Surface输入帧,才能达到控制关键帧数量的目的。
此外,MediaCodec还提供了强制I帧的参数MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME,甚至可以编码出一个全部是关键帧的视频。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
val params = Bundle()
params.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0)
videoEncoder.setParameters(params)
}
输入输出的时间戳单位是毫秒
通过MediaCodec解码音频和视频时,输入的PTS单位必须是微秒,FFmpeg av_read_frame返回的AVPacket,时间戳是基于AVStream的time_base,所以必须将AVPacket的pts和dts从AVStream的time_base转换到time_base = 1000000,再送给MediaCodec;否则会出现异常情况,比如:多个AVPacket的pts间隔非常短,系统会认为视频帧率太高,解码器已经超负荷,导致硬解码器(OMX.qcom.video.decoder.avc)创建失败,转成系统内部的google软解码器(OMX.google.h264.decoder),从而导致解码速度大幅下降。
硬解码丢弃视频帧
用Surface作为硬解码的输出时,可以选择丢弃解码后的视频帧,即可以根据具体场景,决定是否把解码后的视频帧更新到Surface上。比如:Seek的场景,因为要从I帧解码到目标时间戳,但是中间的解码帧并不需要渲染,那么就可以在解码时直接丢弃这些中间帧,减少GPU负担。
// render为true则表示把视频帧更新到Surface,为false则表示丢弃这个视频帧,即Surface的画面不会更新
public final void releaseOutputBuffer(int index, boolean render);
编码参数
Profile和Level
Profile是对视频压缩特性的描述,主要有Baseline、Main Profile(MP)、High Profile(HP)等,从左到右编码效率不断提高,清晰度也有所提升。
Level是对视频本身特性的描述(码率、分辨率、fps),Level越高,视频的码率、分辨率、fps越高。 关于Profile和Level,可以参考H264 Profile对比分析。
MediaCodecInfo.CodecCapabilities有个成员叫profileLevels,其中包含了该类型MediaCodec所支持的Profile和Level,定义如下:
public CodecProfileLevel[] profileLevels;
一般情况下,可以根据目标Profile,从MediaCodecInfo.CodecCapabilities.profileLevels找到对应的CodecProfileLevel,然后设置Profile和Level,如下所示:
mediaFormat.setInteger(MediaFormat.KEY_PROFILE, dstProfileLevel.profile);
mediaFormat.setInteger(MediaFormat.KEY_LEVEL, dstProfileLevel.level);
但是,Android7.0及以上才支持设置Profile,之前版本一直是写死的Baseline。
Bitrate-Mode
Android支持三种码控模式:
- BITRATE_MODE_CQ:0,不控制码率,尽最大可能保证图像质量
- BITRATE_MODE_VBR:1,可变(平均)码率
- BITRATE_MODE_CBR:2,固定码率
Android API21开始引入了EncoderCapabilities.isBitrateModeSupported方法,判断编码器是否支持特定码控模式。
// EncoderCapabilities的方法
boolean isBitrateModeSupported(int mode);
MediaCodec设置Bitrate-Mode方式:
// 设置码控模式
mediaFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, bitrateMode);
// 设置码率
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
一般情况下使用可变码率(BITRATE_MODE_VBR)。
多实例问题
系统API给出的最大实例数,实际测试下来并不准确。
// 理论最大实例数,实际并发实例数可能较少,因为它取决于当前可用资源
int maxCount = MediaCodecInfo.getCapabilitiesForType("video/avc").getMaxSupportedInstances();
厂商给的建议,基于机型编解码最大Buffer,来判断是否可以启动新MediaCodec。
// codecInfo表示一个编码器或者解码器的能力
MediaCodecInfo.CodecCapabilities codecCapabilities = codecInfo.getCapabilitiesForType("video/avc");
// 支持的最大宽度
int maxWidth = codecCapabilities.getVideoCapabilities().getSupportedHeights().getUpper();
// 支持的最大高度
int maxHeight = codecCapabilities.getVideoCapabilities().getSupportedWidths().getUpper();
// 针对最大尺寸,支持的最大帧率
double maxFps = codecCapabilities.getVideoCapabilities().getSupportedFrameRatesFor(maxWidth,maxHeight).getUpper();
// 根据上述尺寸和帧率,计算出最大Buffer
double maxBufferSize = maxWidth * maxHeight * maxFps;
策略: 轨道1是否可以使用硬解码 bufferSize1 = width * height * fps if(bufferSize1 < maxBufferSize) YES else NO
轨道2是否可以使用硬解码 buffeSize2 = width * height * fps if(bufferSize1 + bufferSize2 < maxBufferSize) YES else NO
轨道3是否可以使用硬解码 buffeSize3 = width * height * fps if(bufferSize1 + bufferSize2 + bufferSize3 < maxBufferSize) YES else NO
依次类推,结合机器能力和视频属性,动态判断可以支持的并发MediaCodec数量。
width: 当前视频宽度, height: 当前视频高度,fps:当前视频帧率
硬解码首帧延迟
硬解码需要送入大概5-7帧左右,才能吐出raw数据,会有200-300ms延时?这个可以优化吗
硬解数据处理
MediaCodec硬解到纹理
// 视频像素区域宽高
int width = MediaFormat.getInteger(MediaFormat.KEY_WIDTH);
int height = MediaFormat.getInteger(MediaFormat.KEY_HEIGHT);
// The left-coordinate (x) of the crop rectangle
int cropLeft = MediaFormat.getInteger("crop-left");
// The right-coordinate (x) MINUS 1 of the crop rectangle
int cropRight = MediaFormat.getInteger("crop-right") + 1;
int cropTop = MediaFormat.getInteger("crop-top");
int cropBottom = MediaFormat.getInteger("crop-bottom") + 1;
// 旋转角度
int rotation = MediaFormat.getInteger(MediaFormat.KEY_ROTATION);
width和height是视频帧像素区域尺寸,即OES纹理的尺寸。 Crop圈出了一个矩形区域,是视频帧的有效区域,要通过纹理坐标截取出有效矩形区域。
纹理坐标是(绘制到FBO时):
- 左上:(cropLeft / width, cropBottom / height)
- 左下:(cropLeft / width, cropTop / height)
- 右上:(cropRight / width, cropBottom / height)
- 右下:(cropRight / width, cropTop / height)
然后是处理旋转角度,通过调整纹理坐标可以实现。
即先处理Crop,再处理旋转角度。
MediaCodec硬解出YUV
// 视频像素区域宽高
int width = MediaFormat.getInteger(MediaFormat.KEY_WIDTH);
int height = MediaFormat.getInteger(MediaFormat.KEY_HEIGHT);
// the stride of the video bytebuffer layout
int strideWidth = MediaFormat.getInteger(MediaFormat.KEY_STRIDE);
// the plane height of a multi-planar (YUV) video bytebuffer layout
int strideHeight = MediaFormat.getInteger(MediaFormat.KEY_SLICE_HEIGHT);
// The left-coordinate (x) of the crop rectangle
int cropLeft = MediaFormat.getInteger("crop-left");
// The right-coordinate (x) MINUS 1 of the crop rectangle
int cropRight = MediaFormat.getInteger("crop-right") + 1;
int cropTop = MediaFormat.getInteger("crop-top");
int cropBottom = MediaFormat.getInteger("crop-bottom") + 1;
// 旋转角度
int rotation = MediaFormat.getInteger(MediaFormat.KEY_ROTATION);
待补充
YUV格式
// 编解码器支持的YUV格式
int[] colorFormatArray = MediaCodecInfo.getCapabilitiesForType("video/avc").colorFormats;
// 查看不同编解码器支持的YUV格式
Log.i(TAG,"name: ${mediaCodecInfo.name}, ${mediaCodecInfo.supportedTypes[0]}, colorFormats: ${Arrays.toString(caps.colorFormats)}");
// H264
name: OMX.google.h264.decoder, video/avc, colorFormats: [2135033992, 19, 21, 20, 39]
name: OMX.google.h264.encoder, video/avc, colorFormats: [2135033992, 19, 21, 20, 39, 2130708361]
// H265
name: OMX.qcom.video.decoder.hevc, video/hevc, colorFormats: [2141391878, 2135033992, 2141391876, 21, 19, 2141391877]
name: OMX.qcom.video.encoder.hevc, video/hevc, colorFormats: [2141391878, 2141391876, 2141391872, 2141391881, 2141391882, 2141391880, 2141391879, 2130708361, 2135033992, 21]
// 2135033992 -> COLOR_FormatYUV420Flexible
// 19 -> COLOR_FormatYUV420Planar(I420)
// 20 -> COLOR_FormatYUV420PackedPlanar(I420)
// 21 -> COLOR_FormatYUV420SemiPlanar(NV12)
// 39 -> COLOR_FormatYUV420PackedSemiPlanar(NV12)
// 2130708361 -> COLOR_FormatSurface(编码时,Surface作为输入)
综合H264和H265,编码和解码都支持的YUV格式有两个:
- 21 -> COLOR_FormatYUV420SemiPlanar(NV12)
- 2135033992 -> COLOR_FormatYUV420Flexible
COLOR_FormatYUV420Flexible对应android.graphics.ImageFormat中YUV_420_888格式,可以表示COLOR_FormatYUV420Planar、COLOR_FormatYUV420PackedPlanar、COLOR_FormatYUV420SemiPlanar和COLOR_FormatYUV420PackedSemiPlanar,即:可以具体格式可以是I420,也可以是NV12。
编码和解码都支持的YUV格式是NV12,并且编码器都支持Surface输入,解码器都支持Surface输出。
既然MediaCodec解码视频时,可以输出纹理或者YUV。那么当输出YUV时,具体是什么格式那?
MediaCodec输出纹理或者YUV之前,首先通过MediaCodec.INFO_OUTPUT_FORMAT_CHANGED,给出了MediaFormat,通过"color-format"可以获取到具体的输出格式。
通过实践发现:当通过Surface输出时,MediaFormat的"color-format"值正是2130708361 -> COLOR_FormatSurface;当不配置Surface,输出的YUV格式则取决于视频,MediaFormat的"color-format"值可能是I420或者NV12。
MediaCodec编码YUV时,YUV格式是由开发者决定并且设置给MediaFormat对象,通过MediaCodec.configure传给编码器,由上述代码可知,H264和H265编码器都支持NV12格式的YUV输入和Surface输入。
MediaCodec相关配置信息都需要通过MediaFormat对象去获取,主要包括InputFormat和OutputFormat:
- MediaCodec.getInputFormat():MediaCodec.configure返回成功后可以调用,表示MediaCodec的输入参数。例如:解码时的音视频元数据,编码时的RAW数据格式。
- MediaCodec.getOutputFormat():dequeueOutputBuffer通知INFO_OUTPUT_FORMAT_CHANGED消息后可以调用,表示MediaCodec的输出参数。例如:解码时的RAW数据格式,编码时的音视频元数据等。
OMX
Java层MediaCodec通过JNI桥接层与Native层MediaCodec.cpp一一对应。
Native层MediaCodec持有ACodec,ACodec通过OMX服务申请一个OMXNodeInstance(主要参数是编解码器名字),ACodec持有OMXNodeInstance的唯一标识符mNode和IOMXObserver(用来从OMXNodeInstance接收回调),ACodec通过mNode操作OMX服务端的OMXNodeInstance,通过IOMXObserver接收OMX服务端的回调。客户端的ACodec和OMX服务端的OMXNodeInstance是一一对应的,OMXNodeInstance持有一个具体的编解码器组件实例(由OMXMaster管理的OMXPlugin创建),整个流程如下所示:
MediaCodec相关类如下所示:
OMX是一个管理OMX Plugin和OMXNodeInstance的服务,运行在MediaPlayerService进程,直接持有OMXMaster和所有的编解码器实例OMXNodeInstance。
OMXMaster管理所有OMXPlugin,OMXPlugin管理各自组件,每个组件是一个编码器或者解码器,真正负责编解码工作,OMXPlugin分为硬件和软件插件:
- 硬件:addPlugin("libstagefrighthw.so”);
- 软件:SoftOMXPlugin
硬件编解码器由厂商基于libstagefrighthw.so提供实现,SoftOMXPlugin负责管理所有的软件编解码器组件,每个组件对应一个so,其具体实现在:androidxref.bytedance.net/android/xre…
一个编解码流程:MediaCodec -> ACodec ->(Binder)-> OMXNodeInstance -> SoftOMXComponent
SoftOMXPlugin.cpp文件列出了所有的软件编解码器,每个编解码器对应一个独立so,其名称为libstagefright_soft_${mLibNameSuffix}.so
static const struct {
const char *mName;
const char *mLibNameSuffix;
const char *mRole;
} kComponents[] = {
{ "OMX.google.aac.decoder", "aacdec", "audio_decoder.aac" },
{ "OMX.google.aac.encoder", "aacenc", "audio_encoder.aac" },
{ "OMX.google.amrnb.decoder", "amrdec", "audio_decoder.amrnb" },
{ "OMX.google.amrnb.encoder", "amrnbenc", "audio_encoder.amrnb" },
{ "OMX.google.amrwb.decoder", "amrdec", "audio_decoder.amrwb" },
{ "OMX.google.amrwb.encoder", "amrwbenc", "audio_encoder.amrwb" },
{ "OMX.google.h264.decoder", "avcdec", "video_decoder.avc" },
{ "OMX.google.h264.encoder", "avcenc", "video_encoder.avc" },
{ "OMX.google.hevc.decoder", "hevcdec", "video_decoder.hevc" },
{ "OMX.google.g711.alaw.decoder", "g711dec", "audio_decoder.g711alaw" },
{ "OMX.google.g711.mlaw.decoder", "g711dec", "audio_decoder.g711mlaw" },
{ "OMX.google.mpeg2.decoder", "mpeg2dec", "video_decoder.mpeg2" },
{ "OMX.google.h263.decoder", "mpeg4dec", "video_decoder.h263" },
{ "OMX.google.h263.encoder", "mpeg4enc", "video_encoder.h263" },
{ "OMX.google.mpeg4.decoder", "mpeg4dec", "video_decoder.mpeg4" },
{ "OMX.google.mpeg4.encoder", "mpeg4enc", "video_encoder.mpeg4" },
{ "OMX.google.mp3.decoder", "mp3dec", "audio_decoder.mp3" },
{ "OMX.google.vorbis.decoder", "vorbisdec", "audio_decoder.vorbis" },
{ "OMX.google.opus.decoder", "opusdec", "audio_decoder.opus" },
{ "OMX.google.vp8.decoder", "vpxdec", "video_decoder.vp8" },
{ "OMX.google.vp9.decoder", "vpxdec", "video_decoder.vp9" },
{ "OMX.google.vp8.encoder", "vpxenc", "video_encoder.vp8" },
{ "OMX.google.raw.decoder", "rawdec", "audio_decoder.raw" },
{ "OMX.google.flac.encoder", "flacenc", "audio_encoder.flac" },
{ "OMX.google.gsm.decoder", "gsmdec", "audio_decoder.gsm" },
};
特殊Case
某些手机上MediaCodec Buffer编码比Surface编码更快
ViVo x21(高通665)上,720P 30S的视频,MediaCodec Surface编码耗时约为23S,MediaCodec Buffer编码耗时约为12S左右。
相比于MediaCodec纹理编码,若使用Buffer编码,需要多出glReadPixels + rgbaToNV12两步,然后基于NV12进行MediaCodec Buffer编码。
- 对于720P,glReadPixels + rgbaToNV12 + Buffer硬编,比纹理硬编,有比较好的优化效果。
- 对于1080P,因为glReadPixels + rgbaToNV12耗时较长,所以整体耗时与纹理编码相差不大。
- 对于2K和4K的大分辨率视频,因为glReadPixels + rgbaToNV12耗时更长,所以Buffer硬编的整体耗时,大于纹理编码。
分辨率越大,glReadPixels + rgbaToNV12耗时越高,Buffer编码的优势越来越小。
参考:ViVo x21(高通665)合成导出速度分析和优化、Buffer硬编码技术方案。