Windows远程桌面实现之十一:桌面屏幕通过各种直播服务端直播(RTSP, RTMP, HTTP-FLV, HLS)

                                                    by fanxiushu 2020-01-23 转载或引用请注明原始作者。
此文还是基于xdisp_virt远程项目中的一个子功能。在把xdisp_virt移植到各种平台之后,就想着再做点什么新功能,
于是干脆再次增强原先实现的直播推流功能,在xdisp_virt程序中集成直播服务端,
这样可以不用推流到第三方平台,直接使用播放器就能播放。
(xdisp_virt越玩功能越多,现在基本江郎才尽都不知道还能再添加些什么功能)

本文阐述的就是基于直播服务端比较常用的直播协议:
1,RTSP  经常用于监控行业 ,可以说是监控行业里的老大。
2,RTMP 基于Adobe Flash的协议,虽然Flash面临淘汰,但此协议还在大量使用
3,HTTP-FLV,与2类同,不过使用的是HTTP传输直播流,这样更加适合目前互联网的使用
4,HLS/DASH,HLS和DASH是同样的东西,都是把直播流切成小片组成很多的小音视频文件,
      然后客户端通过HTTP下载这些小文件然后播放。严格得说这算不上直播协议,顶多就是个点播,而且延时非常的高。

以上是查询到的目前经常使用的直播协议,于是打算把这四种协议全部集成到xdisp_virt程序中,
同样的,本文包含的内容较多,也不能面面俱到,如果对xdisp_virt没兴趣,可只关注协议内容即可。
本文是下面链接的扩展篇,并且本文中RTMP和HTTP-FLV阐述的格式,多少会牵涉到其中一些内容。
https://blog.csdn.net/fanxiushu/article/details/80996391 (Windows远程桌面实现之五(FFMPEG实现桌面屏幕RTSP,RTMP推流及本地保存)

(一)首先,我们来看看RTSP。
RTSP和HTTP协议和类似,但是也有用不同。与HTTP协议格式相同的是请求命令,
比如 OPTIONS,DESCRIBE,SETUP,PLAY,PAUSE,TEARDOWN这些命令与HTTP协议的GET,POST命令的协议格式几乎是一样的。
RTSP与HTTP不同的是流传输部分, RTSP传输音视频流使用的是RTP协议,控制流采用的是RTCP协议,
这两个协议底层即可以使用UDP传输,也可以使用TCP传输,这篇文章不介绍UDP传输流的RTP和RTCP,只介绍TCP方式传输流。
同时xdisp_virt也只实现TCP传输流。
UDP传输音视频流看起来比较快,实际上面临许多问题,最严重的就是丢包问题,也许有线网络丢包情况稍微好些,
但是WIFI无线丢包情况就挺糟糕了,因此传输高质量的视频帧的时候如果丢包多,显示的画面经常就会乱七八糟的。
RTSP在TCP中传输音视频帧,使用的是传输OPTIONS,DESCRIBE,SETUP,PLAY这些命令的同一个TCP链接,
不会另外建立链接来传输音视频流。这样的设计其实挺好,节省了建立另外的TCP链接避免浪费资源。
但是会在同一个TCP链接中,同时传输 SETUP,PLAY这些类似HTTP格式的命令,还得传输RTP和RTCP携带音视频流的数据。
同一个链接中传输这么多不同类的数据,处理起来会麻烦一些,但是其实理解了RTSP协议格式种类,也并不难。
就比如xdisp_virt项目中,在同一个TCP链接中,传输的数据种类更多。
总结起来,一个TCP链接中传输三种数据,一是RTSP命令比如PLAY,SETUP等,一个是RTP数据,一个是RTCP数据。
RTP(RTCP)数据包: RTP头(RTCP头)+负载数据。
RTP头和RTCP头定义如下:
(网路序)
struct rtp_header_t
{
    uint32_t ver : 2;        // protocol version
    uint32_t pad : 1;        // padding flag
    uint32_t x : 1;        // header extension flag
    uint32_t cc : 4;        // CSRC count
    uint32_t m : 1;        // marker bit
    uint32_t pt : 7;        // payload type  
    uint16_t seq ;    // sequence number
    uint32_t timestamp; // timestamp
    uint32_t ssrc;        // synchronization source
} ;
struct rtcp_header_t
{
    uint32_t ver : 2;        // version
    uint32_t pad : 1;        // padding
    uint32_t rc : 5;        // reception report count
    uint8_t  pkt_type;  ///packet type
    uint16_t length ; // pkt len in words, w/o this word
} ;
在UDP传输RTP和RTCP的时候,都是分开两个UDP通道(不同的UDP端口)分别传输RTP和RTCP数据包,RTSP命令则使用TCP传输。
因此也非常容易区分,当然浪费的网络资源也很多。
从以上RTP和RTCP头的定义看得出来,如果把三种数据在同一个TCP中传输,是无法区分的,因此必须在RTP头和RTCP头前面再加一个头
这个头叫interleaved,定义如下:
struct rtsp_interleaved_t
{
    unsigned char  magic; // 固定为0x24,'$' 金钱符
    unsigned char  channel; //通道类型,就是指示是传输的RTP包,还是RTCP包
    unsigned short length;  //后面的RTP包或RTCP包的大小,不超过64K
};
这样三种包在同一个TCP中的格式就是:
A), 类似HTTP格式命令
B), rtsp_interleaved_t + rtp_header_t + 音视频负载数据
C), rtsp_interleaved_t + rtcp_header_t + RTCP负载数据
其中 RTCP和RTP可以通过 rtsp_interleaved_t中的channel区分,但是rtsp_interleaved_t头和普通的RTSP请求命令如何区分呢?
其实也很好区分,我们编程的时候,先接收一个字节,
如果这个字节是 ‘$'(0x24)可以确定是rtsp_interleaved_t头,否则就是普通的OPTIONS,DESCRIBE,PLAY这些命令。

我们来看看一个RTSP播放器是如何请求播放流的,
首先播放器会发送OPTIONS命令,服务端返回 OPTIONS,PLAY,PAUSE,TEARDOWN这些命令表示服务端支持哪些RTSP命令。
接着播放器发送DESCRIBE命令请求音视频流的SDP描述信息,播放器获取到SDP之后,就知道了服务端提供了哪些流。
然后播放器发送SETUP给服务端,表示准备开始传流,其中包括采用哪种传输方式,传输通道等信息,服务端回复SETUP之后。
然后播放器发送PLAY命令开始请求播放, 这个时候,服务端就开始推送包含音视频数据的RTP包给播放器。
播放器停止播放的时候,会发送TEARDOWN命令,这个时候,服务端停止推送流。
这就是一个RTSP播放的基本流程。
其实SETUP和PLAY命令是播放器必须发送的,其他的可以可选。

下面是一个请求例子(C是播放器, S是服务端):
C:
       OPTIONS rtsp://192.168.88.1/test RTSP/1.0\r\n
       CSeq: 1\r\n
       \r\n
S:   RTSP/1.0 200 OK\r\n
       CSeq: 1\r\n
       Server: xdisp_virt\r\n
       Public: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE\r\n
      \r\n
C:
      DESCRIBE rtsp://192.168.88.1/test RTSP/1.0\r\n
     CSeq: 2\r\n
      \r\n
S:  RTSP/1.0 200 OK\r\n
      CSeq: 2\r\n
      Server:  xdisp_virt\r\n
      Session: 1234\r\n
      Context-Length: 345\r\n
      \r\n
      SDP 内容
C: SETUP rtsp://192.168.88.1/test/track1 RTSP/1.0\r\n   请求视频帧
     CSeq: 3\r\n
     Transport: Transport: RTP/AVP/TCP;unicast;interleaved=0-1\r\n
     Session: 1234\r\n
     \r\n
S: RTSP/1.0 200 OK\r\n
     CSeq: 3\r\n
     Session: 1234\r\n
     Transport: Transport: RTP/AVP/TCP;unicast;interleaved=0-1\r\n
     \r\n
C:
    PLAY rtsp://192.168.88.1/test RTSP/1.0\r\n
    CSeq: 4\r\n
    Session: 1234\r\n
    \r\n
S: RTSP/1.0 200 OK\r\n
    CSeq: 4\r\n
    Session: 1234\r\n
    \r\n
服务端开始推送携带视频数据的RTP包(rtsp_interleaved_t+rtp_header_t+视频数据)
服务端还会接收到播放器发送来的RTCP报告数据包。服务端根据情况也会发生状态报告RTCP包给播放器。
。。。。
C:
    TEARDOWN rtsp://192.168.88.1/test RTSP/1.0\r\n
    CSeq: 5\r\n
    Session: 1234\r\n
    \r\n
S: RTSP/1.0 200 OK\r\n
     \r\n
服务端停止推送视频流。

接下来就是如何把H264视频帧封包到RTP包中,
一帧H264数据包含许多NALU单元,通常就是把每个NALU单元封包成一个RTP包即可,
但是这个RTP包是有大小限制的,因为如上面rtsp_interleaved_t数据结构里边的length不超过64K,RTP包大小不能超过64K,
有可能一个NALU单元很大,会超过64K大小,因此需要把这个NALU单元拆分成多个部分,每个部分再封装成RTP包。
至于如何拆分和如何再封包成RTP包,这里也就不再赘述,有兴趣可查阅RFC文档的相关介绍。

(二),HTTP-FLV 和 RTMP 直播协议。
之所以要把这两合并到一起来讲,其实是音视频包封包格式都差不多,基本都是基于FLV文件格式。不同的是 采用的通讯协议不同。
RTMP不但支持拉流,更主要的是支持推流。在互联网应用中,采集到的直播流数据可以通过RTMP协议推送到服务器端,
服务器端再拆分成多个拉流协议,比如 HTTP-FLV, HLS,DASH等各种拉流协议。
在互联网应用中,HTTP协议是主流,因此我们对拉流的使用,使用的HTTP-FLV较多,RTMP相对会少一些,尤其在Flash面临淘汰的现在。
HTTP-FLV和RTMP实时性较高,延时一般都在1-2秒内,因此适合用于对应实时性要求高的互动直播环境中。
RTMP在浏览器中需要借助Flash才能播放,PC系统浏览器一般都支持Flash插件,但是基本也是日落西山。
除非是自己在客户端开发播放器专门使用RTMP来播放, 比如VLC播放器可运行在移动系统和PC系统中,且支持多个串流播放协议。
至于HTTP-FLV,可以在浏览器端利用js解码,转成MSE喂给video标签来播放,但是不是所有系统都支持MSE,这给兼容性带来麻烦。
至少目前iOS系统都不支持MSE,也许以后的iOS版本能支持MSE。
目前来看,兼容性最好的还是HLS,能支持所有平台的绝大部分浏览器,但是就是延迟实在太高。

HTTP-FLV是利用HTTP链接,发送一个无限大的flv文件来达到直播的效果。
FLV文件是一个 flv头 + 多个tag组成的序列流, tag包括音频tag, 视频tag,script的tag(比如媒体信息tag)。
FLV不像MP4文件格式,需要文件尾和头才能组成一个完整的MP4文件,
FLV文件只要 flv_header + meta_header(描述音视频信息)+ 无数个音视频数据的tag,
只要前面的flv_header+meta_header信息完整,后面的音视频tag可以无限个,也就是不需要文件尾部,这是实现HTTP-FLV直播的基本条件。
同时在 HTTP协议定义中,在回复中如果没有包含 Content-Length 这个字段,
客户端就会一直接收数据,直到服务端主动关闭这个HTTP链接,这样服务端就会连续不断的推送音视频tag 给客户端。

比如一个HTTP-FLV请求和回复流程:
C:
    GET /flv_stream/test.flv HTTP/1.1\r\n
    Host: ddd.com\r\n
    \r\n
S:
    HTTP/1.1 200 OK\r\n
    Server: xdisp_virt\r\n
    Connection: keep-alive\r\n
    \r\n
    FLV头 + 媒体信息tag
    持续不断的 音频tag 和 视频tag
    。。。。
    直到C或者S关闭这个链接为止。

FLV头一共9个字节,前3个是’FLV‘,第4个字节是版本号,第5个是支持audio+video类型,接下来4个字节是头长度,这里为9,
再接下来4个字节全是0, 这个意思是PreviousTagSize0,就是第一个前Tag的大小。
接下来就是以TAG为单位的组合:
11个字节的TAG头 + 负载数据内容 + 4个字节的这段数据大小(11 + 负载数据大小)

TAG头包括TAG类型(比如 audio,video, script等),负载数据大小,timestamp,流ID。
第一个TAG是 meta类型的TAG, 用来描述接下来的音视频流的相关信息,比如SPS,PPS信息,视频宽高,音频采集率等。
这个是必须有的,否则客户端无法获取到音视频相关信息。
接下来视频帧,除了11个字节的TAG头之外,还包含5个字节描述video相关信息,比如是否关键帧,
接下来就是一帧的H264数据,采用AVCC编码,最后就是4个字节用来指示当前tag的大小。
音频帧,除了11个字节的TAG,还包含2个字节的描述Audio信息,接下来就是一帧Audio数据,最后4个字节就是当前tag的大小。

(三),HLS,DASH切片直播。
严格得说,这不是个直播协议,它是把连续不断的直播流,切片成很多的小文件。
客户端先从服务端获取到一个列表,然后根据列表分别下载这些小文件,然后再连续不断的播放这些小文件。
直播流是连续不断的,小文件会越生成越多,服务端因此会删除先前的小文件,比如同时只保留20个小文件,
文件按照序号不断的生成和不断的删除不用的小文件,总数保持20个不变。
同时这个列表上的小文件名字也会不断更新,客户端会定时重新获取这个列表,从而按照顺序不断的下载新的小文件,然后再播放。
这样的效果就造成好像在不断的播放一样,其实就是在连续不断的下载和播放小视频文件而已。

为何会设计出这么一个不伦不类的直播协议,(不伦不类这么说可能有点过分)
其实我想跟目前浏览器video标签播放视频的方式有关系,
目前浏览器的内嵌的 video标签强项是播放单个视频文件,而不是播放直播流。
这也是直播协议在浏览器中面临的尴尬局面。

以前在研究如何直接在浏览器中实现远程控制的时候,也是面临同样的问题。
在不需要给浏览器安装插件的情况下,而且要兼容各个系统的浏览器,
当时也是折腾来折腾去,最终选择了使用WebSocket传输音视频数据,然后js解码,然后使用canvas画图来解决。
目前来看,这也是目前实现浏览器方式远程控制的最好方式,实时性也是非常高,符合远程控制对实时性的要求。
就是这个时候浏览器占用的系统资源比较高些,但是基本也能给接受。

下图是最新版本的xdisp_virt实现的四种直播服务端:
Windows远程桌面实现之十一:桌面屏幕通过各种直播服务端直播(RTSP, RTMP, HTTP-FLV, HLS)_第1张图片

下图是在iPhone手机浏览器中直接打开上面第4个切片直播的URL地址的播放效果图:
Windows远程桌面实现之十一:桌面屏幕通过各种直播服务端直播(RTSP, RTMP, HTTP-FLV, HLS)_第2张图片

你可能感兴趣的:(C++,多媒体,音视频,直播,音视频)