Android硬编解码利器MediaCodec解析——从猪肉餐馆的故事讲起(一)

3,718 阅读11分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

更多博文,请看音视频系统学习的浪漫马车之总目录

实践项目: 介绍一个自己刚出炉的安卓音视频播放录制开源项目

视频理论基础:
视频基础知识扫盲
音视频开发基础知识之YUV颜色编码
解析H264视频编码原理——从孙艺珍的电影说起(一)
解析H264视频编码原理——从孙艺珍的电影说起(二)
H264码流结构一探究竟

Android平台MediaCodec系列:
Android硬编解码利器MediaCodec解析——从猪肉餐馆的故事讲起(一)
Android硬编解码工具MediaCodec解析——从猪肉餐馆的故事讲起(二)
Android硬编解码工具MediaCodec解析——从猪肉餐馆的故事讲起(三)

轻松入门OpenGL系列
轻松入门OpenGL ES——图形渲染管线的那些事
轻松入门OpenGL ES——再谈OpenGL工作机制

初识MediaCodec

前面视频理论基础几篇篇文章已经比较详细地介绍了H264编辑码基本原理以及码流的基本结构,其中并未叙述具体编解码算法,因为对于一般的工程类开发来说,这些知识已经足矣,算法那是专门做算法的人员需要研究的。而对于一般开发来说,已经有成熟的工具来处理编解码了,其中,MediaCodec就是Android平台中专门处理音视频硬编硬解码的利器。

解码的作用,就是将视频/音频压缩编码数据,解码成为非压缩的视频/音频原始数据。反之,编码的作用,就是将非压缩的视频/音频原始数据转为视频/音频压缩编码数据。

虽然关于MediaCodec的文章百度一下大把,但是很多其实也就是对官网的一个翻译,本文我将尽量结合之前几篇文章中关于编解码相关的内容并将自己一些个人想法写上来,使得此MediaCodec文章非彼(百度)MediaCodec文章。不过这样出现错误的风险也会提高哈哈,所以有什么错误也请各位指教)

首先要解释的是什么是硬编硬解码,有硬就有软,当然这里只是一种行内的约定俗成的说法,并不是真的一种很硬一种很软。一般来说:

软编软解码:使用CPU进行编码,一般是执行代码运行算法指令编码。
硬编硬解码:使用非CPU进行编码,如显卡GPU、专用的DSP、FPGA、ASIC芯片等,一般是算法已经固化在芯片中。

一般来说,软编码会使CPU负载更重,所以性能相对比硬编要低,不过兼容性一般比硬编好,低码率下质量通常比硬编码要好一点。而硬编码一般性能比软编码好一些,但是兼容性就差一些,低码率下通常质量低于软编码的。

按照MediaCodec官网的定义,MediaCodec是Android平台提供的一个底层的音视频编解码框架,它是安卓底层多媒体基础框架的重要组成部分。它经常和 MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface, AudioTrack一起使用。

这里的底层是相对于上层的MediaPlayer这一类封装好的可以几个api的简单调用就能搞定一段视频从解复用、解码、播放的全过程的工具类而言的。那既然有了MediaPlayer这一类傻瓜式调用即可的工具,为啥还要MediaCodec呢?因为我们的需求肯定不会满足于简单的播放视频,我们要根据自己的需要去定制视频,比如最常见的就是抖音那种给视频加滤镜特效,需要对视频每一帧专门进行处理。

之所以说MediaCodec是硬编解码,是因为MediaCodec就相当一个处于CPU中的遥控器,专门遥控音视频编解码芯片进行工作,而非CPU本身去进行编解码处理。大概是这样一个工作流程:

1655031387807.png

MediaCodec工作流程

说起MediaCodec工作流程工作流程,那肯定要祭出官网这张经典的图:

image.png

MediaCodec的api在业内号称Android平台最不人性化之一,多少英雄好汉对其望而生畏。所以理解整个工作流程便成了重中之重。

从图上可以看出,左边是输入端,右边是输出端。其中有输入和输出端各有若干个buffer,输入端不断拿到一个空buffer,装上数据,再传入MediaCodec直到所有数据输入为止。输出端不断从MediaCodec获取到buffer,每次得到处理好的数据后,再将buffer交还给MediaCodec。

根据上面描述的流程,写出来的代码就会是input端一个循环,output端一个循环,并且一些细节还很繁琐,比如状态很多都要手动管理,还有buffer还不能直接获取,还要先拿到buffer在buffer数组中的index才可以获取等,所以说api设计是不人性化的。

对于这一个过程更通俗的理解,个人认为可以把MediaCodec看做一家猪肉餐馆。

首先看左右两端绿色的Client部分,左边可以看做猪肉采购员,右边可以看做吃猪肉的顾客,中间MediaCodec可以看做是餐馆厨师。那么生猪肉就是编码数据,烧熟的猪肉就是解码后的数据

1655130569049.png

输入端:
左边input的empty input buffer可以看做空的生猪肉篮子,当猪肉采购员带着生猪肉来到餐馆的时候,就对餐馆厨师(MediaCodec)说:

“厨师,拿个空篮子给我。”

于是老板拿出一个空篮子(input buffer)交给采购员,采购员装了一些生猪肉(待编解码的数据),然后再交给厨师(MediaCodec)。

1655130782468.png

厨师马不停蹄地做菜煮猪肉,另一边顾客过来问:

“师傅,猪肉做好了没?”

厨师如果已经做好,就立刻将做好的猪肉放到盘子(output buffer)上。交给顾客(输出端Client)。顾客吃完之后,再将盘子归还。

1655131564969.png

是的,MediaCodec的工作就是这样不断循环反复地放生猪肉煮猪肉吃猪肉的过程,直到所有猪肉,即整个视频文件编解码完成为止。

具体的生猪肉有哪些呢?由官网可知。mediacodec接受三种数据格式:压缩数据,原始音频数据和原始视频数据。压缩数据一般是解码端的输入和编码端的输出,反之原始音频数据和原始视频数据一般是编码的输入和解码端的输出。

1.对于压缩数据来说:

压缩数据可以作为解码器的输入数据或者编码器的输出数据,需要指定数据格式,这样编码/解码器才能知道如何处理这些压缩数据。当使用视频时,一般是包含完整的一帧数据,也就是我们要输入给解码器一帧完整的数据或者从编码器得到一帧完整的数据。

一般都不要传递给mediacodec不是完整帧的数据,除非是标记了BUFFER_FLAG_PARTIAL_FRAME的数据。BUFFER_FLAG_PARTIAL_FRAME指示了缓冲区只包含帧的一部分,并且解码器应该对数据进行批处理,直到没有该标志的缓冲区在解码帧之前出现。

2.对于原始视频数据来说: 视频编解码支持三种色彩格式:

native raw video format : COLOR_FormatSurface,可以用来处理surface模式的数据输入输出。

flexible YUV buffers: 例如COLOR_FormatYUV420Flexible,可以用来处理surface模式的输出输出,在使用ByteBuffer模式的时候可以用getInput/OutputImage(int)方法来获取image数据。

specific formats: 支持ByteBuffer模式,有一些厂家会定制, 其他的在MediaCodecInfo.CodecCapabilities中可以看到,格式较多,不列举. 假如是flexible format, 同样可以使用Image来处理数据,getInput/OutputImage(int)。

从LOLLIPOP_MR1(api22)以后,mediacodec支持所有的flexible YUV 4:2:0格式。

(原始音频数据以后博文再讲)

MediaCodec工作生命周期

当然,要学会使用MediaCodec,仅仅知道餐馆吃猪肉还是不够的,我们需要更细致地了解它的工作生命周期,又要祭出官网另一张图了:

1655131960524.png

由上图可知,MediaCodec就是一个状态机,在工作期间会经历多个状态阶段。具体来说是总共有三个大状态:Stopped, Executing ,Released,其中Stopped包含Uninitialized, Configured and Error三个小状态,Executing包含Flushed, Running and End-of-Stream三个小状态。

当MediaCodec对象实例刚创建好的时候,处于Stopped状态中的Uninitialized状态。好比早上晨光熹微的时候,猪肉餐馆老板刚到达餐馆,打开大门,厨房和桌椅都有待收拾。

此时需要调用调用configure方法,就能进入Configured状态,就好比猪肉餐馆老板将厨房和桌椅都收拾好,已经进入新的一天奋斗拉客的准备状态。

一个start方法的调用,标志着老板已经开门准备迎接顾客和生猪肉采购员了,此时进入Executing状态了,目前暂时处于Flushed状态,即采购员还没将生猪肉(待编解码数据)带到餐馆也没有顾客在等待炒好的猪肉(编码或解码好的数据)状态。

随着采购员的脚步到了,向厨师要一个空猪肉篮子(dequeueInputBuffer方法调用),厨师告诉他去拿第bufferIndex个篮子装(dequeueInputBuffer方法的返回值。这也能看出api设计不人性化的地方,不是直接返回对应buffer,还要使用bufferIndex再获取一次buffer),于是采购员去拿第bufferIndex个篮子(getInputBuffer方法)得到对应的篮子(buffer),再将生猪肉装进篮子(queueInputBuffer),于是餐馆正是进入Running状态,真正可以营业啦(进行编解码处理)。

MediaCodec工作阶段大部分时间都处于Running状态中,在Running状态,不断地由采购员(input端)送进生猪肉,顾客拿炒猪肉盘子(output端),形成一个循环,直到猪肉生猪肉采购到上限且顾客已经全部吃完,这时候有个细节,就是到了采购员带来今天最后一篮子生猪肉的时候,采购员会小心翼翼地将一个标签(end-of-stream )和生猪肉一起装入篮子,厨师拿到带有这个标签的生猪肉,心领神会,就任性地不再接收任何采购员带来的生猪肉了(解码器不再接受任何新的数据输入),即进入End-of-Stream状态,然后对应炒出来的猪肉盘子也带上一个标签,让顾客知道这是今天最后一盘子猪肉了,吃完就要关门了。

当顾客吃完最后一盘猪肉并且离开餐馆之后,老板便把餐馆门关上(调用stop),此时又回到了Stopped状态中的Uninitialized状态。此时一般是收拾桌椅和厨房(调用release方法来释放所有的资源),然后老板也要撤了,进入Released状态。

当然在餐馆营业中,可能会出现一些意外,比如厨师突然生病了,于是餐馆进行不下去,被迫停止营业,就会进入Stopped中的Error状态,这时候有2个选择,一个是直接关门(release)进入Released状态。一种是如果还要继续营业,老板可以请另一位厨师,然后一切从Stopped状态中的Uninitialized状态重新开始营业。

上面猪肉餐馆的版本可能有点啰嗦了,各位如果是老司机的话也可以直接结合官网快速看一看 MediaCodec States

还是那句话,“纸上得来终觉浅,绝知此事要躬行”,下一篇博文# Android硬编解码工具MediaCodec解析——从猪肉餐馆的故事讲起(二),就是熟悉的代码实践环节,为了保证代码足够权威,特定找来了Google官方MediaCodec的学习项目 grafika,包含了Android硬编解码、照相机、OpenGL各种常用操作,非常适合新手学习,麻雀虽小五脏俱全,缺点是有些api已经过时。


总结

本文首先介绍了MediaCodec的功能以及基本特点,然后描述了它的具体工作流程和生命周期图。希望猪肉餐馆的故事能够帮助更多人更好理解MediaCodec,而不是起到反作用的效果。

原创不易,如果觉得本文对自己有帮助,别忘了随手点赞和关注,这也是我创作的最大动力~

参考:

MediaCodec官网
安卓解码器MediaCodec解析