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;
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容

  • 今天累到炸,第一次尝试骑行,骑到终点,来回40公里,第一次能这么有毅力的坚持下去还是很好的,后果就是全身酸痛。照顾...
    sunny蝴蝶阅读 187评论 0 0
  • 高考结束,对很多人来说,大学生活也就即将到来,笔记本电脑算是必需品了,没用的人毕竟还是少数,笔记本行业相比手机行业...
    装的阅读 1,176评论 6 30
  • 儿童节
    安言靜语阅读 166评论 0 0
  • 浪花拍打着甲板 哗啦,哗啦 他头戴军帽 独坐于窗前 手中的照片里的女人眼底含笑 婴孩吮吸手指 他嘴角倏地翘起 眼中...
    汀洲_阅读 497评论 5 3