阅读 75

多媒体性能优化(一)-减少丢帧

本文将以一个视频播放的丢帧问题为例,介绍在解决这一性能问题时的思路、所使用的工具、尝试的优化方案和一些测试的结果,具有较强的综合性。当然,我本人在性能优化方面也只是新手,欢迎大家一起讨论交流。

问题

ExoPlayer 是google推出的一款开源java播放器,包括youtube在内的很多视频服务商都在使用它,但是我们发现,在一些性能比较低的硬件平台上,使用exoplayer播放4k HLS视频时会出现大片的丢帧,视频画面也随之变得“一卡一卡的”。

假设

和其他应用不同的是,4k视频的音视频解码会消耗大量的CPU资源,同时这一工作对解码的速度是有硬要求的,如果达不到的话就会产生播放流畅度的问题。所以丢帧的原因很可能是CPU满负载,使得音视频没有足够的资源保持流畅播放。 refer to: 《Android移动性能实战》第4章

验证工具

为了验证上面提出的假设, 这里主要使用了三个工具 :

  1. DS5 & Streamline

arm平台常用的性能分析工具, 基本的介绍和安装流程在网上都能搜到,例如搭建ARM DS-5 STREAMLINE

  1. Systrace & TraceView

这两个是可以直接在android studio配套的device monitor中使用的性能分析工具, 前面的ds5 & streamline虽然强大, 却不能用于分析java代码的性能, 这里就是主要利用systrace和traceview来分析java代码的性能, 关于这两个工具的使用方法, 可以参考下面的链接Analyzing UI Performance with Systrace 和 Profiling with Traceview and dmtracedump

  1. Bento4工具集

利用bento4工具集, 可以自制一些hls或者dash测试流, 同时bento4还支持最新的hls-fmp4

验证结果

利用streamline在播放4k视频的同时抓取系统信息, 可以发现播放器进程中的loader:hls线程存在burst现象, 具体来说, cpu占比(其中两个核)可以达到60%~80%, 如下图所示这里写图片描述

同时, 通过对比实时抓取的log, 也可以看到发生burst的时候一定伴随着大量的丢帧. 由此, 可以确定, loader:hls对cpu资源的大量占用是播放4k视频卡顿的一个很重要的原因.

调查与分析


一,
首先阅读exoplayer的代码, 可以看到loader:hls线程就是在播放器后台进行m3u8和ts切片下载的线程, 既然如此, 就很容易联想到是下载中的IO操作占用了cpu资源, 分析一下播放过程中几个关键的io操作, 或者说涉及到buffer的操作, 可以简单表示为: cdn --> buffer --> player --> 播放器缓存buffer -->  解码器buffer --> decoder 从这个流程来说没什么可以优化的点,看来要从别的地方优化。


二, 详细来看loader:hls部分的代码,发现了一个很可疑的循环

in TsChunk.java@load()
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
    result = extractorWrapper.read(input);
    ...
}复制代码

这里的read方法会调用 TsExtractor.java@read, 如下

public int read(ExtractorInput input, PositionHolder seekPosition)
throws IOException, InterruptedException {
if (!input.readFully(tsPacketBuffer.data, 0, TS_PACKET_SIZE, true)) {
return RESULT_END_OF_INPUT;
  }
  // parse a packet
  .....
  return RESULT_CONTINUE;
}复制代码

上面的readfully方法负责下载工作, 可以看到每次以TS_PACKET_SIZE=188为单位进行下载, 在read方法中接下来再对下载得到的packet进行解封装操作, 由此可以确认, 当有burst时,一定也有ts的下载和parser的频繁循环调用, 循环次数可以用 bitrate*segDuration / (188 * 8) 来计算, 当码率约为8Mbps, 分片时长10s的时候, 循环次数可以达到50000次, 而实际中4k视频的码率并非固定, 低的时候可以仅有7Mbps, 高的时候可以达到16Mbps,这样一来循环次数就非常可观了。

由此可以做出初步的推断:因为在loader线程里面还同时进行了解封装操作,而TS格式的解封装相对fmp4等其他格式来说更加复杂,再加上每次只处理188字节,在播放高码率片源时就更加占用资源.有的朋友可能要问了,那为啥一定要把下载和解封装一起做呢?拆开不行吗?这里有两个考虑:一是如果先下载完毕再解封装的话,势必会影响起播速度;二是很多ts码流的音视频交织做的非常差,为了更好地音视频同步,很有必要提前做解封装。

下面使用traceview来判断到底是download还是parse消耗了更高的cpu, 从traceview得到的结果如下图所示这里写图片描述

图中DefaultExtractorInput.read代表了download操作, PesReader.consume代表了parse操作, 可以看到还是download操作占用了更高的cpu.


三, 作为对比, 使用bento4工具制作了相同码率, 相同分片时长的hls-ts片源和hls-fmp4片源 使用streamline抓取系统信息, 播放ts流时结果如下图所示这里写图片描述

而播放fmp4流时的结果如下图所示这里写图片描述

对比download和parse的循环调用次数, ts流是fmp4流的1000倍结论:  loader:hls线程中伴随频繁download的io操作是cpu占用高的主要原因.

优化方案与结果

本节将介绍所尝试的几种优化方案和相应的结果,测试时考虑了限速和不限速的场景,主要观察的指标有起播时间,卡顿比,丢帧数,CPU占用


一, 降低loader:hls线程的优先级 简单粗暴,直接降低loader线程优先级,相当于提高了解码优先级,保证解码可以流畅完成工作,从而减少丢帧。修改起来也很简单,在loader线程里面加上一句

Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);复制代码

经过测试,发现可以减少一半的丢帧,经过长时间压测,也没发现对起播时间或卡顿比有影响。 再来看看cpu占用方面的变化,修改后这里写图片描述可以看到,loader仍然会有burst,峰值CPU占比仍然可达80%以上,但是有一部分被"打散",峰值持续时间减少,测试发现大约能减少1/3的burst结论: 通过降低loader线程优先级, 可以将cpu占用的burst打散, 减少丢帧数目,未发现对起播时间和loading圈的影响。


二,在loader:hls线程的频繁调用中间强行sleep 与方案一降低线程优先级的思路类似,通过在前面提到的while loop中强行sleep,来空出CPU资源,但是这样很有可能会导致下载ts分片的时间变长,并引发loading圈或增加起播时间,为此,可以设计基于bufferedDuration的动态sleep机制 而具体的sleep逻辑,如下面代码所示

int i = 0;
try {
    int result = Extractor.RESULT_CONTINUE;
    while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
    i++;
    if (i > DEFAULT_EXTRACTOR_READ_INTERVAL && isHighBuffer) {
        i = 0;
        Thread.sleep(DEFAULT_SLEEP_TIME_MS);
    }
    result = extractorWrapper.read(input);
    ...
}复制代码

代码中DEFAULT_EXTRACTOR_READ_INTERVAL表示while loop循环多少次之后进行sleep,DEFAULT_SLEEP_TIME_MS表示每次sleep的时长,设置为10ms, isHighBuffer根据设定的阈值判断现在的缓冲区余量是否充足。 经过测试,同样能降低一半以上的丢帧,但是因为是强行sleep,会增加loading圈出现的次数,通过合理设置buffer阈值可以改善。 CPU占用方面倒没有方案一改善明显,依然存在burst。 其实对于方案一的降低线程优先级,也可以改成根据bufferedDuration来动态修改的逻辑。


三,一次下载多个ts packet 可以一次下载多个ts packet,让io操作不那么频繁,当然,解封装的时候还是只能以单个ts packet为单位进行。做一个极端的测试,使用自制hls流测试,每次下载500个ts packet,使用streamline查看系统信息,如下图这里写图片描述在一定时间段内能降低cpu占用,但是loading圈出现次数明显变多,可以通过更合理的设置一次性的下载数来改善。

以上就是对丢帧这一性能问题从假设验证到提出方案并测试的全过程了。


更新

关于这个问题,我们有和google展开讨论,详见github.com/google/ExoP…这里可以贴出google的一个解释,供参考

Do think that at the root of these HLS cpu peaks there is a more generic choice in the way the default exoplayer data loading operates. It works in a peak pattern that tries to fetch data as quickly as possible with reltatively large intervals (15s). And thus you get these kind of cpu utilization peaks at given intervals for Ts based HLS.

The cpu utilization side effect of the default loader choice grows with the bitrate as as the loader works in the time domain only. So for a 30mbit stream by default it's trying to load and parse 15 seconds of data (56mb) as quick as possible. Ironically this also means that the higher you're connection speed to the server, the more peak utilization the player will put on one of the cpu's. Which in turn can cause scheduling issues on the system the player runs on.


作者:zhanghui_cuc
链接:https://juejin.cn/post/7027297884869492750

文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐