Android多媒体框架之MediaCodec

3,378 阅读7分钟

关键技术

处理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帧。实际测试下来发现,必须通过OpenGLMediaCodec.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支持三种码控模式:

  1. BITRATE_MODE_CQ:0,不控制码率,尽最大可能保证图像质量
  2. BITRATE_MODE_VBR:1,可变(平均)码率
  3. 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格式有两个:

  1. 21 -> COLOR_FormatYUV420SemiPlanar(NV12)
  2. 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-Layer

MediaCodec相关类如下所示: 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硬编码技术方案

相关文章

  1. Android MediaCodec原理剖析及填坑
  2. MediaCodec 解码视频快速取帧
  3. Android MediaCodec硬编码进阶指南
  4. Android MediaCodec stuff