FFmpeg之AVFrame转Android Bitmap
此前很多工作都设计到使用 FFmpeg 对视频帧进行获取,在 FFmpeg 解码视频文件获取到帧数据结构是 AVFrame
, 对于应用层我们没有办法直接拿到进行数据处理,需要转换为 Android 平台特有的处理结构。而我是需要对应的帧图片数据,那么在 Android 侧需要将其转化为 Bitmap
,之前整理的过程中发现了这篇《Android音视频开发】从AVFrame到MediaFrame数组(二)》博客文章 ,觉得写得很不错,非常精简,适合我的需求,于是对齐进行整理,并标注一下自己在过程中遇到的一些坑点。
Native层创建Bitmap
Bitmap
是对 SkBitmap 的包装。具体说来, Bitmap 的实现包括 Java 层和 JNI 层,JNI 层依赖 Skia,SkBitmap
本质上可简单理解为内存中的一个字节数组
想要生成 Bitmap
, 我们首先需要构造一个 Bitmap
对象,Java层有很多种方式可以生成Bitmap对象,最简单的方式如下:
1 | Bitmap.createBitmap(width,height,new Bitmap.Config.ARGB_8888) |
由于整个 FFmpeg
的操作在 JNI 侧进行,对应的操作需要使用 JNIEnv
进行相关的调用,主要逻辑如下:
1 | jobject create_bitmap(JNIEnv *env, int width, int height) { |
获取Bitmap像素数据地址,并锁定
1 | void *addr_pixels; |
解释一下这两句话:
第一句的作用声明并定义一个指向任意类型的指针变量,名称是addr_pixels。我们定义它的目的,是让它指向bitmap像素数据(即:
addr_pixels的值为bitmap像素数据的地址)。注意哦,这时候,addr_pixels的值是一个随机的值(假定此时为:0x01),由系统分配,它还不指向bitmap像素数据。
第二句话的作用就是将bitmap的像素数据地址赋值给addr_pixels,此时它的值被修改(假定为:0x002)。并且锁定该地址,保证不会被移动。【注:地址不会被移动这里我也不太懂什么意思,有兴趣的可以去查看该方法的API文档】
【注:】此时的bitmap由像素数据的地址,但是该地址内还没有任何像素数据哦,或者说它的像素数据为\0
到这里,我们已经有了源像素数据在AVFrame中,有了目的像素数据地址addr_pixels,那么接下来的任务就是将AVFrame中的像素数据写入到addr_pixels指向的那片内存中去。
向Bitmap中写入像素数据
这里要说一下,我们获取到的AVFrame的像素格式通常是YUV格式的,而Bitmap的像素格式通常是RGB格式的。因此我们需要将YUV格式的像素数据转换成RGB格式进行存储。而RGB的存储空间Bitmap不是已经给我门提供好了吗?嘿嘿,直接用就OK了,那现在问题就是YUV如何转换成RGB呢?
关于YUV和RGB之间的转换,我知道的有三种方式:
- 通过公式换算
- FFmpeg提供的libswscale
- Google提供的libyuv
这里我们选择libyuv因为它的性能好、使用简单。
说它使用简单,到底有多简单,嘿,一个函数就够了!!
1 | libyuv::I420ToABGR(frame->data[0], frame->linesize[0], // Y |
解释一下这个函数:
- I420ToABGR: I420表示的是YUV420P格式,ABGR表示的RGBA格式(execuse me?? 是的,你没看错,Google说RGBA格式的数据在底层的存储方式是ABGR,顺序反过来,看下libyuv源码的函数注释就知道了)
- frame->data&linesize: 这些个参数表示的是源YUV数据,上面有标注
- (uint8_t *) addr_pixels: 嘿,这个就是说往这块空间里写入像素数据啦
- linesize: 这个表示的是该图片一行数据的字节大小,Bitmap按照RBGA格式存储,也就是说一个像素是4个字节,那么一行共有:frame->width 个像素,所以:
linesize = frame-> width * 4
【注:】关于这一小块功能的实现,可能其他地方你会看到这样的写法,他们用了如下接口:
// 思路
是:新建一个AVFrame(RGB格式),通过av_image_fill_arrays来实现AVFrame(RGB)中像素数据和Bitmap像素数据的关联,也就是让AVFrame(RGB)像素数据指针等于addr_pixels
pRGBFrame = av_frame_alloc() av_image_get_buffer_size()
av_image_fill_arrays() /*
我也是写到这里的时候,才想到这个问题,为什么要这样用呢,直接使用addr_pixels不是也一样可以么?
不过大家都这么用,应该是有它不可替代的使用场景的。因此这里也说一下av_image_fill_arrays这个函数。
*/// TODO: 解释下这个函数的作用 av_image_fill_arrays(dst_data, dst_linesize,
src_data, pix_fmt, width, height, align); 它的作用就是
- 根据src_data,设置dst_data,事实上根据现象或者自己去调试,可以发现dst_data的值就是src_data的值(我印象中好像值是相同的,这会我忘了,后面我再验证下)
- 根据pix_fmt, width, height设置linesize的值,其实linesize的计算就和我上面给出的那个公式是一样子的值
OK, 函数执行完毕,我们Bitmap就有了像素数据,下面就是把Bitmap上传给Java层
Native回调Java接口
说下Java层
有一个MainActivity.java用于界面的显示
有一个JNIHelper.java用于Java层和Native层的沟通
1 | public class JNIHelper { |
Native层的回调代码如下:
1 | jclass clz = env->FindClass("me/oogh/xplayer/JNIHelper"); |
AndroidBitmap_lockPixels 方法
以上就是整个文章的内容,使用起来也是 no problem! 但使用过程中遇到的问题就是内存回收的问题,最开始使用的时候并没有过多关注JNI层 AndroidBitmap_lockPixels
这个方法,以至于后来我在处理Bitmap内存回收上遇到了一些问题。 AndroidBitmap_lockPixels
与之对应还有一个 AndroidBitmap_unlockPixels
AndroidBitmap_lockPixels
“函数作用锁定了像素缓存以确保像素的内存不会被移动”,这句话看起来好像挺难理解,但是我们在 Java层面有与之类似的操作,那就是 SurfaceHolder.lockCanvas()
,还记得我们在绘制的过程中需要先使用 lockCanvas
锁定画布,返回的画布对象Canvas
然后使用 unlockCanvasAndPost(Canvas canvas)
结束锁定画布,并提交改变。AndroidBitmap_lockPixels
与 AndroidBitmap_unlockPixels
做的是类似的事情,都是锁住一块内存区域,保证其安全。
回到上面说的内存回收的问题,由于自己使用失误,流程大概是这样:
1 | AndroidBitmap_lockPixels(env, bitmap, &addr_pixels); |
然后在Java层有这样的逻辑:
1 | public void onReceived(Bitmap bitmap){ |
但是我发现使用了 bitmap.recycle()
与不使用,内存中 Native区域仍然占了一大部分,后来在AndroidBitmap_lockPixels
的注释才发现不对的地方:
1 | /** |
其中:
if the pixels had been previously purged, they will have been restored.
也就是说在AndroidBitmap_unlockPixels
调用之前,如果像素数据被销毁了,他们会被恢复!至于为什么会被恢复,这个就需要之后再进行研究了。
后来对逻辑进行更改,将 Bitmap.recycle()的逻辑移动到 AndroidBitmap_unlockPixels之后。