villainthr

10 个月前
  • 1530

    浏览
  • 2

    评论
  • 0

    收藏

全面进阶 H5 直播

本文作者:ivweb villainthr 原文出处:IVWEB社区 未经同意,禁止转载     IVWEB公众群2

视频格式?编码?

如果我们想要理解 HTML5 视频,首先需要知道,你应该知道,但你不知道的内容?那怎么去判断呢? ok,很简单,我提几个问题即可,如果某些童鞋知道答案的话,可以直接跳过。

  1. 你知道 ogg,mp4,flv,webm(前面加个点 .)这些叫做什么吗?
  2. 那 FLV,MPEG-4,VP8 是啥?
  3. 如果,基友问你要片源,你会说我这是 mp4 的还是 MPEG-4 的呢?

当然,还有一些问题,我这里就不废话了。上面主要想说的其实就两个概念:视频文件格式(容器格式),视频编解码器(视频编码格式)。当然,还有另外一种,叫做音频编解码器。简而言之,就是这三个概念比较重要:

  • 视频文件格式(容器格式)
  • 视频编解码器(视频编码格式)
  • 音频编解码器(音频编码格式)

这里,我们主要讲解一下前面两个。视频一开始会由两个端采集,一个是视频输入口,是一个音频输入口。然后,采集的数据会分别进行相关处理,简而言之就是,将视频/音频流,通过一定的手段转换为比特流。最终,将这里比特流以一定顺序放到一个盒子里进行存放,从而生成我们最终所看到的,比如,mp4/mp3/flv 等等音视频格式。

视频编码格式

视频编码格式就是我们上面提到的第一步,将物理流转换为比特流,并且进行压缩。同样,它的压缩编码格式会决定它的视频文件格式。所以,第一步很重要。针对于 HTML5 中的 video/audio,它实际上是支持多种编码格式的,但局限于各浏览器厂家的普及度,目前视频格式支持度最高的是 MPEG-4/H.264,音频则是 MP3/AC3。(下面就主要说下视频的,音频就先不谈了。)

目前市面上,主流浏览器支持的几个有:

  • H.264
  • MEPG-4 第 2 部分
  • VP8
  • Ogg
  • WebM(免费)

其它格式,我们这里就不过多赘述,来看一下前两个比较有趣的。如下图:

demo

请问,上面箭头所指的编码格式是同一个吗?

答案是:No~

因为,MPEG-4 实际上是于 1999 年提出的一个标准。而 H.264 则是后台作为优化提出的新的标准。简单来说就是,我们通常说的 MPEG-4 其实就是MPEG-4 Part 2。而,H.264 则是MPEG-4(第十部分,也叫ISO/IEC 14496-10),又可以理解为 MPEG-4 AVC。而两者,不同的地方,可以参考:latthias 的讲解。简单的区别是:H.264 压缩率比以前的 MPEG-4(第 2 部分) 高很多。简单可以参考的就是:

demo

详细参考: 编码格式详解

视频文件格式

视频文件格式实际上,我们常常称作为容器格式,也就是,我们一般生活中最经常谈到的格式,flv,mp4,ogg 格式等。它就可以理解为将比特流按照一定顺序放进特定的盒子里。那选用不同格式来装视频有什么问题吗? 答案是,没有任何问题,但是你需要知道如何将该盒子解开,并且能够找到对应的解码器进行解码。那如果按照这样看的话,对于这些 mp4,ogv,webm等等视频格式,只要我有这些对应的解码器以及播放器,那么就没有任何问题。那么针对于,将视频比特流放进一个盒子里面,如果其中某一段出现问题,那么最终生成的文件实际上是不可用的,因为这个盒子本身就是有问题的。 不过,上面有一个误解的地方在于,我只是将视频理解为一个静态的流。试想一下,如果一个视频需要持续不断的播放,例如,直播,现场播报等。这里,我们就拿 TS/PS 流来进行讲解。

  • PS(Program Stream): 静态文件流
  • TS(Transport Stream): 动态文件流

针对于上面两种容器格式,实际上是对一个视频比特流做了不一样的处理。

  • PS: 将完成视频比特流放到一个盒子里,生成固定的文件
  • TS: 将接受到的视频,分成不同的盒子里。最终生成带有多个盒子的文件。

那么结果就是,如果一个或多个盒子出现损坏,PS 格式无法观看,而 TS 只是会出现跳帧或者马赛克效应。两者具体的区别就是:对于视频的容错率越高,则会选用 TS,对视频容错率越低,则会选用 PS。

常用为:

  • AVI:MPEG-2,DIVX,XVID,AC-1,H.264;
  • WMV:WMV,AC-1;
  • RM、RMVB:RV, RM;
  • MOV:MPEG-2,XVID,H.264;
  • TS/PS:MPEG-2,H.264,MPEG-4;
  • MKV:可以封装所有的视频编码格式。

详细参考:视频文件格式

直播协议

2016 年是直播元年,一是由于各大宽带提供商顺应民意增宽降价,二是大量资本流进了直播板块,促进了技术的更新迭代。市面上,最常用的是 Apple 推出的 HLS 直播协议(原始支持 H5 播放),当然,还有 RTMP、HTTP-FLV、RTP等。 这里,再问一个问题:

  1. HLS 和 MPEG-4/H.264 以及容器格式 TS/PS 是啥关系?

简单来说,没关系。

HLS 根本就不会涉及到视频本身的解码问题。它的存在只是为了确保你的视频能够及时,快速,正确的播放。

现在,直播行业依旧很火,而 HTML5 直播,一直以来都是一个比较蛋疼的内容。一是,浏览器厂商更新速度比较慢,二是,这并不是我们前端专攻的一块,所以,有时候的确很鸡肋。当然,进了前端,你就别想着休息。接下来,我们来详细的看一下市面上主流的几个协议。

HLS

HLS 全称是 HTTP Live Streaming。这是 Apple 提出的直播流协议。目前,IOS 和 高版本 Android 都支持 HLS。那什么是 HLS 呢? HLS 主要的两块内容是 .m3u8 文件和 .ts 播放文件。接受服务器会将接受到的视频流进行缓存,然后缓存到一定程度后,会将这些视频流进行编码格式化,同时会生成一份 .m3u8 文件和其它很多的 .ts 文件。根据 wiki 阐述,HLS 的基本架构为:

  • 服务器:后台服务器接受视频流,然后进行编码和片段化。
    • 编码:视频格式编码采用 H.264。音频编码为 AAC, MP3, AC-3,EC-3。然后使用 MPEG-2 Transport Stream 作为容器格式。
    • 分片:将 TS 文件分成若干个相等大小的 .ts 文件。并且生成一个 .m3u8 作为索引文件(确保包的顺序)
  • 分发:由于 HLS 是基于 HTTP 的,所以,作为分发,最常用的就是 CDN 了。
  • 客户端:使用一个 URL 去下载 m3u8 文件,然后,开始下载 ts 文件,下载完成后,使用 playback software(即时播放器) 进行播放。

这里,我们着重介绍一下客户端的过程。首先,直播之所以是直播,在于它的内容是实时更新的。那 HLS 是怎么完成呢? 我们使用 HLS 直接就用一个 video 进行包括即可:

<video controls autoplay>  
    <source src="http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8" type="application/vnd.apple.mpegurl" /> 
    <p class="warning">Your browser does not support HTML5 video.</p>  
</video>

根据上面的描述,它实际上就是去请求一个 .m3u8 的索引文件。该文件包含了对 .ts 文件的相关描述,例如:

#EXT-X-VERSION:3            PlayList 的版本,可带可不带。下面有说明
#EXTM3U                     m3u文件头
#EXT-X-TARGETDURATION:10    分片最大时长,单位为 s
#EXT-X-MEDIA-SEQUENCE:1     第一个TS分片的序列号,如果没有,默认为 0
#EXT-X-ALLOW-CACHE          是否允许cache
#EXT-X-ENDLIST              m3u8文件结束符
#EXTINF                     指定每个媒体段(ts)的持续时间(秒),仅对其后面的URI有效

不过,这只是一个非常简单,不涉及任何功能的直播流。实际上,HLS 的整个架构,可以分为:

stream_playlists_2x.png-35.5kB

当然,如果你使用的是 masterplaylist 作为链接,如:

<video controls autoplay>  
    <source src="http://devimages.apple.com/iphone/samples/bipbop/masterplaylist.m3u8" type="application/vnd.apple.mpegurl" /> 
    <p class="warning">Your browser does not support HTML5 video.</p>  
</video>

我们看一下,masterplaylist 里面具体的内容是啥:

#EXTM3U
#EXT-X-VERSION:6
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2855600,CODECS="avc1.4d001f,mp4a.40.2",RESOLUTION=960x540
live/medium.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=5605600,CODECS="avc1.640028,mp4a.40.2",RESOLUTION=1280x720
live/high.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1755600,CODECS="avc1.42001f,mp4a.40.2",RESOLUTION=640x360
live/low.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=545600,CODECS="avc1.42001e,mp4a.40.2",RESOLUTION=416x234
live/cellular.m3u8

EXT-X-STREAM-INF 这个标签头代表:当前用户的播放环境。masterplaylist 主要干的事就是根据, 当前用户的带宽,分辨率,解码器等条件决定使用哪一个流。所以,master playlist 是为了更好的用户体验而存在的。不过,弊端就是后台储备流的量会成倍增加。 现在,我们来主要看一下,如果你使用 master playlist,那么整个流程是啥? 当填写了 master playlist URL,那么用户只会下载一次该 master playlist。接着,播放器根据当前的环境决定使用哪一个 media playlist(就是 子 m3u8 文件)。如果,在播放当中,用户的播放条件发生变化时,播放器也会切换对应的 media playlist。关于 master playlist 内容,我们就先介绍到这里。 关于 HLS,感觉主要内容还在 media playlist 上。当然,media playlist 还分为三种 list:

  • live playlist: 动态列表。顾名思义,该列表是动态变化的,里面的 ts 文件会实时更新,并且过期的 ts 索引会被删除。默认,情况下都是使用动态列表。
  • event playlist: 静态列表。它和动态列表主要区别就是,原来的 ts 文件索引不会被删除,该列表是不断更新,而且文件大小会逐渐增大。它会在文件中,直接添加 #EXT-X-PLAYLIST-TYPE:EVENT 作为标识。
  • VOD playlist: 全量列表。它就是将所有的 ts 文件都列在 list 当中。如果,使用该列表,就和播放一整个视频没有啥区别了。它是使用 #EXT-X-ENDLIST 表示文件结尾。

live playlist DEMO:

#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:26
#EXTINF:9.901,
http://media.example.com/wifi/segment26.ts
#EXTINF:9.901,
http://media.example.com/wifi/segment27.ts
#EXTINF:9.501,
http://media.example.com/wifi/segment28.ts

evet playlist DEMO:

#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:EVENT
#EXTINF:9.9001,
http://media.example.com/wifi/segment0.ts
#EXTINF:9.9001,
http://media.example.com/wifi/segment1.ts
#EXTINF:9.9001,
http://media.example.com/wifi/segment2.ts

VOD playlist DEMO:

#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:9.9001,
http://media.example.com/wifi/segment0.ts
#EXTINF:9.9001,
http://media.example.com/wifi/segment1.ts
#EXTINF:9.9001,
http://media.example.com/wifi/segment2.ts
#EXT-X-ENDLIST

上面提到过一个 EXT-X-VERSION 这样的标签,这是用来表示当前 HLS 的版本。那 HLS 有哪些版本呢? 根据 apple 官方文档 的说明,我们可以了解到,不同版本的区别:

page.png-18.4kB

当然,HLS 支持的功能,并不只是分片播放(专门适用于直播),它还包括其他应有的功能。

  • 使用 HTTPS 加密 ts 文件
  • 快/倒放
  • 广告插入
  • 不同分辨率视频切换

HLS 的弊端

由于 HLS 是基于 HTTP 的,所以,它关于 HTTP 的好处,我们大部分都了解,比如,高兼容性,高可扩展性等。不过正由于是 HTTP 协议,所以会在握手协议上造成一定的延迟性。HLS 首次连接时,总共的延时包括:

  1. TCP 握手,2. m3u8 文件下载,3. m3u8 下的 ts 文件下载。

其中,每个 ts 文件,大概会存放 5s~10s 的时长,并且每个 m3u8 文件会存放 3~8 个 ts 文件。我们折中算一下,5 个 ts 文件,每个时长大约 8s 那么,总的下来,一共延时 40s。当然,这还不算上 TCP 握手,m3u8 文件下载等问题。那优化办法有吗?有的,那就是减少每个 m3u8 文件中的 ts 数量和 ts 文件时长,不过,这样也会成倍的增加后台承受流量请求的压力。所以,这还是需要到业务中去探索最优的配置(打个广告:腾讯云的直播视频流业务,做的确实挺棒。) 关于 HLS 的详细内容,可以参考:HLS 详解 关于 m3u8 文件的标签内容,可以参考:HLS 标签头详解 总而言之,HLS 之所以能这么流行,关键在于它的支持度是真的广,所以,对于一般 H5 直播来说,应该是非常友好的。不过,既然是直播,关键在于它的实时性,而 HLS 天生就存在一定的延时,所以,就可以考虑其他低延时的方案,比如 RTMP,HTTP-FLV。下面,我们来看一下 RTMP 内容。

RTMP

RTMP 全称为:Real-Time Messaging Protocol 。它是专门应对实时交流场景而开发出来的一个协议。它爹是 Macromedia,后来卖身给了 Adobe。RTMP 根据不同的业务场景,有很多变种:

  • 纯 RTMP 使用 TCP 连接,默认端口为 1935(有可能被封)。
  • RTMPS: 就是 RTMP + TLS/SSL
  • RTMPE: RTMP + encryption。在 RTMP 原始协议上使用,Adobe 自身的加密方法
  • RTMPT: RTMP + HTTP。使用 HTTP 的方式来包裹 RTMP 流,这样能直接通过防火墙。
  • RTMFP: RMPT + UDP。该协议常常用于 P2P 的场景中,针对延时有变态的要求。

既然是 Adobe 公司开发的(算吧),那么,该协议针对的就是 Flash Video,即,FLV。不过,在移动端上,Flash Player 已经被杀绝了,那为啥还会出现这个呢?简单来说,它主要是针对 PC 端的。RTMP 出现的时候,还是 零几 年的时候,IE 还在大行其道,Flash Player 也并未被各大浏览器所排斥。那时候 RTMP 毋庸置疑的可以在视频界有自己的一席之地。

RTMP 由于借由 TCP 长连接协议,所以,客户端向服务端推流这些操作而言,延时性很低。它会将上传的流分成不同的分片,这些分片的大小,有时候变,有时候不会变。默认情况下就是,64B 的音频数据 + 128B 的视频数据 + 其它数据(比如 头,协议标签等)。但 RTMP 具体传输的时候,会将分片进一步划分为包,即,视频包,音频包,协议包等。因为,RTMP 在进行传输的时候,会建立不同的通道,来进行数据的传输,这样对于不同的资源,对不同的通道设置相关的带宽上限。

RTMP 处理的格式是 MP3/ACC + FLV1。 不过,由于支持性的原因,RTMP 并未在 H5 直播中,展示出优势。下列是简单的对比:

dff.png-15.8kB

HTTP-FLV

HTTP-FLV 和 RTMPT 类似,都是针对于 FLV 视频格式做的直播分发流。但,两者有着很大的区别。

  • 相同点
    • 两者都是针对 FLV 格式
    • 两者延时都很低
    • 两者都走的 HTTP 通道
  • 不同点
    • HTTP-FLv
      • 直接发起长连接,下载对应的 FLV 文件
      • 头部信息简单
    • RTMPT
      • 握手协议过于复杂
      • 分包,组包过程耗费精力大

通过上面来看,HTTP-FLV 和 RTMPT 确实不是一回事,但,如果了解 SRS(simple rtmp server),那么 对 HTTP-FLV 应该清楚不少。SRS 本质上,就是 RTMP + FLV 进行传输。因为 RTMP 发的包很容易处理,通常 RTMP 协议会作为视频上传端来处理,然后经由服务器转换为 FLV 文件,通过 HTTP-FLV 下发给用户。

STRU.png-2.9kB

现在市面上,比较常用的就是 HTTP-FLV 进行播放。但,由于手机端上不支持,所以,H5 的 HTTP-FLV 也是一个痛点。不过,现在 flv.js 可以帮助高版本的浏览器,通过 mediaSource 来进行解析。HTTP-FLV 的使用方式也很简单。和 HLS 一样,只需要添加一个连接即可:

<object type="application/x-shockwave-flash" src="http://s6.pdim.gs/static/a2a36bc596148316.flv"></object>

不过,并不是末尾是 .flv 的都是 HTTP-FLV 协议,因为,涉及 FLV 的流有三种,它们三种的使用方式都是一模一样的。

  • FLV 文件:相当于就是一整个文件,官方称为 渐进 HTTP 流。它的特点是只能渐进下载,不能进行点播。
  • FLV 伪流:该方式,可以通过在末尾添加 ?start=xxx 的参数,指定返回的对应开始时间视频数据。该方式比上面那种就多了一个点播的功能。本质上还是 FLV 直播。
  • FLV 直播流:这就是 HTTP-FLV 真正所支持的流。SRS 在内部使用的是 RTMP 进行分发,然后在传给用户的使用,经过一层转换,变为 HTTP 流,最终传递给用户。

上面说到,HTTP-FLV 就是长连接,简而言之只需要加上一个 Connection:keep-alive 即可。关键是它的响应头,由于,HTTP-FLV 传递的是视频格式,所有,它的 Content-TypeTransfer-Encoding 需要设置其它值。

Content-Type:video/x-flv
Expires:Fri, 10 Feb 2017 05:24:03 GMT
Pragma:no-cache
Transfer-Encoding:chunked

不过,一般而言,直播服务器一般和业务服务是不会放在一块的,所以这里,可能会额外需要支持跨域直播的相关技术。在 XHR2 里面,解决办法也很简单,直接使用 CORS 即可:

// 那么整个响应头,可以为:
Access-Control-Allow-credentials:true
Access-Control-Allow-max-age:86400
Access-Control-Allow-methods:GET,POST,OPTIONS
Access-Control-Allow-Origin:*
Cache-Control:no-cache
Content-Type:video/x-flv
Expires:Fri, 10 Feb 2017 05:24:03 GMT
Pragma:no-cache
Transfer-Encoding:chunked

对于 HTTP-FLV 来说,关键难点在于 RTMP 和 HTTP 协议的转换,这里我就不多说了。因为,我们主要针对的是前端开发,讲一下和前端相关的内容。

接下来,我们在主要来介绍一下 FLV 格式的。因为,后面我们需要通过 mediaSource 来解码 FLV。

FLV 格式浅析

FLV 原始格式,Adobe 可以直接看 flv格式详解。我这里就抽主要的内容讲讲。FLV 也是与时俱进,以前 FLV 的格式叫做 FLV,新版的可以叫做 F4V。两者的区别,简单的区分方法就是:

  • FLV 是专门针对 Flash 播放器的
  • F4V 是有点像 MEPG 格式的 Flash 播放,主要为了兼容 H.264/ACC。F4V 不支持 FLV(两者本来都不是同一个格式)

这里我们主要针对 FLV 进行相关了解。因为,一般情况下,后台发送视频流时,为了简洁快速,就是发送 FLV 视频。FLV 由于年限比较久,它所支持的内容是 H.263,VP6 codec。FLV 一般可以嵌套在 .swf 文件当中,不过,对于 HTTP-FLV 等 FLV 直播流来说,一般直接使用 .flv 文件即可。在 07 年的时候,提出了 F4V 这个视频格式,当然,FLV 等也会向前兼容。

flv

这里,我们来正式介绍一下 FLV 的格式。一个完整的 FLV 流包括 FLV Header + FLV Packets。

FLV Header

FLV 格式头不难,就几个字段:

Field Data Type Default Details
Signature byte[3] “FLV” 有三个B的大小,算是一种身份的象征
Version uint8 1 只有 0x01 是有效的。其实就是默认值
Flags uint8 bitmask 0x05 表示该流的特征。0x04 是 audio,0x01 是 video,0x05 是 audio+video
Header Size uint32_be 9 用来跳过多余的头

FLV Packets

在 FLV 的头部之后,就正式开始发送 FLV 文件。文件会被拆解为数个包(FLV tags)进行传输。每个包都带有 15B 的头。前 4 个字节是用来代表前一个包的头部内容,用来完成倒放的功能。整个包的结构为:

FLV

具体解释如下:

字段 字段大小 默认值 详解
Size of previous packet uint32_be 0 关于前一个包的信息,如果是第一个包,则该部分为 NULL
Packet Type uint8 18 设置包的内容,如果是第一个包,则该部分为 AMF 元数据
Payload Size uint24_be varies 该包的大小
Timestamp Lower uint24_be 0 起始时间戳
Timestamp Upper uint8 0 持续时间戳,通常加上 Lower 实际上戳,代表整个时间。
Stream ID uint24_be 0 流的类型,第一个流设为 NULL
Payload Data freeform varies 传输数据

其中,由于 Packet Type 的值可以取多个, 需要额外说明一下。

  • Packet Type
    • 1: RTMP 包的大小
    • 3: RTMP 字节读包反馈,RTMP ping,RTMP 服务器带宽,RTMP 客户端带宽
    • 8: 音频和视频的数据
    • 15: RTMP flex 流
    • 24: 经过封装的 flash video。

上面是关于 FLV 简单的介绍。不过,如果没有 Media Source Extensions 的帮助,那么上面说的基本上全是废话。由于,Flash Player 已经被时代所遗弃,所以,我们不能在浏览器上,顺利的播放 FLV 视频。接下来,我们先来详细了解一下 MSE 的相关内容。

Media Source Extensions

在没有 MSE 出现之前,前端对 video 的操作,仅仅局限在对视频文件的操作,而并不能对视频流做任何相关的操作。现在 MSE 提供了一系列的接口,使开发者可以直接提供 media stream。

那 MSE 是如何完成视频流的加载和播放呢?

入门实例

这可以参考 google 的 MSE 简介

var vidElement = document.querySelector('video');

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
  console.log("The Media Source Extensions API is not supported.")
}

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp9"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  fetch(videoUrl)
    .then(function(response) {
      return response.arrayBuffer();
    })
    .then(function(arrayBuffer) {
      sourceBuffer.addEventListener('updateend', function(e) {
        if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
          mediaSource.endOfStream();
        }
      });
      sourceBuffer.appendBuffer(arrayBuffer);
    });
}

可以从上面的代码看出,一套完整的执行代码,不仅需要使用 MSE 而且,还有一下这些相关的 API。

  • HTMLVideoElement.getVideoPlaybackQuality()
  • SourceBuffer
  • SourceBufferList
  • TextTrack.sourceBuffer
  • TrackDefault
  • TrackDefaultList
  • URL.createObjectURL()
  • VideoPlaybackQuality
  • VideoTrack.sourceBuffer

我们简单讲解一下上面的流程。根据 google 的阐述,整个过程可以为:

image.png-16kB

  • 第一步,通过异步拉取数据。
  • 第二步,通过 MediaSource 处理数据。
  • 第三步,将数据流交给 audio/video 标签进行播放。

而中间传递的数据都是通过 Buffer 的形式来进行传递的。

image.png-29.5kB

中间有个需要注意的点,MS 的实例通过 URL.createObjectURL() 创建的 url 并不会同步连接到 video.src。换句话说,URL.createObjectURL() 只是将底层的流(MS)和 video.src 连接中间者,一旦两者连接到一起之后,该对象就没用了。

那么什么时候 MS 才会和 video.src 连接到一起呢?

创建实例都是同步的,但是底层流和 video.src 的连接时异步的。MS 提供了一个 sourceopen 事件给我们进行这项异步处理。一旦连接到一起之后,该 URL object 就没用了,处于内存节省的目的,可以使用 URL.revokeObjectURL(vidElement.src) 销毁指定的 URL object。

mediaSource.addEventListener('sourceopen', sourceOpen);

function sourceOpen(){
    URL.revokeObjectURL(vidElement.src)
}

MS 对流的解析

MS 提供了我们对底层音视频流的处理,那一开始我们怎么决定以何种格式进行编解码呢?

这里,可以使用 addSourceBuffer(mime) 来设置相关的编码器:

  var mime = 'video/webm; codecs="opus, vp9"';  
  var sourceBuffer = mediaSource.addSourceBuffer(mime);

然后通过,异步拉取相关的音视频流:

fetch(url)
.then(res=>{
    return res.arrayBuffer();
})
.then(buffer=>{
    sourceBuffer.appendBuffer(buffer);
})

如果视频已经传完了,而相关的 Buffer 还在占用内存,这时候,就需要我们显示的中断当前的 Buffer 内容。那么最终我们的异步处理结果变为:

fetch(url)
.then(res=>{
    return res.arrayBuffer();
})
.then(function(arrayBuffer) {
      sourceBuffer.addEventListener('updateend', function(e) {
      // 是否有持续更新的流
        if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
        // 没有,则中断连接
          mediaSource.endOfStream();
        }
      });
      sourceBuffer.appendBuffer(arrayBuffer);
    });

上面我们大致了解了一下关于 Media Source Extensions 的大致流程,但里面的细节我们还没有细讲。接下来,我们来具体看一下 MSE 一篮子的生态技术包含哪些内容。首先是,MediaSource

MediaSource

MS(MediaSource) 可以理解为多个视频流的管理工具。以前,我们只能下载一个清晰度的流,并且不能平滑切换低画质或者高画质的流,而现在我们可以利用 MS 实现这里特性。我们先来简单了解一下他的 API。

MS 的创建

创建一个 MS:

var mediaSource = new MediaSource();

相关方法

addSourceBuffer()

该是用来返回一个具体的视频流,接受一个 mimeType 表示该流的编码格式。例如:

var mimeType = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"';
var sourceBuffer = mediaSource.addSourceBuffer(mimeType);

sourceBuffer 是直接和视频流有交集的 API。例如:

function sourceOpen (_) {
  var mediaSource = this;
  var sourceBuffer = mediaSource.addSourceBuffer(mimeCodec);
  fetchAB(assetURL, function (buf) {
    sourceBuffer.addEventListener('updateend', function (_) {
      mediaSource.endOfStream();
      video.play();
    });
    // 通过 fetch 添加视频 Buffer
    sourceBuffer.appendBuffer(buf);
  });
};

它通过 appendBuffer 直接添加视频流,实现播放。不过,在使用 addSourceBuffer 创建之前,还需要保证当前浏览器是否支持该编码格式。

removeSourceBuffer()

用来移除某个 sourceBuffer。移除也主要是考虑性能原因,将不需要的流移除以节省相应的空间,格式为:

mediaSource.removeSourceBuffer(sourceBuffer);

endOfStream()

用来表示接受的视频流的停止,注意,这里并不是断开,相当于只是下好了一部分视频,然后你可以进行播放。此时,MS 的状态变为:ended。例如:


  var mediaSource = this;
  var sourceBuffer = mediaSource.addSourceBuffer(mimeCodec);
  fetchAB(assetURL, function (buf) {
    sourceBuffer.addEventListener('updateend', function (_) {
      mediaSource.endOfStream(); // 结束当前的接受
      video.play(); // 可以播放当前获得的流
    });
    sourceBuffer.appendBuffer(buf);
  });

isTypeSupported()

该是用来检测当前浏览器是否支持指定视频格式的解码。格式为:

var isItSupported = mediaSource.isTypeSupported(mimeType); // 返回值为 Boolean

mimeType 可以为 type 或者 type + codec。

例如:

// 不同的浏览器支持不一样,不过基本的类型都支持。
MediaSource.isTypeSupported('audio/mp3'); // false,这里应该为 audio/mpeg 
MediaSource.isTypeSupported('video/mp4'); // true
MediaSource.isTypeSupported('video/mp4; codecs="avc1.4D4028, mp4a.40.2"'); // true

这里有一份具体的 mimeType 参考列表。

MS 的状态

当 MS 从创建开始,都会自带一个 readyState 属性,用来表示其当前打开的状态。MS 有三个状态:

  • closed: 当前 MS 没有和 media element(比如:video.src) 相关联。创建时,MS 就是该状态。
  • open: source 打开,并且准备接受通过 sourceBuffer.appendBuffer 添加的数据。
  • ended: 当 endOfStream() 执行完成,会变为该状态,此时,source 依然和 media element 连接。
var mediaSource = new MediaSource;
mediaSource.readyState; // 默认为 closed

当由 closed 变为 open 状态时,需要监听 sourceopen 事件。

video.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', sourceOpen);

MS 针对这几个状态变化,提供了相关的事件:sourceopensourceendedsourceclose

  • sourceopen: 当 "closed" to "open" 或者 "ended" to "open" 时触发。
  • sourceended: 当 "open" to "ended" 时触发。
  • sourceclose: 当 "open" to "closed" 或者 "ended" to "closed" 时触发。

MS 还提供了其他的监听事件 sourceopen,sourceended,sourceclose,updatestart,update,updateend,error,abort,addsourcebuffer,removesourcebuffer. 这里主要选了比较重要的,其他的可以参考官方文档。

MS 属性

比较常用的属性有: duration,readyState。

  • duration: 获得当前媒体播放的时间,既可以设置(get),也可以获取(set)。单位为 s(秒)
mediaSource.duration = 5.5; // 设置媒体流播放的时间
var myDuration = mediaSource.duration; // 获得媒体流开始播放的时间

在实际应用中为:

sourceBuffer.addEventListener('updateend', function (_) {
      mediaSource.endOfStream();
      mediaSource.duration = 120; // 设置当前流播放的时间
      video.play();
    });
  • readyState: 获得当前 MS 的状态。取值上面已经讲过了: closedopenended
var mediaSource = new MediaSource;
  //此时的 mediaSource.readyState 状态为 closed

以及:

sourceBuffer.addEventListener('updateend', function (_) {
      mediaSource.endOfStream(); // 调用该方法后结果为:ended
      video.play();
    });

除了上面两个属性外,还有 sourceBuffersactiveSourceBuffers 这两个属性。用来返回通过 addSourceBuffer() 创建的 SourceBuffer 数组。这没啥过多的难度。

接下来我们就来看一下靠底层的 sourceBuffer

SourceBuffer

SourceBuffer 是由 mediaSource 创建,并直接和 HTMLMediaElement 接触。简单来说,它就是一个流的容器,里面提供的 append()remove() 来进行流的操作,它可以包含一个或者多个 media segments。同样,接下来,我们再来看一下该构造函数上的基本属性和内容。

基础内容

前面说过 sourceBuffer 主要是一个用来存放流的容器,那么,它是怎么存放的,它存放的内容是啥,有没有顺序等等。这些都是 sourceBuffer 最最根本的问题。OK,接下来,我们来看一下的它的基本架构有些啥。

参考 W3C,可以基本了解到里面的内容为:

interface SourceBuffer : EventTarget {
             attribute AppendMode          mode;
    readonly attribute boolean             updating;
    readonly attribute TimeRanges          buffered;
             attribute double              timestampOffset;
    readonly attribute AudioTrackList      audioTracks;
    readonly attribute VideoTrackList      videoTracks;
    readonly attribute TextTrackList       textTracks;
             attribute double              appendWindowStart;
             attribute unrestricted double appendWindowEnd;
             attribute EventHandler        onupdatestart;
             attribute EventHandler        onupdate;
             attribute EventHandler        onupdateend;
             attribute EventHandler        onerror;
             attribute EventHandler        onabort;
    void appendBuffer(BufferSource data);
    void abort();
    void remove(double start, unrestricted double end);
};

上面这些属性决定了其 sourceBuffer 整个基础。

首先是 mode。上面说过,SB(SourceBuffer) 里面存储的是 media segments(就是你每次通过 append 添加进去的流片段)。SB.mode 有两种格式:

  • segments: 乱序排放。通过 timestamps 来标识其具体播放的顺序。比如:20s的 buffer,30s 的 buffer 等。
  • sequence: 按序排放。通过 appendBuffer 的顺序来决定每个 mode 添加的顺序。timestamps 根据 sequence 自动产生。

那么上面两个哪个是默认值呢?

看情况,讲真,没骗你。

media segments 天生自带 timestamps,那么 mode 就为 segments ,否则为 sequence。所以,一般情况下,我们是不用管它的值。不过,你可以在后面,将 segments 设置为 sequence 这个是没毛病的。反之,将 sequence 设置为 segments 就有问题了。

var bufferMode = sourceBuffer.mode;
if (bufferMode == 'segments') {
  sourceBuffer.mode = 'sequence';
}

然后另外两个就是 bufferedupdating

  • buffered:返回一个 timeRange 对象。用来表示当前被存储在 SB 中的 buffer。
  • updating: 返回 Boolean,表示当前 SB 是否正在被更新。例如: SourceBuffer.appendBuffer(), SourceBuffer.appendStream(), SourceBuffer.remove() 调用时。

另外还有一些其他的相关属性,比如 textTracks,timestampOffset,trackDefaults,这里就不多说了。实际上,SB 是一个事件驱动的对象,一些常见的处理,都是在具体的事件中完成的。那么它又有哪些事件呢?

事件触发

在 SB 中,相关事件触发包括:

  • updatestart: 当 updating 由 false 变为 true。
  • update:当 append()/remove() 方法被成功调用完成时,updating 由 true 变为 false。
  • updateend: append()/remove() 已经结束
  • error: 在 append() 过程中发生错误,updating 由 true 变为 false。
  • abort: 当 append()/remove() 过程中,使用 abort() 方法废弃时,会触发。此时,updating 由 true 变为 false。

注意上面有两个事件比较类似:updateupdateend。都是表示处理的结束,不同的是,update 比 updateend 先触发。

sourceBuffer.addEventListener('updateend', function (e) {
    // 当指定的 buffer 加载完后,就可以开始播放
      mediaSource.endOfStream();
      video.play();
    });

相关方法

SB 处理流的方法就是 +/- : appendBuffer, remove。另外还有一个中断处理函数 abort()

  • appendBuffer(ArrayBuffer):用来添加 ArrayBuffer。该 ArrayBuffer 一般是通过 fetch 的 response.arrayBuffer(); 来获取的。
  • remove(start, end): 用来移除具体某段的 media segments。
    • @param start/end: 都是时间单位(s)。用来表示具体某段的 media segments 的范围。
  • abort(): 用来放弃当前 append 流的操作。不过,该方法的业务场景也比较有限。它只能用在当 SB 正在更新流的时候。即,此时通过 fetch,已经接受到新流,并且使用 appendBuffer 添加,此为开始的时间。然后到 updateend 事件触发之前,这段时间之内调用 abort()。有一个业务场景是,当用户移动进度条,而,此时 fetch 已经获取前一次的 media segments,那么可以使用 abort 放弃该操作,转而请求新的 media segments。具体可以参考:abort 使用

上面主要介绍了处理音视频流需要用的 Web 技术,后面章节,我们接入实战,具体来讲一下,如何做到使用 MSE 进行 remux 和 demux。

2 条评论