dashjs
in 个人博客
dash.js是一个MPEG-DASH视频播放器。在这里记录一下dash.js的学习过程吧。本文参考的dash.js的版本是3.0.0(Vesion.js中定义的)
开始使用dash.js
使用教程可以访问官网,这里会出现一些问题,就是初次编译的时候会出现一些缺失npm包的问题(某些网络原因),可以手动下载放到对应的目录就行。
另外在修改dash的代码时,注意代码的风格,一般使用编辑器的自动格式排版就好了,或者使用--force
命令选项。
dash.js的文件目录
dash.js目录下主要有两个子目录,/smaples和/src,前者是前端展示逻辑相关的,后者是播放器的一些核心代码(播放控制,网络,码率决策等)。
samples目录
该目录下主要关注的是dash-if-reference-player
这个目录。该目录下,index.html是前端显示页面,dashjs_config.json是页面加载时会loading的一些默认配置(这个文件是最终的默认配置,不管是Setting.js还是main.js中的默认配置都是没用的),app/main.js是index.html页面的控制逻辑,包括以下初始化的工作和显示逻辑,app/sources.json是添加播放源的地方(另外根据不同的播放源设置不同的播放参数,比如缓冲区长度等)。
src目录
/src目录下主要关注/core, /dash和/streaming这三目录,后面再细聊。/mss目录应该是MicroSoft Smooth Streaming相关的代码,这里不做分析。
dash.js核心代码分析
/core: 这个目录中主要是定义一些系统相关的类,比如FactoryMaker
, Debug
, Setting
以及一些Errors和Events的定义。Setting.js
可以在系统运行过程中获取系统的设置,这些设置都是全局的,可以在任何地方被修改,也可以在任何地方被获取。
/dash: 这个目录主要涉及的是DASH规范,包括一些Segment,timeline等,应该是DASH的核心逻辑(编码相关的),这里不多赘述,先埋一个坑吧。
/streaming: 这个目录就和MPEG-DASH不那么相关了,属于流传输相关的内容,包括调度逻辑,ABR规则,系统运行逻辑,网络请求,播放器状态等。后面会着重分析这一块的代码。
FactoryMaker
dash.js的核心代码中每一个类都需要通过FactoryMaker进行注册, 比如ScheduleController
的创建:
ScheduleController.__dashjs_factory_name = 'ScheduleController';
export default FactoryMaker.getClassFactory(ScheduleController);
上面的两行代码是必须的,当然还有其它的赋值操作(比如常量等)
dash.js中对象之间的协作包括输入输出两方面,输入即在创建对象的时候将其他对象以构造函数参数的形式传入,输出即当前对象能够提供的接口。下面以ScheduleController
的为例介绍
ScheduleController
的创建会传入模型和控制器作为参数,下面是dash.js中创建一个ScheduleController
对象的方法
// StreamProcessor.js
scheduleController = ScheduleController(context).create({
type: type,
mimeType: mimeType,
adapter: adapter,
dashMetrics: dashMetrics,
timelineConverter: timelineConverter,
mediaPlayerModel: mediaPlayerModel,
abrController: abrController,
playbackController: playbackController,
streamController: streamController,
textController: textController,
streamProcessor: instance,
mediaController: mediaController,
settings: settings,
metricsModel: dashMetrics.getMetricsModel()
});
再看看ScheduleController
的定义:
function ScheduleController(config) {
config = config || {};
const context = this.context;
const eventBus = EventBus(context).getInstance();
const adapter = config.adapter;
const dashMetrics = config.dashMetrics;
const timelineConverter = config.timelineConverter;
const mediaPlayerModel = config.mediaPlayerModel;
const abrController = config.abrController;
const playbackController = config.playbackController;
const streamController = config.streamController;
const textController = config.textController;
const type = config.type;
const streamProcessor = config.streamProcessor;
const mediaController = config.mediaController;
const settings = config.settings;
const metricsModel1 = config.metricsModel;
// ... others
}
dash.js中类都是通过ClassName(context).create({})
来创建的,其中create
应该是FactoryMaker中默认的创建方法,这里没有找到明确的定义,create
中传入的参数才是真正的类定义中所需的参数,而context
目前没看出有什么作用,字面理解是当前的运行环境?
ScheduleController
也会对外提供一些接口:
instance = {
initialize: initialize,
getType: getType,
setSeekTarget: setSeekTarget,
setTimeToLoadDelay: setTimeToLoadDelay,
replaceRequest: replaceRequest,
switchTrackAsked: switchTrackAsked,
isStarted: isStarted,
start: start,
stop: stop,
reset: reset,
getBufferTarget: getBufferTarget,
finalisePlayList: finalisePlayList,
getInitPushSegments: getInitPushSegments,
getLastEndBufferLength: getLastEndBufferLength,
getLastStartBufferLength: getLastStartBufferLength,
getDuration: getDuration
};
return instance;
大概就是通过这个方式来显式的标识当前类的public方法和属性,一般通过“instance”的内容就可以大概判断出这个类的作用是什么。
streaming
先看看/streaming的目录结构:
这里可以先从controllers开始,比较重要的有:AbrController.js
, BufferController.js
, ScheduleController.js
,分别和码率自适应,缓冲区管理,播放器调度相关。
AbrController
先说码率自适应的控制器。
这里主要关注AbrController.checkPlaybackQuality()
就好了,该方法也只需要关注
const switchRequest = abrRulesCollection.getMaxQuality(rulesContext);
上面的代码中,SwitchRequest
就是经过码率自适应算法计算得出的码率切换结果,结果存储在SwitchRequest.quality
中(码率级别的index)。AbrRulesCollection
主要就是管理各个rules,包括确定哪些rules是生效的,以及从已经生效的rule中选择“合适”的码率作为结果,具体可以参考AbrRulesCollection.getMaxQuality(rulesContext)
。
各种规则定义在/rules目录下,rule的定义有特定的结构,即都是通过getMaxIndex()
方法返回码率选择的结果(这和旧版本不同)。
BufferController
一般不会在这个类上进行改动,通常用来获取缓冲区信息。
ScheduleController
这个涉及的就是播放器的调度规则了。主要考虑下载规则
function schedule() {
const bufferController = streamProcessor.getBufferController();
if (isStopped || isFragmentProcessingInProgress || !bufferController ||
(playbackController.isPaused() && !settings.get().streaming.scheduleWhilePaused) ||
((type === Constants.FRAGMENTED_TEXT || type === Constants.TEXT) && !textController.isTextEnabled())) {
logger.debug('Schedule stop!');
return;
}
if (bufferController.getIsBufferingCompleted()) {
logger.debug('Schedule stop because buffering is completed!');
return;
}
validateExecutedFragmentRequest();
const isReplacement = replaceRequestArray.length > 0;
const streamInfo = streamProcessor.getStreamInfo();
if (bufferResetInProgress || isNaN(lastInitQuality) || switchTrack || isReplacement ||
hasTopQualityChanged(currentRepresentationInfo.mediaInfo.type, streamInfo.id) ||
bufferLevelRule.execute(streamProcessor, streamController.isTrackTypePresent(Constants.VIDEO))) {
const getNextFragment = function () {
if ((currentRepresentationInfo.quality !== lastInitQuality || switchTrack) && (!bufferResetInProgress)) {
logger.debug('Quality has changed, get init request for representationid = ' + currentRepresentationInfo.id);
if (switchTrack) {
bufferResetInProgress = mediaController.getSwitchMode(type) === MediaController.TRACK_SWITCH_MODE_ALWAYS_REPLACE ? true : false;
logger.debug('Switch track has been asked, get init request for ' + type + ' with representationid = ' + currentRepresentationInfo.id + 'bufferResetInProgress = ' + bufferResetInProgress);
streamProcessor.switchInitData(currentRepresentationInfo.id, bufferResetInProgress);
switchTrack = false;
} else {
streamProcessor.switchInitData(currentRepresentationInfo.id);
}
lastInitQuality = currentRepresentationInfo.quality;
} else {
const replacement = replaceRequestArray.shift();
if (replacement && replacement.isInitializationRequest()) {
// To be sure the specific init segment had not already been loaded
streamProcessor.switchInitData(replacement.representationId);
} else {
let request;
// Don't schedule next fragments while pruning to avoid buffer inconsistencies
if (!streamProcessor.getBufferController().getIsPruningInProgress()) {
request = nextFragmentRequestRule.execute(streamProcessor, seekTarget, replacement);
setSeekTarget(NaN);
if (request && !replacement) {
if (!isNaN(request.startTime + request.duration)) {
streamProcessor.setIndexHandlerTime(request.startTime + request.duration);
}
request.delayLoadingTime = new Date().getTime() + timeToLoadDelay;
setTimeToLoadDelay(0);
}
if (!request && streamInfo.manifestInfo && streamInfo.manifestInfo.isDynamic) {
logger.debug('Next fragment seems to be at the bleeding live edge and is not available yet. Rescheduling.');
}
}
if (request) {
logger.debug('Next fragment request url is ' + request.url);
fragmentModel.executeRequest(request);
} else { // Use case - Playing at the bleeding live edge and frag is not available yet. Cycle back around.
setFragmentProcessState(false);
startScheduleTimer(settings.get().streaming.lowLatencyEnabled ? 100 : 500);
}
}
}
};
setFragmentProcessState(true);
if (!isReplacement && !switchTrack) {
abrController.checkPlaybackQuality(type);
}
getNextFragment();
} else {
startScheduleTimer(500);
}
}
关键逻辑就是schedule()
方法。这里涉及到码率决策,url的生成以及触发http请求等操作。 此外还应该关注的是一个下载完成的监听事件:
function onFragmentLoadingCompleted(e) {
if (e.sender !== fragmentModel) {
return;
}
logger.info('OnFragmentLoadingCompleted - Url:', e.request ? e.request.url : 'undefined',
', Range:', e.request.range ? e.request.range : 'undefined');
if (adapter.getIsTextTrack(type)) {
setFragmentProcessState(false);
}
if (e.error && e.request.serviceLocation && !isStopped) {
replaceRequest(e.request);
setFragmentProcessState(false);
startScheduleTimer(0);
}
if (bufferResetInProgress) {
mediaRequest = e.request;
}
}
其他
上图中框出来的还有其他相对重要的文件,但是代码可改动的地方都不多,也就不做分析了,像MediaPlayer
,StreamProcessor
等。下面主要再分析一些起着数据结构作用的类。
首先是HTTPRequest
。直接看代码吧(这里只保留比较重要的属性),
class HTTPRequest {
constructor() {
/**
* 请求的URL
* @public
*/
this.url = null;
/**
* 请求发送时间,可以通过getTime()获取到时间戳。
* @public
*/
this.trequest = null;
/**
* 请求接收到第一个字节的时间(响应时间)
* @public
*/
this.tresponse = null;
/**
* Throughput traces的时间间隔,仅用于成功的请求,(trequest - _tfinish)
* @public
*/
this.interval = null;
/**
* Throughput traces, for successful requests only.
* 属于HTTPRequestTrace
* @public
*/
this.trace = [];
/**
* 请求完成时间
* @public
*/
this._tfinish = null;
}
}
再看看HTTPRequestTrace
class HTTPRequestTrace {
constructor() {
/**
* Real-Time | Measurement stream start.
* @public
*/
this.s = null;
/**
* Measurement stream duration (ms).
* @public
*/
this.d = null;
/**
* List of integers counting the bytes received in each trace interval within the measurement stream.
* 接收数据的大小(byte), 通常只有一个元素,即访问b[0]就好了。
* @public
*/
this.b = [];
}
}
看看HTTPRequest的例子
还有一个类似的数据结构FragmentRequest
,出现在ScheduleController.onFragmentLoadingCompleted(e)
中的e.request,看看代码(仅保留重要的属性):
class FragmentRequest {
constructor() {
this.mediaType = null;
this.mediaInfo = null; // 媒体信息,如下图所示
this.duration = NaN;
this.url = null;
this.requestStartDate = null; // 同HTTPRequest.trequest
this.firstByteDate = null; //同HTTPRequest.tresponse
this.requestEndDate = null; // 同HTTPRequest._tfinish
this.quality = NaN; // 码率级别
this.bytesTotal = NaN; // 请求数据的大小(byte)
}
}
当然,除了上面提到的,还有可能会用到的类,比如MetricsModel
通常可以用来定义一些需要全局使用的数据结构,辅助编程;比如XHRLoader
是用来发送http请求的,这里可以自定义http请求。等等…
暂时就写到这里吧。
补充一下dash.js是如何发送http请求的。
发送请求
dash.js是如何发送请求的(也就是下载逻辑)? 这要从ScheduleController.schedule()
说起。
下面一段代码是可以用来控制是否发送请求的:
if (bufferResetInProgress || isNaN(lastInitQuality) || switchTrack || isReplacement ||
hasTopQualityChanged(currentRepresentationInfo.mediaInfo.type, streamInfo.id) ||
bufferLevelRule.execute(streamProcessor, streamController.isTrackTypePresent(Constants.VIDEO))) {
比如说,bufferLevelRule.execute
就是用来判断缓冲区是否已满,从而影响播放器的下载。
ScheduleController.schedule()
中fragmentModel.executeRequest(request);
是发送请求的。下面分析一下,发送请求到接收请求,dash.js都做了什么。
从FragmentModel.executeRequest()
开始,它的参数request为FragmentRequest
。
// FragmentModel.js
function executeRequest(request) {
switch (request.action) {
// ...
case FragmentRequest.ACTION_DOWNLOAD:
addSchedulingInfoMetrics(request, FRAGMENT_MODEL_LOADING);
loadingRequests.push(request);
loadCurrentFragment(request);
break;
// ...
}
}
function loadCurrentFragment(request) {
eventBus.trigger(Events.FRAGMENT_LOADING_STARTED, {
sender: instance,
request: request
});
fragmentLoader.load(request);
}
可以看到,FragmentModel.executeRequest()
主要就是调用了FragmentLoader.load()
。往下看,
// FragmentLoader.js
function load(request) {
const report = function (data, error) {
eventBus.trigger(Events.LOADING_COMPLETED, {
request: request,
response: data || null,
error: error || null,
sender: instance
});
};
if (request) {
httpLoader.load({
request: request,
progress: function (event) {
eventBus.trigger(Events.LOADING_PROGRESS, {
request: request,
stream: event.stream
});
if (event.data) {
eventBus.trigger(Events.LOADING_DATA_PROGRESS, {
request: request,
response: event.data || null,
error: null,
sender: instance
});
}
},
success: function (data) {
report(data);
},
error: function (request, statusText, errorText) {
report(
undefined,
new DashJSError(
Errors.FRAGMENT_LOADER_LOADING_FAILURE_ERROR_CODE,
errorText,
statusText
)
);
},
abort: function (request) {
if (request) {
eventBus.trigger(Events.LOADING_ABANDONED, {request: request, mediaType: request.mediaType, sender: instance});
}
}
});
} else {
report(
undefined,
new DashJSError(
Errors.FRAGMENT_LOADER_NULL_REQUEST_ERROR_CODE,
Errors.FRAGMENT_LOADER_NULL_REQUEST_ERROR_MESSAGE
)
);
}
}
首先注意触发的Events.LOADING_COMPLETED
事件,该事件对应的响应是FragmentModel.onLoadingCompleted()
,然后继续触发Events.FRAGMENT_LOADING_COMPLETED
事件,到这里触发的响应是ScheduleController.onFragmentLoadingCompleted()
,也就是下载逻辑里请求响应后的处理函数。
回到正题,请求是继续传递给HTTPLoader.load()
,传进来的参数是一个对象,包含了前面的FragmentRequest
以及一些回调函数,这些函数都是用来触发一些事件的,也就是告知调度逻辑模块,现在请求的状态是什么。插一句题外话,不同模块之间,想要进行监听处理逻辑,可以使用回调函数(callback)来实现。
再看看HTTPLoader.load()
,
// HTTPLoader.js
/**
* Initiates a download of the resource described by config.request
* @param {Object} config - contains request (FragmentRequest or derived type), and callbacks
* @memberof module:HTTPLoader
* @instance
*/
function load(config) {
if (config.request) {
internalLoad(
config,
mediaPlayerModel.getRetryAttemptsForType(
config.request.type
)
);
} else {
if (config.error) {
config.error(config.request, 'error');
}
}
}
HTTPLoader.load()
主要是初始化了一个下载任务,config包含了一个FragmentRequest和一些callbacks,然后初始化逻辑在internalLoad()
中实现(代码太长了,只保留部分代码):
// HTTPLoader.js
function internalLoad(config, remainingAttempts) {
const request = config.request;
const traces = [];
let firstProgress = true;
let needFailureReport = true;
let requestStartTime = new Date();
let lastTraceTime = requestStartTime;
let lastTraceReceivedCount = 0;
let httpRequest;
if (!requestModifier || !dashMetrics || !errHandler) {
throw new Error('config object is not correct or missing');
}
const handleLoaded = function (success) {
// ...
};
const onloadend = function () {
// 当请求结束时触发, 无论请求成功(load)还是失败(abort或error)。
};
const progress = function (event) {
// 接收数据开始周期触发。
};
const onload = function () {
// XMLHttpRequest请求成功完成时触发。
};
const onabort = function () {
// 当 request 被停止时触发,例如当程序调用 XMLHttpRequest.abort() 时。
};
let loader;
if (useFetch && window.fetch && request.responseType === 'arraybuffer' && request.type === HTTPRequest.MEDIA_SEGMENT_TYPE) {
loader = FetchLoader(context).create({
requestModifier: requestModifier,
boxParser: boxParser
});
} else {
loader = XHRLoader(context).create({
requestModifier: requestModifier
});
}
const modifiedUrl = requestModifier.modifyRequestURL(request.url);
const verb = request.checkExistenceOnly ? HTTPRequest.HEAD : HTTPRequest.GET;
const withCredentials = mediaPlayerModel.getXHRWithCredentialsForType(request.type);
httpRequest = {
url: modifiedUrl,
method: verb,
withCredentials: withCredentials,
request: request,
onload: onload,
onend: onloadend,
onerror: onloadend,
progress: progress,
onabort: onabort,
loader: loader
};
// Adds the ability to delay single fragment loading time to control buffer.
let now = new Date().getTime();
if (isNaN(request.delayLoadingTime) || now >= request.delayLoadingTime) {
// no delay - just send
requests.push(httpRequest);
loader.load(httpRequest);
} else {
// delay
let delayedRequest = { httpRequest: httpRequest };
delayedRequests.push(delayedRequest);
delayedRequest.delayTimeout = setTimeout(function () {
if (delayedRequests.indexOf(delayedRequest) === -1) {
return;
} else {
delayedRequests.splice(delayedRequests.indexOf(delayedRequest), 1);
}
try {
requestStartTime = new Date();
lastTraceTime = requestStartTime;
requests.push(delayedRequest.httpRequest);
loader.load(delayedRequest.httpRequest);
} catch (e) {
delayedRequest.httpRequest.onerror();
}
}, (request.delayLoadingTime - now));
}
}
首先代码是从初始化loader开始的,也就是选择使用XMLHttpRequest
还是Fetch
,这里默认使用前者。然后下来定义httpRequest这个对象,包含url等属性和一些callbacks,这些callbacks在设置http请求回调时使用的。然后接下来的代码则是设置请求的延迟,默认是不设置的。
handleLoaded()
主要是在请求结束后记录一些请求数据的,比如,请求开始时间,firstByte时间,以及请求结束时间;然后还会往DashMetrics里面添加当前请求的一些信息。
接下来看看XMLHttpRequest
请求的回调函数们:
// HTTPLoader.js
const progress = function (event) {
const currentTime = new Date();
if (firstProgress) {
firstProgress = false;
if (!event.lengthComputable ||
(event.lengthComputable && event.total !== event.loaded)) {
request.firstByteDate = currentTime;
}
}
if (event.lengthComputable) {
request.bytesLoaded = event.loaded;
request.bytesTotal = event.total;
}
if (!event.noTrace) {
traces.push({
s: lastTraceTime,
d: event.time ? event.time : currentTime.getTime() - lastTraceTime.getTime(),
b: [event.loaded ? event.loaded - lastTraceReceivedCount : 0]
});
lastTraceTime = currentTime;
lastTraceReceivedCount = event.loaded;
}
if (config.progress && event) {
config.progress(event);
}
};
process()
主要是在接收到数据后触发的,从代码中看,主要是记录一些请求过程中的数据,比如traces
;不过涉及到时间的都是以客户端视角来看的,也就是说一个“完整”的网络请求的数据是基本正确的,但如果读取的是浏览器缓存的数据,则时间不具有参考价值(比如server push)。
// HTTPLoader.js
const onload = function () {
if (httpRequest.response.status >= 200 && httpRequest.response.status <= 299) {
handleLoaded(true);
if (config.success) {
config.success(httpRequest.response.response, httpRequest.response.statusText, httpRequest.response.responseURL);
}
if (config.complete) {
config.complete(request, httpRequest.response.statusText);
}
}
};
onload()
是在请求成功完成时触发的,主要就是调用handleLoaded()
记录请求成功的一些数据,然后触发前文提到的config
里面的回调,用来触发一些事件告知播放器当前请求的状态。
onloadend()
和onabort()
就不细说了…
下面主要看看XHRLoader.load()
,
// XHRLoader.js
function load(httpRequest) {
// Variables will be used in the callback functions
const requestStartTime = new Date();
const request = httpRequest.request;
let xhr = new XMLHttpRequest();
xhr.open(httpRequest.method, httpRequest.url, true);
if (request.pushSegments) {
xhr.setRequestHeader('pushSegments', request.pushSegments);
logger.debug('this is pushSegments ' + request.pushSegments);
}
xhr.onload = httpRequest.onload;
xhr.onloadend = httpRequest.onend;
xhr.onerror = httpRequest.onerror;
xhr.onprogress = httpRequest.progress;
xhr.onabort = httpRequest.onabort;
xhr.send();
httpRequest.response = xhr;
}
大概就是调用XMLHttpRequest.open()
, XMLHttpRequest.setRequestHeader()
, XMLHttpRequest.send()
等发送http请求的API,不多说。