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 与内存抖动优化的更多相关文章

  1. WebRTC源码分析四:视频模块结构

    转自:http://blog.csdn.net/neustar1/article/details/19492113 本文在上篇的基础上介绍WebRTC视频部分的模块结构,以进一步了解其实现框架,只有了 ...

  2. JVM源码分析之警惕存在内存泄漏风险的FinalReference(增强版)

    概述 JAVA对象引用体系除了强引用之外,出于对性能.可扩展性等方面考虑还特地实现了四种其他引用:SoftReference.WeakReference.PhantomReference.FinalR ...

  3. JVM源码分析之堆外内存完全解读

    JVM源码分析之堆外内存完全解读   寒泉子 2016-01-15 17:26:16 浏览6837 评论0 阿里技术协会 摘要: 概述 广义的堆外内存 说到堆外内存,那大家肯定想到堆内内存,这也是我们 ...

  4. 使用react全家桶制作博客后台管理系统 网站PWA升级 移动端常见问题处理 循序渐进学.Net Core Web Api开发系列【4】:前端访问WebApi [Abp 源码分析]四、模块配置 [Abp 源码分析]三、依赖注入

    使用react全家桶制作博客后台管理系统   前面的话 笔者在做一个完整的博客上线项目,包括前台.后台.后端接口和服务器配置.本文将详细介绍使用react全家桶制作的博客后台管理系统 概述 该项目是基 ...

  5. WebRTC 源码分析(三):安卓视频硬编码

    数据怎么送进编码器? 怎么从编码器取数据? 如何做流控? 在开始之前,我们先了解一下 MediaCodec 的基本知识. MediaCodec 基础 Developer 官网 上的描述已经很清楚了,下 ...

  6. Spark源码分析之九:内存管理模型

    Spark是现在很流行的一个基于内存的分布式计算框架,既然是基于内存,那么自然而然的,内存的管理就是Spark存储管理的重中之重了.那么,Spark究竟采用什么样的内存管理模型呢?本文就为大家揭开Sp ...

  7. ABP源码分析四:Configuration

    核心模块的配置 Configuration是ABP中设计比较巧妙的地方.其通过AbpStartupConfiguration,Castle的依赖注入,Dictionary对象和扩展方法很巧妙的实现了配 ...

  8. ABP源码分析四十七:ABP中的异常处理

    ABP 中异常处理的思路是很清晰的.一共五种类型的异常类. AbpInitializationException用于封装ABP初始化过程中出现的异常,只要抛出AbpInitializationExce ...

  9. WebRTC源码分析(一):安卓相机采集实现分析

    WebRTC 的代码量不小,一次性看明白不太现实,在本系列中,我将试图搞清楚三个问题: 客户端之间如何建立连接? 客户端之间如何实现数据传输? 音视频数据的采集.预览.编码.传输.解码.渲染完整流程. ...

随机推荐

  1. 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 ...

  2. 定期删除elasticsearch 的index 索引

    #!/bin/bashfind /data/elasticsearch/data/pro-kz-log/nodes/0/indices/ -type d -mtime +7 | awk -F" ...

  3. Android 版本对于 API

    Android版本 API 代号 官网链接 Android 2.3.3 API 10 Gingerbread 官网 Android 3.0 API 11 Android 3.1 API 12 Andr ...

  4. macbook中vagrant升级新版本

    macbook由于缺少卸载机制,有时候不知道该如何升级软件. vagant的升级到时简单,经测试,只需直接官网下载新软件,安装即可. 旧版本不用管,新的会直接替换.

  5. 初学FPGA

    刚开始感觉FPGA不过也就是和51,ARM单片机那样写写程序就完事了,现在看来根本不是那么回事.从夏宇闻老师的Verilog HDL,黑金教程开始学起,但是感觉看到黑金时序篇时感觉少点什么,原来是缺少 ...

  6. 【Unity】4.6 灯光

    分类:Unity.C#.VS2015 创建日期:2016-04-11 一.简介 灯光(Light,也叫光源)是每一个场景的重要组成部分,用于照亮场景和对象,从而让游戏具有自己的个性和风格,比如利用灯光 ...

  7. 数据库的ACID

    一.事务 定义:所谓事务,它是一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位. 准备工作:为了说明事务的ACID原理,我们使用银行账户及资金管理的案例进行分析. [sql] ...

  8. Splunk Web页面的登录密码忘记了怎么办

    splunk的web登录密码忘记的话,可以使用以下方法重置. 一.关闭splunk服务 /opt/splunk/bin/splunk stop 二.删除默认密码配置文件 三.重启启动splunk服务, ...

  9. [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 ...

  10. (原创)c++11改进我们的模式之改进观察者模式

    和单例模式面临的是同样的问题,主题更新的接口难以统一,很难做出一个通用的观察者模式,还是用到可变模板参数解决这个问题,其次还用到了右值引用,避免多余的内存移动.c++11版本的观察者模式支持注册的观察 ...