春天的时候花椒作了一个创新项目, 这是一个直播综艺节目的项目,前端的工做主要是作出一个PC主站点,在这个站点中的首页须要一个播放器, 既能播放FLV直播视频流,还要在用户点击视频回顾按钮的时候, 弹出窗口播放HLS视频流;咱们开始开发这个播放器的时候也没有多想, 直接使用了你们都能想到的 最简单的套路,flv.js和hls.js一块儿用!在播放视频时,调用中间件video.js来输出的Player来实现播放,这个Player根据视频地址的结尾字符来初始化播放器:new HLS 或者 flvjs.createPlayer, 对外提供一致的接口,对HLS.js和FLV.js建立的播放器进行调用。完美的实现了产品的需求,不过写代码的时候总感受有点蠢,HLS.js(208KB)和FLV.js(169KB)体积加起来有点太让人热泪盈眶了。 这时咱们就有了一个想法,这两能不能合起来成为一个lib,既能播放flv视频,又能播放hls视频。理想很丰满,现实很骨感,这2个lib虽然都是JavaScript写的,可是它们的范畴都是视频类,之前只是调用, 彻底没有深刻了解过,不过咱们仍是在领导的大(wei)力(bi)支(li)持(you)下,开始了尝试。javascript
FLV.js的工做原理是下载flv文件转码成IOS BMFF(MP4碎片)片断, 而后经过Media Source Extensions
将MP4片断传输给HTML5的Video标签进行播放; 它的结构以下图所示:前端
src/flv.js 是对外输出FLV.js的一些组件, 事件和错误, 方便用户根据抛出的事件进行各类操做和获取相应的播放信息; 最主要是flv.js下返回的2个player: NativePlayer
和 FLVPlayer
; NativePlayer
是对浏览器自己播放器的一个再包装, 使之能和FLVPlayer
同样, 相应共同的事件和操做; 你们最主要使用的仍是FLVPlayer
这个播放器; 而 FLVPlayer
中最重要东西可分为两块: 1. MSEController; 2. Transmuxer;java
这个MSEController负责给HTML Video Element 和 SourceBuffer之间创建链接, 接受 InitSegment(ISO BMFF 片断中的 FTYP + MOOV)和 MediaSegment (ISO BMFF 片断中的 MOOF + MDATA); 将这2个片断按照顺序添加到SourceBuffer中, 和对SouceBuffer的一些控制和状态反馈;git
Transmuxer 主要负责的就是下载, 解码, 转码, 发送Segment的工做; 它的下面主要包含了 2个模块, TransmuxingWorker
和 TransmuxingController
; TransmuxingWorker
是启用多线程执行 TransmuxingController
, 并对 TransmuxingController
抛出的事件就行转发; TransmuxingController
才是真正执行 下载, 解码, 转码, 发送Segment的苦力部门, 苦活累活都是这个部门干的, Transmuxer
(真上级) 和 TransmuxingController
(伪上级)都是在调用它的功能和传递它的输出;程序员
下面有请这个劳苦功高的部门登场github
TransmuxingController
也是一个大部门, 他的手下有三个小组: IOController
, demuxer
和 remuxer
;浏览器
IOController主要有三个功能, 一是负责遴选他手下的小小弟(loaders), 选出最适合当前浏览器环境的loader, 去从服务器搬运媒体流; 二是存储小小弟(loader)发上来的数据; 三是把数据发送给demuxer(解码)并存储demuxer未处理完的数据;服务器
demuxer 是负责解码工做的员工, 他须要把IOController发送过来的FLV data, 解析整理成 videoTrack 和 audioTrack; 并把解析后的数据发送给 remuxer
转码器; 解码完成后, 他会把已经处理的数据的长度返回给 IOController, IOController会把未处理的数据(总数据 - 已经处理的数据)存储, 等待下次发送数据的时候发从头部追加未处理的数据, 一块儿发送给 demuxer.markdown
remuxer 是负责将 videoTrack 和 audioTrack 转成 InitSegment 和 MediaSegment并向上发送, 并在转化的过程当中进行音视频同步的操做.网络
总的流程就是 FLVPlayer喊了一声启动以后, loader 加载数据 => IOController 存储和转发数据 => demuxer 解码数据 => remuxer 转码数据 => TransmuxingWorker 和 Transmuxer 转发数据 =>
MSEController 接受数据 => SourceBuffer; 一系列操做以后视频就能够播放了;
HLS.js的工做原理是先下载index.m3u8文件, 而后解析该文档, 取出Level, 再根据Levels中的片断(Fragments)信息去下载相应的TS文件, 转码成IOS BMFF(MP4碎片)片断, 而后经过Media Source Extensions
将MP4片断传输给HTML5的Video标签进行播放;
HLS.js
的结构以下
相对于 flv.js的多层分级, hls.js到是有一点扁平化的味道, hls这个公司老总在继承 Observer 的trigger功能以后, 深刻各个部门(即各类controller和loader)发号施令(进行hls.trigger(HlsEvents.xxx, data)
的操做); 而各个部门继承EventHandler以后, 实例化时就分配好本身所负责的工做; 以 buffer-controller.js
为例:
constructor (hls: any) { super(hls, Events.MEDIA_ATTACHING, Events.MEDIA_DETACHING, Events.MANIFEST_PARSED, Events.BUFFER_RESET, Events.BUFFER_APPENDING, Events.BUFFER_CODECS, Events.BUFFER_EOS, Events.BUFFER_FLUSHING, Events.LEVEL_PTS_UPDATED, Events.LEVEL_UPDATED); this.config = hls.config; } 复制代码
buffer-controller.js
这个部门主要负责如下功能:
buffer-controller.js
初始化时就定义了本身只响应 Events.MEDIA_ATTACHING
, Events.MEDIA_DETACHING
等等这些工做, 它会本身实现 onMediaAttaching
, onMediaDetaching
等方法来响应和完成这些工做, 其余的一律无论, 它完成本身的任务后会经过hls向其余部门告知已经完成了本身的工做, 并将工做结果移交给其余部门, 例如 buffer-controller.js
中的 581行 this.hls.trigger(Events.BUFFER_FLUSHED)
, 这行代码就是向其余部门(其余controllers)告知已经完成BUFFER_FLUSHED
的工做;
注: 你们在读取hls.js的源码的时候, 看到 `this.hls.trigger(Events.xxxx)`时, 查找下一步骤时, 只要在所有代码中搜索 onXXX(去掉事件中的下划线) 方法便可找到下一步操做
复制代码
明白了HLS.JS代码的读取套路以后咱们能够更清晰的了解hls.js实现播放HLS流的大体过程了;
FLVPlayer
, 直接提供API, 响应外界的各类操做和发送信息; 在开始准备播放的时候它会发令HlsEvents.MANIFEST_LOADING
,LEVEL_LOADED
的事件并携带level信息;FRAG_LOADING
事件, 并初始化 解码器和转码器 (Demuxer对象, Remuxer会在Demuxer实例化中初始化)FRAG_LOADING
以后会去加载相应的TS文件, 并在加载TS文件完毕以后发出 FRAG_LOADED
事件, 并把TS的Uint8数据和fragment的其余信息一并发送出;stream-controller
接收 FRAG_LOADED
事件后, 他会调用它的 onFragLoaded
方法, 在这个方法中 demuxer 会解析 TS 的文件, 通过demuxer和remuxer的通力协做, 生成InitSegment(FRAG_PARSING_INIT_SEGMENT事件 所携带的数据) 和 MediaSegment(FRAG_PARSING_DATA事件 所携带的数据), 经由 steam-controller 传输给 buffer-controller, 最后添加进SourceBuffer;经过对FLV.js和HLS.js 进行分析, 它们共同的流程都是 下载, 解码, 转码, 传输给SourceBuffer; 同样的loader(FragmentLoader和FetchStreamLoader), 同样的解码和转码(demuxer和remuxer), 同样的 SourceBuffer Controller (MSEController 和 Buffer-controller ); 不一样的就是他们的控制流程不同, 还有hls流多了一步解析文档的步骤;
下面咱们就思考怎么去结合两个lib:
根据项目目的: 项目是一个主直播, 次点播的站点; FLV直播功能是最重要的功能, HLS流的回放只在用户点击视频回顾和查看过去节目视频才会使用;
根据其余项目的需求: 花椒的主站如今也是HTTP-FLV的形式去进行直播展现, 而HLS流计划用于播放主播小视频(点播);
根据业界状况: 如今业界直播基本仍是用的HTTP-FLV这种形式(基础设施成熟, 技术简单, 延迟小), 而HLS流通常仍是用在移动端直播;
因此咱们决定采用在 FLV.js 的基础上, 加上HLS.js中的 loader, demuxer 和 remuxer 这三部分去组成一个新的播放器library, 既能播放FLV视频, 也能播放HLS流(根据项目的须要只包含单码率流的直播和点播, 不包含多码率流, 自动切换码率, 解密等功能);
首先咱们先规划了一下内嵌的功能怎么接入:
HLS.js中加载HLS流须要 FragmentLoader, XHRLoader, M3U8Parser, LevelController, StreamController 这些, 其中 FragmentLoader 是控制XHR加载TS文件和反馈Fragment加载状态的组件, XHRLoader是执行加载 TS 文件和 playlist 文件 的组件, LevelController 是 选择符合当前码率的level 和 playlist加载间隔的, streamController是负责判断加载当前Level中哪一个TS文件的组件; 在接入FLV.js时, 须要 FragmentLoader 本身去承担 LevelController 和 StreamController 中相应的工做, 当 IOController 调用 startLoad 方法时, 它本身要去获取并解析playlist, 存储 Level的详细信息, 选择Level, 经过判断 Fragment 的 sequenceNum 来获取下一个TS文件地址, 让XHRLoader 去加载; (FragmentLoader 这娃来到了新公司, 身上担子变重了).
由于FLV和TS文件的解析方式不一样, 可是在TransmuxingController中, 两个都要接入IOController这个统一数据源, 因此把FLV的解码和转码放入到一个FLVCodec的对象中对外输出功能, TS的解码和转码则集中放入TSCodec中对外输出功能; 根据传进来媒体类型实例化解码器和转码器.
在 TransmuxingController 中则用 一个 _mediaCodec 对象来管理FLVCodec和TSCodec, 接入数据源IOController时调用二者都拥有的bindDataSource方法; 这里有一点须要注意的是 FLVCodec功能会返回一个 number 类型 consumed; 此参数表示FLVCodec功能已解码和转码的输出长度, 须要返回给 IOController, 让 IOController 刨除已解码的数据, 存储未解码的数据, 等下次一块儿再传给 FLVCodec 功能, 而TSCodec由于TS的文件结构特色(每一个TS包都是188字节的整数倍), 因此每次都是所有处理, 只须要返回 consumed = 0 便可;
在FLV.js中, 每当SEEK操做时都会MediaInfo中的KeyFrame信息, 去查找相应的Range点, 而后从Range点去加载; 对于hls点播流, 须要对FragmentLoader中的Level信息进行查询, 对每一个Fragment进行循环判断 seek的时间点是否处于当前 Fragment 的播放时间, 若是是, 就当即加载便可;
在嵌入的组件中加入logger打印日志, 并将错误返回接入到FLV.JS框架中, 使之能返回响应的错误信息和日志信息;
具体结构以下图:
除此以外, 咱们还作了如下几点:
HJPlayer.Events.GET_SEI_INFO
事件能够获得自定义SEI信息, 格式为Uint8Array;在项目中, 主持人会在节目播放过程当中提供事件发展方向的选项, 而后前端会弹出面板, 让用户选择方向, 节目根据答案的方向进行直播表演; 按照以往的方案, 通常这种状况都是选择由 Socket 服务器下发消息, 前端接到消息后展现选项, 而后用户选择, 点击提交答案这么一个流程; 去年阿里云推出了一项新颖的直播答题解决方案; 选项再也不由Socket服务器下发, 而是由视频云服务器随视频下发; 播放SDK解析视频中的视频补充加强信息, 展现选项; 咱们对此方案进行了实践, 大概流程以下:
当主持人提出问题后, 后台人员会在后台填写问题, 经视频云SDK传输给360视频云, 视频云对视频进行处理, 加入视频补充加强信息, 当播放SDK收到带有SEI信息的视频后, 通过解码去重, 将其中包含的信息传递给综艺直播间的互动组件, 互动组件展现, 用户点击选择答案后提交给后台进行汇总, 节目根据汇总后的答案进行节目内容的变动;
与传统方案相比, 采用视频SEI信息传递互动的方案有如下几项优势:
视频补充加强信息的内容通常由云服务器来指定内容, 除前16位UUID以外, 内容不尽相同, 因此本播放器直接将SEI信息(Uint8Array格式数据)经GET_SEI_INFO
事件抛出, 用户需自行按照己方视频云给定的格式去解析信息; 另外注意SEI信息是一段时间内重复发送的, 因此用户须要自行去重.
咱们完成了此项目后, 将它应用到花椒的主站播放FLV直播, 除此以外咱们还将项目开源HJPlayer, 但愿能帮助那些遇见一样项目需求的程序员; 若是使用中有问题, 能够在ISSUES中提出, 让咱们共同讨论解决.
答: 点击视频回顾的时候, 须要播放过去5分钟播过的内容, 若是采用 FLV 文件的话, 那么每次就要从存储的视频中截取一段视频生成 FLV 文件, 而后前端拉取文件播放, 这样会增长一大堆的视频碎片文件, 随之会带来一系列的存储问题; 若是采用HLS流的话, 能够根据前端传回的时间戳, 在存储的HLS回顾文件中查找相应的TS文件, 并生成一份m3u8文档就能够了;
视频补充加强信息是H.264视频压缩标准的特性之一, 提供了向视频码流中加入信息的办法; 它并非解码过程当中的必须存在的, 有可能对解码有帮助, 可是没有也没有关系; 在视频内容的生成端、传输过程当中,均可以插入SEI 信息。插入的信息,和其余视频内容一块儿通过网络传输到播放SDK; 在H264/AVC编码格式中NAL uint 中的头部, 有type字段指明 NAL uint的类型, 当 type = 6 时 该NAL uint 携带的信息即为 补充加强信息(SEI);
NAL uint type 后下一位即为 SEI 的type, 通常自定义的SEI信息的type 为 5, 即 user_data_unregistered; SEI type 的下一位直到0xFF为止即为所携带的数据的长度, 而后就是16位的UUID, 在16位的UUID以后一直到0x00的结束符之间, 即为自定义信息内容, 因此信息内容长度 = SEI信息所携带的数据的长度 - 16位UUID; 自定义信息内容的解析方式就要根据己方视频云给定的数据格式定义了;