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。下面是算法的基本原理和解释:
-
缓冲区分级:
- 规则将缓冲区分为两个区间:reservoir 和 cushion。Reservoir 是一个较小的缓冲区域,cushion 是一个较大的缓冲区域。
- 如果当前缓冲区低于 reservoir,则会选择最高比特率。
- 如果当前缓冲区高于 cushion + reservoir,则会选择最低比特率。
- 在两个区间之间,根据当前缓冲区的位置,线性地分配比特率。
-
比特率调整:
- 如果当前缓冲区低于 reservoir,选择最高比特率。
- 如果当前缓冲区高于 cushion + reservoir,选择最低比特率。
- 如果当前缓冲区在两个区间之间,则根据当前缓冲区的位置线性地调整比特率。
- 计算当前缓冲区在两个区间之间的相对位置: 首先,算法计算当前缓冲区水平相对于 reservoir 的位置,即当前缓冲区水平减去 reservoir。然后,它将这个相对位置除以 cushion 减去 reservoir,得到一个介于 0 到 1 之间的值,表示当前缓冲区在两个区间之间的相对位置。
- 线性插值: 然后,算法使用这个相对位置来进行线性插值。它将相对位置乘以 bitrateList 中相邻比特率的差异,然后加上 reservoir 对应的比特率。这样就得到了一个介于最小和最大比特率之间的插值比特率,这个插值比特率取决于当前缓冲区的水平。
- 选择比特率: 最后,根据线性插值得到的比特率,算法将其作为下一个选择的比特率。这个插值比特率在两个区间之间提供了一个平滑的过渡,使得在缓冲区水平变化时,比特率的调整更加连续和平稳。
-
变量:
- reservoir:较小的缓冲区域大小。
- cushion:较大的缓冲区域大小。
- ratePrev:上一个选择的比特率。
- rateMap:用于存储不同缓冲区水平下的比特率。
-
实现细节:
- 规则通过获取媒体信息、ABR 控制器和 Dash 指标等来执行决策。
- 通过调整当前缓冲区水平来选择适当的比特率。
- 记录每次的选择,以便下一次选择时使用。
-
日志输出:
- 在选择比特率时输出相关的日志信息,包括当前缓冲区水平、比特率列表、上一个选择的比特率、下一个选择的比特率和选择的质量等信息。
这种规则的核心思想是根据当前缓冲区的状态来调整比特率,以平衡视频质量和播放的连续性。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也在不断的消耗和补充。