web端直播技术初步研究(上)

引言

在之前很长的一段时间之内,原生网页端对音视频媒体内容的处理能力比较薄弱,主要依靠flash插件来进行音视频处理,如:采集,编码,解码,播放。

近年来web技术不断发展与完善,尤其是html5、es6+以及新api(如webRTC) 等现代web技术的发布,对媒体技术的支持有了很大进步,使原生网页处理音视频(流)成为可能。

本文主要描述通过现代web技术探索视频流中最基础功能的实现,实际的视频流业务要比本文描述的更加复杂。

传统实时视频直播核心基础功能

传统直播核心基础功能就是视频流的采集和播放。

传统直播流程中一般包含多个角色: - 推流端(主播端):音视频采集,将数据流传送到媒体服务器,简称推流 - 媒体服务端:音视频基础服务,解码,转码,编码 - web服务器 : 用户连接,房间信息及拉流信息 - 拉流端(观看端):拉取服务器端音视频进行播放,简称拉流

简单结构示意图如下:

直播流程示意图

传统直播核心功能技术实现

主播端:音视频采集

遗憾的是,目前web端并没有非常成熟的技术进行主播端采集及推流。 这里需要提一下的是webRTC技术。

webRTC(Web Real-Time Communication) , 网页实时通信技术,是一个支持网页浏览器,不需要安装额外插件就可以实现实时音视频对话的API。webRTC提供了视频会议的核心技术,包括音视频的采集、编解码、网络传输、显示等功能,并且还支持跨平台:windows,linux,mac,android。

webRTC(Web Real-Time Communication) , 网页实时通信技术,是一个支持网页浏览器,不需要安装额外插件就可以实现实时音视频对话的API。webRTC提供了视频会议的核心技术,包括音视频的采集、编解码、网络传输、显示等功能,并且还支持跨平台:windows,linux,mac,android。 看起来webRTC似乎具备了所有主播端需要的技术条件,但是,到目前(2019.02)为止,兼容性还有一定的问题,在PC端, Edge浏览器只提供了部分API支持,在移动端兼容性更差一些。另外, webRTC并没有给web开发者开放前处理接口,也就是说目前国内主流直播平台所标配的一些功能无法实现,如:美颜,变声,视频风格切换等。

而且,编解码器、抖动缓冲等功能只能依靠webRTC内置处理,无法根据自身情况自定义优化处理。

所以,webRTC目前还不是一个成熟的可用于直播平台的采集方案,国内主流直播平台主要使用客户端程序进行采集和推流。

观看端:音视频播放

视频流传输的协议

说到音视频播放,需要先说一下视频流传输的几种主流协议和编码。

目前主流使用的传输协议有:

  • rtmp
  • http-flv
  • dash
  • hls

RTMP是Real-Time Messaging Protocol(实时消息传输协议),是一个协议族,包括RTMP基本协议及RTMPT/RTPMS/RTMPE等多种变种。RTMP是一种设计用来进行实时数据通信的网络协议,主要用来在flash/AIR 平台和支持RTMP协议的流媒体/交互服务器之间进行音视频和数据通信。

HTTP-FLV是将flv数据封装在HTTP之上的协议,可以更好的穿透防火墙。

HLS(Http Live Streaming) 是苹果公司提出的基于http的流媒体网络传输协议。

DASH (MPEG-DASH Dynamic Adaptive Streaming over HTTP) 是由运动图像专家组(MPEG)开发的基于http的动态自适应流媒体标准,是一种自适应比特率流技术,使高质量流媒体可以通过传统的HTTP网络服务器以互联网传递。类似苹果公司的HTTP Live Streaming(HLS)方案,MPEG-DASH会将内容分解成一系列小型的基于HTTP的文件片段.它是为了解决在其之前出现的Adobe Systems HTTP Dynamic Streaming(HDS)、苹果公司HTTP Live Streaming(HLS)微软Smooth Streaming相关的技术互不兼容的情况而出现的。

几种协议的简单对比如下:

协议 传输方式 视频封装格式 延时 数据分段 Html5播放
rtmp tcp流 flv 连续流 不支持
http-flv http flv 连续流 可通过html5解封包播放
hls http ts 切片文件 可在移动端直接播放
dash http Mp4/3gp/webm 切片文件 如果是mp4,webm可直接播放,否则需要解封包播放

播放方式

RTMP

目前HTML5 video播放器并不直接支持RTMP协议的视频流,RTMP协议主要使用在flash播放器中或者是实现了RTMP协议的客户端软件中。

HTTP-FLV

HTTP-FLV与RTMP协议传输的数据是一样的,都是flv文件的tag。 http-flv是一个无限大的http流的文件。原生的html5 video标签并不支持flv格式,需要使用javascript转码之后播放,一般是将flv格式转成ISO BMFF(mp4片段)格式。开源的flv.js可以实现此功能,flv.js的原理后面进行分析。 使用flv.js播放video示意代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport"
    content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible"
    content="ie=edge">
  <title>flv视频播放</title>
</head>

<body>
  <video id="videoEl">
    你的浏览器不支持video视频播放
  </video>
  <script src="flv.min.js"></script>
  <script>
    (function () {
      if (flvjs.isSupported()) {
        const videoEl = document.querySelector('#videoEl')
        const flvPlayer = flvjs.createPlayer({
          type: 'flv',
          isLive: true, // 是否实时流
          cors: true, // 是否启用cors
          hasAudio: true, // 是否有音频
          hasVideo: true, // 是否有视频
          url: 'http://localhost:3001/flv/demo.flv' // 源地址
        })
        flvPlayer.attchMediaElement(videoEl)
        flvPlayer.load();
        flvPlayer.play();
      }
    })()
  </script>
</body>

</html>
HLS

HLS视频播放目前移动端ios和android都能比较好的支持,而PC端只有safari支持。目前绝大部分国内主流直播平台在移动wap端采用此方案进行视频流播放。

HLS协议包含一个描述文件和多个视频分片文件,描述文件一般以.m3u8为后缀名,分片文件有多种封装格式,一般以.ts为后缀名较为常见。

一个简单的描述文件内容示意如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:8
#EXT-X-MEDIA-SEQUENCE:1

#EXTINF:7.975,
https://xx.com /1.ts
#EXTINF:7.941,
https://xx.com/2.ts
#EXTINF:7.975,
https://xx.com/3.ts
…
#EXT-X-ENDLIST

#EXTM3U 表明这是一个m3u格式的文件,每个m3u文件第一行必须是这个tag

#EXT-X-VERSION 指定协议版本

#EXT-X-TARGETDURATION 表示每个媒体段(ts)最大持续时间(秒)

#EXT-X-MEDIA-SEQUENCE 定义当前m3u8文件中第一个文件的序列号,每个ts文件在m3u8文件中都有固定唯一的序列号,该序列号用于在MBR时切换码率进行对齐。

#EXTINF:指定每个媒体段(ts)的持续时间,这个仅对其后面的URI有效,每两个媒体段URI间被这个tag分隔开

#EXT-X-ENDLIST:表示列表的结束

HLS视频的播放比较简单,直接使用m3u8文件地址作为视频源进行播放:

1
2
3
4
5
6
7
8
<video playsinline="true"
    webkit-playsinline="true"
    x-webkit-airplay="allow"
    airplay="allow"
    preload="auto"
    width="399"
    height="311"
    src="https://xxx. com/live/520529_1551080541203/playlist.m3u8?wsSecret=d8cb6b44562ff4970b21395bc94715ac&wsTime=5C73B679&get_url=6"></video>
DASH

DASH的标准与HLS类似,也有描述文件和切片文件,描述文件以.mpd为后缀名,mpd文件内容为xml格式。切片文件一般以.m4s为后缀名。

一个简单的mpd文件内容示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<?xml version="1.0" ?>
<MPD mediaPresentationDuration="PT14.040S" minBufferTime="PT4.00S" profiles="urn:mpeg:dash:profile:isoff-live:2011" type="static" xmlns="urn:mpeg:dash:schema:mpd:2011">
  <!-- Created with Bento4 mp4-dash.py, VERSION=1.7.0-613 -->
  <Period>
    <!-- Video -->
    <AdaptationSet maxHeight="1080" maxWidth="1920" mimeType="video/mp4" minHeight="360" minWidth="640" segmentAlignment="true" startWithSAP="1">
      <Representation bandwidth="760472" codecs="avc1.42C01E" frameRate="25" height="360" id="video/1" scanType="progressive" width="640">
        <SegmentList duration="4000" timescale="1000">
          <Initialization sourceURL="video/1/init.mp4"/>
          <SegmentURL SI="62.66" TI="26.09" media="video/1/seg-1.m4s"/>
          <SegmentURL SI="78.02" TI="15.77" media="video/1/seg-2.m4s"/>
          <SegmentURL SI="66.68" TI="14.83" media="video/1/seg-3.m4s"/>
          <SegmentURL SI="72.02" TI="11.77" media="video/1/seg-4.m4s"/>
        </SegmentList>
      </Representation>
      <Representation bandwidth="1167336" codecs="avc1.42C01F" frameRate="25" height="720" id="video/2" scanType="progressive" width="1280">
        <SegmentList duration="4000" timescale="1000">
          <Initialization sourceURL="video/2/init.mp4"/>
          <SegmentURL SI="43.08" TI="26.05" media="video/2/seg-1.m4s"/>
          <SegmentURL SI="64.89" TI="16.14" media="video/2/seg-2.m4s"/>
          <SegmentURL SI="50.92" TI="15.09" media="video/2/seg-3.m4s"/>
          <SegmentURL SI="58.49" TI="11.98" media="video/2/seg-4.m4s"/>
        </SegmentList>
      </Representation>
      <Representation bandwidth="1683750" codecs="avc1.42C028" frameRate="25" height="1080" id="video/3" scanType="progressive" width="1920">
        <SegmentList duration="4000" timescale="1000">
          <Initialization sourceURL="video/3/init.mp4"/>
          <SegmentURL SI="32.57" TI="25.99" media="video/3/seg-1.m4s"/>
          <SegmentURL SI="54.54" TI="16.21" media="video/3/seg-2.m4s"/>
          <SegmentURL SI="40.21" TI="15.18" media="video/3/seg-3.m4s"/>
          <SegmentURL SI="48.33" TI="12.04" media="video/3/seg-4.m4s"/>
        </SegmentList>
      </Representation>
    </AdaptationSet>
    <!-- Audio -->
    <AdaptationSet mimeType="audio/mp4" segmentAlignment="true" startWithSAP="1">
      <Representation audioSamplingRate="48000" bandwidth="68943" codecs="mp4a.40.2" id="audio/und/mp4a/1">
        <AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
        <SegmentList duration="4000" timescale="1000">
          <Initialization sourceURL="audio/und/mp4a/1/init.mp4"/>
          <SegmentURL media="audio/und/mp4a/1/seg-1.m4s"/>
          <SegmentURL media="audio/und/mp4a/1/seg-2.m4s"/>
          <SegmentURL media="audio/und/mp4a/1/seg-3.m4s"/>
          <SegmentURL media="audio/und/mp4a/1/seg-4.m4s"/>
          <SegmentURL media="audio/und/mp4a/1/seg-5.m4s"/>
        </SegmentList>
      </Representation>
      <Representation audioSamplingRate="48000" bandwidth="134618" codecs="mp4a.40.2" id="audio/und/mp4a/2">
        <AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
        <SegmentList duration="4000" timescale="1000">
          <Initialization sourceURL="audio/und/mp4a/2/init.mp4"/>
          <SegmentURL media="audio/und/mp4a/2/seg-1.m4s"/>
          <SegmentURL media="audio/und/mp4a/2/seg-2.m4s"/>
          <SegmentURL media="audio/und/mp4a/2/seg-3.m4s"/>
          <SegmentURL media="audio/und/mp4a/2/seg-4.m4s"/>
          <SegmentURL media="audio/und/mp4a/2/seg-5.m4s"/>
        </SegmentList>
      </Representation>
    </AdaptationSet>
  </Period>
</MPD>

以下进行一些名词解释:

  • Periods

一个MPD文件可以包含一个或多个Periods,每个Period代表某一个时间段,比如某条码流有60s长,Period1为0~15s,Period2为16~40s,Period3为41~60s。在同一个Period内,可用的媒体内容及其各个可用码率(Representation)不会发生变更。直播情况下,可能需要周期性地去服务器请求新的MPD文件,服务器可能会移除已过时的Period,添加新的Period,而新的Period中可能会添加新的可用码率,或去掉上一个Period中存在的某些码率。

  • AdaptationSet

一个Period由一个或多个AdaptationSets组成,AdaptationSet包含了逻辑一致的媒体呈现的格式,例如,相同的codec、language、resolution,以及音频通道数(5.1,stereo等)的媒体内容可以组成一个AdaptationSet。每个AdaptationSet由一组可供切换的不同码率的码流(Representation)组成,这些码流中可能包含一个(ISO profile)或多个(TS profile)media content components,因为ISO profile的mp4或fmp4 segment中通常只含有一个视频或者音频内容,而TS profile中的TS segment同时含有视频和音频内容,当同时含有多个media component content时,每个被复用的media contentcomponent将被单独描述。

  • Representation

一个AdaptationSet由一组媒体内容版本可切换的Representations构成。每个Representation包含了相同媒体内容的不同版本,即不同的分辨率、码率等,以供客户端根据自身的网络条件和性能限制来选择合适的版本下载播放。

  • Segment

每个Representation中的内容都被切分成一段段Segments,使得客户端在播放时能够方便在不同的Representations之间切换。每个Segment由一个对应的URL指定,也可能由相同的URL与不同的byte range指定。DASH客户端可以通过HTTP协议来获取URL对应的分片数据。MPD中描述Segment URL的形式包括Segment list,Segment template,Single segment。

DASH播放与HLS类似,也是通过描述文件为入口进行播放。但目前的H5播放器并不能原生直接播放DASH标准的视频流,需要借助javascript,目前比较官方的是dash.js。代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport"
    content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible"
    content="ie=edge">
  <title>Document</title>

  <style>
    video {
      width: 640px;
      height: 360px;
    }
  </style>
  <script src="https://cdn.dashjs.org/latest/dash.all.min.js"></script>
</head>
<body>
  <div>
    <video data-dashjs-player
      autoplay
      src="https://dash.akamaized.net/envivio/EnvivioDash3/manifest.mpd"
      controls></video>
  </div>
</body>

</html>

小结

本篇主要介绍了web直播中的技术,以及如何播放视频流,下篇将主要介绍播放原理。

《web端直播技术初步研究(下)》

由于本人才学有限,如有错误,敬请斧正。

参考文档

调研参考了大量的文章,无法一一列出。列出一部分,表示深深的感谢: