全面进阶 H5 直播
EdithTan
8年前
<h2>视频格式?编码?</h2> <p>如果我们想要理解 HTML5 视频,首先需要知道,你应该知道,但你不知道的内容?那怎么去判断呢? ok,很简单,我提几个问题即可,如果某些童鞋知道答案的话,可以直接跳过。</p> <ol> <li>你知道 ogg,mp4,flv,webm(前面加个点 . )这些叫做什么吗?</li> <li>那 FLV,MPEG-4,VP8 是啥?</li> <li>如果,基友问你要片源,你会说我这是 mp4 的还是 MPEG-4 的呢?</li> </ol> <p>当然,还有一些问题,我这里就不废话了。上面主要想说的其实就两个概念: <a href="/misc/goto?guid=4959746517938865296" rel="nofollow,noindex">视频文件格式</a> (容器格式), <a href="/misc/goto?guid=4959746589412303450" rel="nofollow,noindex">视频编解码器</a> (视频编码格式)。当然,还有另外一种,叫做音频编解码器。简而言之,就是这三个概念比较重要:</p> <ul> <li>视频文件格式(容器格式)</li> <li>视频编解码器(视频编码格式)</li> <li>音频编解码器(音频编码格式)</li> </ul> <p>这里,我们主要讲解一下前面两个。视频一开始会由两个端采集,一个是视频输入口,是一个音频输入口。然后,采集的数据会分别进行相关处理,简而言之就是,将视频/音频流,通过一定的手段转换为比特流。最终,将这里比特流以一定顺序放到一个盒子里进行存放,从而生成我们最终所看到的,比如,mp4/mp3/flv 等等音视频格式。</p> <h3>视频编码格式</h3> <p>视频编码格式就是我们上面提到的第一步,将物理流转换为比特流,并且进行压缩。同样,它的压缩编码格式会决定它的视频文件格式。所以,第一步很重要。针对于 HTML5 中的 video/audio,它实际上是支持多种编码格式的,但局限于各浏览器厂家的普及度,目前视频格式支持度最高的是 MPEG-4/H.264,音频则是 MP3/AC3。(下面就主要说下视频的,音频就先不谈了。)</p> <p>目前市面上,主流浏览器支持的几个有:</p> <ul> <li>H.264</li> <li>MEPG-4 第 2 部分</li> <li>VP8</li> <li>Ogg</li> <li>WebM(免费)</li> </ul> <p>其它格式,我们这里就不过多赘述,来看一下前两个比较有趣的。如下图:</p> <p><img src="https://simg.open-open.com/show/1d7c6812739eb88bc4d4d550dcc237b4.png"></p> <p>请问,上面箭头所指的编码格式是同一个吗?</p> <p>答案是:No~</p> <p>因为,MPEG-4 实际上是于 1999 年提出的一个标准。而 H.264 则是后台作为优化提出的新的标准。简单来说就是, 我们通常说的 MPEG-4 其实就是MPEG-4 Part 2。而,H.264 则是MPEG-4(第十部分,也叫ISO/IEC 14496-10),又可以理解为 MPEG-4 AVC 。而两者,不同的地方,可以参考: <a href="/misc/goto?guid=4959746517748861033" rel="nofollow,noindex">latthias</a> 的讲解。简单的区别是:H.264 压缩率比以前的 MPEG-4(第 2 部分) 高很多。简单可以参考的就是:</p> <p><img src="https://simg.open-open.com/show/c4c659ccc466224e9b14acaa2ba02b4c.png"></p> <p>详细参考: <a href="/misc/goto?guid=4959746517748861033" rel="nofollow,noindex">编码格式详解</a></p> <h3>视频文件格式</h3> <p>视频文件格式实际上,我们常常称作为容器格式,也就是,我们一般生活中最经常谈到的格式,flv,mp4,ogg 格式等。**它就可以理解为将比特流按照一定顺序放进特定的盒子里。**那选用不同格式来装视频有什么问题吗? 答案是,没有任何问题,但是你需要知道如何将该盒子解开,并且能够找到对应的解码器进行解码。那如果按照这样看的话,对于这些 mp4,ogv,webm等等视频格式,只要我有这些对应的解码器以及播放器,那么就没有任何问题。那么针对于,将视频比特流放进一个盒子里面,如果其中某一段出现问题,那么最终生成的文件实际上是不可用的,因为这个盒子本身就是有问题的。 不过,上面有一个误解的地方在于,我只是将视频理解为一个静态的流。试想一下,如果一个视频需要持续不断的播放,例如,直播,现场播报等。这里,我们就拿 <a href="/misc/goto?guid=4959746517848483893" rel="nofollow,noindex">TS/PS</a> 流来进行讲解。</p> <ul> <li>PS(Program Stream): 静态文件流</li> <li>TS(Transport Stream): 动态文件流</li> </ul> <p>针对于上面两种容器格式,实际上是对一个视频比特流做了不一样的处理。</p> <ul> <li>PS: 将完成视频比特流放到一个盒子里,生成固定的文件</li> <li>TS: 将接受到的视频,分成不同的盒子里。最终生成带有多个盒子的文件。</li> </ul> <p>那么结果就是,如果一个或多个盒子出现损坏,PS 格式无法观看,而 TS 只是会出现跳帧或者马赛克效应。两者具体的区别就是: 对于视频的容错率越高,则会选用 TS,对视频容错率越低,则会选用 PS。</p> <p>常用为:</p> <ul> <li>AVI:MPEG-2,DIVX,XVID,AC-1,H.264;</li> <li>WMV:WMV,AC-1;</li> <li>RM、RMVB:RV, RM;</li> <li>MOV:MPEG-2,XVID,H.264;</li> <li>TS/PS:MPEG-2,H.264,MPEG-4;</li> <li>MKV:可以封装所有的视频编码格式。</li> </ul> <p>详细参考: <a href="/misc/goto?guid=4959746517938865296" rel="nofollow,noindex">视频文件格式</a></p> <h2>直播协议</h2> <p>2016 年是直播元年,一是由于各大宽带提供商顺应民意 增宽降价 ,二是大量资本流进了直播板块,促进了技术的更新迭代。市面上,最常用的是 Apple 推出的 HLS 直播协议(原始支持 H5 播放),当然,还有 RTMP、HTTP-FLV、RTP等。 这里,再问一个问题:</p> <ol> <li>HLS 和 MPEG-4/H.264 以及容器格式 TS/PS 是啥关系?</li> </ol> <p>简单来说,没关系。</p> <p>HLS 根本就不会涉及到视频本身的解码问题。它的存在只是为了确保你的视频能够及时,快速,正确的播放。</p> <p>现在,直播行业依旧很火,而 HTML5 直播,一直以来都是一个比较蛋疼的内容。一是,浏览器厂商更新速度比较慢,二是,这并不是我们前端专攻的一块,所以,有时候的确很鸡肋。当然,进了前端,你就别想着休息。接下来,我们来详细的看一下市面上主流的几个协议。</p> <h3>HLS</h3> <p>HLS 全称是 HTTP Live Streaming。这是 <a href="/misc/goto?guid=4959746589576853008" rel="nofollow,noindex">Apple</a> 提出的直播流协议。目前,IOS 和 高版本 Android 都支持 HLS。那什么是 HLS 呢? HLS 主要的两块内容是 .m3u8 文件和 .ts 播放文件。接受服务器会将接受到的视频流进行缓存,然后缓存到一定程度后,会将这些视频流进行编码格式化,同时会生成一份 .m3u8 文件和其它很多的 .ts 文件。根据 <a href="/misc/goto?guid=4959746518026113455" rel="nofollow,noindex">wiki</a> 阐述,HLS 的基本架构为:</p> <ul> <li>服务器:后台服务器接受视频流,然后进行编码和片段化。 <ul> <li>编码:视频格式编码采用 H.264。音频编码为 AAC, MP3, AC-3,EC-3。然后使用 MPEG-2 Transport Stream 作为容器格式。</li> <li>分片:将 TS 文件分成若干个相等大小的 .ts 文件。并且生成一个 .m3u8 作为索引文件(确保包的顺序)</li> </ul> </li> <li>分发:由于 HLS 是基于 HTTP 的,所以,作为分发,最常用的就是 CDN 了。</li> <li>客户端:使用一个 URL 去下载 m3u8 文件,然后,开始下载 ts 文件,下载完成后,使用 playback software (即时播放器) 进行播放。</li> </ul> <p>这里,我们着重介绍一下客户端的过程。首先,直播之所以是直播,在于它的内容是实时更新的。那 HLS 是怎么完成呢? 我们使用 HLS 直接就用一个 video 进行包括即可:</p> <pre> <code class="language-javascript"><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> </code></pre> <p>根据上面的描述,它实际上就是去请求一个 .m3u8 的索引文件。该文件包含了对 .ts 文件的相关描述,例如:</p> <pre> <code class="language-javascript">#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有效</code></pre> <p>不过,这只是一个非常简单,不涉及任何功能的直播流。实际上,HLS 的整个架构,可以分为:</p> <p><img src="https://simg.open-open.com/show/6a1cac8c2810357e4cdc0624da971ae5.png"></p> <p>当然,如果你使用的是 masterplaylist 作为链接,如:</p> <pre> <code class="language-javascript"><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> </code></pre> <p>我们看一下,masterplaylist 里面具体的内容是啥:</p> <pre> <code class="language-javascript">#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</code></pre> <p>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:</p> <ul> <li>live playlist: 动态列表。顾名思义,该列表是动态变化的,里面的 ts 文件会实时更新,并且过期的 ts 索引会被删除。默认,情况下都是使用动态列表。</li> <li>event playlist: 静态列表。它和动态列表主要区别就是,原来的 ts 文件索引不会被删除,该列表是不断更新,而且文件大小会逐渐增大。它会在文件中,直接添加 #EXT-X-PLAYLIST-TYPE:EVENT 作为标识。</li> <li>VOD playlist: 全量列表。它就是将所有的 ts 文件都列在 list 当中。如果,使用该列表,就和播放一整个视频没有啥区别了。它是使用 #EXT-X-ENDLIST 表示文件结尾。</li> </ul> <p>live playlist DEMO:</p> <pre> <code class="language-javascript">#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</code></pre> <p>evet playlist DEMO:</p> <pre> <code class="language-javascript">#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</code></pre> <p>VOD playlist DEMO:</p> <pre> <code class="language-javascript">#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</code></pre> <p>上面提到过一个 EXT-X-VERSION 这样的标签,这是用来表示当前 HLS 的版本。那 HLS 有哪些版本呢? 根据 <a href="/misc/goto?guid=4959746518107506628" rel="nofollow,noindex">apple 官方文档</a> 的说明,我们可以了解到,不同版本的区别:</p> <p><img src="https://simg.open-open.com/show/063ac33e91b9296ec72f783165e64125.png"></p> <p>当然,HLS 支持的功能,并不只是分片播放(专门适用于直播),它还包括其他应有的功能。</p> <ul> <li>使用 HTTPS 加密 ts 文件</li> <li>快/倒放</li> <li>广告插入</li> <li>不同分辨率视频切换</li> </ul> <p>HLS 的弊端</p> <p>由于 HLS 是基于 HTTP 的,所以,它关于 HTTP 的好处,我们大部分都了解,比如,高兼容性,高可扩展性等。不过正由于是 HTTP 协议,所以会在握手协议上造成一定的延迟性。HLS 首次连接时,总共的延时包括:</p> <ol> <li>TCP 握手,2. m3u8 文件下载,3. m3u8 下的 ts 文件下载。</li> </ol> <p>其中,每个 ts 文件,大概会存放 5s~10s 的时长,并且每个 m3u8 文件会存放 3~8 个 ts 文件。我们折中算一下,5 个 ts 文件,每个时长大约 8s 那么,总的下来,一共延时 40s。当然,这还不算上 TCP 握手,m3u8 文件下载等问题。那优化办法有吗?有的,那就是减少每个 m3u8 文件中的 ts 数量和 ts 文件时长,不过,这样也会成倍的增加后台承受流量请求的压力。所以,这还是需要到业务中去探索最优的配置(打个广告:腾讯云的直播视频流业务,做的确实挺棒。) 关于 HLS 的详细内容,可以参考: <a href="/misc/goto?guid=4959746518107506628" rel="nofollow,noindex">HLS 详解</a> 关于 m3u8 文件的标签内容,可以参考: <a href="/misc/goto?guid=4959746518195022383" rel="nofollow,noindex">HLS 标签头详解</a> 总而言之,HLS 之所以能这么流行,关键在于它的支持度是真的广,所以,对于一般 H5 直播来说,应该是非常友好的。不过,既然是直播,关键在于它的实时性,而 HLS 天生就存在一定的延时,所以,就可以考虑其他低延时的方案,比如 RTMP,HTTP-FLV。下面,我们来看一下 RTMP 内容。</p> <h3>RTMP</h3> <p>RTMP 全称为:Real-Time Messaging Protocol 。它是专门应对实时交流场景而开发出来的一个协议。它爹是 <a href="/misc/goto?guid=4959746518271400351" rel="nofollow,noindex">Macromedia</a> ,后来卖身给了 Adobe。RTMP 根据不同的业务场景,有很多变种:</p> <ul> <li>纯 RTMP 使用 TCP 连接,默认端口为 1935(有可能被封)。</li> <li>RTMPS: 就是 RTMP + TLS/SSL</li> <li>RTMPE: RTMP + encryption。在 RTMP 原始协议上使用,Adobe 自身的加密方法</li> <li>RTMPT: RTMP + HTTP。使用 HTTP 的方式来包裹 RTMP 流,这样能直接通过防火墙。</li> <li>RTMFP: RMPT + UDP。该协议常常用于 P2P 的场景中,针对延时有变态的要求。</li> </ul> <p>既然是 Adobe 公司开发的(算吧),那么,该协议针对的就是 Flash Video,即,FLV。不过,在移动端上,Flash Player 已经被杀绝了,那为啥还会出现这个呢?简单来说,它主要是针对 PC 端的。RTMP 出现的时候,还是 零几 年的时候,IE 还在大行其道,Flash Player 也并未被各大浏览器所排斥。那时候 RTMP 毋庸置疑的可以在视频界有自己的一席之地。</p> <p>RTMP 由于借由 TCP 长连接协议,所以,客户端向服务端推流这些操作而言,延时性很低。它会将上传的流分成不同的分片,这些分片的大小,有时候变,有时候不会变。默认情况下就是,64B 的音频数据 + 128B 的视频数据 + 其它数据(比如 头,协议标签等)。但 RTMP 具体传输的时候,会将分片进一步划分为包,即,视频包,音频包,协议包等。因为,RTMP 在进行传输的时候,会建立不同的通道,来进行数据的传输,这样对于不同的资源,对不同的通道设置相关的带宽上限。</p> <p>RTMP 处理的格式是 MP3/ACC + FLV1。 不过,由于支持性的原因,RTMP 并未在 H5 直播中,展示出优势。下列是简单的对比:</p> <p><img src="https://simg.open-open.com/show/c357f40abdf5c6443f903b2727b61628.png"></p> <h3>HTTP-FLV</h3> <p>HTTP-FLV 和 RTMPT 类似,都是针对于 FLV 视频格式做的直播分发流。但,两者有着很大的区别。</p> <ul> <li>相同点 <ul> <li>两者都是针对 FLV 格式</li> <li>两者延时都很低</li> <li>两者都走的 HTTP 通道</li> </ul> </li> <li>不同点 <ul> <li>HTTP-FLv <ul> <li>直接发起长连接,下载对应的 FLV 文件</li> <li>头部信息简单</li> </ul> </li> <li>RTMPT <ul> <li>握手协议过于复杂</li> <li>分包,组包过程耗费精力大</li> </ul> </li> </ul> </li> </ul> <p>通过上面来看,HTTP-FLV 和 RTMPT 确实不是一回事,但,如果了解 <a href="/misc/goto?guid=4959746589785612207" rel="nofollow,noindex">SRS</a> (simple rtmp server),那么 对 HTTP-FLV 应该清楚不少。SRS 本质上,就是 RTMP + FLV 进行传输。因为 RTMP 发的包很容易处理,通常 RTMP 协议会作为视频上传端来处理,然后经由服务器转换为 FLV 文件,通过 HTTP-FLV 下发给用户。</p> <p><img src="https://simg.open-open.com/show/62ddd516d6b095331e5a48ecbddbdade.png"></p> <p>现在市面上,比较常用的就是 HTTP-FLV 进行播放。但,由于手机端上不支持,所以,H5 的 HTTP-FLV 也是一个痛点。不过,现在 <a href="/misc/goto?guid=4959746589869380429" rel="nofollow,noindex">flv.js</a> 可以帮助高版本的浏览器,通过 <a href="/misc/goto?guid=4959746589952770179" rel="nofollow,noindex">mediaSource</a> 来进行解析。HTTP-FLV 的使用方式也很简单。和 HLS 一样,只需要添加一个连接即可:</p> <pre> <code class="language-javascript"><object type="application/x-shockwave-flash" src="http://s6.pdim.gs/static/a2a36bc596148316.flv"></object> </code></pre> <p>不过,并不是末尾是 .flv 的都是 HTTP-FLV 协议,因为,涉及 FLV 的流有 <a href="/misc/goto?guid=4959746590049779484" rel="nofollow,noindex">三种</a> ,它们三种的使用方式都是一模一样的。</p> <ul> <li>FLV 文件:相当于就是一整个文件,官方称为 渐进 HTTP 流。它的特点是只能渐进下载,不能进行点播。</li> <li>FLV 伪流:该方式,可以通过在末尾添加 ?start=xxx 的参数,指定返回的对应开始时间视频数据。该方式比上面那种就多了一个点播的功能。本质上还是 FLV 直播。</li> <li>FLV 直播流:这就是 HTTP-FLV 真正所支持的流。SRS 在内部使用的是 RTMP 进行分发,然后在传给用户的使用,经过一层转换,变为 HTTP 流,最终传递给用户。</li> </ul> <p>上面说到,HTTP-FLV 就是长连接,简而言之只需要加上一个 Connection:keep-alive 即可。关键是它的响应头,由于,HTTP-FLV 传递的是视频格式,所有,它的 Content-Type 和 Transfer-Encoding 需要设置其它值。</p> <pre> <code class="language-javascript">Content-Type:video/x-flv Expires:Fri, 10 Feb 2017 05:24:03 GMT Pragma:no-cache Transfer-Encoding:chunked</code></pre> <p>不过,一般而言,直播服务器一般和业务服务是不会放在一块的,所以这里,可能会额外需要支持跨域直播的相关技术。在 XHR2 里面,解决办法也很简单,直接使用 CORS 即可:</p> <pre> <code class="language-javascript">// 那么整个响应头,可以为: 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</code></pre> <p>对于 HTTP-FLV 来说,关键难点在于 RTMP 和 HTTP 协议的转换,这里我就不多说了。因为,我们主要针对的是前端开发,讲一下和前端相关的内容。</p> <p>接下来,我们在主要来介绍一下 FLV 格式的。因为,后面我们需要通过 mediaSource 来解码 FLV。</p> <h2>FLV 格式浅析</h2> <p>FLV 原始格式,Adobe 可以直接看 <a href="/misc/goto?guid=4959746590130164200" rel="nofollow,noindex">flv格式详解</a> 。我这里就抽主要的内容讲讲。FLV 也是与时俱进,以前 FLV 的格式叫做 FLV,新版的可以叫做 F4V。两者的区别,简单的区分方法就是:</p> <ul> <li>FLV 是专门针对 Flash 播放器的</li> <li>F4V 是有点像 MEPG 格式的 Flash 播放,主要为了兼容 H.264/ACC。F4V 不支持 FLV(两者本来都不是同一个格式)</li> </ul> <p>这里我们主要针对 FLV 进行相关了解。因为,一般情况下,后台发送视频流时,为了简洁快速,就是发送 FLV 视频。FLV 由于年限比较久,它所支持的内容是 H.263,VP6 codec。FLV 一般可以嵌套在 .swf 文件当中,不过,对于 HTTP-FLV 等 FLV 直播流来说,一般直接使用 .flv 文件即可。在 07 年的时候,提出了 F4V 这个视频格式,当然,FLV 等也会向前兼容。</p> <p><img src="https://simg.open-open.com/show/da873f15774f47fc1b9133d0db99ddee.png"></p> <p>这里,我们来正式介绍一下 FLV 的格式。一个完整的 FLV 流包括 FLV Header + FLV Packets。</p> <h3>FLV Header</h3> <p>FLV 格式头不难,就几个字段:</p> <p>|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|用来跳过多余的头|</p> <h3>FLV Packets</h3> <p>在 FLV 的头部之后,就正式开始发送 FLV 文件。文件会被拆解为数个包(FLV tags)进行传输。每个包都带有 15B 的头。前 4 个字节是用来代表前一个包的头部内容,用来完成倒放的功能。整个包的结构为:</p> <p><img src="https://simg.open-open.com/show/d50df7d4cfe5d89a0660de8b595035f7.png"></p> <p>具体解释如下:</p> <table> <thead> <tr> <th>字段</th> <th>字段大小</th> <th>默认值</th> <th>详解</th> </tr> </thead> <tbody> <tr> <td>Size of previous packet</td> <td>uint32_be</td> <td>0</td> <td>关于前一个包的信息,如果是第一个包,则该部分为 NULL</td> </tr> <tr> <td>Packet Type</td> <td>uint8</td> <td>18</td> <td>设置包的内容,如果是第一个包,则该部分为 AMF 元数据</td> </tr> <tr> <td>Payload Size</td> <td>uint24_be</td> <td>varies</td> <td>该包的大小</td> </tr> <tr> <td>Timestamp Lower</td> <td>uint24_be</td> <td>0</td> <td>起始时间戳</td> </tr> <tr> <td>Timestamp Upper</td> <td>uint8</td> <td>0</td> <td>持续时间戳,通常加上 Lower 实际上戳,代表整个时间。</td> </tr> <tr> <td>Stream ID</td> <td>uint24_be</td> <td>0</td> <td>流的类型,第一个流设为 NULL</td> </tr> <tr> <td>Payload Data</td> <td>freeform</td> <td>varies</td> <td>传输数据</td> </tr> </tbody> </table> <p>其中,由于 Packet Type 的值可以取多个, 需要额外说明一下。</p> <ul> <li>Packet Type <ul> <li>1: RTMP 包的大小</li> <li>3: RTMP 字节读包反馈,RTMP ping,RTMP 服务器带宽,RTMP 客户端带宽</li> <li>8: 音频和视频的数据</li> <li>15: RTMP flex 流</li> <li>24: 经过封装的 flash video。</li> </ul> </li> </ul> <p>上面是关于 FLV 简单的介绍。不过,如果没有 Media Source Extensions 的帮助,那么上面说的基本上全是废话。由于,Flash Player 已经被时代所遗弃,所以,我们不能在浏览器上,顺利的播放 FLV 视频。接下来,我们先来详细了解一下 MSE 的相关内容。</p> <h2>Media Source Extensions</h2> <p>在没有 MSE 出现之前,前端对 video 的操作,仅仅局限在对视频文件的操作,而并不能对视频流做任何相关的操作。现在 MSE 提供了一系列的 <a href="/misc/goto?guid=4959746518354877547" rel="nofollow,noindex">接口</a> ,使开发者可以直接提供 media stream。</p> <p>那 MSE 是如何完成视频流的加载和播放呢?</p> <h3>入门实例</h3> <p>这可以参考 google 的 <a href="/misc/goto?guid=4959746518441142794" rel="nofollow,noindex">MSE 简介</a></p> <pre> <code class="language-javascript">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); }); }</code></pre> <p>可以从上面的代码看出,一套完整的执行代码,不仅需要使用 MSE 而且,还有一下这些相关的 API。</p> <ul> <li>HTMLVideoElement.getVideoPlaybackQuality()</li> <li>SourceBuffer</li> <li>SourceBufferList</li> <li>TextTrack.sourceBuffer</li> <li>TrackDefault</li> <li>TrackDefaultList</li> <li>URL.createObjectURL()</li> <li>VideoPlaybackQuality</li> <li>VideoTrack.sourceBuffer</li> </ul> <p>我们简单讲解一下上面的流程。根据 google 的阐述,整个过程可以为:</p> <p><img src="https://simg.open-open.com/show/9faeebe3ea9a3c1804fe94d5110f9462.png"></p> <ul> <li>第一步,通过异步拉取数据。</li> <li>第二步,通过 MediaSource 处理数据。</li> <li>第三步,将数据流交给 audio/video 标签进行播放。</li> </ul> <p>而中间传递的数据都是通过 Buffer 的形式来进行传递的。</p> <p><img src="https://simg.open-open.com/show/8ab4ae98b3b087548c65781ef45158bb.png"></p> <p>中间有个需要注意的点,MS 的实例通过 URL.createObjectURL() 创建的 url 并不会同步连接到 video.src。换句话说, URL.createObjectURL() 只是将底层的流(MS)和 video.src 连接中间者,一旦两者连接到一起之后,该对象就没用了。</p> <p>那么什么时候 MS 才会和 video.src 连接到一起呢?</p> <p>创建实例都是同步的,但是底层流和 video.src 的连接时异步的。MS 提供了一个 sourceopen 事件给我们进行这项异步处理。一旦连接到一起之后,该 URL object 就没用了,处于内存节省的目的,可以使用 URL.revokeObjectURL(vidElement.src) 销毁指定的 URL object。</p> <pre> <code class="language-javascript">mediaSource.addEventListener('sourceopen', sourceOpen); function sourceOpen(){ URL.revokeObjectURL(vidElement.src) }</code></pre> <p>MS 对流的解析</p> <p>MS 提供了我们对底层音视频流的处理,那一开始我们怎么决定以何种格式进行编解码呢?</p> <p>这里,可以使用 addSourceBuffer(mime) 来设置相关的编码器:</p> <pre> <code class="language-javascript">var mime = 'video/webm; codecs="opus, vp9"'; var sourceBuffer = mediaSource.addSourceBuffer(mime);</code></pre> <p>然后通过,异步拉取相关的音视频流:</p> <pre> <code class="language-javascript">fetch(url) .then(res=>{ return res.arrayBuffer(); }) .then(buffer=>{ sourceBuffer.appendBuffer(buffer); })</code></pre> <p>如果视频已经传完了,而相关的 Buffer 还在占用内存,这时候,就需要我们显示的中断当前的 Buffer 内容。那么最终我们的异步处理结果变为:</p> <pre> <code class="language-javascript">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); });</code></pre> <p>上面我们大致了解了一下关于 Media Source Extensions 的大致流程,但里面的细节我们还没有细讲。接下来,我们来具体看一下 MSE 一篮子的生态技术包含哪些内容。首先是,MediaSource</p> <h2>MediaSource</h2> <p>MS(MediaSource) 可以理解为多个视频流的管理工具。以前,我们只能下载一个清晰度的流,并且不能平滑切换低画质或者高画质的流,而现在我们可以利用 MS 实现这里特性。我们先来简单了解一下他的 API。</p> <h3>MS 的创建</h3> <p>创建一个 MS:</p> <pre> <code class="language-javascript">var mediaSource = new MediaSource();</code></pre> <h3>相关方法</h3> <p>addSourceBuffer()</p> <p>该是用来返回一个具体的视频流,接受一个 mimeType 表示该流的编码格式。例如:</p> <pre> <code class="language-javascript">var mimeType = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"'; var sourceBuffer = mediaSource.addSourceBuffer(mimeType);</code></pre> <p>sourceBuffer 是直接和视频流有交集的 API。例如:</p> <pre> <code class="language-javascript">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); }); };</code></pre> <p>它通过 appendBuffer 直接添加视频流,实现播放。不过,在使用 addSourceBuffer 创建之前,还需要保证当前浏览器是否支持该编码格式。</p> <p>removeSourceBuffer()</p> <p>用来移除某个 sourceBuffer。移除也主要是考虑性能原因,将不需要的流移除以节省相应的空间,格式为:</p> <pre> <code class="language-javascript">mediaSource.removeSourceBuffer(sourceBuffer);</code></pre> <p>endOfStream()</p> <p>用来表示接受的视频流的停止,注意,这里并不是断开,相当于只是下好了一部分视频,然后你可以进行播放。此时,MS 的状态变为: ended 。例如:</p> <pre> <code class="language-javascript">var mediaSource = this; var sourceBuffer = mediaSource.addSourceBuffer(mimeCodec); fetchAB(assetURL, function (buf) { sourceBuffer.addEventListener('updateend', function (_) { mediaSource.endOfStream(); // 结束当前的接受 video.play(); // 可以播放当前获得的流 }); sourceBuffer.appendBuffer(buf); });</code></pre> <p>isTypeSupported()</p> <p>该是用来检测当前浏览器是否支持指定视频格式的解码。格式为:</p> <pre> <code class="language-javascript">var isItSupported = mediaSource.isTypeSupported(mimeType); // 返回值为 Boolean</code></pre> <p>mimeType 可以为 type 或者 type + codec。</p> <p>例如:</p> <pre> <code class="language-javascript">// 不同的浏览器支持不一样,不过基本的类型都支持。 MediaSource.isTypeSupported('audio/mp3'); // false,这里应该为 audio/mpeg MediaSource.isTypeSupported('video/mp4'); // true MediaSource.isTypeSupported('video/mp4; codecs="avc1.4D4028, mp4a.40.2"'); // true</code></pre> <p>这里有一份具体的 <a href="/misc/goto?guid=4959746590264131488" rel="nofollow,noindex">mimeType</a> 参考列表。</p> <h3>MS 的状态</h3> <p>当 MS 从创建开始,都会自带一个 readyState 属性,用来表示其当前打开的状态。MS 有三个状态:</p> <ul> <li>closed: 当前 MS 没有和 media element(比如:video.src) 相关联。创建时,MS 就是该状态。</li> <li>open: source 打开,并且准备接受通过 sourceBuffer.appendBuffer 添加的数据。</li> <li>ended: 当 endOfStream() 执行完成,会变为该状态,此时,source 依然和 media element 连接。</li> </ul> <pre> <code class="language-javascript">var mediaSource = new MediaSource; mediaSource.readyState; // 默认为 closed</code></pre> <p>当由 closed 变为 open 状态时,需要监听 sourceopen 事件。</p> <pre> <code class="language-javascript">video.src = URL.createObjectURL(mediaSource); mediaSource.addEventListener('sourceopen', sourceOpen);</code></pre> <p>MS 针对这几个状态变化,提供了相关的事件: sourceopen , sourceended , sourceclose 。</p> <ul> <li>sourceopen: 当 “closed” to “open” 或者 “ended” to “open” 时触发。</li> <li>sourceended: 当 “open” to “ended” 时触发。</li> <li>sourceclose: 当 “open” to “closed” 或者 “ended” to “closed” 时触发。</li> </ul> <p>MS 还提供了其他的监听事件 sourceopen,sourceended,sourceclose,updatestart,update,updateend,error,abort,addsourcebuffer,removesourcebuffer. 这里主要选了比较重要的,其他的可以参考官方文档。</p> <h3>MS 属性</h3> <p>比较常用的属性有: duration,readyState。</p> <ul> <li>duration: 获得当前媒体播放的时间,既可以设置(get),也可以获取(set)。单位为 s(秒)</li> </ul> <pre> <code class="language-javascript">mediaSource.duration = 5.5; // 设置媒体流播放的时间 var myDuration = mediaSource.duration; // 获得媒体流开始播放的时间</code></pre> <p>在实际应用中为:</p> <pre> <code class="language-javascript">sourceBuffer.addEventListener('updateend', function (_) { mediaSource.endOfStream(); mediaSource.duration = 120; // 设置当前流播放的时间 video.play(); });</code></pre> <ul> <li>readyState: 获得当前 MS 的状态。取值上面已经讲过了: closed , open , ended 。</li> </ul> <pre> <code class="language-javascript">var mediaSource = new MediaSource; //此时的 mediaSource.readyState 状态为 closed</code></pre> <p>以及:</p> <pre> <code class="language-javascript">sourceBuffer.addEventListener('updateend', function (_) { mediaSource.endOfStream(); // 调用该方法后结果为:ended video.play(); });</code></pre> <p>除了上面两个属性外,还有 sourceBuffers , activeSourceBuffers 这两个属性。用来返回通过 addSourceBuffer() 创建的 SourceBuffer 数组。这没啥过多的难度。</p> <p>接下来我们就来看一下靠底层的 sourceBuffer 。</p> <h2>SourceBuffer</h2> <p>SourceBuffer 是由 mediaSource 创建,并直接和 HTMLMediaElement 接触。简单来说,它就是一个流的容器,里面提供的 append() , remove() 来进行流的操作,它可以包含一个或者多个 media segments 。同样,接下来,我们再来看一下该构造函数上的基本属性和内容。</p> <h3>基础内容</h3> <p>前面说过 sourceBuffer 主要是一个用来存放流的容器,那么,它是怎么存放的,它存放的内容是啥,有没有顺序等等。这些都是 sourceBuffer 最最根本的问题。OK,接下来,我们来看一下的它的基本架构有些啥。</p> <p>参考 <a href="/misc/goto?guid=4959746518540778194" rel="nofollow,noindex">W3C</a> ,可以基本了解到里面的内容为:</p> <pre> <code class="language-javascript">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); };</code></pre> <p>上面这些属性决定了其 sourceBuffer 整个基础。</p> <p>首先是 mode 。上面说过,SB(SourceBuffer) 里面存储的是 media segments(就是你每次通过 append 添加进去的流片段)。SB.mode 有两种格式:</p> <ul> <li>segments: 乱序排放。通过 timestamps 来标识其具体播放的顺序。比如:20s的 buffer,30s 的 buffer 等。</li> <li>sequence: 按序排放。通过 appendBuffer 的顺序来决定每个 mode 添加的顺序。 timestamps 根据 sequence 自动产生。</li> </ul> <p>那么上面两个哪个是默认值呢?</p> <p>看情况,讲真,没骗你。</p> <p>当 media segments 天生自带 timestamps ,那么 mode 就为 segments ,否则为 sequence 。所以,一般情况下,我们是不用管它的值。不过,你可以在后面,将 segments 设置为 sequence 这个是没毛病的。反之,将 sequence 设置为 segments 就有问题了。</p> <pre> <code class="language-javascript">var bufferMode = sourceBuffer.mode; if (bufferMode == 'segments') { sourceBuffer.mode = 'sequence'; }</code></pre> <p>然后另外两个就是 buffered 和 updating 。</p> <ul> <li>buffered:返回一个 <a href="/misc/goto?guid=4959746590379498388" rel="nofollow,noindex">timeRange</a> 对象。用来表示当前被存储在 SB 中的 buffer。</li> <li>updating: 返回 Boolean,表示当前 SB 是否正在被更新。例如: SourceBuffer.appendBuffer(), SourceBuffer.appendStream(), SourceBuffer.remove() 调用时。</li> </ul> <p>另外还有一些其他的相关属性,比如 textTracks,timestampOffset,trackDefaults,这里就不多说了。实际上,SB 是一个事件驱动的对象,一些常见的处理,都是在具体的事件中完成的。那么它又有哪些事件呢?</p> <h3>事件触发</h3> <p>在 SB 中,相关事件触发包括:</p> <ul> <li>updatestart: 当 updating 由 false 变为 true。</li> <li>update:当 append()/remove() 方法被成功调用完成时,updating 由 true 变为 false。</li> <li>updateend: append()/remove() 已经结束</li> <li>error: 在 append() 过程中发生错误,updating 由 true 变为 false。</li> <li>abort: 当 append()/remove() 过程中,使用 abort() 方法废弃时,会触发。此时,updating 由 true 变为 false。</li> </ul> <p>注意上面有两个事件比较类似: update 和 updateend 。都是表示处理的结束,不同的是,update 比 updateend 先触发。</p> <pre> <code class="language-javascript">sourceBuffer.addEventListener('updateend', function (e) { // 当指定的 buffer 加载完后,就可以开始播放 mediaSource.endOfStream(); video.play(); });</code></pre> <h3>相关方法</h3> <p>SB 处理流的方法就是 +/- : appendBuffer, remove。另外还有一个中断处理函数 abort() 。</p> <ul> <li>appendBuffer(ArrayBuffer):用来添加 ArrayBuffer。该 ArrayBuffer 一般是通过 fetch 的 response.arrayBuffer(); 来获取的。</li> <li>remove(start, end): 用来移除具体某段的 media segments。 <ul> <li>@param start/end: 都是时间单位(s)。用来表示具体某段的 media segments 的范围。</li> </ul> </li> <li>abort(): 用来放弃当前 append 流的操作。不过,该方法的业务场景也比较有限。它只能用在当 SB 正在更新流的时候。即,此时通过 fetch,已经接受到新流,并且使用 appendBuffer 添加,此为开始的时间。然后到 updateend 事件触发之前,这段时间之内调用 abort() 。有一个业务场景是,当用户移动进度条,而,此时 fetch 已经获取前一次的 media segments,那么可以使用 abort 放弃该操作,转而请求新的 media segments。具体可以参考: <a href="/misc/goto?guid=4959746518627845432" rel="nofollow,noindex">abort 使用</a></li> </ul> <p>上面主要介绍了处理音视频流需要用的 Web 技术,后面章节,我们接入实战,具体来讲一下,如何做到使用 MSE 进行 remux 和 demux。</p> <p> </p> <p>来自:https://www.villainhr.com/page/2017/03/31/全面进阶 H5 直播</p> <p> </p>