前言
由于国内基础设施非常优秀,在平时的开发中,很少会关注网络情况,很容易忽略弱网情况下的网络状况,如果项目属于国外App,则需要考虑到当前的基础设施和网络情况,特别是播放视频的时候,需要通过动态调整码率去选择当前的播放码率。这时,就找到ExoPlayer源码中的宽带预测方案,其本质上使用的是移动平均算法,来获取当前时间段的平均网络情况。我们通过对当地宽带预测,从而选择更适应网速的码率去播放视频
疑问
1、为什么用移动平均算法呢?
设想一下,假如我们用的是普通的平均算法,那么就是取整段时间的平均值,这时候问题就来
- 如果我们取1小时的网速平均值作为当前的网速,你觉得合适吗?
- 如果整个网络的上下波动很大的情况下,平均值又能代表什么呢?
最合适的就是圈定一段短的时间,在这段的时间内算平均网速,圈定的时间段称为滑动窗口
。随着时间的流逝,滑动窗口也会随着时间移动,进而在滑动窗口中获取平均值,这样推断出来的网络情况才是接近当前时间段内的网络情况,这就是简单的理解移动平均算法。举例子,股票中的K线图的5日均线,就是通过圈定5日时间,算出近5日的平均价格,用的就是此方法。
2、滑动窗口的本质是什么?
- 在概念上,是对数据的采集,对过时的数据进行丢弃,对新的数据进行采样,保证数据一直是最新状态,这就是滑动
- 在代码上,是一段存储在数组的数据,通过不断的采集和丢弃,保证数据一直是最新状态
3、滑动窗口圈定时间的标准是什么?
滑动窗口圈定时间的标准是人为定义的,你可以
- 通过时间戳去定义固定的时间段,随着时间流逝,通过移动时间戳来移动我们的滑动窗口
- 通过用户下载的固定数据量,圈定固定的下载数据量总和,随着数据下载量增加来移动我们的滑动窗口(本案例用此方案)
概念
- 采集:通过对网速的采样,获取我们的滑动窗口
- 获取:获取我们采样后当前的网络情况
- 权重:定义滑动窗口的值
使用
1、定义采样的滑动窗口的大小
private var mVideoSlidingPercentile: SlidingPercentile =
SlidingPercentile(3000) // 滑动值在900k的数据
采取固定数据量的采样方式,定义最大权重为3000的滑动窗口,3000^2 = 9000000 ≈ 900k,为什么会这样计算下面会解释
2、采样下载数据量和网络情况
// p1.speed:下载速度
// p1.costTime:下载耗时
addSpeedSample(p1.speed * p1.costTime, p1.speed)
通过下载器的回调中,我们可以对数据量和网络情况进行采样
/**
* @data:Long 下载数据量 = 下载速度 * 下载耗时
* @value:Long 下载数据量的速度
*/
fun addSpeedSample(data: Long, value: Long) {
//这里的数据量是下载累加的,这里会对数据量做开根号处理
//当采集到达:data = 9000000 ≈ 900k,开根号= 3000,这个时候窗口开始滑动
val weight = Math.sqrt(data.toDouble()).toInt()
Log.i(TAG, "[addSpeedSample][采样速度]下载数据量=$data, 权重=$weight, 速度=$value")
mVideoSlidingPercentile.addSample(weight, value)
}
为了更好的理解,我们模拟数据,通过日志输出看到更直观的表现
1、[addSpeedSample][采样速度]下载数据量=1000000, 权重=100, 速度=50
2、[addSpeedSample][采样速度]下载数据量=2000000, 权重=200, 速度=60
3、[addSpeedSample][采样速度]下载数据量=3000000, 权重=300, 速度=70
4、[addSpeedSample][采样速度]下载数据量=4000000, 权重=400, 速度=80
5、[addSpeedSample][采样速度]下载数据量=5000000, 权重=500, 速度=90
6、[addSpeedSample][采样速度]下载数据量=3000000, 权重=300, 速度=70
7、[addSpeedSample][采样速度]下载数据量=2000000, 权重=200, 速度=60
8、[addSpeedSample][采样速度]下载数据量=1000000, 权重=100, 速度=50
- 首先会依次采样,步骤1、2、3,此时权重总和600,此时并没有超过我们预先设置的900
- 步骤4,此时权重总和1000,此时的数据量已经下载超过900k,滑动窗口被确定下来,此时窗口开始移动,超过900则会舍弃旧的点100,此时权重刚好900
- 步骤5,此时权重总和900+500=1400,超过900,开始移动窗口,舍弃旧点200,300,此时权重刚好900
- 步骤6,此时权重总和900+300=1200,超过900,开始移动窗口,如果舍弃旧点400,权重到达800,权重不够900,所以不能舍弃,只能降维,将旧点400降维至300,让权重保持在900
- 依次类推,让权重始终保持在900内,旧点可以选择舍弃或者降维来保持权重的稳定
3、获取网络情况
val avgSpeed = mVideoSlidingPercentile.getPercentile(0.5f)
Log.i(TAG, "[getAvgSpeed][获取视频下载速度]avgSpeed=$avgSpeed")
通过getPercentile
获取滑动窗口中的值,参数0.5f
表示当前滑动窗口中的中间位置,在获取滑动窗口值之前,会对滑动窗口的所有值进行有序排列,故获取的是中间位置的值
源码
从ExoPlayer中我们可以找到这样的源码,我们重点关注采集和获取,分别是addSpeedSample
和getPercentile
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.remo.mobile.smallvideo.sdk.videoDownload;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import tv.athena.klog.api.KLog;
/**
* Calculate any percentile over a sliding window of weighted values. A maximum weight is
* configured. Once the total weight of the values reaches the maximum weight, the oldest value is
* reduced in weight until it reaches zero and is removed. This maintains a constant total weight,
* equal to the maximum allowed, at the steady state.
* <p>
* This class can be used for bandwidth estimation based on a sliding window of past transfer rate
* observations. This is an alternative to sliding mean and exponential averaging which suffer from
* susceptibility to outliers and slow adaptation to step functions.
*
* @see <a href="http://en.wikipedia.org/wiki/Moving_average">Wiki: Moving average</a>
* @see <a href="http://en.wikipedia.org/wiki/Selection_algorithm">Wiki: Selection algorithm</a>
*/
public class SlidingPercentile {
private static final String TAG = "SlidingPercentile";
// Orderings.
private static final Comparator<Sample> INDEX_COMPARATOR = (a, b) -> a.index - b.index;
private static final Comparator<Sample> VALUE_COMPARATOR =
(a, b) -> Float.compare(a.value, b.value);
private static final int SORT_ORDER_NONE = -1;
private static final int SORT_ORDER_BY_VALUE = 0;
private static final int SORT_ORDER_BY_INDEX = 1;
private static final int MAX_RECYCLED_SAMPLES = 5;
private final int maxWeight;
private final List<Sample> samples;
private final Sample[] recycledSamples;
private int currentSortOrder;
private int nextSampleIndex;
private int totalWeight;
private int recycledSampleCount;
/**
* @param maxWeight The maximum weight.
*/
public SlidingPercentile(int maxWeight) {
this.maxWeight = maxWeight;
recycledSamples = new Sample[MAX_RECYCLED_SAMPLES];
// samples = new ArrayList<>();
samples = Collections.synchronizedList(new ArrayList<>());
currentSortOrder = SORT_ORDER_NONE;
}
/**
* Resets the sliding percentile.
*/
public void reset() {
synchronized (samples) {
samples.clear();
currentSortOrder = SORT_ORDER_NONE;
nextSampleIndex = 0;
totalWeight = 0;
}
}
/**
* 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, long value) {
ensureSortedByIndex();
synchronized (samples) {
Sample newSample = recycledSampleCount > 0 ? recycledSamples[--recycledSampleCount]
: new Sample();
newSample.index = nextSampleIndex++;
newSample.weight = weight;
newSample.value = value;
samples.add(newSample);
totalWeight += weight;
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 long getPercentile(float percentile) {
ensureSortedByValue();
float desiredWeight = percentile * totalWeight;
int accumulatedWeight = 0;
synchronized (samples) {
for (int i = 0; i < samples.size(); i++) {
Sample currentSample = samples.get(i);
if (currentSample != null) {
accumulatedWeight += currentSample.weight;
if (accumulatedWeight >= desiredWeight) {
return currentSample.value;
}
}
}
// Clamp to maximum value or NaN if no values.
if (samples.isEmpty() || samples.get(samples.size() - 1) == null) {
return 0;
}
return samples.get(samples.size() - 1).value;
}
}
public long getLimitPercentile(float percentile) {
float desiredWeight = percentile * totalWeight;
if (desiredWeight < maxWeight * 0.2) {
return 0;
}
return getPercentile(percentile);
}
/**
* Sorts the samples by index.
*/
private void ensureSortedByIndex() {
synchronized (samples) {
try {
if (currentSortOrder != SORT_ORDER_BY_INDEX) {
Collections.sort(samples, INDEX_COMPARATOR);
currentSortOrder = SORT_ORDER_BY_INDEX;
}
} catch (Exception e) {
KLog.e(TAG, e.toString());
}
}
}
/**
* Sorts the samples by value.
*/
private void ensureSortedByValue() {
synchronized (samples) {
try {
if (currentSortOrder != SORT_ORDER_BY_VALUE) {
Collections.sort(samples, VALUE_COMPARATOR);
currentSortOrder = SORT_ORDER_BY_VALUE;
}
} catch (Exception e) {
KLog.e(TAG, e.toString());
}
}
}
private static class Sample {
public int index;
public int weight;
public long value;
}
}
1、采集的样本
private static class Sample {
public int index;
public int weight;
public long value;
}
数据样本的存储
private final List<Sample> samples;
samples = Collections.synchronizedList(new ArrayList<>());
2、定义滑动窗口
/**
* @param maxWeight The maximum weight.
*/
public SlidingPercentile(int maxWeight) {
this.maxWeight = maxWeight;
......
}
3、数据采样
数据的采样就是滑动窗口的逻辑
public void addSample(int weight, long value) {
ensureSortedByIndex();
synchronized (samples) {
// 1、采样累加权重
Sample newSample = recycledSampleCount > 0 ? recycledSamples[--recycledSampleCount]
: new Sample();
newSample.index = nextSampleIndex++;
newSample.weight = weight;
newSample.value = value;
samples.add(newSample);
totalWeight += weight;
// 2、当权重超过设置的滑动窗口,则开始进入滑动阶段
while (totalWeight > maxWeight) {
// 拿到当前要滑动的权重的差值
int excessWeight = totalWeight - maxWeight;
// 拿到旧点开始滑动
Sample oldestSample = samples.get(0);
if (oldestSample.weight <= excessWeight) {
// 3、如果权重超过,则直接移除旧点
totalWeight -= oldestSample.weight;
samples.remove(0);
......
} else {
// 4、权重不够时则对旧点进行降维
oldestSample.weight -= excessWeight;
totalWeight -= excessWeight;
}
}
}
}
4、获取预测值
public long getPercentile(float percentile) {
// 1、先排序
ensureSortedByValue();
// 2、算出要取的权重值,假如是0.5,则是所有采样点权重的一半
float desiredWeight = percentile * totalWeight;
int accumulatedWeight = 0;
synchronized (samples) {
for (int i = 0; i < samples.size(); i++) {
// 3、遍历所有采样点,取权重刚好超过要取的权重的值
Sample currentSample = samples.get(i);
if (currentSample != null) {
accumulatedWeight += currentSample.weight;
if (accumulatedWeight >= desiredWeight) {
return currentSample.value;
}
}
}
if (samples.isEmpty() || samples.get(samples.size() - 1) == null) {
return 0;
}
return samples.get(samples.size() - 1).value;
}
}
5、性能优化
private static final int MAX_RECYCLED_SAMPLES = 5;
public SlidingPercentile(int maxWeight) {
recycledSamples = new Sample[MAX_RECYCLED_SAMPLES];
......
}
public void addSample(int weight, long value) {
ensureSortedByIndex();
synchronized (samples) {
// 1、询问缓存里面是否有存在采样点,有就拿来用
Sample newSample = recycledSampleCount > 0 ? recycledSamples[--recycledSampleCount]
: new Sample();
......
samples.add(newSample);
while (totalWeight > maxWeight) {
if (oldestSample.weight <= excessWeight) {
......
if (recycledSampleCount < MAX_RECYCLED_SAMPLES) {
// 2、由于是要舍弃的旧点,弃之可惜,缓存起来备用
recycledSamples[recycledSampleCount++] = oldestSample;
}
}
}
}
}
由于整个过程是一直采样,会频繁创建采样点的对象,所以这里做了个简单的缓存,将采样点保存在数组中,进行复用