Android 硬编码(MediaCodec)

直播行业的风潮,短视频的爆红,各种现象使得视频成为了现在内容生产的重要载体。这种现象导致各位产品都想在自己的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,都是加水印,两处代码两种形式,加大了代码整体的复杂度。而硬编码绘制渲染的代码可以同时用于预览和编码。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350