dashjs

dashjs的学习笔记。

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的目录结构:
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的例子 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,不多说。

建议不要用于商业用途, 转载请注明原文地址: https://Soo-Q6.github.io/blog/2019-12-27-dashjs/


© 2019. All rights reserved.

Powered by shouqin v1.0