前言
由於國內基礎設施非常優秀,在平時的開發中,很少會關注網絡情況,很容易忽略弱網情況下的網絡狀況,如果項目屬於國外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;
}
}
}
// 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;
}
}
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;
}
}
}
}
}
由於整個過程是一直採樣,會頻繁創建採樣點的對象,所以這裏做了個簡單的緩存,將採樣點保存在數組中,進行復用