H5直播系列十 flv.js源码之IO部分

一、loader.js

export class BaseLoader 有一些基本属性和回调

Loader has callbacks which have following prototypes:

  • funcction onContentLengthKnown(contentLength: number): void
  • funcction onURLRedirect(url: string): void
  • funcction onDataArrival(chunk: ArrayBuffer, byteStart: number, receivedLength: number): void
  • funcction onError(errorType: number, errorInfo: {code: number, msg: string}): void
  • funcction onComplete(rangeFrom: number, rangeTo: number): void
二、BaseLoader的子类

参见io-controller.js,如果xhr连responseType=arraybuffer也不支持,那么就用不了这个库

_selectLoader() {
    if (this._config.customLoader != null) {
        this._loaderClass = this._config.customLoader;
    } else if (this._isWebSocketURL) {
        this._loaderClass = WebSocketLoader;
    } else if (FetchStreamLoader.isSupported()) {
        this._loaderClass = FetchStreamLoader;
    } else if (MozChunkedLoader.isSupported()) {
        this._loaderClass = MozChunkedLoader;
    } else if (RangeLoader.isSupported()) {
        this._loaderClass = RangeLoader;
    } else {
        throw new RuntimeException('Your browser doesn\'t 
        support xhr with arraybuffer responseType!');
    }
}

1.websocket-loader.js
class WebSocketLoader extends BaseLoader
websocket基础知识参考HTML5 WebSocket

    _dispatchArrayBuffer(arraybuffer) {
        let chunk = arraybuffer;
        let byteStart = this._receivedLength;
        this._receivedLength += chunk.byteLength;

        if (this._onDataArrival) {
            this._onDataArrival(chunk, byteStart, this._receivedLength);
        }
    }

2.在JS异步处理系列二 XHR Fetch中,介绍了Fetch的流式传输在直播/点播中很重要

我们可以当数据进来时就缓存下来,而且我们也不必等到数据全部读取完毕才显示内容。使响应体流式化减少了该站点的内存占用,并在网络连接很慢时为展示内容提供了更快的感知速度。而 XHR 只能缓存整个响应体,不能以小块的形式操作数据。虽然用 XHR 建立一个流是有可能的,然而这会导致 responseText 持续增长,而且你必须不断手动地从中获取数据。除此之外,当在流式传输时,Fetch APIs 还提供了访问数据的实际字节的方法,而 XHR 的 responseText 只有文本形式,这意味着在某些场景下它的作用可能非常有限。

3.fetch-stream-loader.js
class FetchStreamLoader extends BaseLoader
优先考虑使用fetch来加载

fetch + stream IO loader. Currently working on chrome 43+.
fetch provides a better alternative http API to XMLHttpRequest

if (this._onDataArrival) {
    this._onDataArrival(chunk, byteStart, this._receivedLength);
}

4.xhr-moz-chunked-loader.js
class MozChunkedLoader extends BaseLoader

For FireFox browser which supports xhr.responseType = 'moz-chunked-arraybuffer'

如果fetch不支持,优先考虑moz-chunked-arraybuffer,这个可以参考WebKit equivalent to Firefox's “moz-chunked-arraybuffer” xhr responseType

5.xhr-range-loader.js
class RangeLoader extends BaseLoader

Universal IO Loader, implemented by adding Range header in xhr's request header

这里参考XMLHttpRequest 206 Partial Content

var xhr = new XMLHttpRequest;

xhr.onreadystatechange = function () {
  if (xhr.readyState != 4) {
    return;
  }
  alert(xhr.status);
};

xhr.open('GET', 'http://fiddle.jshell.net/img/logo.png', true);
 // the bytes (incl.) you request
xhr.setRequestHeader('Range', 'bytes=100-200');
xhr.send(null);

You have to make sure that the server allows range requests, though.

在用 FileSystem API 实现文件下载器中提到了大文件分段下载:
要实现并发下载,首先要合理分配任务。HTTP 协议中规定可以使用请求头的 Range 字段指定请求资源的范围。例如服务端收到「Range : bytes=10-100」这样的请求头,只需要返回资源的 10-100 字节这部分就可以了,这样的响应状态码为 206。有些服务器不支持 Range,本文继续忽略。比如在在 Nginx 配置文件中,add_header Access-Control-Allow-Headers Range;

这里使用Range方式的,封装到range-seek-handler.js中。还有一种使用param方式的,也就是url后面跟?,然后加bstart和bend=xxx的,封装到param-seek-handler.js,在io-controller.js中可以看到:

_selectSeekHandler() {
    let config = this._config;

    if (config.seekType === 'range') {
        this._seekHandler = new RangeSeekHandler(this._config.rangeLoadZeroStart);
    } else if (config.seekType === 'param') {
        let paramStart = config.seekParamStart || 'bstart';
        let paramEnd = config.seekParamEnd || 'bend';

        this._seekHandler = new ParamSeekHandler(paramStart, paramEnd);
    } else if (config.seekType === 'custom') {
        if (typeof config.customSeekHandler !== 'function') {
            throw new InvalidArgumentException('Custom seekType specified in config but invalid customSeekHandler!');
        }
        this._seekHandler = new config.customSeekHandler();
    } else {
        throw new InvalidArgumentException(`Invalid seekType in config: ${config.seekType}`);
    }
}

6.xhr-msstream-loader.js 目前在源码中未使用此类
class MSStreamLoader extends BaseLoader

For IE11/Edge browser by microsoft which supports xhr.responseType = 'ms-stream'

三、nginx服务器试一下Range方式

搭建服务器可以参考H5直播系列六 flv.js demo
1.配置nginx.conf
这里为了在Chrome里测试方便,直接把io-controller.js里的_selectLoader中部分代码修改:

else if (FetchStreamLoader.isSupported()) {
            // this._loaderClass = FetchStreamLoader;
            this._loaderClass = RangeLoader;
        } 

设置了add_header Access-Control-Allow-Headers Range;之后,是不行的,会遇到405,也就是nginx没配置OPTIONS请求。参考flv.js/issues/159 快进播放,出现跨越问题,这里理论知识没仔细看,可以参考阮一峰 跨域资源共享 CORS 详解。

说一下怎么解决的吧,先是参考Nginx跨域配置,支持DELETE,PUT请求

location / {
    if ($request_method = 'OPTIONS') { 
        add_header Access-Control-Allow-Origin *; 
        add_header Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS;
        return 204; 
    }

但是没有效果哎,原因不明……然后参考nginx静态服务器405报错和nginx Cors跨域请求OPTIONS方法405 Method Not Allowed问题

error_page   405 =200 @405;
location @405{
            add_header Content-Length 0;
            add_header Content-Type text/plain;
            add_header Access-Control-Allow-Headers *;
            add_header Access-Control-Allow-Methods *;
            add_header Access-Control-Allow-Origin *;
            return 200;
        }

这样就可以了
2.简单观察
首先右键看一下要播放的文件jay.flv


image.png

第一次XHR请求

image.png

image.png

可以看到分段请求

四、直播流的支持度
static supportNetworkStreamIO() {
    let ioctl = new IOController({}, createDefaultConfig());
    let loaderType = ioctl.loaderType;
    ioctl.destroy();
    return loaderType == 'fetch-stream-loader' ||
    loaderType == 'xhr-moz-chunked-loader';
}
...
features.mseLiveFlvPlayback = features.mseFlvPlayback 
&& features.networkStreamIO;

这样的话,连loaderType==websocket-loader也给排除了??

五、io-controller.js

在上面的各种loader分析中,最后抛出数据都是给onDataArrival,在io-controller.js中,实际由_onLoaderChunkArrival来接管

_createLoader() {
    this._loader = new this._loaderClass(this._seekHandler, this._config);
    if (this._loader.needStashBuffer === false) {
        this._enableStash = false;
    }
    this._loader.onContentLengthKnown = this._onContentLengthKnown.bind(this);
    this._loader.onURLRedirect = this._onURLRedirect.bind(this);
    this._loader.onDataArrival = this._onLoaderChunkArrival.bind(this);
    this._loader.onComplete = this._onLoaderComplete.bind(this);
    this._loader.onError = this._onLoaderError.bind(this);
}

在_onLoaderChunkArrival中这样一段:

this._speedSampler.addBytes(chunk.byteLength);

// adjust stash buffer size according to network speed dynamically
let KBps = this._speedSampler.lastSecondKBps;
if (KBps !== 0) {
    let normalized = this._normalizeSpeed(KBps);
    if (this._speedNormalized !== normalized) {
        this._speedNormalized = normalized;
        this._adjustStashSize(normalized);
    }
}

作者在 使用flv.js做直播中回复这样解释:

在 flv.js 的 IOController 中,有一个用于缓存数据的 stashBuffer。buffer 大小会根据实时网速动态适应调整,以维持一个较合适的向外 dispatch buffer 的频率,减少解析频率来降低开销。

这里提到的_speedSampler,就是speed-sampler.js(Utility class to calculate realtime network I/O speed)

这个缓存功能,默认是打开的

export const defaultConfig = {
    enableWorker: false,
    enableStashBuffer: true,
    stashInitialSize: undefined,

// default initial size: 384KB
this._stashInitialSize = 1024 * 384;  
if (config.stashInitialSize != undefined
 && config.stashInitialSize > 0) {
    // apply from config
    this._stashInitialSize = config.stashInitialSize;
}

你可能感兴趣的:(H5直播系列十 flv.js源码之IO部分)