Android进阶——ExoPlayer源码分析之宽带预测策略的算法详解

前言

由于国内基础设施非常优秀,在平时的开发中,很少会关注网络情况,很容易忽略弱网情况下的网络状况,如果项目属于国外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. 首先会依次采样,步骤1、2、3,此时权重总和600,此时并没有超过我们预先设置的900
  2. 步骤4,此时权重总和1000,此时的数据量已经下载超过900k,滑动窗口被确定下来,此时窗口开始移动,超过900则会舍弃旧的点100,此时权重刚好900
  3. 步骤5,此时权重总和900+500=1400,超过900,开始移动窗口,舍弃旧点200,300,此时权重刚好900
  4. 步骤6,此时权重总和900+300=1200,超过900,开始移动窗口,如果舍弃旧点400,权重到达800,权重不够900,所以不能舍弃,只能降维,将旧点400降维至300,让权重保持在900
  5. 依次类推,让权重始终保持在900内,旧点可以选择舍弃或者降维来保持权重的稳定

3、获取网络情况

val avgSpeed = mVideoSlidingPercentile.getPercentile(0.5f)
Log.i(TAG, "[getAvgSpeed][获取视频下载速度]avgSpeed=$avgSpeed")

通过getPercentile获取滑动窗口中的值,参数0.5f表示当前滑动窗口中的中间位置,在获取滑动窗口值之前,会对滑动窗口的所有值进行有序排列,故获取的是中间位置的值

源码

从ExoPlayer中我们可以找到这样的源码,我们重点关注采集和获取,分别是addSpeedSamplegetPercentile

/*
 * 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;
                }
            }
        }
    }
}

由于整个过程是一直采样,会频繁创建采样点的对象,所以这里做了个简单的缓存,将采样点保存在数组中,进行复用

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

许英俊潇洒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值