DASH系统搭建流程

客户端搭建

首先,用git命令将dash.js下载到本地.

 git clone https://github.com/Dash-Industry-Forum/dash.js.git

在dash.js目录下,编译运行dash.js.

{% message color:danger size: “icon:fa-brands fa-npm” title: %} cd dash.js npm install npm run start {% endmessage %}

编译完成后,会自动在浏览器页面打开dash系统的网页,点击超链接here进入。点击Load按钮,如果可以正常播放演示视频,说明客户端没有问题。

部署服务器

使用nginx来部署HTTP服务器。由于后续服务器限速等操作在Linux系统中较为方便,将服务器部署在虚拟机上。

采用的操作系统版本为Ubuntu 22.04.4 LTS

可以使用Linux apt直接安装nginx。

sudo apt-get install nginx

(也可以去官网下载安装包进行安装)

打开/nginx/conf文件夹下的nginx.conf配置文件,将配置文件内容全部替换为下面代码。这里使用8888端口对外提供下载能力。

user root;
worker_processes  4;

events {
    use epoll;
    worker_connections 204800;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    tcp_nopush     on;

    keepalive_timeout  65;
    tcp_nodelay     on;
    gzip            on;
    client_header_buffer_size 4k;


    server {
        listen 8888;
        server_name 127.0.0.1;
        
        add_header Access-Control-Allow-Origin *;
        add_header Access-Control-Allow-Headers X-Requested-With;
        add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
        
        location / {
            root home/miyan/Videos; //这里填写所要播放的视频存放的绝对路径
            autoindex on;
        }
    }
}

在nginx.conf所在的文件夹右键打开terminal,输入下面命令启动nginx。

nginx -p . -c ./nginx.conf 

如果要关闭nginx,则使用下面命令来结束进程。

pkill -9 nginx

(如果操作权限不够,可以使用下面命令后输入系统密码进入root模式)

su root

从服务器中获取视频

首先需要关闭浏览器的缓存,保证客户端请求的视频来自服务器。

在客户端上方地址栏中填写URL,格式为:http://服务器ip:端口号/视频的mpd文件存放在服务器的路径

以我的URL为例:

http://192.168.133.128:8888/Video/bbb-manifest-refresh.mpd

点击Load按钮,即可播放服务器中的视频,Show Options按钮中可以选择视频流的算法,可以通过视频下方的统计图来观察实时的视频缓冲区大小和比特率。

实现ABR算法

这里我们采用BBA0算法来实现。

BBA0算法配置

Dash.js的结构如下图所示:

配置播放器需要进入文件夹dash.js/samples/dash-if-reference-player。文件夹结构如下:

  • dashjs_config.json是播放器配置文件
  • index.html是播放器前端页面
  • app/main.js是页面控制逻辑实现
  • app/rules中包括ABR算法

首先在app/rules中添加算法文件ABB0Rule.js,在index.html中引用该文件:

<script src="app/main.js"></script>
<script src="app/rules/DownloadRatioRule.js"></script>
<script src="app/rules/ThroughputRule.js"></script>
+ <script src="app/rules/BBA0Rule.js"></script>

app/main.js中修改规则:

if ($scope.customABRRulesSelected) {
- $scope.player.addABRCustomRule('qualitySwitchRules',
'DownloadRatioRule', DownloadRatioRule); /* jshint ignore:line */
- $scope.player.addABRCustomRule('qualitySwitchRules',
'ThroughputRule', CustomThroughputRule); /* jshint ignore:line */
+ // $scope.player.addABRCustomRule('qualitySwitchRules',
'DownloadRatioRule', DownloadRatioRule); /* jshint ignore:line */
+ // $scope.player.addABRCustomRule('qualitySwitchRules',
'ThroughputRule', CustomThroughputRule); /* jshint ignore:line */
+ $scope.player.addABRCustomRule('qualitySwitchRules', 'BBA0Rule',
CustomBBA0Rule); /* jshint ignore:line */
} else {
- $scope.player.removeABRCustomRule('DownloadRatioRule');
- $scope.player.removeABRCustomRule('ThroughputRule');
+ // $scope.player.removeABRCustomRule('DownloadRatioRule');
+ // $scope.player.removeABRCustomRule('ThroughputRule');
+ $scope.player.removeABRCustomRule('BBA0Rule');
	}
};

修改配置文件dashjs_config.json来修改buffer大小,使得更适应实验环境。

{
    "streaming": {
        "buffer": {
        "stableBufferTime": 24,
        "bufferTimeAtTopQuality": 24,
        "bufferTimeAtTopQualityLongForm": 20
        }
        },
        
    "debug": {
        "logLevel": 4
    }
}

Dash协议和ABR算法

在解释BBA0算法原理之前,先来了解一下视频流式传播的原理。

区别于之前将整个视频缓存到本地再播放的技术,流式传输允许一边下载一边播放。所采取的方法是将视频和音频切割成一小段一小段的视频,每个视频包含几秒钟的内容。客户端会在本地维护一个缓冲区buffer,每次播放到T时刻后,会请求后续[T,T+k]时间段内的视频块,然后保存在本地的buffer里,这样就可以实现边下边播。

在现实生活中,不同客户端面临的网络状况不同,因此服务器需要准备不同码率的视频块,以根据网络情况,动态调整传输的视频质量。

Dash协议简述

Dash (Dynamic Adaptive Streaming over HTTP)协议是一种自适应动态选择传输码率的传输协议。Dash要求服务器准备不同码率和分辨率的视频切片和MPD文件,视频传输时,首先请求MPD文件并进行解析。之后,客户端会根据网络情况,buffer水平等信息对后续视频码率的请求进行动态调整。下图很好地描述了Dash系统的工作原理。

其中的MPD文件包含了视频切片列表,以及每个切片的描述信息(包括码率、分辨率等)。文件结构如下图所示:

ABR算法简介

ABR算法(自适应码率调节算法)的目的是让用户有更好的观看视频体验。ABR算法的评价标准为用户体验质量 (QoD, Quality of Experience),包括高视频质量、低卡顿时间、少质量切换、低启动延迟等。

BBA0算法

BBA0算法的js实现代码如下:

/*global dashjs*/


let CustomBBA0Rule;

function CustomBBA0RuleClass() {

    let factory = dashjs.FactoryMaker;
    let SwitchRequest = factory.getClassFactoryByName('SwitchRequest');
    let DashMetrics = factory.getSingletonFactoryByName('DashMetrics');
    let Debug = factory.getSingletonFactoryByName('Debug');

    let context = this.context;
    let instance,
        logger;
    
    const reservoir = 5;
    const cushion = 10;
    let ratePrev = 0;

    function setup() {

        logger = Debug(context).getInstance().getLogger(instance);
    }

    function getMaxIndex(rulesContext) {
        let mediaInfo = rulesContext.getMediaInfo();
        let mediaType = mediaInfo.type;
        if (mediaType != "video") {
            return SwitchRequest(context).create(0);
        }

        let abrController = rulesContext.getAbrController();
        let dashMetrics = DashMetrics(context).getInstance();

        let rateMap = {};

        let bitrateList = abrController.getBitrateList(mediaInfo)
                            .map(function(bitrateInfo){
                                return bitrateInfo.bitrate;
                            });
        let bitrateCnt = bitrateList.length;

        let step = cushion / (bitrateCnt - 1);
        for (let i = 0; i < bitrateCnt; i++) {
            rateMap[reservoir + i * step] = bitrateList[i];
        }

        let rateMin = bitrateList[0];
        let rateMax = bitrateList[bitrateCnt - 1];
        ratePrev = ratePrev > rateMin ? ratePrev : rateMin;
        let ratePlus = rateMax;
        let rateMinus = rateMin;

        if (ratePrev === rateMax) {
            ratePlus = rateMax;
        } else {
            for (let i = 0; i < bitrateCnt; i++) {
                if (bitrateList[i] > ratePrev) {
                    ratePlus = bitrateList[i];
                    break;
                }
            }
        }

        if (ratePrev === rateMin) {
            rateMinus = rateMin;
        } else {
            for (let i = bitrateCnt - 1; i >= 0; i--) {
                if (bitrateList[i] < ratePrev) {
                    rateMinus = bitrateList[i];
                    break;
                }
            }
        }

        let currentBufferLevel = dashMetrics.getCurrentBufferLevel(mediaType, true);

        let func = function(bufferLevel) {
            if (bufferLevel < reservoir) {
                return rateMap[cushion + reservoir];
            } else if (bufferLevel > cushion + reservoir) {
                return rateMap[reservoir];
            } else {
                let index = Math.round((bufferLevel - reservoir) / step) *step + reservoir;
                return rateMap[index];
            }
        };

        let fBufferLevel = func(currentBufferLevel);
        
        let rateNext;
        if(currentBufferLevel <= reservoir) {
            rateNext = rateMin;
        } else if (currentBufferLevel >= cushion + reservoir) {
            rateNext = rateMax;
        } else if (fBufferLevel >= ratePlus) {
            for (let i = bitrateCnt; i >= 0; i--) {
                if (bitrateList[i] <= fBufferLevel) {
                    rateNext = bitrateList[i];
                    break;
                }
            }
        } else if (fBufferLevel <= rateMinus) {
            for (let i = 0; i < bitrateCnt; i++) {
                if (bitrateList[i] > fBufferLevel) {
                    rateNext = bitrateList[i];
                    break;
                }
            }
        } else {
            rateNext = ratePrev;
        }

        let quality = 0;
        for (let i = 0; i < bitrateCnt; i++) {
            if (bitrateList[i] == rateNext) {
                quality = i;
                break;
            }
        }

        logger.info("[BBA0Rule] CurrentBufferLevel = " + currentBufferLevel);
        logger.info("[BBA0Rule] Bitrate list = " + bitrateList);
        logger.info("[BBA0Rule] Previous bitrate = " + ratePrev);
        logger.info("[BBA0Rule] Next bitrate = " + rateNext);
        logger.info("[BBA0Rule] Quality = " + quality);

        ratePrev = rateNext;

        return SwitchRequest(context).create(
            quality,
            { name: CustomBBA0RuleClass.__dashjs_factory_name },
            SwitchRequest.PRIORITY.STRONG
        );
    }

    instance = {
        getMaxIndex: getMaxIndex
    };

    setup();

    return instance;
}

CustomBBA0RuleClass.__dashjs_factory_name = 'CustomBBA0Rule';
CustomBBA0Rule = dashjs.FactoryMaker.getClassFactory(CustomBBA0RuleClass);

这段代码实现了一个自定义的基于缓冲区的自适应比特率调整规则,称为 BBA0Rule。下面是算法的基本原理和解释:

  1. 缓冲区分级

    • 规则将缓冲区分为两个区间:reservoir 和 cushion。Reservoir 是一个较小的缓冲区域,cushion 是一个较大的缓冲区域。
    • 如果当前缓冲区低于 reservoir,则会选择最高比特率。
    • 如果当前缓冲区高于 cushion + reservoir,则会选择最低比特率。
    • 在两个区间之间,根据当前缓冲区的位置,线性地分配比特率。
  2. 比特率调整

    • 如果当前缓冲区低于 reservoir,选择最高比特率。
    • 如果当前缓冲区高于 cushion + reservoir,选择最低比特率。
    • 如果当前缓冲区在两个区间之间,则根据当前缓冲区的位置线性地调整比特率。
      • 计算当前缓冲区在两个区间之间的相对位置: 首先,算法计算当前缓冲区水平相对于 reservoir 的位置,即当前缓冲区水平减去 reservoir。然后,它将这个相对位置除以 cushion 减去 reservoir,得到一个介于 0 到 1 之间的值,表示当前缓冲区在两个区间之间的相对位置。
      • 线性插值: 然后,算法使用这个相对位置来进行线性插值。它将相对位置乘以 bitrateList 中相邻比特率的差异,然后加上 reservoir 对应的比特率。这样就得到了一个介于最小和最大比特率之间的插值比特率,这个插值比特率取决于当前缓冲区的水平。
      • 选择比特率: 最后,根据线性插值得到的比特率,算法将其作为下一个选择的比特率。这个插值比特率在两个区间之间提供了一个平滑的过渡,使得在缓冲区水平变化时,比特率的调整更加连续和平稳。
  3. 变量

    • reservoir:较小的缓冲区域大小。
    • cushion:较大的缓冲区域大小。
    • ratePrev:上一个选择的比特率。
    • rateMap:用于存储不同缓冲区水平下的比特率。
  4. 实现细节

    • 规则通过获取媒体信息、ABR 控制器和 Dash 指标等来执行决策。
    • 通过调整当前缓冲区水平来选择适当的比特率。
    • 记录每次的选择,以便下一次选择时使用。
  5. 日志输出

    • 在选择比特率时输出相关的日志信息,包括当前缓冲区水平、比特率列表、上一个选择的比特率、下一个选择的比特率和选择的质量等信息。

这种规则的核心思想是根据当前缓冲区的状态来调整比特率,以平衡视频质量和播放的连续性。Buffer水平与视频速率的关系如下图所示。

限速条件下测试Dash系统

linux系统自带的tc命令可以对网络带宽进行限制。

首先可以执行下面命令来获取网卡名称:

ifconfig

其中,ens33即为网卡名称,下面的192.168.133.128为主机ip地址。

执行下面的命令可以为网卡限制带宽:

tc qdisc add dev ens33 root tbf rate 500Kbit latency 50ms burst 15kb
#将eth0网卡限速到500Kbit/s,15bk的buffer,TBF最多产生50ms的延迟
#tbf是Token Bucket Filter的简写,适合于把流速降低到某个值

执行下面命令可以取消限制:

tc qdisc del dev ens33 root

单一限速条件

无限速条件

可以看到缓冲区大小稳定的上下波动,视频播放为最高质量,说明此时网络较为稳定且网速较快,可以完成“边下边播”的任务。

限速300kb,15kb buffer,最大50ms延迟

此时网速较慢,视频很卡顿。视频质量和buffer水平都很低。

限速800kb,15kb buffer,最大50ms延迟

此时视频质量和buffer水平较300kb时均有上升,但视频质量仍然一般。

模拟网络波动情况

在服务器端运行该python文件,来控制网速不断发生变化。

#控制带宽随时间变化
import os
import json
import time

tpt = [300,500,700,900,1200,1800,2000,1500,1200,900,700,500,300]

while 1:
    for v in tpt:
        os.system("sudo tc qdisc add dev eth0 root tbf rate {}kbit latency 50ms burst 15kb".format(v))
        print(v)
        time.sleep(5)
        os.system("sudo tc qdisc del dev eth0 root")

buffer水平和视频码率变化如下图所示:

可以看出,当buffer水平升高时,会导致视频切换到更高的码率;buffer水平下降时,会让视频切换到更低的码率。同时buffer也在不断的消耗和补充。