从0到1打造直播 App

概要

分享内容

互联网内容载体变迁历程,文字——图片/声音——视频——VR/AR——…….。从直播1.0秀场时代(YY),2.0游戏直播(斗鱼、虎牙、熊猫)到如今全民直播3.0泛生活娱乐时代(映客、花椒),国外直播app(Meerkat 、Periscope),随着VA/AR/MR提出的沉浸式视听体验,直播4.0时代很快就能到来。

在这个全民娱乐的时代,直播已经火得不要不要的,各大公司都有自己的直播产品。本文主要从直播的一些基本知识,一步步打造直播app。直播那么火的背后有什么样的技术支撑呢?

先将这些APP按照视频网站按照视频网站、弹幕视频、直播平台、在线秀场、移动短视频、移动直播来划分类别。再按照内容和社交这个维度来进行区分,可以明显看出视频网站、弹幕网站和直播平台更偏内容,他们对内容的需求更加高,用户在上面进行社交沉淀相对比较浅。

从0到1打造直播 App

而后面三者更加偏向社交,他们强调人而不强调内容。所以短期内不会有大的竞争关系,只是前三类、后三者之间的竞争会出现。

大体框架

基本是下图这个套路:

从0到1打造直播 App

录制->编码->网络传输->解码->播放

以上为直播的整体流程,根据该流程分为以下技术点:

  • 怎样录制直播视频

  • 怎样实时上传直播视频

  • 怎样播放直播视频

  • 直播间的用户是如何交互

  • 一、移动视频直播发展

    PC直播(固定场所)——>移动端(形式自由)。

    随着越来越多的直播类 App 上线,移动直播进入了前所未有的爆发阶段,目前大多数移动直播以 Native 客户端为主。但是H5端的直播在移动直播端也承载着不可替代的作用,例如 H5 有着传播快,易发布的优势。

    完整的直播包括:

  • 视频录制端 电脑上的音视频输入设备或者手机端的摄像头或者麦克风,目前以移动端的手机视频为主。

  • 视频播放端 可以是电脑上的播放器,手机端的 Native 播放器,还有 H5 的 video 标签等。

  • 流媒体服务器端 用来接受视频录制端提供的视频源,同时提供给视频播放端流服务。目前开源的流媒体有RED5,CRTMPD,NGINX-RTMP,SRS。

  • 二、录制视频

    如何生产视频数据

    从0到1打造直播 App

    封装格式的主要作用是把视频码流和音频码流按照一定的格式存储在一个文件中。

    为什么要分封装格式和视频编码格式呢?

    这个其实跟网络分七层模型一个原理。解耦和,降低依赖,底层给上层提供基础功能,底层和上层都都可以单独扩展,可以以多种方案组合编码与封装,比如MP4与H264、MP4与MPEG、TS与H264等等。比如这里面的这边文章的编码就只负责将最原始的音频和视频数据就行压缩,而压缩完的数据要怎么组织就拜托给上层的封装,封装接到视频音频数据负责给数据编号,指定同步协议,加入字幕等操作。经过封装后,得到的就是可以播放的上面提到的视频文件MP4或者MKV等等。把这个过程反过来就是上图描述的视频播放的过程。

    1、流媒体源
  • PC端的摄像头、屏幕

    对于PC端的流媒体源,可以使用Open Broadcaster Software串流(支持多种直播平台)。

  • 移动端iOS、Android的摄像头和麦克风。

    iOS、Android主要是系统提供的API实现。

  • webRTC (Web Real-Time Communication)

    webRTC是一个支持网页浏览器进行实时语音对话或视频对话的技术,可以在网页浏览器中进行采集、传输、播放,缺点是只在 PC 的 Chrome 上支持较好,移动端支持不太理想。

  • 使用 webRTC 录制视频基本流程是

  • 调用window.webkitRTCPeerConnection

     

    (一种视频流数据格式)。

  • 利用

    <video

    controls

    autoplay

    >

    <source

    src

    =

    "xxx.m3u8"

    type

    =

    "application/vnd.apple.mpegurl"

    />

    </video>

    HTTP Live Streaming

    HLS是一个由苹果公司提出的基于HTTP的流媒体网络传输协议。

    HLS直播最大的不同在于,直播客户端获取到的,并不是一个完整的数据流。

    HLS协议在服务器端将直播数据流存储为连续的、很短时长的媒体文件(MPEG-TS格式),而客户端则不断的下载并播放这些小文件,因为服务器端总是会将最新的直播数据生成新的小文件,这样客户端只要不停的按顺序播放从服务器获取到的文件,就实现了直播。

    由此可见,基本上可以认为,HLS是以点播的技术方式来实现直播。由于数据通过HTTP协议传输,所以不用考虑防火墙或者代理的问题,而且分段文件的时长很短,客户端可以很快的选择和切换码率,以适应不同带宽条件下的播放。不过HLS的这种技术特点决定了延迟一般总是会高于普通的流媒体直播协议。

    每一个 .m3u8 文件,分别对应若干个 ts 文件,这些 ts 文件才是真正存放视频的数据,m3u8 文件只是存放了一些 ts 文件的配置信息和相关路径,当视频播放时,.m3u8 是动态改变的,video 标签会解析这个文件,并找到对应的 ts 文件来播放,所以一般为了加快速度,.m3u8 放在 Web 服务器上,ts 文件放在 CDN 上。

    支持的视频流编码为H.264,音频流编码为AAC。

    简单讲就是把整个流分成一个个小的,基于 HTTP 的文件来下载,每次只下载一些,前面提到了用于 H5 播放直播视频时引入的一个 .m3u8 的文件,这个文件就是基于 HLS 协议,存放视频流元数据的文件。

    .m3u8 文件,其实就是以 UTF-8 编码的 m3u 文件,这个文件本身不能播放,只是存放了播放信息的文本文件。

    打开之后就是这个样子:

    #

    EXTM3U

    #

    EXT

    -

    X

    -

    TARGETDURATION

    :

    11

    #

    EXT

    -

    X

    -

    VERSION

    :

    3

    #

    EXT

    -

    X

    -

    MEDIA

    -

    SEQUENCE

    :

    0

    #

    EXT

    -

    X

    -

    PLAYLIST

    -

    TYPE

    :

    VOD

    #

    EXTINF

    :

    10.133333

    ,

    fileSequence0

    .

    ts

    #

    EXTINF

    :

    10.000666

    ,

    fileSequence1

    .

    ts

    #

    EXTINF

    :

    10.667334

    ,

    fileSequence2

    .

    ts

    #

    EXTINF

    :

    9.686001

    ,

    fileSequence3

    .

    ts

    #

    EXTINF

    :

    9.768665

    ,

    fileSequence4

    .

    ts

    #

    EXTINF

    :

    10.000000

    ,

    fileSequence5

    .

    ts

    #

    EXT

    -

    X

    -

    ENDLIST

    ts 文件,就是存放视频的文件:

    从0到1打造直播 App

    HLS只请求基本的HTTP报文,与实时传输协议(RTP)不同,HLS可以穿过任何允许HTTP数据通过的防火墙或者代理服务器。它也很容易使用内容分发网络来传输媒体流。

    HLS 的请求播放流程

    从0到1打造直播 App

    HLS直播延时

    我们知道 hls 协议是将直播流分成一段一段的小段视频去下载播放的,所以假设列表里面的包含5个 ts 文件,每个 TS 文件包含5秒的视频内容,那么整体的延迟就是25秒。因为当你看到这些视频时,主播已经将视频录制好上传上去了,所以时这样产生的延迟。当然可以缩短列表的长度和单个 ts 文件的大小来降低延迟,极致来说可以缩减列表长度为1,并且 ts 的时长为1s,但是这样会造成请求次数增加,增大服务器压力,当网速慢时回造成更多的缓冲,所以苹果官方推荐的 ts 时长时10s,所以这样就会大改有30s的延迟。所以服务器接收流,转码,保存,切块,再分发给客户端,这里就延时的根本原因。

    更多关于延迟的问题可以参考苹果官方地址:

    http://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/StreamingMediaGuide/FrequentlyAskedQuestions/FrequentlyAskedQuestions.html

    HLS优势

    但是 H5 直播视频却有一些不可替代的优势:

    2、RTMP

    RTMP协议是应用层协议,是要靠底层可靠的传输层协议(通常是TCP)来保证信息传输的可靠性的。在基于传输层协议的链接建立完成后,RTMP协议也要客户端和服务器通过“握手”来建立基于传输层链接之上的NetConnection链接,在Connection链接上会传输一些控制信息,如SetChunkSize,SetACKWindowSize。其中CreateStream命令会创建一个NetStream链接,用于传输具体的音视频数据和控制这些信息传输的命令信息。他们的关系如图所示:

    从0到1打造直播 App

    RTMP Message

    RTMP协议传输时会对数据做自己的格式化,这种格式的消息我们称之为RTMP Message,而实际传输的时候为了更好地实现多路复用、分包和信息的公平性,发送端会把Message划分为带有Message ID的Chunk,每个Chunk可能是一个单独的Message,也可能是Message的一部分,在接受端会根据chunk中包含的data的长度,message id和message的长度把chunk还原成完整的Message,从而实现信息的收发。

    从0到1打造直播 App

    RTMP在收发数据的时候并不是以Message为单位的,而是把Message拆分成Chunk发送,而且必须在一个Chunk发送完成之后才能开始发送下一个Chunk。每个Chunk中带有MessageID代表属于哪个Message,接受端也会按照这个id来将chunk组装成Message。

    举个例子

    chunk表示例1

    从0到1打造直播 App

    首先包含第一个Message的chunk的Chunk Type为0,因为它没有前面可参考的chunk,timestamp为1000。type为0的header占用11个字节,假定CSID为3<127,因此Basic Header占用1个字节,再加上Data的32个字节,因此第一个chunk共44=11+1+32个字节。

    第二个chunk和第一个chunk的CSID,TypeId,Data的长度都相同,因此采用Chunk Type=2,timestamp delta=1020-1000=20,因此第二个chunk占用36=3+1+32个字节。

    第三个chunk和第二个chunk的CSID,TypeId,Data的长度和时间戳差都相同,因此采用Chunk Type=3省去全部Message Header的信息,占用33=1+32个字节。

    第四个chunk和第三个chunk情况相同,也占用33=1+32个字节。

    最后实际发送的chunk如下:

    从0到1打造直播 App

    chunk表示例2

    从0到1打造直播 App


    注意到Data的Length=307>128,因此这个Message要切分成几个chunk发送,第一个chunk的Type=0,Timestamp=1000,承担128个字节的Data,因此共占用140=11+1+128个字节。

    第二个chunk也要发送128个字节,其他字段也同第一个chunk,因此采用Chunk Type=3,此时时间戳也为1000,共占用129=1+128个字节。

    第三个chunk要发送的Data的长度为307-128-128=51个字节,还是采用Type=3,共占用1+51=52个字节。

    最后实际发送的chunk如下:

    从0到1打造直播 App

    Q:为什么RTMP要将Message拆分成不同的Chunk呢?

    A:通过拆分,数据量较大的Message可以被拆分成较小的“Message”,这样就可以避免优先级低的消息持续发送阻塞优先级高的数据,比如在视频的传输过程中,会包括视频帧,音频帧和RTMP控制信息,如果持续发送音频数据或者控制数据的话可能就会造成视频帧的阻塞,然后就会造成看视频时最烦人的卡顿现象。同时对于数据量较小的Message,可以通过对Chunk Header的字段来压缩信息,从而减少信息的传输量。

    阻塞 vs. CPU

    Chunk的默认大小是128字节,在传输过程中,通过一个叫做Set Chunk Size的控制信息可以设置Chunk数据量的最大值,在发送端和接受端会各自维护一个Chunk Size,可以分别设置这个值来改变自己这一方发送的Chunk的最大大小。大一点的Chunk减少了计算每个chunk的时间从而减少了CPU的占用率,但是它会占用更多的时间在发送上,尤其是在低带宽的网络情况下,很可能会阻塞后面更重要信息的传输。小一点的Chunk可以减少这种阻塞问题,但小的Chunk会引入过多额外的信息(Chunk中的Header),少量多次的传输也可能会造成发送的间断导致不能充分利用高带宽的优势,因此并不适合在高比特率的流中传输。在实际发送时应对要发送的数据用不同的Chunk Size去尝试,通过抓包分析等手段得出合适的Chunk大小,并且在传输过程中可以根据当前的带宽信息和实际信息的大小动态调整Chunk的大小,从而尽量提高CPU的利用率并减少信息的阻塞机率。

    来源于《带你吃透RTMP》

    播放一个RTMP协议的流媒体需要经过以下几个步骤:握手,建立连接,建立流,播放。RTMP连接都是以握手作为开始的。建立连接阶段用于建立客户端与服务器之间的“网络连接”;建立流阶段用于建立客户端与服务器之间的“网络流”;播放阶段用于传输视音频数据。

    握手(HandShake)

    一个RTMP连接以握手开始,双方分别发送大小固定的三个数据块

    从0到1打造直播 App

    理论上来讲只要满足以上条件,如何安排6个Message的顺序都是可以的,但实际实现中为了在保证握手的身份验证功能的基础上尽量减少通信的次数,一般的发送顺序是这样的:

    建立网络连接(NetConnection)

    服务器在收到客户端发送的连接请求后发送如下信息:

    从0到1打造直播 App


    主要是告诉客户端确认窗口大小,设置节点带宽,然后服务器把“连接”连接到指定的应用并返回结果,“网络连接成功”。并且返回流开始的的消息(Stream Begin 0)。

    建立网络流(NetStream)推流流程播流流程五、直播中的用户交互

    对于直播中的用户交互大致可以分为:

    对于弹幕来说,要稍微复杂一些,可能需要关注以下几点:

    Html5直播聊天室组件

    该组件主要适用于基于Html5的web 大群互动直播场景。具备如下特点:

    1)支持匿名身份入群,粉丝与主播进行亲密互动

    2)支持多人聊天,主播同一个帐号多标签页收发消息,粉丝再多也不用愁

    3)支持多种聊天方式,文本,表情,红包,点赞,想怎么互动就怎么互动

    4)支持不同优先级消息的频率控制,一键在手,权利尽在掌握中

    5)对互动直播场景进行了专门的优化,参与人数多,消息量再大也能从容应对

    前端技术点六、总结

    目前较为成熟的直播产品,大致都是以 Server 端、 H5直播前端 和 Native(Android,iOS)搭配实现直播。

    主要从android客户端出发,从最初的录制视频到客户端观看直播的整个流程,给出了各个技术点的概要和解决方案,从0到1完成了简单的直播实现。从0到1易,从1到100还有更多的技术细节有待研究。

  • timestamp(时间戳):占用3个字节,因此它最多能表示到16777215=0xFFFFFF=2 24-1, 当它的值超过这个最大值时,这三个字节都置为1,这样实际的timestamp会转存到Extended Timestamp字段中,接受端在判断timestamp字段24个位都为1时就会去Extended timestamp中解析实际的时间戳。

  • message length(消息数据的长度):占用3个字节,表示实际发送的消息的数据如音频帧、视频帧等数据的长度,单位是字节。注意这里是Message的长度,也就是chunk属于的Message的总数据长度,而不是chunk本身Data的数据的长度。

  • message type id(消息的类型id):占用1个字节,表示实际发送的数据的类型,如8代表音频数据、9代表视频数据。

  • msg stream id(消息的流id):占用4个字节,表示该chunk所在的流的ID,和Basic Header的CSID一样,它采用小端存储的方式,

  • HLS的分段策略,基本上推荐是10秒一个分片,当然,具体时间还要根据分好后的分片的实际时长做标注

  • 为了缓存等方面的原因,在索引文件中会保留最新的三个分片地址,以“滑动窗口”的形式进行刷新。

  • 秒开

  • 时延

  • 流畅

  • 清晰度

  • 弹幕实时性,可以利用 webscoket 来实时发送和接收新的弹幕并渲染出来。

  • 对于不支持 webscoket 的浏览器来说,只能降级为长轮询或者前端定时器发送请求来获取实时弹幕。

  • 弹幕渲染时的动画和碰撞检测(即弹幕不重叠)等等

  • 送礼物

  • 发表评论或者弹幕

  • 对于送礼物,在 H5 端可以利用 DOM 和 CSS3 实现送礼物逻辑和一些特殊的礼物动画,实现技术难点不大。

  • 客户端发送命令消息中的“播放”(play)命令到服务器。

  • 接收到播放命令后,服务器发送设置块大小(ChunkSize)协议消息。

  • 服务器发送用户控制消息中的“streambegin”,告知客户端流ID。

  • 播放命令成功的话,服务器发送命令消息中的“响应状态” NetStream.Play.Start & NetStream.Play.reset,告知客户端“播放”命令执行成功。

  • 在此之后服务器发送客户端要播放的音频和视频数据。

     

    从0到1打造直播 App

  • 客户端发送publish推流指令。

  • 服务器发送用户控制消息中的“流开始”(Stream Begin)消息到客户端。

  • 客户端发送元数据(分辨率、帧率、音频采样率、音频码率等等)。

  • 客户端发送音频数据。

  • 客户端发送服务器发送设置块大小(ChunkSize)协议消息。

  • 服务器发送命令消息中的“结果”(_result),通知客户端推送的状态。

  • 客户端收到后,发送视频数据直到结束。

     

    从0到1打造直播 App

  • 客户端发送命令消息中的“创建流”(createStream)命令到服务器端。

  • 服务器端接收到“创建流”命令后,发送命令消息中的“结果”(_result),通知客户端流的状态。

  • 客户端发送命令消息中的“连接”(connect)到服务器,请求与一个服务应用实例建立连接。

  • 服务器接收到连接命令消息后,发送确认窗口大小(Window Acknowledgement Size)协议消息到客户端,同时连接到连接命令中提到的应用程序。

  • 服务器发送设置带宽(Set Peer Bandwitdh)协议消息到客户端。

  • 客户端处理设置带宽协议消息后,发送确认窗口大小(Window Acknowledgement Size)协议消息到服务器端。

  • 服务器发送用户控制消息中的“流开始”(Stream Begin)消息到客户端。

  • 服务器发送命令消息中的“结果”(_result),通知客户端连接的状态。

  • 客户端在收到服务器发来的消息后,返回确认窗口大小,此时网络连接创建完成。

  • Client发送C0+C1到Sever

  • Server发送S0+S1+S2到Client

  • Client发送C2到Server,握手完成

  • 握手开始于客户端发送C0、C1块。服务器收到C0或C1后发送S0和S1。

  • 当客户端收齐S0和S1后,开始发送C2。当服务器收齐C0和C1后,开始发送S2。

  • 当客户端和服务器分别收到S2和C2后,握手完成。

  • Basic Header(基本的头信息) chunk stream ID(流通道Id)和chunk type(chunk的类型,2位fmt),chunk stream id一般被简写为CSID,用来唯一标识一个特定的流通道,chunk type决定了后面Message Header的格式。长度有1、2或3个字节

  • Message Header(消息的头信息) Message Header的格式和长度取决于Basic Header的chunk type,共有4种不同的格式,由上面所提到的Basic Header中的fmt字段控制。包含timestamp,timestamp delta,message length,message type id,msg stream id,和0(表示与上一个相同)。

  • Extended Timestamp(扩展时间戳) 4个字节,当扩展时间戳启用时,timestamp字段或者timestamp delta要全置为1,表示应该去扩展时间戳字段来提取真正的时间戳或者时间戳差。注意扩展时间戳存储的是完整值,而不是减去时间戳或者时间戳差的值。

  • Chunk Data(块数据) 用户层面上真正想要发送的与协议无关的数据,长度在(0,chunkSize]之间。

  • 传播性好,利于分享等操作。

  • 可以动态发布,有利于实时迭代产品需求并迅速上线。

  • 不用安装 App,直接打开浏览器即可。

  • HTTP 请求 m3u8 的 url。

  • 服务端返回一个 m3u8 的播放列表,这个播放列表是实时更新的,一般一次给出5段数据的 url。

  • 客户端解析 m3u8 的播放列表,再按序请求每一段的 url,获取 ts 数据流。