WebRTC 源码分析(四):VideoCRE 与内存抖动优化
WebRTC 是个宝,初窥这部分代码时就被它的 Capturer 类的设计惊艳到了,仔细品鉴后越发佩服起来,里面简直填了太多坑了,如此宝贝,如不能为我所用,岂非一大憾事!而前三篇的解读,正是为了今天能将其剥离出来所做的铺垫,现在就有请我们今天的主角——VideoCRE, Video Capture, Render and Encode——闪亮登场。
VideoCRE 结构
我们当然可以直接使用 Capturer/Renderer/Encoder,但如果能将它们进行一定的封装,让基本的需求实现起来更加简单,岂不妙哉。
下面介绍一下 VideoCRE 的结构:
- 视频数据由 VideoCapturer 采集,例如 Camera1Capturer;
- VideoCapturer、SurfaceTextureHelper 等由 VideoSource 类管理;
- VideoCapturer 采集到的数据会回调给 VideoCapturer.CapturerObserver,VideoSink 实现了该接口;
- VideoSink 会把数据发送给多个 VideoRenderer.Callbacks,例如 SurfaceViewRenderer 负责预览,HwAvcEncoder 负责视频编码;
- HwAvcEncoder 则会把编码后的数据发送给多个 MediaCodecCallback,例如由 Streamer 进行网络传输实现直播功能,Mp4Recorder 负责本地录制;
同一路视频数据可以被多路消费,例如预览、低码率编码、高码率编码,而同一路编码数据,也可以被多路消费,例如推流、存文件。
VideoCRE 使用
demo 工程里实现了高低码率两路本地 MP4 录制功能,下面我们看看如何一步步实现这个功能。
首先是配置参数,标清和高清:
VideoConfigconfig=VideoConfig.builder().previewWidth(1280).previewHeight(720).outputWidth(448).outputHeight(800).fps(30).outputBitrate(800).build();VideoConfighdConfig=VideoConfig.builder().previewWidth(1280).previewHeight(720).outputWidth(720).outputHeight(1280).fps(30).outputBitrate(2000).build();
接下来是创建 VideoCapturer:
VideoCapturercapturer=createVideoCapturer();privateVideoCapturercreateVideoCapturer(){switch(MainActivity.sVideoSource){caseVideoSource.SOURCE_CAMERA1:returnVideoCapturers.createCamera1Capturer(true);caseVideoSource.SOURCE_CAMERA2:returnVideoCapturers.createCamera2Capturer(this);default:returnnull;}}
准备 Renderer 和 Encoder:
mVideoView=(SurfaceViewRenderer)findViewById(R.id.mVideoView1);try{Stringfilename="video_source_record_"+System.currentTimeMillis();mMp4Recorder=newMp4Recorder(newFile(Environment.getExternalStorageDirectory(),filename+".mp4"));mHdMp4Recorder=newMp4Recorder(newFile(Environment.getExternalStorageDirectory(),filename+"-hd.mp4"));}catch(IOExceptione){e.printStackTrace();Toast.makeText(this,"start Mp4Recorder fail!",Toast.LENGTH_SHORT).show();finish();return;}mHwAvcEncoder=newHwAvcEncoder(config,mMp4Recorder);mHdHwAvcEncoder=newHwAvcEncoder(hdConfig,mHdMp4Recorder);
创建 VideoSource和VideoSink ,VideoSource 也需要视频配置,但只需要使用预览尺寸、帧率,所以用 config 或者 hdConfig 都可以:
mVideoSink=newVideoSink(mVideoView,mHwAvcEncoder,mHdHwAvcEncoder);mVideoSource=newVideoSource(getApplicationContext(),config,capturer,mVideoSink);
初始化:
mVideoView.init(mVideoSource.getRootEglBase().getEglBaseContext(),null);mHwAvcEncoder.start(mVideoSource.getRootEglBase());mHdHwAvcEncoder.start(mVideoSource.getRootEglBase());
开始采集、录制:
@OverrideprotectedvoidonStart(){super.onStart();mVideoSource.start();}
内存抖动优化
完成了 VideoCRE 的剥离后,我发现内存抖动非常严重,CPU 占用也很高:
排查内存抖动,当然首选 Allocation Tracker 了,结果如下:
这里我们可以看到,60% 的内存分配都发生在 BufferInfo 对象上,但这个对象非常小,只有几个 primitive 数据成员,怎么会出现这么多分配呢?我们看次数,15s 内发生了 2.6 万次,每毫秒分配了 1.7 次。看代码发现,是我在单独的线程调用 dequeueOutputBuffer 时传入的 timeout 为 0,所以在疯狂的创建 BufferInfo 对象。
单独的线程设置 timeout 为 0 显然不合理,除了这里的内存分配,CPU 占用也会更高,所以我们可以设置一个合适的值,这里我换成 3000,也就是 3ms,结果如下:
我们可以看到,内存抖动减缓了很多,但仍比较明显。CPU 占用率倒是下降很多了。
这时 Allocation Tracker 的结果如下:
优化性能切忌盲目,要找准瓶颈,并且测量对比成效。
我们发现最大的分配竟是由一条日志代码引起的!所以不要以为在日志工具函数内通过变量控制是否打日志就够了,即便日志最终没有打印出来,但拼接日志字符串就可能已经成为瓶颈。
除了日志,还存在两处很高的分配:allocateDirect 和 BufferInfo。
其实 BufferInfo 没必要每次创建,我们消费 MediaCodec 输出是单线程行为,只需要分配一次即可。同理,容纳输出数据的 buffer 也没必要每次分配,只有需要扩容时创建即可。
经过上述优化,内存抖动再次减弱:
分析 Allocation Tracker,较高的内存分配分别为:
- Display#getRotation:19.58%;
- 取到 MediaCodec 输出后,构造 ByteBuffer 对象的拷贝:17.31%;
- 帧数据传递过程中 matrix 数组分配:16.32%;
上面这几点都改了之后,再剩下的就是 I420Frame 的创建、日志字符串拼接、Runnable 对象创建了。此外还发现了一个注意点:使用 ExecutorService 时,每次 submit 任务,还会创建一个链表节点对象,而 Handler 会复用 Message 对象,所以我把 ExecutorService 换成了 HandlerThread + Handler 的组合。当然,for each 遍历每次都会创建一个 Iterator 对象,虽然没有成为瓶颈,但也确实可观,何况可以一行代码进行优化,顺手就给做了。
I420Frame 也许可以用对象池来优化,Runnable 则可以把局部变量成员化,但现在其实已经优化了很多(对比测试一分钟的内存分配从 2613976 优化到 240352,优化为了 9.2%),而这些做法需要比较复杂的处理才能确保不会发生消费者-生产者的竞争问题,所以就先告一段落啦!另外,这里并没有贴出具体优化代码,想看代码的朋友,可以查看 GitHub 仓库的这个 commit。
最后在 Nexus 5X 安卓 7.1.2 上测试发现,Camera2 采集时,存在大量的 Binder 通信,内存抖动严重得多,而其中 48.86% 都是由 Binder 通信导致的。
- Camera1 采集:一分钟内存增长 0.32MB;
- Camera2 采集:一分钟内存增长 3.13MB;
对此我就真是黔驴技穷了,只能寄希望于大谷歌了 :(
总结
至此,WebRTC(安卓流媒体)视频的前段就已经差不多了,我们了解了采集、预览、编码的大体实现思路,也详细分析了这些步骤里面可能遇到的坑,最后将这三块相关的代码剥离成为了一个可以单独使用的模块:VideoCRE,并对其运行过程中的内存抖动进行了极大的优化,一分钟内存分配优化为了 9.2%。
当然,流媒体前段还有不少内容没有涵盖:美颜、特效(结合人脸识别、场景分割)、更复杂的渲染……这些内容我还需要更深入的学习和理解,才敢分享,而流媒体的中段(传输)、后段(解码播放)则还有更多的内容等着我们
https://blog.piasy.com/2017/08/11/VideoCRE-and-Memory-Churn-Opt/
WebRTC 源码分析(四):VideoCRE 与内存抖动优化的更多相关文章
- WebRTC源码分析四:视频模块结构
转自:http://blog.csdn.net/neustar1/article/details/19492113 本文在上篇的基础上介绍WebRTC视频部分的模块结构,以进一步了解其实现框架,只有了 ...
- JVM源码分析之警惕存在内存泄漏风险的FinalReference(增强版)
概述 JAVA对象引用体系除了强引用之外,出于对性能.可扩展性等方面考虑还特地实现了四种其他引用:SoftReference.WeakReference.PhantomReference.FinalR ...
- JVM源码分析之堆外内存完全解读
JVM源码分析之堆外内存完全解读 寒泉子 2016-01-15 17:26:16 浏览6837 评论0 阿里技术协会 摘要: 概述 广义的堆外内存 说到堆外内存,那大家肯定想到堆内内存,这也是我们 ...
- 使用react全家桶制作博客后台管理系统 网站PWA升级 移动端常见问题处理 循序渐进学.Net Core Web Api开发系列【4】:前端访问WebApi [Abp 源码分析]四、模块配置 [Abp 源码分析]三、依赖注入
使用react全家桶制作博客后台管理系统 前面的话 笔者在做一个完整的博客上线项目,包括前台.后台.后端接口和服务器配置.本文将详细介绍使用react全家桶制作的博客后台管理系统 概述 该项目是基 ...
- WebRTC 源码分析(三):安卓视频硬编码
数据怎么送进编码器? 怎么从编码器取数据? 如何做流控? 在开始之前,我们先了解一下 MediaCodec 的基本知识. MediaCodec 基础 Developer 官网 上的描述已经很清楚了,下 ...
- Spark源码分析之九:内存管理模型
Spark是现在很流行的一个基于内存的分布式计算框架,既然是基于内存,那么自然而然的,内存的管理就是Spark存储管理的重中之重了.那么,Spark究竟采用什么样的内存管理模型呢?本文就为大家揭开Sp ...
- ABP源码分析四:Configuration
核心模块的配置 Configuration是ABP中设计比较巧妙的地方.其通过AbpStartupConfiguration,Castle的依赖注入,Dictionary对象和扩展方法很巧妙的实现了配 ...
- ABP源码分析四十七:ABP中的异常处理
ABP 中异常处理的思路是很清晰的.一共五种类型的异常类. AbpInitializationException用于封装ABP初始化过程中出现的异常,只要抛出AbpInitializationExce ...
- WebRTC源码分析(一):安卓相机采集实现分析
WebRTC 的代码量不小,一次性看明白不太现实,在本系列中,我将试图搞清楚三个问题: 客户端之间如何建立连接? 客户端之间如何实现数据传输? 音视频数据的采集.预览.编码.传输.解码.渲染完整流程. ...
随机推荐
- databus编译:Could not resolve all dependencies for configuration ':databus2-relay:databus2-event-producer-mock:compile
FAILURE: Build failed with an exception. * What went wrong: Could not resolve all dependencies for c ...
- 定期删除elasticsearch 的index 索引
#!/bin/bashfind /data/elasticsearch/data/pro-kz-log/nodes/0/indices/ -type d -mtime +7 | awk -F" ...
- Android 版本对于 API
Android版本 API 代号 官网链接 Android 2.3.3 API 10 Gingerbread 官网 Android 3.0 API 11 Android 3.1 API 12 Andr ...
- macbook中vagrant升级新版本
macbook由于缺少卸载机制,有时候不知道该如何升级软件. vagant的升级到时简单,经测试,只需直接官网下载新软件,安装即可. 旧版本不用管,新的会直接替换.
- 初学FPGA
刚开始感觉FPGA不过也就是和51,ARM单片机那样写写程序就完事了,现在看来根本不是那么回事.从夏宇闻老师的Verilog HDL,黑金教程开始学起,但是感觉看到黑金时序篇时感觉少点什么,原来是缺少 ...
- 【Unity】4.6 灯光
分类:Unity.C#.VS2015 创建日期:2016-04-11 一.简介 灯光(Light,也叫光源)是每一个场景的重要组成部分,用于照亮场景和对象,从而让游戏具有自己的个性和风格,比如利用灯光 ...
- 数据库的ACID
一.事务 定义:所谓事务,它是一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位. 准备工作:为了说明事务的ACID原理,我们使用银行账户及资金管理的案例进行分析. [sql] ...
- Splunk Web页面的登录密码忘记了怎么办
splunk的web登录密码忘记的话,可以使用以下方法重置. 一.关闭splunk服务 /opt/splunk/bin/splunk stop 二.删除默认密码配置文件 三.重启启动splunk服务, ...
- [Windows Azure] How to Create and Configure SQL Database
How to Create and Configure SQL Database In this topic, you'll step through logical server creation ...
- (原创)c++11改进我们的模式之改进观察者模式
和单例模式面临的是同样的问题,主题更新的接口难以统一,很难做出一个通用的观察者模式,还是用到可变模板参数解决这个问题,其次还用到了右值引用,避免多余的内存移动.c++11版本的观察者模式支持注册的观察 ...