Andoid MediaCodec 解码视频快速取帧

MediaCodec 解码视频快速取帧

开发背景

所以考虑在需要 1s 视频取 30 帧缩略图时,采取 MediaCodec 硬解视频,获取 YUV 数据,再使用 libyuv 库,编码 YUV 为 ARGB 生成 bitmap 的优化方案,该方案输出一帧 1080p 视频帧耗时在 50ms 左右,并且还有优化空间

MediaCodec 解码取帧流程

mediaCodec 解码流程

image.png

mediaCodec 的使用比较流程化,对于使用者来说,更关注输入源与输出源。

输入源

使用 MediaExtractor 作为输入源,首先区分轨道,选择视频轨进行操作,然后在解码线程循环中,不断的从 MediaExtractor 中取出视频 buffer 作为 MediaCodec 的输入源即可

 mediaExtractor = MediaExtractor()
 mediaExtractor.setDataSource(path)
 var videoFormat: MediaFormat? = null
 for (i in 0..mediaExtractor.trackCount) {
        val mediaFormat = mediaExtractor.getTrackFormat(i)
        if (mediaFormat.getString(MediaFormat.KEY_MIME).contains("video")) {
               mediaExtractor.selectTrack(i)
               videoFormat = mediaFormat
               break
            }
        }
        if (videoFormat == null) {
            throw IllegalStateException("video format is null")
        }
}

读取数据

val sampleSize = mediaExtractor.readSampleData(inputBuffer!!, 0)
val presentationTimeUs = mediaExtractor.sampleTime
mediaExtractor.advance()

输出源

由于不需要渲染到屏幕上,这里选择的输出源是 ImageReader,并且 MediaCodec 使用 ImageReader 会更高效一些

you should use a Surface for raw video data to improve codec performance. Surface uses native video buffers without mapping or copying them to ByteBuffers; thus, it is much more efficient. You normally cannot access the raw video data when using a Surface, but you can use the ImageReader class to access unsecured decoded (raw) video frames. This may still be more efficient than using ByteBuffers

初始化 ImageLoader

 imageReader = ImageReader.newInstance(
            videoFormat.getInteger(MediaFormat.KEY_WIDTH),
            videoFormat.getInteger(MediaFormat.KEY_HEIGHT),
            ImageFormat.YUV_420_888,
            3)
 imageReaderThread = ImageReaderHandlerThread()
 codec.configure(videoFormat, imageReader.surface, null, 0)
 codec.start()
 imageReader.setOnImageAvailableListener(MyOnImageAvailableListener(path),imageReaderThread.handler)

然后调用 mediaCodec 的 releaseOutputBuffer 方法,在 ImageReader 回调之中就能拿到 Image 对象。

处理输出得到 Bitmap

从 ImagerReader 的回调之中,我们能够得到 一个 ImagerReader 对象

            img = reader.acquireLatestImage()
            if (img != null) {
                val outputTime = readCount * intervalTime * 1000 * 1000L
                if (img.timestamp >= outputTime) {
                    if (debugLog) {
                        BLog.d(TAG, "start get bitmap $readCount timestamp is " + img.timestamp)
                    }
                    val planes = img.planes
                    if (planes[0].buffer == null) {
                        return
                    }
                    var bitmap: Bitmap? = null
                    val cacheKey = "$path#${readCount * intervalTime}"
                    if (DiskCacheProvider.diskCache.get(cacheKey)?.exists() != true) {
                        bitmap = getBitmapScale(img, rotation)
                        BLog.d(TAG, "write cache by cache key $cacheKey")
                        DiskCacheProvider.diskCache.put(cacheKey, object : IWriter {
                            override fun write(file: File): Boolean {
                                return FileUtil.saveBmpToFile(bitmap, file, Bitmap.CompressFormat.JPEG)
                            }
                        })
                    }
                    bitmap?.let {
                        callback?.invoke(readCount, bitmap)
                    }
                    readCount++
                    if (debugLog) {
                        BLog.d(TAG, "end get bitmap $readCount timestamp is " + img.timestamp + " cache key id " + cacheKey)
                    }
                }
            }

从 ImageReader 中,我们能够取出 Image 对象,然后从 Image 对象中取得 YUV 数据的 三个分量,然后通过 libyuv 库将 yuv 数据转换为 Bitmap 对象,写入 LruDiskCache中。

MediaCodec 使用中遇到的问题

整个方案的流程应该是比较清晰简单的,但在实际做的过程中,遇到了很多阻塞的问题,在解决这些问题的过程中,才能更深入的了解到 MediaCodec 解码与 YUV 数据处理。

  1. 使用 mediaExtractor.seekTo() 定位需要取帧时间戳的输入源,发现会有很多的重复帧,并且,帧和预览画面对不上 (seekTo 是以关键帧为基准的,当视频关键帧间隔较远的时候,会出现这样的情况)
  2. 使用 mediaExtractor.advance() 与 mediaExtractor.seekTo() 配合取帧,会发现有些帧取出来时间特别长,不流畅 (同样是视频关键帧间隔较远的时候,会出现这样的情况)
  3. 发现上述两个问题应该是和关键帧有关,突发奇想,使用 mediaExtractor.getSampleFlags() 来判断帧是不是关键帧,把所有的关键帧与需要的时间戳的帧都扔进解码器,这样确实效率高了很多,但也没有得到正确的结果,很多帧是花的。(解码不止需要关键帧,视频帧分为)

这三个问题都是由于对视频的帧间编码不够了解导致的,向增辉学习了很多下,然后深入了解了一下帧间编码,与解码原理之后,换了一个方案,就解决了这几个问题。之后又遇到了新的问题。

最终方案:关键是关键帧的间隔,如果视频源比较可靠,关键帧间隔比较小,并且取帧的间隔比较大,可以直接seek到目标时间取帧。

我们因为视频源不确定,而且取帧间隔特别小,这样的话会遇到上述的问题,所以我就把所有的帧全都扔给解码器,然后在输出短,通过 Image 的 pts 去过滤我要的帧,因为解码一帧的耗时比较小,编码 yuv 到 bitmap 的耗时可以省掉,性能还可以接受,但这个应该还是可以优化的点

  1. 在一些机型上,生成的 bitmap 色彩值不对,有虚影。(不同机型上 YUV 格式不同)
  2. libyuv 转换 yuv to bitmap 效率不高,耗时很大。 (libyuv 未开启 neno 指令集优化)

这两个问题,主要是对 YUV 数据格式不太了解,对 libyuv 的使用不够熟悉导致,在向老敏学习了很多下,然后深入了解了 YUV 格式与 libyuv 的使用,解决了这两个问题。

帧格式

I frame :帧内编码帧 又称 intra picture,I 帧通常是每个 GOP(MPEG 所使用的一种视频压缩技术)的第一个帧,经过适度地压缩,做为随机访问的参考点,可以当成图象。I帧可以看成是一个图像经过压缩后的产物。

P frame: 前向预测编码帧 又称 predictive-frame,通过充分将低于图像序列中前面已编码帧的时间冗余信息来压缩传输数据量的编码图像,也叫预测帧;

B frame: 双向预测内插编码帧 又称bi-directional interpolated prediction frame,既考虑与源图像序列前面已编码帧,也顾及源图像序列后面已编码帧之间的时间冗余信息来压缩传输数据量的编码图像,也叫双向预测帧;

两个I frame之间形成一个GOP,在x264中同时可以通过参数来设定bf的大小,即:I 和p或者两个P之间B的数量。

  1. I 帧自身可以通过视频解压算法解压成一张单独的完整视频画面
  2. P 帧需要参考前面一个 I 帧或者 P 帧来解码
  3. B 帧需要参考前一个 I 帧 或者 P 帧,以及后面一个 P 帧来解码

PTS:Presentation Time Stamp。PTS 显示时间戳

DTS:Decode Time Stamp。DTS 解码时间戳。

image.png

YUV 格式与 Image

Image类在API 19中引入,Image作为相机得到的原始帧数据的载体(Camera 2);硬件编解码的 MediaCodec类 加入了对Image和Image的封装ImageReader的全面支持。可以预见,Image将会用来统一Android内部混乱的中间图片数据(这里中间图片数据指如各式YUV格式数据,在处理过程中产生和销毁)管理。

每个Image当然有自己的格式,这个格式由ImageFormat确定。对于YUV420,ImageFormat在API 21中新加入了YUV_420_888类型,其表示YUV420格式的集合,888表示Y、U、V分量中每个颜色占8bit。s

YUV420格式分为 YUV420P 和 YUV420SP两种。
其中YUV420P格式,分为 I420 和 YV12 两种,YUV420SP格式分为 NV12 和 NV21 两种。他们的存储格式,区别是 Planar 的 uv 分量是平面型的,SemiPlanar 的 uv 分量是交织的。

  1. I420: YYYYYYYY UUVV => YUV420P (Android 格式)
  2. YV12: YYYYYYYY VVUU => YUV420P
  3. NV12: YYYYYYYY UVUV => YUV420SP
  4. NV21: YYYYYYYY VUVU => YUV420SP (Android 格式)

Image 中的 Y、U和V三个分量的数据分别保存在三个Plane类中,可以通过getPlanes()得到。Plane实际是对ByteBuffer的封装。Image保证了plane #0一定是Y,#1一定是U,#2一定是V。且对于plane #0,Y分量数据一定是连续存储的,中间不会有U或V数据穿插,也就是说我们一定能够一次性得到所有Y分量的值。

接下来看看U和V分量,我们考虑其中的两类格式:Planar,SemiPlanar。

Planar 下U和V分量是分开存放的,所以我们也应当能够一次性从plane #1和plane #2中获得所有的U和V分量值,事实也是如此。

而SemiPlanar,此格式下U和V分量交叉存储,Image 并没有为我们将U和V分量分离出来。

所以,看到这里,对于开发过程中遇到的问题已经有了答案,在一些机型上,生成的 bitmap 色彩值不对,有虚影。是因为,一些机型上,解码获得的 YUV 数据是交织的,也就是 NV21 格式的 YUV 数据,而在使用libyuv 对 yuv 数据进行处理获得 bitmap 对象的操作时,由于认为 Image 对象会完整的分离 uv 分量,所以并没有考虑到这个问题,导致对 NV21 的数据格式不能正确处理,导致了色彩值不对,并且有虚影。

优化方向

由于项目时间比较赶,留下了几个可以优化的点:

  1. 不同机型支持的 MediaCodec 实例数量不一样,目前是把解码器做成了单例,然后把需要解码的视频做成 List 传入,串行解码,后续可以尝试判断不同机型支持的实例数量,并行解码。
  2. 使用 libyuv 库 转换 YUV 到 bitmap 过程中,先把 NV21 格式转成了 I420,然后再进行了缩放、旋转的操作,可以更改接口,直接先用 NV21 缩放,然后再转换,效率上能够得到提升
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339