花椒开源项目实时互动流媒体播放器

项目背景

春天的时候花椒作了一个创新项目, 这是一个直播综艺节目的项目,前端的工做主要是作出一个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.js的工做原理是下载flv文件转码成IOS BMFF(MP4碎片)片断, 而后经过Media Source Extensions将MP4片断传输给HTML5的Video标签进行播放; 它的结构以下图所示:前端

FLV.js工做流程

src/flv.js 是对外输出FLV.js的一些组件, 事件和错误, 方便用户根据抛出的事件进行各类操做和获取相应的播放信息; 最主要是flv.js下返回的2个player: NativePlayerFLVPlayer; NativePlayer 是对浏览器自己播放器的一个再包装, 使之能和FLVPlayer同样, 相应共同的事件和操做; 你们最主要使用的仍是FLVPlayer这个播放器; 而 FLVPlayer中最重要东西可分为两块: 1. MSEController; 2. Transmuxer;java

MSEController

这个MSEController负责给HTML Video Element 和 SourceBuffer之间创建链接, 接受 InitSegment(ISO BMFF 片断中的 FTYP + MOOV)和 MediaSegment (ISO BMFF 片断中的 MOOF + MDATA); 将这2个片断按照顺序添加到SourceBuffer中, 和对SouceBuffer的一些控制和状态反馈;git

Transmuxer

Transmuxer 主要负责的就是下载, 解码, 转码, 发送Segment的工做; 它的下面主要包含了 2个模块, TransmuxingWorkerTransmuxingController; TransmuxingWorker是启用多线程执行 TransmuxingController, 并对 TransmuxingController抛出的事件就行转发; TransmuxingController 才是真正执行 下载, 解码, 转码, 发送Segment的苦力部门, 苦活累活都是这个部门干的, Transmuxer(真上级) 和 TransmuxingController(伪上级)都是在调用它的功能和传递它的输出;程序员

下面有请这个劳苦功高的部门登场github

TransmuxingController

TransmuxingController也是一个大部门, 他的手下有三个小组: IOController, demuxerremuxer;浏览器

  1. IOController

IOController主要有三个功能, 一是负责遴选他手下的小小弟(loaders), 选出最适合当前浏览器环境的loader, 去从服务器搬运媒体流; 二是存储小小弟(loader)发上来的数据; 三是把数据发送给demuxer(解码)并存储demuxer未处理完的数据;服务器

  1. demuxer

demuxer 是负责解码工做的员工, 他须要把IOController发送过来的FLV data, 解析整理成 videoTrack 和 audioTrack; 并把解析后的数据发送给 remuxer 转码器; 解码完成后, 他会把已经处理的数据的长度返回给 IOController, IOController会把未处理的数据(总数据 - 已经处理的数据)存储, 等待下次发送数据的时候发从头部追加未处理的数据, 一块儿发送给 demuxer.markdown

  1. remuxer

remuxer 是负责将 videoTrack 和 audioTrack 转成 InitSegment 和 MediaSegment并向上发送, 并在转化的过程当中进行音视频同步的操做.网络

总的流程就是 FLVPlayer喊了一声启动以后, loader 加载数据 => IOController 存储和转发数据 => demuxer 解码数据 => remuxer 转码数据 => TransmuxingWorker 和 Transmuxer 转发数据 =>

MSEController 接受数据 => SourceBuffer; 一系列操做以后视频就能够播放了;

HLS.JS分析

HLS.js的工做原理是先下载index.m3u8文件, 而后解析该文档, 取出Level, 再根据Levels中的片断(Fragments)信息去下载相应的TS文件, 转码成IOS BMFF(MP4碎片)片断, 而后经过Media Source Extensions将MP4片断传输给HTML5的Video标签进行播放;

HLS.js的结构以下

hls

相对于 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 这个部门主要负责如下功能:

  1. 响应BUFFER_RESET事件, 重置媒体缓冲区
  2. 响应BUFFER_CODECS事件, 接收时使用适当的编解码器信息初始化SourceBuffer
  3. 响应BUFFER_APPENDING事件, 给SourceBuffer中添加MP4 片断
  4. 成功添加缓冲区后触发BUFFER_APPENDED事件
  5. 响应BUFFER_FLUSHING事件, 刷新指定的缓冲区范围
  6. 成功刷新缓冲区后触发BUFFER_FLUSHED事件

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流的大体过程了;

  1. hls.js只播放HLS流, 没有NativePlayer, 因此顶级src/hls.js 对应着 flv.js中的 FLVPlayer, 直接提供API, 响应外界的各类操做和发送信息; 在开始准备播放的时候它会发令HlsEvents.MANIFEST_LOADING,
  2. playlist-loader 收到 HlsEvents.MANIFEST_LOADING 后, 它会使用XHRLoader去加载 M3U8文档, 文档通过解析以后会获得该文档含有的level(对于直播行业来讲通常就是一个level, level[0] 就是咱们想要的数据); playlist-loader 会发出 LEVEL_LOADED 的事件并携带level信息;
  3. level-controller会记录level信息, 并计算更新m3u8的时间间隔, 不断加载m3u8文件更新level; 而 stream-controller 则会通过一系列的操做以后去加载 fragment(即m3u8文档中的ts文件); 发出 FRAG_LOADING 事件, 并初始化 解码器和转码器 (Demuxer对象, Remuxer会在Demuxer实例化中初始化)
  4. FragmentLoader 收到 FRAG_LOADING 以后会去加载相应的TS文件, 并在加载TS文件完毕以后发出 FRAG_LOADED 事件, 并把TS的Uint8数据和fragment的其余信息一并发送出;
  5. 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:

  1. 根据项目目的: 项目是一个主直播, 次点播的站点; FLV直播功能是最重要的功能, HLS流的回放只在用户点击视频回顾和查看过去节目视频才会使用;

  2. 根据其余项目的需求: 花椒的主站如今也是HTTP-FLV的形式去进行直播展现, 而HLS流计划用于播放主播小视频(点播);

  3. 根据业界状况: 如今业界直播基本仍是用的HTTP-FLV这种形式(基础设施成熟, 技术简单, 延迟小), 而HLS流通常仍是用在移动端直播;

因此咱们决定采用在 FLV.js 的基础上, 加上HLS.js中的 loader, demuxer 和 remuxer 这三部分去组成一个新的播放器library, 既能播放FLV视频, 也能播放HLS流(根据项目的须要只包含单码率流的直播和点播, 不包含多码率流, 自动切换码率, 解密等功能);

具体实施过程

首先咱们先规划了一下内嵌的功能怎么接入:

  1. Loader的接入

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 这娃来到了新公司, 身上担子变重了).

  1. demuxer和remuxer的接入,

由于FLV和TS文件的解析方式不一样, 可是在TransmuxingController中, 两个都要接入IOController这个统一数据源, 因此把FLV的解码和转码放入到一个FLVCodec的对象中对外输出功能, TS的解码和转码则集中放入TSCodec中对外输出功能; 根据传进来媒体类型实例化解码器和转码器.

  1. IOController和 _mediaCodec 的接入

在 TransmuxingController 中则用 一个 _mediaCodec 对象来管理FLVCodec和TSCodec, 接入数据源IOController时调用二者都拥有的bindDataSource方法; 这里有一点须要注意的是 FLVCodec功能会返回一个 number 类型 consumed; 此参数表示FLVCodec功能已解码和转码的输出长度, 须要返回给 IOController, 让 IOController 刨除已解码的数据, 存储未解码的数据, 等下次一块儿再传给 FLVCodec 功能, 而TSCodec由于TS的文件结构特色(每一个TS包都是188字节的整数倍), 因此每次都是所有处理, 只须要返回 consumed = 0 便可;

  1. hls流的点播seek功能的接入

在FLV.js中, 每当SEEK操做时都会MediaInfo中的KeyFrame信息, 去查找相应的Range点, 而后从Range点去加载; 对于hls点播流, 须要对FragmentLoader中的Level信息进行查询, 对每一个Fragment进行循环判断 seek的时间点是否处于当前 Fragment 的播放时间, 若是是, 就当即加载便可;

  1. 对各类意外状况的处理

在嵌入的组件中加入logger打印日志, 并将错误返回接入到FLV.JS框架中, 使之能返回响应的错误信息和日志信息;

具体结构以下图:

播放器设计

除此以外, 咱们还作了如下几点:

  1. 咱们在进行改造的时候还接入了Typescript , 实现对功能参数的类型检查;
  2. 在FLV-MP4Remuxer中集成了 jamken (感谢❤ jamken) 对 FLV.js 推送的 354PR, 修正FLV.JS中音视频不一样步的问题;
  3. 还加入了视频补充加强信息(Supplemental Enhancement Information)的解析, 经过监听HJPlayer.Events.GET_SEI_INFO事件能够获得自定义SEI信息, 格式为Uint8Array;

对视频直播实时互动的尝试

在项目中, 主持人会在节目播放过程当中提供事件发展方向的选项, 而后前端会弹出面板, 让用户选择方向, 节目根据答案的方向进行直播表演; 按照以往的方案, 通常这种状况都是选择由 Socket 服务器下发消息, 前端接到消息后展现选项, 而后用户选择, 点击提交答案这么一个流程; 去年阿里云推出了一项新颖的直播答题解决方案; 选项再也不由Socket服务器下发, 而是由视频云服务器随视频下发; 播放SDK解析视频中的视频补充加强信息, 展现选项; 咱们对此方案进行了实践, 大概流程以下:

视频实时互动

当主持人提出问题后, 后台人员会在后台填写问题, 经视频云SDK传输给360视频云, 视频云对视频进行处理, 加入视频补充加强信息, 当播放SDK收到带有SEI信息的视频后, 通过解码去重, 将其中包含的信息传递给综艺直播间的互动组件, 互动组件展现, 用户点击选择答案后提交给后台进行汇总, 节目根据汇总后的答案进行节目内容的变动;

与传统方案相比, 采用视频SEI信息传递互动的方案有如下几项优势:

  1. 能够实现与主持人的音视频同步出现, 避免因服务器群发消息不及时致使主持人已经宣布开始, 可是面板迟迟不出现的问题.
  2. 成本低, 问题是由视频下发而不是由服务器下发, 但延迟会高一点(可提早在视频中插入, 主持人后提出问题, 减小延迟);

视频补充加强信息的内容通常由云服务器来指定内容, 除前16位UUID以外, 内容不尽相同, 因此本播放器直接将SEI信息(Uint8Array格式数据)经GET_SEI_INFO事件抛出, 用户需自行按照己方视频云给定的格式去解析信息; 另外注意SEI信息是一段时间内重复发送的, 因此用户须要自行去重.

最后

咱们完成了此项目后, 将它应用到花椒的主站播放FLV直播, 除此以外咱们还将项目开源HJPlayer, 但愿能帮助那些遇见一样项目需求的程序员; 若是使用中有问题, 能够在ISSUES中提出, 让咱们共同讨论解决.

题外

  1. 有人可能会问 为何大家的视频回顾不采用FLV文件, 这样就只使用FLV.JS不就能够播放了吗?

答: 点击视频回顾的时候, 须要播放过去5分钟播过的内容, 若是采用 FLV 文件的话, 那么每次就要从存储的视频中截取一段视频生成 FLV 文件, 而后前端拉取文件播放, 这样会增长一大堆的视频碎片文件, 随之会带来一系列的存储问题; 若是采用HLS流的话, 能够根据前端传回的时间戳, 在存储的HLS回顾文件中查找相应的TS文件, 并生成一份m3u8文档就能够了;

  1. 视频补充加强信息(Supplemental Enhancement Information) 是什么?

视频补充加强信息是H.264视频压缩标准的特性之一, 提供了向视频码流中加入信息的办法; 它并非解码过程当中的必须存在的, 有可能对解码有帮助, 可是没有也没有关系; 在视频内容的生成端、传输过程当中,均可以插入SEI 信息。插入的信息,和其余视频内容一块儿通过网络传输到播放SDK; 在H264/AVC编码格式中NAL uint 中的头部, 有type字段指明 NAL uint的类型, 当 type = 6 时 该NAL uint 携带的信息即为 补充加强信息(SEI);

  1. 关于 SEI信息的解析:

NAL uint type 后下一位即为 SEI 的type, 通常自定义的SEI信息的type 为 5, 即 user_data_unregistered; SEI type 的下一位直到0xFF为止即为所携带的数据的长度, 而后就是16位的UUID, 在16位的UUID以后一直到0x00的结束符之间, 即为自定义信息内容, 因此信息内容长度 = SEI信息所携带的数据的长度 - 16位UUID; 自定义信息内容的解析方式就要根据己方视频云给定的数据格式定义了;

相关文章
相关标签/搜索