短视频多清晰度调研

1,837 阅读10分钟

1. 什么是自适应码流

image.png

自适应码流,是一种将视频内容制作成多种分辨率版本,然后终端播放器自动选择版本播放的技术。

如图所示,内容服务器在提供视频内容之前,预先将视频转成了流畅、标清、高清等多种分辨率的版本。播放器播放视频时,将首先播放分辨率最低的视频,随后播放器根据当前网络的带宽情况,在播放过程中切换到其他分辨率的版本,当网络带宽充足时使用高分辨率版本,而当网络较差时,使用低分辨率的版本。

自适应码流技术的应用,为视频播放带来了如下高质量的体验:

  1. 秒开:视频从低分辨率的视频开始播放,因此加载出首帧画面消耗的时间较少,达到秒开;
  2. 高清:自适应码流通常包含不同分辨率的规格,在网络条件好的播放器会选择清晰度更高的规格播放。
  3. 无卡顿:播放器会根据网络条件,向上或向下切换不同清晰度的规格,防止弱网络环境播放高清视频产生的卡顿;同时,不同分辨率规格的视频因为做了 IDR 帧对齐,切换过程中也不会产生卡顿。

2. 自适应码流技术分析

自适应码流技术,关键主要在于两点:

1. 如何描述一个视频有哪些分辨率的版本,每种版本所需要的网络条件是什么;
2. 播放器如何根据当前的网络条件,决定是否切换,以及切换到哪个分辨率的版本。

image.png

  1. 以 HLS 为例,使用 master playlist 索引一个视频不同分辨率的版本。如上图所示,视频一共包含了3种不同的版本,分辨率分别是 426x240,852x480 和 1280x720。BANDWIDTH 表示了该版本对应的码率,分辨率越高的的版本,码率也越大。

image.png

  1. 播放器在切换不同分辨率规格的策略,依赖于其采取的码率自适应算法。主要的码率自适应算法有:基于带宽预测的算法、基于 buffer 的算法,以及混合带宽预测和 buffer 的算法。

image.png

2.1. 基于带宽预测的算法,可以获得一个预测的视频码率,播放器选择一个不高于预测带宽的视频进行播放。预测的方法是:以当前时间之前的固定时间段的样本(k1,k2)作为参考,计算调和平均值(倒数的平均的倒数),作为预测的视频码率(p1)。该算法仅基于历史的样本预测带宽,准确性不佳。

2.1. 基于 buffer 的算法,则放弃直接的带宽预测,用 buffer 驱动码率选择,buffer 大则选择高码率,否则选择低码率。但是这里潜在的问题是,当播放器的 buffer 发生变化时,可能造成不同分辨率规格的视频频繁切换。

2.3. 目前主流的码率自适应算法,是混合带宽预测和 buffer 的算法,即以带宽预测为主,buffer 为辅,该算法结合了带宽预测和 buffer 两种算法的优势。

行业中的几种主要的自适应码流协议,除了 Apple 的 HLS 之外,还有 Google 的 DASH,Adobe 的 HDS,以及 Microsoft 的 Smooth(后两种实际上已经逐渐被 DASH 替代,即 HLS 和 DASH 成为自适应码流协议的两大阵营)。对比 HLS 和 DASH:

HLS(apple 私有):视频格式为 ts,索引文件为 m3u8,单码率采用一级索引,多码率采用二级索引;

DASH(ISO标准):视频格式为 fmp4(也宣称支持ts),索引文件为 mpd,只包含一级索引。

4. 带宽预测算法

利用ExoPlayer源码中的宽带预测方案,其本质上使用的是移动平均算法,来获取当前时间段的平均网络情况。

4.1. 为什么用移动平均算法呢?

设想一下,假如我们用的是普通的平均算法,那么就是取整段时间的平均值,这时候问题就来

1. 如果我们取1小时的网速平均值作为当前的网速,你觉得合适吗?
2. 如果整个网络的上下波动很大的情况下,平均值又能代表什么呢?

最合适的就是圈定一段短的时间,在这段的时间内算平均网速,圈定的时间段称为滑动窗口。随着时间的流逝,滑动窗口也会随着时间移动,进而在滑动窗口中获取平均值,这样推断出来的网络情况才是接近当前时间段内的网络情况,这就是简单的理解移动平均算法。

4.2. 滑动窗口的本质是什么?

在概念上,是对数据的采集,对过时的数据进行丢弃,对新的数据进行采样,保证数据一直是最新状态,这就是滑动, 在代码上,是一段存储在数组的数据,通过不断的采集和丢弃,保证数据一直是最新状态.

4.3. 滑动窗口圈定时间的标准是什么?

滑动窗口圈定时间的标准是人为定义的,你可以通过时间戳去定义固定的时间段,随着时间流逝,通过移动时间戳来移动我们的滑动窗口通过用户下载的固定数据量,圈定固定的下载数据量总和,随着数据下载量增加来移动我们的滑动窗口。

4.4. Exoplayer SlidingPercentile

4.4.1. 源码分析

/**
   * Adds a new weighted value.
   *
   * @param weight The weight of the new observation.
   * @param value The value of the new observation.
   */
  public void addSample(int weight, float value) {
     //1. 按照index排序
    ensureSortedByIndex();
    
    //2. 构建 sample 对象
    Sample newSample = recycledSampleCount > 0 ? recycledSamples[--recycledSampleCount]
        : new Sample();
    newSample.index = nextSampleIndex++;
    newSample.weight = weight;
    newSample.value = value;
    samples.add(newSample);
    totalWeight += weight;
    
    //3. 已存在的样本总权重超过窗口值 ,则需要依次从链表的头部开始删除。
    // 如果待删样本的权重大于溢出权重 ,则直接从待删样本的权重中减去但不删除
    // 反之则直接删除待删样本
    while (totalWeight > maxWeight) {
      int excessWeight = totalWeight - maxWeight;
      Sample oldestSample = samples.get(0);
      if (oldestSample.weight <= excessWeight) {
        totalWeight -= oldestSample.weight;
        samples.remove(0);
        if (recycledSampleCount < MAX_RECYCLED_SAMPLES) {
          recycledSamples[recycledSampleCount++] = oldestSample;
        }
      } else {
        oldestSample.weight -= excessWeight;
        totalWeight -= excessWeight;
      }
    }
  }
/**
   * Computes a percentile by integration.
   *
   * @param percentile The desired percentile, expressed as a fraction in the range (0,1].
   * @return The requested percentile value or {@link Float#NaN} if no samples have been added.
   */
  public float getPercentile(float percentile) {
    ensureSortedByValue();
    //1. 获取期望权重
    float desiredWeight = percentile * totalWeight;
    int accumulatedWeight = 0;
    for (int i = 0; i < samples.size(); i++) {
      Sample currentSample = samples.get(i);
      2. 对样本权重累加
      accumulatedWeight += currentSample.weight;
      3. 大于期望权重返回期望值
      if (accumulatedWeight >= desiredWeight) {
        return currentSample.value;
      }
    }
    // Clamp to maximum value or NaN if no values.
    return samples.isEmpty() ? Float.NaN : samples.get(samples.size() - 1).value;
  }

4.4.2. 测试代码

private fun testSlidingPercentile() {
        //6位最大窗口
        val slidingPercentile = SlidingPercentile(7)
        //加入样本数据,第一个参数是权重(可对应下载量),第二个参数是值(可对于下载速度)
        slidingPercentile.addSample(1, 1f)
        slidingPercentile.addSample(2, 2f)
        slidingPercentile.addSample(2, 3f)
        slidingPercentile.addSample(2, 4f)
        slidingPercentile.addSample(2, 5f)
        slidingPercentile.addSample(2, 6f)
        
        获取与测试值
        val avgSpeed = slidingPercentile.getPercentile(0.5f)
        Log.i("testSlidingPercentile", "avgSpeed=$avgSpeed")
    }
    输出为5.0

分析,由于最大窗口为6,一次加入了6组样本,加入样本权重超过了最大窗口值,就会淘汰,可以看出淘汰了前两个样本,最后三个样本2+2+2+2的权重是8,还是大于7,固将第三个样本的权重减1,第三个样本权重变为1。7的0.5是3.5,1+2+2>3.5 所以均值为5。

4.4.3. 数据->权重

data为下载量
val weight = Math.sqrt(data.toDouble()).toInt()

4.5. 总结

可以以30秒为一个单位,得到下载量和下载速度,映射出权重和速度,利用滑动窗口预测算法,预测当前下载速度,从而选择对应清晰度的视频进行播放。

4. 网络质量定级

网络质量定级可参考facebook方案

public enum ConnectionQuality {
  /**
   * Bandwidth under 150 kbps.
   */
  POOR,
  /**
   * Bandwidth between 150 and 550 kbps.
   */
  MODERATE,
  /**
   * Bandwidth between 550 and 2000 kbps.
   */
  GOOD,
  /**
   * EXCELLENT - Bandwidth over 2000 kbps.
   */
  EXCELLENT,
  /**
   * Placeholder for unknown bandwidth. This is the initial value and will stay at this value
   * if a bandwidth cannot be accurately found.
   */
  UNKNOWN
}

5. 网络测速

5.1. 利用下载一个资源判断瞬间网速。

优点:简单;

缺点:1,瞬间值可能不准,下载要消耗网络。

5.2. 利用ping值监听网速。

优点:更简单;

缺点:1,瞬间值可能不准,要消耗网络,但是消耗网络较少。

5.3. 监听视频下载计算网速,要知道视频下载开始和结束,否则会出现噪点数据。

优点:不需要额外请求;

缺点:确定视频网络请求开始和结束困难,处理噪点数据困难。

2021-05-21 23:36:41.539 23761-23761/com.fenbi.android.zebraenglish I/dfdsafsadf: 1454.92 kb/s
2021-05-21 23:36:42.540 23761-23761/com.fenbi.android.zebraenglish I/dfdsafsadf: 1454.0 kb/s
2021-05-21 23:36:43.540 23761-23761/com.fenbi.android.zebraenglish I/dfdsafsadf: 1242.516 kb/s
2021-05-21 23:36:44.545 23761-23761/com.fenbi.android.zebraenglish I/dfdsafsadf: 22.978 kb/s
2021-05-21 23:36:45.542 23761-23761/com.fenbi.android.zebraenglish I/dfdsafsadf: 22.0 kb/s
2021-05-21 23:36:46.542 23761-23761/com.fenbi.android.zebraenglish I/dfdsafsadf: 22.0 kb/s
2021-05-21 23:36:47.547 23761-23761/com.fenbi.android.zebraenglish I/dfdsafsadf: 20.960 kb/s
2021-05-21 23:36:48.543 23761-23761/com.fenbi.android.zebraenglish I/dfdsafsadf: 21.21 kb/s
2021-05-21 23:36:49.553 23761-23761/com.fenbi.android.zebraenglish I/dfdsafsadf: 22.0 kb/s
2021-05-21 23:36:50.548 23761-23761/com.fenbi.android.zebraenglish I/dfdsafsadf: 7.993 kb/s
2021-05-21 23:36:51.545 23761-23761/com.fenbi.android.zebraenglish I/dfdsafsadf: 0.0 kb/s
2021-05-21 23:36:52.555 23761-23761/com.fenbi.android.zebraenglish I/dfdsafsadf: 0.0 kb/s
2021-05-21 23:36:53.549 23761-23761/com.fenbi.android.zebraenglish I/dfdsafsadf: 0.1000 kb/s
2021-05-21 23:36:54.560 23761-23761/com.fenbi.android.zebraenglish I/dfdsafsadf: 1.0 kb/s
2021-05-21 23:36:55.555 23761-23761/com.fenbi.android.zebraenglish I/dfdsafsadf: 0.0 kb/s
2021-05-21 23:36:56.550 23761-23761/com.fenbi.android.zebraenglish I/dfdsafsadf: 0.0 kb/s
2021-05-21 23:36:57.550 23761-23761/com.fenbi.android.zebraenglish I/dfdsafsadf: 0.0 kb/s
2021-05-21 23:36:58.555 23761-23761/com.fenbi.android.zebraenglish I/dfdsafsadf: 0.0 kb/s
2021-05-21 23:36:59.550 23761-23761/com.fenbi.android.zebraenglish I/dfdsafsadf: 0.0 kb/s

6. 总结

  1. 一个基本原则是,我们希望大多数视频播放正常清晰度版本,网络很差时播放低清晰度版本,所以可以以一批视频的清晰度为一个单位。

  2. 由于短视频时长较短,最长为60s,大部分视频为30s左右,所以对短视频分片的意义不大。

  3. 视频数据,下发视频的多码率版本列表(对应简化版的m3u8文件)或者 上报网速由服务端根据网速限制清晰度版本下发(会增加网络请求)。

  4. 由于斑马拍自带预下载,预下载有明显的开始和结束分界线,可以监听预下载时的网速,并结合滑动窗口得出当前网速。

  5. 由于斑马拍自带预下载,所以在预下载时就要决定这个视频的清晰度版本,无法做到,网络变化时,切换已经缓存的视频的清晰度,也无法将缓存作为改变视频清晰度的变量。