zoukankan      html  css  js  c++  java
  • 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/

  • 相关阅读:
    centos7安装KVM
    keepalived高可用
    Jenkins-Pipeline 流水线发布部署项目
    centos7部署jenkins
    版本控制gitlab
    c语言寻找3000以内亲密数对
    c语言寻找1000以内的完全数
    c语言分解因式
    c语言判断给定日期是当年的第几天
    c语言计算程序运行时间
  • 原文地址:https://www.cnblogs.com/elesos/p/9511007.html
Copyright © 2011-2022 走看看