播放器网络带宽预测方法

news/2024/11/25 8:50:59/

简介

带宽预测是播放器实现码率自适应的技术基础。只有对当前的带宽预测的足够准确,才能够选择出当前场景下最优的码率进行播放。下面分别介绍下ijkplayer、exoplayer及VLC的带宽预测方法。

ijkplayer

ijkplayer的带宽预测代码位于ijksdl_timer.c中,核心思想是对过去时间点的网速进行采样,使用过去所有采样点的加权均值对当前带宽进行预测。其核心结构体为SDL_SpeedSampler2:

typedef struct SDL_SpeedSampler2

{

    int64_t sample_range;//采样点的时间范围,初始化时定义,后续不会修改,默认值是2000ms

    int64_t last_profile_tick;//上一次采样的时间

    int64_t last_profile_duration;//之前所有采样下载时间的加权均值

    int64_t last_profile_quantity;//之前所有采样下载数据量的加权均值

    int64_t last_profile_speed;//上一次采样时的平均带宽

} SDL_SpeedSampler2;

SDL_SpeedSampler2结构体的last_profile_duration和last_profile_quantity变量比较难理解,需要结合代码逻辑才能更好地解释,首先看下增加采样点的代码:

//函数参数只有两个,分别是采样结构体的指针和本次采样下载的数据量

int64_t SDL_SpeedSampler2Add(SDL_SpeedSampler2 *sampler, int quantity)

{

    if (quantity < 0)

        return 0;

    int64_t sample_range  = sampler->sample_range;

    int64_t last_tick     = sampler->last_profile_tick;

    int64_t last_duration = sampler->last_profile_duration;

    int64_t last_quantity = sampler->last_profile_quantity;

    int64_t now           = (int64_t)SDL_GetTickHR();

    //这里使用两次采样的间隔作为本次采样的下载时间

    int64_t elapsed       = (int64_t)llabs(now - last_tick);

    //如果两次采样的间隔时间超过了sample_range,那么之前的采样信息便不再置信,因此需要重新统计采样信息,将本次采样作为第一次采样

    if (elapsed < 0 || elapsed >= sample_range) {

        // overflow, reset to initialized state

        sampler->last_profile_tick     = now;

        sampler->last_profile_duration = sample_range;

        sampler->last_profile_quantity = quantity;

        sampler->last_profile_speed    = quantity * 1000 / sample_range;

        return sampler->last_profile_speed;

    }

    //这里就是加权算法的部分,这里的last_quantity和last_duration都是从0开始增长的,即new_quantity和new_duration也是从0开始增长的。当new_duration小于sample_range时,

    //即第一个采样点与最后一个采样点的时间差在sample_range范围内时,new_duration和new_quantity的权重都还是1,即未经过加权;一旦new_duration大于sample_range后,new_duration

    //的值就会被固定为sample_range,new_quantity则按比例进行缩小,这也就变相地实现了,越靠前的采样点,随着时间的推移,所占的比重会越来越小。

    int64_t new_quantity = last_quantity + quantity;

    int64_t new_duration = last_duration + elapsed;

    if (new_duration > sample_range) {

        new_quantity = new_quantity * sample_range / new_duration;

        new_duration = sample_range;

    }

    //这部分逻辑比较简单了,更新变量然后算出当前平均带宽。

    sampler->last_profile_tick     = now;

    sampler->last_profile_duration = new_duration;

    sampler->last_profile_quantity = new_quantity;

    if (new_duration > 0)

        sampler->last_profile_speed = new_quantity * 1000 / new_duration;

    return sampler->last_profile_speed;

}

上面介绍了采样的过程,预测的过程就十分简单了,和采样的流程基本一致:

int64_t SDL_SpeedSampler2GetSpeed(SDL_SpeedSampler2 *sampler)

{

    int64_t sample_range  = sampler->sample_range;

    int64_t last_tick     = sampler->last_profile_tick;

    int64_t last_quantity = sampler->last_profile_quantity;

    int64_t last_duration = sampler->last_profile_duration;

    int64_t now           = (int64_t)SDL_GetTickHR();

    int64_t elapsed       = (int64_t)llabs(now - last_tick);

    if (elapsed < 0 || elapsed >= sample_range)

        return 0;

    //这里存在一个问题,就是elapsed是已知的,因此可以直接加到duration里,但是这段时间下载的数据量quantity是未知的,因此new_quantity是比实际值要小的,最终导致预测的带宽也要比

    //实际值小一点,且elapsed越大,这个误差会越明显

    int64_t new_quantity = last_quantity;

    int64_t new_duration = last_duration + elapsed;

    if (new_duration > sample_range) {

        new_quantity = new_quantity * sample_range / new_duration;

        new_duration = sample_range;

    }

    if (new_duration <= 0)

        return 0;

    return new_quantity * 1000 / new_duration;

}

总结:ijkplayer巧妙地使用一个结构体保存了”所有“采样点的信息,并且实现了带宽的加权平均预测,这种方法相对于单独记录每个采样点信息的方式会比较省内存,但是一些较老的采样点是否仍应该被计算进来则有待商榷。(其实就是指数移动平均)

exoplayer

exoplayer同样使用采样的方式进行带宽预测,其代码位于SlidingPercentile.java中,使用了滑动窗口的方式限制了采样点的数量,先看下该类的主要成员变量:

//重载了两种COMPARATOR,分别比较采样点的index和value

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);

//排序方式,按照index排序或按照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 ArrayList<Sample> samples;

//回收区采样点列表,这里为了避免采样点的频繁创建与销毁,新增了回收区

private final Sample[] recycledSamples;

//当前排序方式

private int currentSortOrder;

//下一个采样点的index

private int nextSampleIndex;

//当前权重和

private int totalWeight;

//已回收的采样点数量

private int recycledSampleCount;

采样点的结构比较简单,仅包含三个成员变量:

private static class Sample {

  public int index;

  //当前采样点的权重,为下载数据量的平方根

  public int weight;

  //当前采样点的下载速度

  public float value;

}

然后看下新的采样点是如何加入的:

//参数有两个,分别是采样点的权重和下载速度

public void addSample(int weight, float value) {

  //确保采样点是按照index即时间顺序排序的

  ensureSortedByIndex();

  //如果回收区有采样点,直接复用即可;如果没有,则需要新建,这样能够一定程度地避免内存频繁申请与释放

  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) {

    //计算totalWeight与maxWeight的差值excessWeight

    int excessWeight = totalWeight - maxWeight;

    Sample oldestSample = samples.get(0);

    //如果最老采样点的权重小于excessWeight,直接删除

    if (oldestSample.weight <= excessWeight) {

      totalWeight -= oldestSample.weight;

      samples.remove(0);

      if (recycledSampleCount < MAX_RECYCLED_SAMPLES) {

        recycledSamples[recycledSampleCount++] = oldestSample;

      }

    //如果最老采样点的权重大于excessWeight,该点的权重减掉excessWeight

    else {

      oldestSample.weight -= excessWeight;

      totalWeight -= excessWeight;

    }

  }

}

最后看下预测部分的逻辑:

//只有一个参数,取值范围是(0,1]

public float getPercentile(float percentile) {

  //将采样点按照下载速度进行排序

  ensureSortedByValue();

  //算出要取的权重值desiredWeight

  float desiredWeight = percentile * totalWeight;

  int accumulatedWeight = 0;

  //遍历采样点,找到累加权重值大于等于desiredWeight的采样点,并返回该采样点的下载速度

  for (int i = 0; i < samples.size(); i++) {

    Sample currentSample = samples.get(i);

    accumulatedWeight += currentSample.weight;

    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;

}

总结:exoplayer的核心思想就是使用滑动窗口对带宽进行预测。个人认为目前的计算方式有两点问题:1、目前的滑动窗口大小取决于数据量的大小,不能很好地确保采样点间的时间相关性;2、最终预测时将采样点列表按照下载速度排序,再一次弱化了时间相关性,暂时只能理解为exoplayer选择了较为保守的中值策略。

VLC

VLC的带宽预测同样使用了滑动窗口的思想,其主要逻辑位于MovingAverage.hpp中:

   class MovingAverage

   {

       public:

           MovingAverage(unsigned = 10);

           T push(T);

       private:

           //保存了采样点带宽的列表

           std::list<T> values;

           //上一个被丢弃的采样点的带宽

           T previous;

           //列表中采样点的最大数量

           unsigned maxobs;

           //带宽均值

           T avg;

   };

   template <class T>

   T MovingAverage<T>::push(T v)

   {

       //滑动窗口的逻辑

       if(values.size() >= maxobs)

       {

           previous = values.front();

           values.pop_front();

       }

       values.push_back(v);

       /* compute for deltamax */

       T omin = *std::min_element(values.begin(), values.end());

       T omax = *std::max_element(values.begin(), values.end());

       //diffsums保存了列表中每一个采样点和其上一个采样点的带宽差值的绝对值之和

       //列表中第一个采样点的上一个采样点即previous

       MovingAverageSum<T> diffsums = std::for_each(values.begin(), values.end(),

                                                    MovingAverageSum<T>(previous));

       /* Vertical Horizontal Filter / Moving Average

        *

        * stability during observation window alters the alpha parameter

        * and then defines how fast we adapt */

       //这里用列表中最大带宽和最小带宽作差取得deltamax,然后用deltamax除以diffsums作为权重

       //代码里没有给出这种算法的出处,大体可以理解为这是一种衡量网络波动的算法?

       const T deltamax = omax - omin;

       double alpha = (diffsums.sum) ? 0.33 * ((double)deltamax / diffsums.sum) : 0.5;

       avg = alpha * avg + (1.0 - alpha) * (*values.rbegin());

       return avg;

   }

总结:VLC同样使用滑动窗口对网络带宽进行预测,预测算法中侧重于网络波动的影响,也淡化了时间的概念。(改进的指数移动平均)


http://www.ppmy.cn/news/171705.html

相关文章

简单的安卓网络音乐视频播放器app

目录 开发环境 功能清单 部分源码 login.java MainActivity.java MyDatabase.java build.gradle 源码下载 学习安卓开发时做的一个小demo&#xff0c;知识点包括&#xff1a;intent、UI、界面切换、API调用、播放器调用、内部存储、list控件等。 具体可看视频&#xff…

android带投屏播放器,投屏播放器app下载

投屏播放器APP是一款简单好用的投屏工具&#xff0c;界面简洁操作简单&#xff0c;一键即可搜索附近可投屏的电视并进行智能投放设置&#xff0c;不论是电视电影还是歌曲都可以进行投放哦&#xff0c;有需要的用户赶紧来下载吧! 应用介绍 当手机、平板等移动设备和电视、盒子在…

Mybatais-plus超详细教程

文章目录 前言什么是Mybatis-plus特性引入依赖配置日志Service CRUD 接口SaveSaveOrUpdateRemoveUpdateGetListPageCount Chainqueryupdate Mapper CRUD 接口InsertDeleteUpdateSelect 赠送 前言 在学习Mybatis-plus之前&#xff0c;这里默认大家都已经对mybatis使用有了一定的…

lingo与excel

model: sets: bbb/1..4/:c,x,t; endsetsdata: cole(‪D:\桌面\0722.xlsx,nah); &#xff01;读excel中内容 ole(‪D:\桌面\0722.xlsx,nahh)t; &#xff01;写出lingo中内容 enddata公式——定义名称 &#xff08;引用的时候都选中&#xff0c;自己起个名称&#xff08;如n…

Python实现发射爱心代码,Python情人节520表白代码

Python实现发射爱心代码&#xff0c;Python情人节520表白代码 运行截图 完整程序代码 from turtle import * import turtle as t import turtle #t.speed(0) #Turtle().screen.delay(0)t.up() t.goto(-300,0) t.color(black) t.pensize(2) t.down() t.left(40) t.forward(50)…

P520骨牌铺法(domino)

Description 有 1n 的一个长方形&#xff0c;用一个 11、12 和 13 的骨牌铺满方格。例如当 n3 时为 13 的方格。此时用 11、12 和 13 的骨牌铺满方格&#xff0c;共有四种铺法。如下图&#xff1a; Input 一个整数 n (1≤n≤36),1n 的长方形。 Output 一个整数&#xff0c;表示…

输入nvidia-smi 显示NVIDIA-SMI has failed because it couldn‘t communicate wi

现象描述 输入 nvidia-smi显示如下错误&#xff1a; jiangjiang-ThinkStation-P520:~$ nvidia-smi NVIDIA-SMI has failed because it couldnt communicate with the NVIDIA driver. Make sure that the latest NVIDIA driver is installed and running.NVIDIA-SMI has faile…

hpcp5225设置linux网络,hp cp5225驱动下载

惠普cp5225驱动是一款专业的打印机驱动软件&#xff0c;帮助你解决惠普打印机在连接电脑时遇上的难题&#xff0c;体积小巧&#xff0c;使用方便&#xff0c;感兴趣的小伙伴快来当易网下载体验吧&#xff01; 惠普hp cp5225打印机驱动内容介绍 惠普cp5225打印机驱动是专门对于该…