一、背景介绍
随着4G网络的推广和网络带宽的提升,视频成为互联网用户主要的消费载体,用户通过短视频来分享和浏览信息。由此视频的编辑功能越来越重要、越来越普遍。视频编辑的App也如雨后春笋般涌现。
为更好地推动得物App社区业务的发展,得物也自研符合得物需求的视频编辑工具。我们致力于打造一个“更快、更强”的视频编辑工具。
二、视频编辑工具介绍
为了让大家更好地了解得物App的视频编辑工具,我们先简单介绍一下视频编辑工具的主要功能。
下面是得物App视频编辑工具的主要功能:
视频编辑工具的重点如下:
- 视频编辑工具需要操作的资源:
- 文字:包括普通的文字、特殊的艺术字、花字等等;
- 图片:包括静态图,如JPEG/PNG等等,也包括HEIC/GIF等动态图;
- 视频:包括各种各样的视频(各种编码和封装格式),主流的格式一般是MP4的封装格式、H264视频编码格式、AAC音频编码格式等等;
- 音频:包括各种各样的音频(各种编码和封装格式),当然视频当然也是包含音频轨道的。
- 视频编辑工具主要的操作方式:
- 操作图片、视频帧:我们知道视频是一帧一帧的图片组成的,所以操作视频帧和操作图片是一样的道理,我们通过添加一些特效在图片和视频帧上面,实现一些有趣的效果来吸引用户。
- 操作音频:主流的操作音频方式如倍速、调整音量、变调等等,都是现今短视频的主要玩法。
- 视频编辑工具最终生成的是一个新的视频,这个视频将特定的资源应用一些特效生成一个新的视频。
下面的流程图可以很方便地让大家了解视频编辑的工作流程。为了方便,我们输入一个视频,加上一些特效,生成一个新的视频。
从上面的流程可以看出来,原始视频A.mp4经过解封装分离出音频轨道和视频轨道,对它们解码之后,对音频数据应用音频特效、对视频帧数据应用视频特效,然后编码封装合成一个新的视频。当然解码和编码都是有一个队列控制的,流程图上标注了,没有深入展开,大家了解即可。
经过上面的介绍,大家对视频编辑工具有了大概得了解,其实衡量一个视频编辑工具做得好不好,主要从下面这几个方面着手:
- 内存占用情况
- 导出视频的速度如何
- 导出视频的清晰度如何
下面从这三方面详细展开给大家阐述得物App的视频编辑工具优化的心路历程。
三、内存优化
性能是所有程序好不好的首要指标,一个工具即使功能再强大,但是一点就崩溃,或者用着用着内存暴涨、应用卡死,估计这个应用不能称为一个优秀的应用,下面我们具体谈一谈视频编辑工具的优化检测方案。
优化内存从良好的编码习惯开始,尤其对音视频这种对内存需求非常高的应用而言。例如一个1080 1920的视频,解码出来原始数据一帧图片大小也是1080 1920,占用内存是1080 1920 (8 * 3 ) / 8 = 5.93 MB,一个视频帧就占用这么大,1秒一般有30帧,那得占用177.9MB,如果不加控制,那不管多高性能的手机也经不住这样的折腾。希望下面的内存检测和优化方案可以给你带来一些帮助。
3.1 合理设计队列
上面我们在介绍视频编辑流程的视频谈到了解码队列和编码队列的概念。其实队列这个概念在音视频中使用非常频繁,正是因为内存的限制,所以才引入队列这个控制方式。大家可能还有点懵,但是看完下面的流程图,我相信你一定会豁然开朗。
我们仅选取解码的部分来分析一下队列的重要应用。
在视频编辑工具中有几个重要的队列:
- 解码过程中:
- Video Packet Queue:视频解码之前Packet存放的队列,一般建议的队列大小是100
- Audio Packet Queue:音频解码之前Packet存放的队列,一般建议的队列大小是150
- Video Frame Queue:视频解码之后Frame存放的队列,一般建议的队列大小是3
- Audio Frame Queue:音频解码之后Frame存放的队列,一般建议的队列大小是8
- 编码过程中:
- Encode Video Packet Queue:视频编码之后Packet存放的队列,一般建议的大小是100
- Encode Audio Packet Queue:音频编码之后的Packet存放的队列,一般建议的大小是150
按照上面的方式设计队列的大小,可以在保证功能正常的情况下最大程度的降低内存占用,提升用户体验。
3.2 排查内存泄漏
Android上排查内存泄漏的方式有很多,这里介绍两种:
- Asan检测
- Profile检测
Asan全称是AddressSanitizer是一种基于编译器的快速检测的工具,用于检测原生代码中的内存错误问题,Asan可以解决如下四种核心问题:
- 堆栈和堆缓冲区上溢、下溢
- 释放之后堆重新使用问题
- 超过范围的堆栈使用情况
- 重复释放、错误释放问题
Asan的使用方式建议参考google官方文档,这儿就不多作介绍了:
https://github.com/google/sanitizers/wiki/AddressSanitizer
关于Profile的使用,如果需要检测Native内存使用情况,需要满足API>=29,大家在使用的时候需要非常注意。
下面是我们在demo中应用Asan抓取的堆栈:
代码语言:txt复制20042-20042/? A/DEBUG: *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
20042-20042/? A/DEBUG: Build fingerprint: 'samsung/t2qzcx/t2q:11/RP1A.200720.012/G9960ZCU2AUGE:user/release-keys'
20042-20042/? A/DEBUG: Revision: '13'
20042-20042/? A/DEBUG: ABI: 'arm64'
20042-20042/? A/DEBUG: Timestamp: 2021-09-17 00:32:31 0800
20042-20042/? A/DEBUG: pid: 19946, tid: 20011, name: AudioTrack >>> com.jeffmony.audioplayer <<<
20042-20042/? A/DEBUG: uid: 10350
20042-20042/? A/DEBUG: signal 6 (SIGABRT), code -1 (SI_QUEUE), fault addr --------
2021-09-17 00:32:31.157 20042-20042/? A/DEBUG: Abort message: '=================================================================
==19946==ERROR: AddressSanitizer: heap-use-after-free on address 0x004ac1e41080 at pc 0x007157f69580 bp 0x00705c0bb350 sp 0x00705c0bab08
READ of size 1792 at 0x004ac1e41080 thread T32 (AudioTrack)
#0 0x7157f6957c (/data/app/~~G094WKQQj7KZvdhvGYDLDA==/com.jeffmony.audioplayer-kcu1nmgzpBIQDRJDxCJDOQ==/lib/arm64/libclang_rt.asan-aarch64-android.so 0x9f57c)
#1 0x706549c228 (/data/app/~~G094WKQQj7KZvdhvGYDLDA==/com.jeffmony.audioplayer-kcu1nmgzpBIQDRJDxCJDOQ==/lib/arm64/libltpaudio.so 0x14228)
#2 0x706549bcd4 (/data/app/~~G094WKQQj7KZvdhvGYDLDA==/com.jeffmony.audioplayer-kcu1nmgzpBIQDRJDxCJDOQ==/lib/arm64/libltpaudio.so 0x13cd4)
#3 0x70654994f0 (/data/app/~~G094WKQQj7KZvdhvGYDLDA==/com.jeffmony.audioplayer-kcu1nmgzpBIQDRJDxCJDOQ==/lib/arm64/libltpaudio.so 0x114f0)
#4 0x70654a9cbc (/data/app/~~G094WKQQj7KZvdhvGYDLDA==/com.jeffmony.audioplayer-kcu1nmgzpBIQDRJDxCJDOQ==/lib/arm64/libltpaudio.so 0x21cbc)
#5 0x70654a91d4 (/data/app/~~G094WKQQj7KZvdhvGYDLDA==/com.jeffmony.audioplayer-kcu1nmgzpBIQDRJDxCJDOQ==/lib/arm64/libltpaudio.so 0x211d4)
#6 0x715af9d188 (/system/lib64/libwilhelm.so 0x1c188)
#7 0x71570ea290 (/system/lib64/libaudioclient.so 0x8b290)
#8 0x71570e9480 (/system/lib64/libaudioclient.so 0x8a480)
#9 0x7156b664d4 (/system/lib64/libutils.so 0x154d4)
#10 0x71593e9974 (/system/lib64/libandroid_runtime.so 0xa5974)
#11 0x7156b65db0 (/system/lib64/libutils.so 0x14db0)
#12 0x7156ace234 (/apex/com.android.runtime/lib64/bionic/libc.so 0xb6234)
#13 0x7156a68e64 (/apex/com.android.runtime/lib64/bionic/libc.so 0x50e64)
0x004ac1e41080 is located 0 bytes inside of 1792-byte region [0x004ac1e41080,0x004ac1e41780) freed by thread T32 (AudioTrack) here: #0 0x7157f74c64 (/data/app/~~G094WKQQj7KZvdhvGYDLDA==/com.jeffmony.audioplayer-kcu1nmgzpBIQDRJDxCJDOQ==/lib/arm64/libclang_rt.asan-aarch64-android.so 0xaac64) #1 0x70654a6d2c (/data/app/~~G094WKQQj7KZvdhvGYDLDA==/com.jeffmony.audioplayer-kcu1nmgzpBIQDRJDxCJDOQ==/lib/arm64/libltpaudio.so 0x1ed2c) #2 0x70654a6af0 (/data/app/~~G094WKQQj7KZvdhvGYDLDA==/com.jeffmony.audioplayer-kcu1nmgzpBIQDRJDxCJDOQ==/lib/arm64/libltpaudio.so 0x1eaf0) #3 0x706549bf4c (/data/app/~~G094WKQQj7KZvdhvGYDLDA==/com.jeffmony.audioplayer-kcu1nmgzpBIQDRJDxCJDOQ==/lib/arm64/libltpaudio.so 0x13f4c) #4 0x706549bcd4 (/data/app/~~G094WKQQj7KZvdhvGYDLDA==/com.jeffmony.audioplayer-kcu1nmgzpBIQDRJDxCJDOQ==/lib/arm64/libltpaudio.so 0x13cd4) #5 0x70654994f0 (/data/app/~~G094WKQQj7KZvdhvGYDLDA==/com.jeffmony.audioplayer-kcu1nmgzpBIQDRJDxCJDOQ==/lib/arm64/libltpaudio.so 0x114f0) #6 0x70654a9cbc (/data/app/~~G094WKQQj7KZvdhvGYDLDA==/com.jeffmony.audioplayer-kcu1nmgzpBIQDRJDxCJDOQ==/lib/arm64/libltpaudio.so 0x21cbc) #7 0x70654a91d4 (/data/app/~~G094WKQQj7KZvdhvGYDLDA==/com.jeffmony.audioplayer-kcu1nmgzpBIQDRJDxCJDOQ==/lib/arm64/libltpaudio.so 0x211d4) #8 0x715af9d188 (/system/lib64/libwilhelm.so 0x1c188) #9 0x71570ea290 (/system/lib64/libaudioclient.so 0x8b290)
显示message是:heap-use-after-free on address 0x004ac1e41080 说明是使用了已经释放掉的内存了,再继续看,这个内存具体在什么地方被释放的?0x004ac1e41080 is located 0 bytes inside of 1792-byte region [0x004ac1e41080,0x004ac1e41780) Asan一个很大的优势就是可以追踪内存释放的路径,防止出现内存泄漏和野指针问题,特别是野指针,一旦出现特别难排查,简直是C 开发的噩梦,希望大家用好工具,同时培养良好的C 编码习惯。
3.3 优化线程
另一个影响内存的重要因素是线程,视频编辑工具涉及到的线程非常多,线程的使用得遵循一些基本的原则:
- 尽量少创建线程
- 尽量少使用pthread_mutex_t
- 本着功能隔绝原则使用线程
- 能同步就别异步
以编辑模块为例,这儿列一下我们使用到的所有线程:
- GL处理线程
- 视频解封装线程
- 视频中视频轨道解码线程
- 视频音频轨道解码线程
- 抽取缩略图线程
- 音频编码线程
- 视频编码线程
- 视频封装线程
如果插入了独立的音频文件,还需要添加两个额外的线程:
- 音乐文件播放线程
- 音乐文件解码线程
上面列出的是一个视频编辑工具能正常工作所必备的最少线程,如果你的视频编辑工具中多了什么线程,我们建议可以适当优化一下,毕竟少一个线程,可以少一分开销,而且少一分线程同步的工作。
我们在底层也按照Android的消息机制重写了一套C 层的消息分发SDK,这个我们后续会另外分享文章阐释我们定制的消息分发SDK,这儿点到为止。
四、提升导出视频的速度
我们使用视频编辑工具,最终是希望导出一个视频,如果这个导出的过程很慢,那肯定是无法忍受的,从上面的介绍我们已知视频的导出需要经过“解码——应用特效——编码”的过程,其中解码和编码这两个过程对速度的影响至关重要。因为解码和编码视频需要耗费大量的资源,目前主要有两种方式——“软解/编码”和“硬解/编码”。
如果你使用过FFmpeg或者其他使用CPU进行视频编解码的来处理视频的话,你可能已经遇到了处理速度慢的问题。这主要是因为软编码和软解码使用CPU进行运算,而CPU在处理视频上的速度远低于DSP芯片;简而言之“软解/编码”主要通过CPU来工作,通过CPU来主导大量的计算工作,是原始的处理方式,当然耗费的时间也比较长;“硬解/编码”是通过GPU来处理,GPU是专用的图形处理芯片,对视频的解码和编码有专门的优化,所以编码和解码的速度非常快。
Android上使用MediaCodec来实现“硬解/编码”,iOS上使用VideoToolBox来实现“硬解/编码”,这里着重介绍Android上编码解码的速度优化。
从上面的流程我们可以看出,编码在解码的后面,一个时长60s(30fps)的视频,需要解码1800帧,然后编码1800帧视频才能完整生成另外一个视频,这样串行的等待是耗时的主要原因。
这时候我们参考多线程方案,将一个60s的视频均分为两段,然后这两段视频同时进行解码操作,生成导出了两个30s的临时缓存视频文件,随后将这两个30s的视频合并为一个60s的B.mp4视频,最后删除临时缓存文件,这样我们只需要同时处理900帧的数据,理论上可以提升一倍的导出速度。
这就是并行导出,下面是得物App并行导出的基本流程。
首先我们要明确导出视频是需要消耗资源的,这个资源就是MediaCodec,最终是送入到GPU中处理,一个手机中的MediaCodec实例是有限的,正常情况下,一个手机可以提供的MediaCodec实例最多有16个,如果当前使用的MediaCodec实例超过16个,那么手机将无法正常工作。MediaCodec资源是手机中的所有App共同持有。所以并行分段的个数不是越多越好。
- 只有一段,需要两个MediaCodec(一个用来解码视频,一个用来编码视频),注意:音频的解码和编码可以不要用MediaCodec,毕竟音频的耗时少多了,不是瓶颈。
- 分成两段需要四个MediaCodec,分成三段需要六个MediaCodec,分成四段需要八个MediaCodec,以此类推。
下面是并行导出的测试结果:
两段并行速度提升50% ~ 70%,内存增加20%, 三段并行速度提升60% ~ 90%,内存增加80%;并行超过三段的话就无法明显提升速度了。我们比较建议并行两段,在一些性能很好的机型上并行三段。
如果有些同学对视频导出过程中文件操作还有疑问的,下面的示意图可以比较清楚地看出并行导出操作本地文件的过程:
- 并行导出的过程中,生成了两个临时文件
- 并行导出完成后,这两个临时文件合并为一个新的文件,两个临时生成的文件被删除了(节省用户宝贵的存储空间)
- 原始文件jeffmony_out.mp4并没有被删除/修改
Tips:目前我们在处理过程中生成的临时文件和最终的适配文件都会保存在/sdcard/Pictures/duapp/Compile/下,而在处理完成后的临时文件清理过程会触发在某些机型上的保护机制,建议后续调整到App的私有目录下。
当然还有其他的提升导出速度的建议,例如在视频帧特效处理的过程中,我们建议:
- 尽量采用FBO/EBO/ABO方式处理texture
- 纹理如果过大要进行压缩
- 严禁采用glFinish()
这些做法都是我们在视频编辑开发过程中的切实经验,希望能给大家带来一些帮助。
五、提升导出视频的清晰度
一个视频编辑功能是否足够优秀,其中的一个重要指标就是同等条件下导出的视频是否足够清楚,通常而言,衡量视频是否清晰的有两种方式:
- 主观标准:找一些用户观看不同的视频,根据用户的观感输出视频清晰度的对比结果,用户一般根据色彩、画面亮度、柔和度等来评估清晰度。
- 客观标准:利用算法计算视频画面质量分,目前比较推荐Netflix推出的开源库VMAF来计算视频帧的质量分。
实际上主观标准是比较准确的,但是可操作性比较差,特别是处理海量视频的时候,需要大量的人力,无法有效开展,因此日常工作中还是推荐客观标准进行海量计算,主观标准进行重点判断。具体的可以结合业务的重要程度来开展。
下面结合我们实际的工作给出具体提升视频清晰度的方式:
- 视频基础编码信息优化
- Profile优化:Profile有三种Level,分别是Baseline、Main、High,其中Baseline Profile对应清晰度最低,Android 3.0之后的版本都支持的,Main Profile清晰度比Baseline Profile清晰度要好,但是从Android 7.0之后才支持,High Profile清晰度最高,也是从Android 7.0之后才支持。我们在设置Encoder Profile Level之前,需要判断一下当前是否支持。
- Bitrate码率设置: 视频码率是视频数据传输时单位时间内传送的数据位数。单位是kbps,望文生义,码率越大,单位时间填充的数据就越多,视频质量就越高。但码率也不是设置的越大越好,超过必要限度,对视频画质的提升已不明显,建议采用合适的factor来调整码率。Bitrate = width height frameRate * factor,其中factor=0.15。
- Bitrate Mode: 有三种通过的编码模式——VBR(可变码率)、CBR(固定码率)、ABR(平均码率),其中ABR是最好的方式,可以兼顾质量和视频大小。
- B帧设置: 视频有I帧、P帧、B帧构成,其中I帧最大,P帧次之,B帧最小,我们在编码时尽量多设置B帧(在合理的范围内),并不会降低清晰度,但是可以大大降低视频的大小,这样我们就可以相应地调大码率,最终实现了提升清晰度的目标。
- HEVC编码优化: 使用HEVC编码,可以保证在不增加文件大小的情况下,大大提升视频的清晰度。在相同的图像质量下,HEVC编码的视频比H.264编码的视频约减少40%
- 色彩调优
- 综合调整亮度、对比度、色温、饱和度、锐度等颜色参数,进而优化整体的视频画面,让视频画面看上去“更清晰”。
- 超分算法: 采用ESRGAN算法,利用机器学习的优势对图片和视频进行去模糊、Resize、降噪、锐化等处理,重建图片,实现对图片的超分辨率处理。
- 特征提取:计算噪点
- 非线性映射:放大,模糊化噪点
- 图像重建:差分,平滑过度,去噪
- 下面是使用超分算法处理前后的对比图,可以很明显地看出右边的图更加清晰,少了很多噪点、图片更亮、过度更平滑。
如果大家想了解视频清晰度优化的技术细节,可以参考文章--视频清晰度优化指南
六、总结
本文开篇从介绍得物App的主要功能展开,提出了视频编辑工具优化的三个维度:
- 优化内存占用
- 提升视频导出速度
- 提升导出视频的清晰度
其中在“提升视频导出速度”时重点谈到了“并行导出”的技术方案,从最终的结果来看,视频导出速度的提升非常明显,同时也非常清楚地解释了“并行导出”过程中为什么生成临时文件?为什么有必要在导出完成之后删除临时文件?尽力给用户带来较好的体验。
最后在“提升导出视频的清晰度”中重点提到的超分算法应用效果提升明显,超分之后的视频帧相比原帧图更加清晰、噪点更少,而且细节部分更加真实。
后续我们还会结合AR特效输出更多有意义的技术分享,敬请期待。
*文 /Jeff Mony
关注得物技术,每周一三五晚18:30更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~