本文主要讲解 客户端 通过 RTSP 协议推流到 服务器的 整个交互过程,以及这个过程中的一些交互,原理。
服务器采用 ZLMediaKits
,服务器的搭建请看 ZL的官方文档。
推流客户端是 ffmpeg
,推流命令如下:
ffmpeg.exe -re -i juren.mp4 -vcodec h264 -acodec aac -f rtsp -rtsp_transport udp rtsp://192.168.0.123/live/test
上面我用了 一个命令,指定编码格式,然后通过 rtsp 协议把一个动画片推到了 ZLMediaKit 的 RSTP 服务器。
下面就用 wireshark
来抓包看下整个通信过程,过滤规则如下:
host 192.168.0.123
为了方便读者对照,这里提供一下 pcapng
包文件,可以下载 用 wireshark
打开,就跟本文的一致了。下载地址:百度网盘,提取码:vg4y
上图中,192.168.0.114 是推流客户端,192.168.0.123 是服务器端。
下面就开始一步一步 分析整个交互流程,在 NO.4 数据包,客户端发了一个 OPTIONS
的 rtsp
包,这个其实就跟 http 协议的 option 差不多,都是查询服务器有哪些方法,能力。
其实,RTSP 跟 HTTP 有不少地方是相似的,他们都是文本协议,用 wireshark
可以看到,它的每一个字段 的key,value 都是 ASII 码,单字节数据,所以也就不会有大小端问题。
这里我稍微提及一下 文本协议 跟 二进制协议 的区别。
文本协议就是用 ASII 码来表达一些逻辑,或者数据结构,文本协议通常需要经过 parse 的过程,什么是 parse?json
就是一种文本表达方式,把 json
转成 C 语言的结构体,这就是 parse。特别是复杂的数据,parse 的过程会有点慢。
二进制协议 就是 直接传输 原始的数据,也就是比特流,例如 C 语言变量 int a = 0x11223344
, 通过 udp 传输 这个 int
变量,4个字节,直接 send 4个字节的数据就行,对面接受 4个字节,直接把 4个字节强制转成 int
类型就能直接用。这里不需要经过 parse,因为内存数据可以直接用,网络传输传的就是内存数据。不过这是多字节传输,通常需要处理大小端问题,不过大端机器已经很少,有一些服务器不搞大端,例如SRS。
回到 本文的主题,客户端发完 OPTIONS
之后,服务器就会回应一个 Reply
,告诉客户端能用哪些 method (方法),如下:
上面比较常用的方法 是 SETUP,PLAY,PAUSE。
接着,客户端就会发一个 ANNOUNCE
的包,这个包 里面嵌套了一个 SDP (Session Description Protocol),如下图:
ANNOUNCE
顾名思义,就是向服务器宣告自己的一些信息,主要有以下信息:
告诉服务器,自己要推送多少个流,流的编码格式。从上图可以看出,一个有 两个流,视频 跟音频,视频流的 type 是 96,音频流的 type 是 97。
服务器收到 客户端的 ANNOUNCE
的包之后,就会回复一个 reply,里面有个 session,如下图:
然后,客户端开始发 SETUP
,SETUP
可以理解为配置过程,因为 RSTP udp 推流,每一个流,都需要使用两个端口,一个端口用来传输 RTP 音视频帧,另一个端口用来传输 RTCP report 报告,如下图所示:
上图的 SETUP
包,告诉服务器,我客户端有两个 UDP 端口可以用,6860 端口用来传输 RTP 数据,6861 用来传输 RTCP 数据。
然后 服务器会 回复一个 reply
,里面有服务器的端口信息,如下:
这个 reply 是对上一个 setup 的回应,可以看到 服务器说,我可以用 30012端口 传输 RTP,用 30013 端口传输 RTCP,至此,一个流的端口配对就完成了。
- Client:6860 -> Server:30012 (RTP数据,音视频帧)
- Client:6861 -> Server:30013 (RTCP数据,report报告)
第一个 setup
,reply
是视频流的端口配置,第二个是音频流的,原理一样。
端口配对完成之后,客户端就会发 RECORD
包,如下,注意 npt
时间跟 session
字段。
然后 服务器在 回复 一个 reply
,如下:
RTSP 协议,每一个 request
都会有一个 reply
,他们的 Cseq
字段是一样的。
到这里,RTSP 的交互就完成了,就开始后面大部分数据都是通过 RTP,RTCP 传输,只有暂停,停止等操作会再次使用 RTSP,要不都很少见到 RTSP 包了。
这时候,客户端会开始发一个 RTCP 包,如下:
如上图所示,使用的端口正是 之前 setup 阶段协商好的 6861 跟 30013 端口。这里注意一下 这个 Sender SSRC 字段。
接着,客户端会再向服务器发一个 RTP 包,如下:
注意,第一个 RTP 包是有一些编码信息的,例如上图中可以看到 x264 字符串。
数据通信到这里,两个新的通信端口,都是客户端先向服务器发送 UDP 包。而不是服务器向客户端先发 UDP包。所以服务器 RTSP 配置流程走完之后需要 listen 监听 4 个新的端口,每个流占用两个端口,所以这样很浪费端口,一般是局域网的摄像头这样做,摄像头就是一个 RTSP 服务器,APP 从 摄像头拉流。如果用 云服务器搭建 RTSP,防火墙配置会很麻烦。
另外的音频流也是类似的逻辑,通过 RTCP 跟 RTP来传输,如下:
96 类型是 视频帧数据,97类型是音频帧数据,可以看到视频数据比音频数据多很多。
最后,总结一下这种 多端口 传输数据的优势:
可以有效隔离不同类型的流数据,如果 底层协议实现了顺序传输,TCP 就是顺序传输,TCP 协议栈丢给上层应用的数据肯定是顺序到达的。这样会导致一个阻塞问题,例如 视频帧100kb,音频帧 1kb,音频帧在视频帧后面,也就是TCP 协议栈内部只有一个队列,音频帧视频帧都在同一个队列里面,我们知道 TCP 底层是 IP 层,IP 层会丢包,所以有可能会重传,然后有可能导致后面的音频不能发送到网络上。这里提一点,TCP协议栈的实现每个版本都有点差异,实际上有可能即使重传阻塞,后面的音频帧也能发到网络上,只是会在对面的协议栈内部缓存,不丢给上层应用,因为要按顺序到达。等前面丢失的IP包来了,才能按顺序传递。所以后面的音频帧有可能会发到网络上,也有可能不会发到网络上,具体看 tcp 的窗口大小。因为网络通道是很大的,tcp 不是发一个包,确认一个 ack再发一个包,他可以一次发多个。但是,即使在窗口允许内,能发多个包,但是由于对面TCP 是要按顺序传递数据给上层的,即使音频包到了对面,因为前面缺少一个 视频的IP 包,也不能丢上去解码,这样就会加大延迟。
所以如果分开端口之后,音频跟视频流就不会相互干扰。同时 SCTP 等信息也不会被视频阻塞。
虽然 本文使用的 是 UDP,但是上面套了一层 RTP,RTP 有序列号,所以可以实现有序到达。
使用以下命令测试一下 TCP,UDP 的差异。
ffmpeg.exe -re -i juren.mp4 -c copy -f rtsp -rtsp_transport tcp rtsp://192.168.0.123/live/test
ffmpeg.exe -re -i juren.mp4 -c copy -f rtsp -rtsp_transport udp rtsp://192.168.0.123/live/test
ffplay -fflags nobuffer -x 800 -f rtsp -i rtsp://192.168.0.123/live/test
因为上面我 ffplay 开启了 nobuffer,所以会导致 丢帧,主要测试下 tcp 下丢帧多,还是 udp 下丢帧多。
上面只是简单测试,但是可以看出 udp 的方式比较略胜一筹,ffplay 开启 nobuffer 就是有帧来就播放,超低延迟,满足不了延迟就会丢帧来满足,因此 udp 的流畅性更高一些,因为他丢帧少。
相关阅读:
由于笔者的水平有限, 加之编写的同时还要参与开发工作,文中难免会出现一些错误或者不准确的地方,恳请读者批评指正。如果读者有任何宝贵意见,可以加我微信 Loken1。QQ:2338195090。