安卓平台下音视频编解码相关的库,在Android10以前,通过OpenMax适配到多媒体框架下;从Android10开始,启用了新的一套方案Codec2.0来对接(软件)编解码库,旨
在于取代ACodec与OpenMAX,它可以看作是一套新的对接MediaCodec的中间件,其往上对接MediaCodec Native层,往下提供新的API标准供编解码使用,相当于ACodec 2.0,
再往后的版本(A12)会慢慢移除掉omx,来执行mainline计划,目标有几个:减小碎片化、新的buffer管理机制提高性能、组件间更多功能支持。
本篇文章基于以前整理的对Android下OMX插件的一些总结,分析了sprd对OMX的(解码)实现内在机制,当然这些实现,其实还是参考了谷歌的软件omx组件的实现方案。
谷歌的omx组件都是对接软件编解码库,而各个芯片原厂都有自己的硬件编解码器,因此需要实现omx组件来对接自家的编解码驱动。可以将omx组件理解为HAL层。
在这里需要明确几个概念,从高层往底层依次介绍:
OMX框架——OMXCodec,谷歌的一套omx解决方案,参考这里的文件,OMXMaster.cpp通过addPlugin("libstagefrighthw.so")来加载vendor提供的插件库。
OMX插件——plugin,vendor厂商简单封装OMX组件,提供libstagefrighthw.so库出去。
OMX组件——component,直接对接codec_lib,这层实现跨平台api接口(对外透出OMX_Core.h中定义的接口,例如OMX_Set/GetParameter、SendCommand、Empty/FillThisBuffer),
屏蔽掉底层编解码的细节,供外部client调用。
codec实现——软件或硬件方案实现音视频编解码。
基于OMX解码组件实现,一些思考总结如下:
Q1:do{...} while (pBufCtrl->iRefCount > 0)针对outQueue是干什么的?
大概是找到pBufCtrl->iRefCount=0的那个,更新outHeader指针,即将要被填充yuv的那个。
Q2:带B帧的视频文件解封装与解码的行为模式
解封装:其携带的pts不一定是依次递增(即inHeader->nTimeStamp的值为当前码流所携带的pts,即码流对应的yuv的渲染时间点)。出现这种情况的原因是,编码顺序并非按照输入YUV的顺序(pts)来的。
例如, 假设帧率为25fps,输入yuv序列分别为: Y0 Y1 Y2 Y3 Y4 Y5 Y6
假设上面每帧图像对应输出帧类型为: I0 B1 B2 P3 B4 B5 P6
那么,每帧图像pts(ms)为: 0 40 80 120 160 200 240 (依次递增,即按输入yuv所携带的pts来依次递增)
然而,其每帧对应的dts(编码顺序)为:0 80 120 40 200 240 160 (即先编码Y0为I帧,Y1和Y2先缓冲不编码,收到Y3后再编码为P帧,收到Y4和Y5时再编码Y1和Y2为B帧)
编码器输出码流顺序为编码顺序,即先吐出Y0图像的码流I0,其携带的pts和dts都为0;再吐出Y3的码流P3,其pts和dts分别为120和40;接下来再吐出Y1的B1,pts和dts分别为40和80。
而真正进行文件封装(例如mp4容器格式)时,肯定按照输出码流顺序来顺序写码流数据,即先放I0的,再放P3的,再放B1的,最后再放B2的。。。
回到逆过程中,在播放时(渲染到屏幕被我们看到时),我们肯定期望先放Y0图像,再Y1,再Y2等等依次。。。
解封装时,依次输出的码流所携带的时间戳,经常看到不是线性递增的,因为其是pts,类似这样的:0 120 40 80 240 160 200。。。(其潜在要求,如果该帧能够被成功解码,则该
图像应该在这个时间点进行渲染显示)
而对于解码器来说,即使按照送来的码流帧顺序可以依次解码(先I0再P3,再B1,再B2),但其不一定为会立即吐出yuv图像,其会按照pts大小来依次吐出图像,先吐出pts=0对应的Y0,
再吐出pts=40的Y1,接下来是pts=80的Y2,最后才是pts=120的Y3。。。例如,libavcodec解码时,使用AVFrame中的pkt_pts参数(该bitstream所带的pts)来表示该yuv的待渲染时间戳(pts)。
mp4封装标准中用ctts和stts来共同表示dts和pts(无B帧时,就没有ctts的box)。
解码: 送入的inHeader中的data,在将其送入解码器时,不一定能成功解码(更确切说是解码器吐出yuv图,即使是I和P帧,各种decoder都是这种行为?),空了需要深入调查一下原因。
--->测试了ffmpeg带的解码器是这样的行为,送P3码流后,receive_packet()并不能拿到yuv图像(其实送这笔码流时可以解码,应该也解码了),只有送B1后,才能拿到该笔对应的yuv图像(Y1),这样做
的结果是decoder输出yuv图形顺序跟camera采集后送encoder的YUV的顺序一致,优点是吐出的yuv可以直接交给显示模块直接渲染显示,而外部模块不用管理哪帧先显示后显示的问题。
因此,带B帧的视频,最终会导致,从MediaCodec使用者的角度上看,其拿到解码后的buf_idx不是按照顺序来的,而不带B帧的,则得到的是按顺序来的。
Q3:inHeader的data送入到decoder进行解码后,都会notifyEmptyBufferDone()吗?
是的,只有在将这笔数据完全cusume后,才会返回给omx框架层。更精确来说,inHeader->nFilledLen的数据送往decoder,但有可能分两次送给decoder才能用完,例如x264转码得到的文件,
经extractor解析分流后,得到的第一帧是enc_info+I_frame,enc_info通常是编码器构建版本,编码时使用的参数列表。
Q4:pts/dts相关
outHeader链表中,成功解码出的yuv(dec_out.frameEffective: 1),才会drainOneOutputBuffer()(其中会notifyFillBufferDone()),但送出去的pts值不一定是本inHeader所携带的pts(见下条解释)。
解码一帧(出yuv图),解码器内部会修改dec_out.pts,使其线性自增的方式增长,注意不是根据dec_in.nTimeStamp来修改的(因为其不是线性递增的),真正原因是driver内部调用了
H264Dec_find_smallest_pts(),其实这里可以将其做到OMX组件中,但做到组件中会导致OMX组件的设计太臃肿,不易读,因为做组件的同事跟做解码器的同事理解的深入情况不同,做组件的同事
可能不太清楚pts为什么会这样变化。
Q5:outHeader被解码后yuv填充后,在被通过notifyFillBufferDone()被render使用后,会被release吗?
换另外一种说法,有没有可能被解码器内部解码其他帧时参考使用?VSP_bind/unbind_cb就是由codec driver控制pBufCtrl->iRefCount变化的。虽然还给render模块,但毕竟没有释放(iRefCount=0的
才可以被释放——重复使用),因此可以被后续解码所使用,这就是Q1问题的原因。
Q6:一个outHeader绑定一个固定的BufferCtrlStruct(outHeader->pOutputPortPrivate)吗?
是的!
Q7:组件component管理被引用yuv_buf的内在逻辑
一方面是解耦的需要,另一方面又引入了组件设计的复杂性,另一个方面是效率的考虑。
从解耦方面说,outport侧需要dma_buf(ion),而这些buf最合适在comp模块去分配,或者由disp模块分配后传给comp最后送给codec_driver使用,ion内存在codec_driver侧分配则会导致二者过耦合。
从复杂性方面说,outHeader队列中包含已成功解码出yuv的,这些buf有可能被后续帧解码时所引用,因此有些(iRefCount>1)不能直接在下次解码时被用作output_buf以防止被覆盖,因此需要comp侧
去管理这些outHeader何时能被使用,并且codec_driver需要bind/unbind_cb的回调函数来修改iRefCount的引用计数值,因为只有codec才知道哪个buf被引用或解引用。如果是codec_driver内部自己
分配和管理buf(解码参考引用队列,其时上面由comp侧来分配的,codec也需要维护解码参考引用队列,只是这些buf在外部),则就不需要外部comp来管理这些buf何时可以被重复使用了,在解码后
只需告诉comp是否解码出yuv和其addr,这就是安卓原生SoftAvcDec的工作模式(大部分中间buf由cb进行分配,配解码一帧的dec_out的yuv_buf可以由comp分配,指示codec在成功解码出一帧后将图
像copy到这个addr处)。
从效率上说,dma_buf最好是disp模块分配,避免yuv数据重复搬运,如果是codec模块内部分配,则需要将已解码出的yuv搬到disp模块,如果是comp侧分配,也需要拷贝,因为disp模块使用的内存是固定
区域位置的,其他模块只能map得到其addr,但disp模块中不能用map方式得到其他模块分配的mem。
Q8:disp到comp的内存map
针对output侧,使用安卓framework的graphic native buffer(用buffer_handle_t来描述该buffer,yuv空间由gralloc模块来申请和维护),即用iUseAndroidNativeBuffer[OMX_DirOutput]=true表示output侧内存
使用方式,其优点是不用deep copy来减少数据搬运。
映射方法:
GraphicBufferMapper &mapper = GraphicBufferMapper::get();
usage = GRALLOC_USAGE_SW_READ_OFTEN | GRALLOC_USAGE_SW_WRITE_NEVER;
mapper.lock((const native_handle_t*)outHeader->pBuffer, usage, bounds, &vaddr); //拿到disp中buf的虚拟地址:vaddr
dec_out参数配给codec:
uint8_t *yuv = (uint8_t *)((uint8_t *)vaddr + outHeader->nOffset);
(*mH264Dec_SetCurRecPic)(mHandle, yuv, (uint8 *)picPhyAddr, (void *)outHeader, mPicId); //配输出内存,yuv为vir_addr,picPhyAddr为其对应的phy_addr
(*mH264DecDecode)(mHandle, &dec_in,&dec_out); //配输入内存并解码
Q9:显示模块mem到编码/解码组件的内存映射
参考gralloc.cpp模块mem数据映射。
编码组件的buf可能来自于gralloc模块或camera模块,使用如下方式映射:
第一个字节(在mStoreMetaData=true条件下,inHeader->pBuffer+inHeader->nOffset)的值表明了数据来源type:kMetadataBufferTypeCameraSource/kMetadataBufferTypeGrallocSource
虽然可能是kMetadataBufferTypeCameraSource,但其还是从grlloc模块分配用的,即sensor采集一帧图像填数据,即可进行预览一帧,同时将其送往enc模块进行编码。
编码组件的设计,也期望将解码出的数据直接送到gralloc而不希望大块内存的memcpy,因此需要从gralloc模块映射得到out_buf。