一、默认ABR算法寻径
1. 寻找默认ABRRule文件
VS Code打开dash.js可以找到默认ABR算法源代码位置:/src/streaming/rules/abr
主要规则有:ThroughputRule.js,BolaRule.js(根据isDynamic()选择是否进行主要规则的动态使用);
次要规则有:AbandonRequestRule.js,DroppedFramesRule.js,InsufficientBufferRule.js,SwitchHistoryRule.js(默认状态下播放器将对比所在主要规则和所有次要规则的使用效果)。
2. 默认ABRRule文件调用过程回溯
以ThroughputRule为例,检索所有源码中ThroughputRule的引用情况,可以找到ABRRulesCollection.js中有import该文件。实际上,在ABRRulesCollection.js的import list中,可以看到所有默认ABRRule都被引用。
import ThroughputRule from './ThroughputRule';
import InsufficientBufferRule from './InsufficientBufferRule';
import AbandonRequestsRule from './AbandonRequestsRule';
import DroppedFramesRule from './DroppedFramesRule';
import SwitchHistoryRule from './SwitchHistoryRule';
import BolaRule from './BolaRule';
import FactoryMaker from '../../../core/FactoryMaker';
import SwitchRequest from '../SwitchRequest';
其中,ABRRulesCollection.js把所有ABR算法都归类为qualitySwitchRules。
const QUALITY_SWITCH_RULES = 'qualitySwitchRules';
在ABRRulesCollection.js中不难找到initialize()方法,可以看到该方法定义了qualitySwitchRules数组,并将所有import的ABRRule都push进去了,当然前提是Settings.js中的属性useDefaultABRRules为true。这里先忽略后面加入自定义ABRRule的步骤,后文会讲。
function initialize() {
qualitySwitchRules = [];
abandonFragmentRules = [];
if (settings.get().streaming.abr.useDefaultABRRules) {
// Only one of BolaRule and ThroughputRule will give a switchRequest.quality !== SwitchRequest.NO_CHANGE.
// This is controlled by useBufferOccupancyABR mechanism in AbrController.
qualitySwitchRules.push(
BolaRule(context).create({
dashMetrics: dashMetrics,
mediaPlayerModel: mediaPlayerModel,
settings: settings
})
);
qualitySwitchRules.push(
ThroughputRule(context).create({
dashMetrics: dashMetrics
})
);
qualitySwitchRules.push(
InsufficientBufferRule(context).create({
dashMetrics: dashMetrics
})
);
qualitySwitchRules.push(
SwitchHistoryRule(context).create()
);
qualitySwitchRules.push(
DroppedFramesRule(context).create()
);
abandonFragmentRules.push(
AbandonRequestsRule(context).create({
dashMetrics: dashMetrics,
mediaPlayerModel: mediaPlayerModel,
settings: settings
})
);
}
// add custom ABR rules if any
const customRules = mediaPlayerModel.getABRCustomRules();
customRules.forEach(function (rule) {
if (rule.type === QUALITY_SWITCH_RULES) {
qualitySwitchRules.push(rule.rule(context).create());
}
if (rule.type === ABANDON_FRAGMENT_RULES) {
abandonFragmentRules.push(rule.rule(context).create());
}
});
}
既然所有ABRRule都被放入ABRRulesCollection的qualitySwitchRules中,不妨检索ABRRulesCollection的引用情况,可以看到AbrController.js中有ABRRulesCollection初始化的步骤。
function createAbrRulesCollection() {
abrRulesCollection = ABRRulesCollection(context).create({
dashMetrics: dashMetrics,
mediaPlayerModel: mediaPlayerModel,
settings: settings
});
abrRulesCollection.initialize();
}
另外通过进一步检索可以找到MediaPlayer.js里面有AbrController的设置步骤。
function createPlaybackControllers() {
// ...
abrController.setConfig({
streamController: streamController,
domStorage: domStorage,
mediaPlayerModel: mediaPlayerModel,
dashMetrics: dashMetrics,
adapter: adapter,
videoModel: videoModel,
settings: settings
});
abrController.createAbrRulesCollection();
// ...
}
到这一步ABRRule的调用过程回溯就差不多了,因为MediaPlayer.js就是集成整个客户端媒体播放器大部分实现方法的地方,包括一些常用的get方法也能在这里找到。对于默认ABRRule的调用过程我们只需要知道个大概,主要是在进行自定义修改前最好可以知道整个框架是怎么走下来的,也对之后自定义时各种方法定义和接口调用有一个大概的了解。
3. 自定义ABRRule文件的使用
因为篇幅限制本文暂不阐述自定义ABRRule的代码实现,先说明一下在已经有了代码实现的情况下怎么把它放入dash.js中使用。本文以dash.js自带的自定义ABRRule文件LowestBitrateRule.js作为示例,这个文件是我在dash.js samples文档中学习Advanced section的Custom ABR Rules时找到的,通过网址栏找到dash.js中该网页HTML文件的位置,用Chrome进入示例播放器页面按F12查看HTML源码可以找到LowestBitrateRule.js和该index.html在同一个目录下:/samples/advanced/abr/LowestBitrateRule.js。
回看ABRRulesCollection.js中加入自定义ABRRule的步骤:
function initialize() {
qualitySwitchRules = [];
abandonFragmentRules = [];
// ...
// add custom ABR rules if any
const customRules = mediaPlayerModel.getABRCustomRules();
customRules.forEach(function (rule) {
if (rule.type === QUALITY_SWITCH_RULES) {
qualitySwitchRules.push(rule.rule(context).create());
}
if (rule.type === ABANDON_FRAGMENT_RULES) {
abandonFragmentRules.push(rule.rule(context).create());
}
});
}
这里可以看到所有加入的自定义ABRRule都在rule里,而function(rule)是customRules.forEach()的参数,customRules由mediaPlayerModel.getABRCustomRules()返回值确定。再次通过检索可以找到mediaPlayerModel.js中关于ABRCustomRule的方法:
function MediaPlayerModel() {
let instance,
UTCTimingSources,
xhrWithCredentials,
customABRRule;
const DEFAULT_UTC_TIMING_SOURCE = {
scheme: 'urn:mpeg:dash:utc:http-xsdate:2014',
value: 'http://time.akamai.com/?iso&ms'
};
const context = this.context;
const settings = Settings(context).getInstance();
function setup() {
UTCTimingSources = [];
xhrWithCredentials = {
default: DEFAULT_XHR_WITH_CREDENTIALS
};
customABRRule = [];
}
//TODO Should we use Object.define to have setters/getters? makes more readable code on other side.
function findABRCustomRuleIndex(rulename) {
let i;
for (i = 0; i < customABRRule.length; i++) {
if (customABRRule[i].rulename === rulename) {
return i;
}
}
return -1;
}
function getABRCustomRules() {
return customABRRule;
}
function addABRCustomRule(type, rulename, rule) {
if (typeof type !== 'string' || (type !== ABRRulesCollection.ABANDON_FRAGMENT_RULES && type !== ABRRulesCollection.QUALITY_SWITCH_RULES) ||
typeof rulename !== 'string') {
throw Constants.BAD_ARGUMENT_ERROR;
}
let index = findABRCustomRuleIndex(rulename);
if (index === -1) {
// add rule
customABRRule.push({
type: type,
rulename: rulename,
rule: rule
});
} else {
// update rule
customABRRule[index].type = type;
customABRRule[index].rule = rule;
}
}
function removeABRCustomRule(rulename) {
if (rulename) {
let index = findABRCustomRuleIndex(rulename);
//if no rulename custom rule has been found, do nothing
if (index !== -1) {
// remove rule
customABRRule.splice(index, 1);
}
} else {
//if no rulename is defined, remove all ABR custome rules
customABRRule = [];
}
}
// ...
}
其中,findABRCustomRuleIndex(rulename)用于检索现有customABRRule里有没有和CustomRule一样的rulename,如果没有则返回-1,如果有则返回相应index。addABRCustomRule(type, rulename, rule)用于将CustomRule加入到customABRRule集合中,如果index为-1则push一个新Rule,否则更新现有同名的Rule。getABRCustomRules()用于返回customABRRule集合。
那么问题来了,我们如何调用addABRCustomRule(type, rulename, rule)。之后通过查阅dash.js的API documentation可以发现这个方法可以直接再HTML代码中调用。以下给出示例完整的HTML代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>Custom ABR Rules</title>
<script src="../../../dist/dash.all.debug.js"></script>
<!--dash.all.min.js should be used in production over dash.all.debug.js
Debug files are not compressed or obfuscated making the file size much larger compared with dash.all.min.js-->
<!--<script src="../../dist/dash.all.min.js"></script>-->
<script src="LowestBitrateRule.js" class="code"></script>
<script class="code">
function init() {
var video,
player,
url = "https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd";
video = document.querySelector("video");
player = dashjs.MediaPlayer().create();
/* don't use dash.js default rules */
player.updateSettings({
'streaming': {
'abr': {
'useDefaultABRRules': false
}
}
});
/* add my custom quality switch rule. Look at LowestBitrateRule.js to know more */
/* about the structure of a custom rule */
player.addABRCustomRule('qualitySwitchRules', 'LowestBitrateRule', LowestBitrateRule);
player.initialize(video, url, true);
}
</script>
<style>
video {
width: 640px;
height: 360px;
}
</style>
</head>
<body>
<div>
<video></video>
</div>
<script>
document.addEventListener("DOMContentLoaded", function () {
init();
});
</script>
<script src="../../highlighter.js"></script>
</body>
</html>
可以发现示例网页中直接把LowestBitrateRule.js作为src脚本导入,然后通过player.addABRCustomRule()调用API进行自定义ABRRule嵌入。值得注意的是在此之前代码中调用player.updateSettings()把默认Settings中的useDefaultABRRules设为false,即屏蔽所有默认ABRRule,这样在拉流的时候播放器只会按照LowestBitrateRule拉取最低码率的视频,通过运行HTML网页可以测试调用成功。