实时通信协议族
因特网最初设计用于数据的传输,例如文件、电子邮件。那时的语音传输由专门的电话网络负责。随着节点数量的增加、网络带宽的提高,基于因特网的多媒体需求逐渐出现,包括在线影视、在线视频会议。为了响应这类需求,研究人员开发出了专门的协议,包括:
- 实时传输协议(Realtime Transmission Protocol,RTP),用于传输媒体
- RTP的控制部分:实时传输控制协议(Realtime Transmission Control Protocol,RTCP)
- 实时流协议(Realtime Streaming Protocol)
注意:RTP这个术语有时候指RTP协议标准,有时候则指RTP协议标准中的RTP部分(不包含RTCP)。
我们都知道,TCP/IP协议族的基础是IP协议,此协议能很好的处理包的路由递送,但是却无法防止丢包、控制延迟。但是此协议让路由器逻辑很简单、网络易于扩容,这是因特网能繁荣的基础。
为了增强端到端的可靠性,TCP协议被引入,这是因特网上最广泛使用的协议。TCP能够自动重发丢失的包,并且保证包的顺序,TCP还提供了拥塞控制机制。
TCP的某些特性,在用来传递多媒体时,反而成为了障碍,原因是:
- 很多多媒体应用,例如视频监控,对延迟非常敏感
- 多媒体传输可以容忍某些丢包情况,其质量不会受到影响
因此,很多多媒体传输都是在UDP协议之上进行的。
多媒体应用程序可以分为两个类别:
- 交互式应用。例如视频会议、VoIP
- 非交互式应用。又可以细分为:
- 实时流媒体,例如视频监控预览、网络赛事直播
- 非实时流媒体,例如视频点播网站、歌曲点播应用、视频监控回放
除了非实时流媒体应用之外,多媒体应用不能容忍过长时间的缓冲以避免抖动,也不允许延迟的出现。
大量多媒体应用程序,使用了不同的编码方式,这些编码方式在媒体质量、带宽消耗、计算资源消耗之间做了不同的权衡。
不同的多媒体应用(例如两个独立开发的VoIP应用)要能够相互通信,就必须以某种双方都能理解的编码方式进行媒体的交换。
直接使用UDP传输流媒体数据包不能满足需求,因为UDP没有任何关于延迟、抖动的语义,或者它不理解何为“实时”。
实时传输协议(RTP)是一种专门处理实时需求的传输协议标准,主要用于处理音频、视频数据。 RTP允许接受者知晓接收到的数据包们在时间维上的关系,这样:
- 在缓冲媒体流并播放时不会出现顺序混乱的情况
- 多个媒体流(例如音频、视频)能够在播放时保持同步关系
此外RTP协议:
- 允许通信双方进行协商(Negotiation),选取两者都认可的编码方式。这解决互操作性问题
- 具有识别包丢失的问题,这样端点应用程序能够进行适当的处理
- 允许进行拥塞控制,媒体接收者能够向发送者进行网络拥塞状况的反馈(Feedback),这样发送者可以对码流质量进行调整,以改变带宽占用
- 支持帧指示(Frame Indication),例如媒体接收者需要知道那些数据包是属于相同的视频场景(Video Scene,帧)的,这样才能进行合适的处理
通过网络进行音频传输的尝试从1970年代就开始了,在70-80年代多个语音包传输、时间戳、序列号相关的专利被批准。在1991年DARTnet成功完成了一系列语音传输的尝试,DARTnet使用的音频会议工具最终成为RTP版本0。
1992年RTP版本1发布,包含了若干因特网草案,此操作最终在1995年成为RTP版本2,包含:
- RFC1889,RTP
- RFC1890,RTP Profile —— AVP(用于音视频会议的RTP profile,最小化控制)
1996年,网景基于RTP和其他协议发布了Netscape LiveMidea。微软的NetMeeting软件也支持RTP。
RTP的设计确定了一个后续被广泛认同的原则 —— 应用层分帧原则(Application Level Framing ALF )。ALF认为应用程序更了解自己的需要,网络协议应该尽可能保持简单。例如MPEG解码器明白怎么样处理丢帧,如何从I帧、B帧丢失中恢复。
RTP支持大量种类的应用程序,对于每一类应用程序,RTP定义了一种Profile。Profile可以是:
- 一种对RTP协议头结构的约定
- 定义对RTP协议的扩展或者修改
RTP的载荷格式规定,则解释了RTP头之后的数据的结构。
RTP作为一个标准,实际上定义了一对协议:
- RTP,用于交换媒体数据
- RTCP,用于传输的控制,例如周期性的获得数据流传输质量的反馈信息、负责多个媒体流的同步。RTCP也负责传输组会话(Group Session)的参与者信息
尽管RTP协议相对于传输层是独立的,但是它通常在UDP/IP之上运行。当基于UDP/IP传输时,RTP和RTCP使用连续的两个端口号。
为了发起一个RTP会话,应用程序需要定义一对特定的目的传输地址(Destination Transport Addresses,DTA)—— 一个网络地址加上两个端口(分别用于RTP、RTCP)。在一个多媒体(eg,音频 + 视频 + 文本)会话中,每个媒体都在单独的RTP会话中传输。
在视频会议中,音频、视频媒体在不同的RTP会话中传输,这些会话使用的DTA是不同的,也就是它们使用两对不同的UDP端口。
视频会议也可能基于组播技术实现,这种情况下,需要使用两对多播地址的DTA。
音频、视频的RTP会话没有直接的关联,这允许接收者仅仅接收音频或者视频流。为了实现一个源的音视频同步,接收者可以使用RTCP包中的时序信息。
每个会议参与者都使用视频、音频应用程序,以块的方式发送数据。这些数据作为RTP包的载荷,RTP头中有专门的字段识别这些数据是如何编码的。
RTP头包含时序信息、序列号,接收者可以用这些信息重新构造音视频流的时序,不同源(音视频)时序信息都是单独构建的。
RTP数据包的整体结构、在网络和应用中的传递方式,如下图所示:
关于此图的说明如下:
- RTP协议通常运行在UDP之上,这意味着数据都是无状态、推送的方式传递
- RTP数据包是一个瘦协议,对需要持续传递数据(流式)的应用程序提供支持:
- 时序重构(Timing Reconstruction)
- 丢帧检测(Frame Loss Detection)
- 数据安全(Data Security)
- 内容识别(Content Identification)
- RTP协议不负责处理带宽保留(Reserve Bandwidth)和保证QoS
- RTP数据包的载荷部分是数字化(由编码器负责)的媒体流
除了发送者、接收者角色之外,RTP协议还定义了另外两个参与此协议处理的角色 —— 转换器(Translater)、混合器(Mixer)它们位于发送者、接收者角色之间,对经过(Passthrough)它们的RTP包做出处理:
- 转换器:对经过的RTP载荷进行转换,例如可以降低视频码流的比特率,降低带宽需求
- 混合器:用于混合来自多个媒体源的流,例如可以混合多个视频会议参与者的视频流,形成一个单独的流
注意,仅仅当若干RTP流经过混合器,混合器才起作用。例如在一个电话会议的应用场景中,多个音频流通常会经过混合器混合为一个流,以节约带宽占用。
RTP协议的头格式如下图所示:
- 最前面的12字节(到SSRC为止)总是存在(上图中1、2、3……表示bit)
- V:2bit的版本号,一般取值2
- P:1bit的补白标记,如果此标记被设置,RTP包的尾部会包含1-N个补白字节。这些字节不属于载荷。补白的最后一个字节记录了补白的总数(包含它自己)。之所以需要补白,是为了满足某些加密算法对块(Block)长度的规定
- X:1bit的扩展标记,如果此标记被设置,则在标准头后面包含1个扩展头
- CC:4bit的CSRC标识符的计数器。如果载荷包含来自多个源的数据,则此计数大于1
- M:1bit的Marker标记,此标记的意义由Profile定义,此标记通常用于提示重要事件的发生,例如帧边界
- PT:7bit的载荷类型,指示载荷的数据类型。支持的类型包括PCM、MPEG1、MPEG2、JPEG视频、H.261等等。更多载荷类型可以通过Profile规范、载荷格式规范添加
- 16bit序列号:每当会话发送一个新的RTP包后,此序列号增加1。接收者可以基于此序列号进行丢包检测。此序列号的初始值是随机的,这样RTP包被加密后,尝试破解变得更加困难。当丢包出现后RTP协议层不做任何操作,应用程序负责对丢包事件做出响应,例如:某些视频应用可能在丢包时自动重放前一帧;另一些视频应用可能因为丢包而降低比特率
- 32bit时间戳:记录载荷中第一个字节的采样发生的时间。此字段的用途包括:让接收者可以按照适当的时间间隔来播放采样;允许多个媒体流保持同步;在计算抖动平滑(Jitter smoothing)时使用。时间戳使用的时钟解析度必须足够高,以满足同步精度、抖动度量精度。时间戳的初始值也是随机的,RTP没有规定时间戳的计量单位 —— 时间戳仅仅是时钟的tick计数,两个tick之间对应真实时间是多少,也是和应用程序相关的,这些仍然由Profile、载荷格式规定。频率表示每秒内有多少tick,因而tick数量 / 频率即得到对应真实世界的时间
- 32bit的SSRC标识符:用于识别同步源的标识符,此标识符被随机的生成,确保同一媒体会话中,任何两个同步源的标识符都不同。但是即使随机生成也有一定的概率出现重复,因此RTP实现必须有能力识别、解决冲突。当一个信号源改变自己的传递地址后,SSRC标识符必须也更改
- 32bit的CSRC标识符,此标识符最多有15个(取决于CC),用于识别此包的载荷部分由哪些(Contributing )源构成。CSRC标识符由混合器(Mixer)插入到包头中,其值就是Contributing源的SSRC头
- 后面是可选的扩展头
- 在RTP后面,是1-N个音视频帧,作为RTP载荷
RTCP协议专门用于配合RTP协议使用。
在RTP会话中,参与者定期向RTP会话的所有参与者通过组播发送RTCP包。RTCP包中包含媒体发送者/接收者的报告,其内容包括发送数据包的数量、丢失数据包的数量、抖动信息(Jitter)。
应用程序可能使用RTCP中的信息,来自适应的改变媒体流的质量,以适应可用网络带宽。
RTCP为来自同一个发送者的不同媒体流提供了一种协作、同步的机制。例如,当SSRC取值冲突时,需要某个流改变SSRC,这就是通过RTCP完成的。
对于牵涉到多个单独的多媒体流的应用程序,它们之间的同步基于一个通用的系统时钟完成。最初发起会话的那个系统提供此时钟,RTCP消息可以保证会话的所有参与者都使用相同的时钟。
RTCP还用于传输会话中各成员之间的关系。
当会话参与者越来越多时,RTCP数据报的总量会变多。为了防止影响网络,RTCP包占据会话总数据量不会超过5%,这意味着随着参与者的增加RTCP包发送频率会降低
- 2bit的版本号,使用的RTP协议的版本
- 1bit的补白标记,RTP包的最后是否具有补白
- 5bit的接收报告计数(Reception Report Count ),此包中包含的接收报告块的数量
- 8bit的消息类型
- 16bit的长度,指示此包的总长度
- 32bit的SSRC,同步源标识
RFC 3550定义了五种类型的RTCP报文:
报文类型 | 说明 |
RR | 接收者报告(Receiver Report),由不作为活动发送者的会话参与者生成,包含接收质量反馈信息。具体内容包括:接收到的最高包序列号、丢包数量、抖动情况、用于计算收发者之间延迟的时间戳信息 |
SR | 发送者报告(Sender Report),由活动的发送者生成,除了RR中的信息外,SR还包含一个发送者信息段。此段提供媒体间(Inter-midea)同步需要的信息、累计发包数量、立即发送字节数 |
SDES | 源描述条目(Source Description Items),包含对源的描述信息。在RTP包中源由32bit的一个头字段标识,但是这个名字不适合人类阅读,SDES则提供了一个所谓规范化名称(Canonical Names)作为会话参与者的唯一性标识。规范化名称可能包括用户名、电话号码、电子邮件地址或者其他信息 |
BYE | 提示发送者结束会话的参与 |
APP | 应用特定功能(Application Specific Functions),主要用于新应用、新特性开发时的实验性功能 |
RTSP由RFC2326定义,他是一个应用层多媒体展现协议,支持对实时媒体流进行控制 —— 例如暂停播放、Seek、快进、倒放,这些控制行为类似于DVD播放器。RTSP协议本身通常不进行媒体流的传递。
RTSP服务器为客户端维护一个会话,此会话由一个标识符来识别。RTSP协议支持TCP或者UDP传输,在一个RTSP会话中,客户端可能打开、关闭多个传输连接,以发送RTSP请求。
RTSP需要和低层的RTP或者RSVP之类的协议协同,才能在因特网上提供完整的流媒体服务。RTSP在RTP的基础上提供了选择传输通道(TCP/单播UDP/组播UDP)、传输机制的方法。RTSP报文独立于媒体流发送。
RTSP在客户端和流媒体服务器之间创建、控制音视频媒体流。服务器负责提供回放、录制等服务。
RTSP中的每个展现(Presentation)和媒体流都通过一个RTSP URL来识别,整体的展现信息和媒体属性在一个展现描述文件(Presenttation Description File)中记录,此文件中的信息可能包括编码方式、语言、RTSP URLs、目的地址/端口以及其它参数。客户端可以通过HTTP、电子邮件等方式获得展现描述文件。
RTSP协议有意的模仿HTTP协议的设计,但是两者有以下重要的不同:
- RTSP是有状态的,它必须维护会话状态,让RTSP请求和某个流关联
- RTSP是对称的,媒体服务器和客户端都可以发起请求。例如服务器可以发起请求,来设置流的回放参数
RTSP支持以下方法:
方法 | 说明 | ||
OPTIONS |
返回服务器接收的请求类型。报文示例:
|
||
DESCRIBE |
客户端发起此报文,或者RTSP URL所代表的展现/媒体对象的描述信息。报文示例:
|
||
ANNOUNCE |
当由客户端发起时,更新RTSP URL所代表的展现/媒体对象的描述信息 当由服务器发起时,实时的更新会话描述 |
||
SETUP |
客户端请求服务器为某个流分配资源,并启动一个RTSP会话。报文示例:
|
||
PLAY |
客户端请求服务器通过SETUP分配的流推送数据。报文示例:
|
||
PAUSE | 客户端临时停止流的递送,但是不释放服务器资源 | ||
TEARDOWN | 客户端请求服务器停止流的递送,并释放分配的资源 | ||
GET_PARAMETER |
获取RTSP URL所代表的展现/流的某个参数的值。报文示例:
|
||
SET_PARAMETER |
设置RTSP URL所代表的展现/流的某个参数的值。报文示例:
|
||
REDIRECT |
服务器发起,通知客户端,必须重新连接到一个媒体位置,此报文的location头指示媒体的新位置。报文示例:
|
||
RECORD | 客户端基于展现描述,发起媒体数据某个范围的录制请求 |
Live555项目提供了一套C++库,用于RTP/RTCP、RTSP、SIP等标准协议下的多媒体应用开发。Live555库被用来实现Live555媒体服务器、Live555代理服务器。
Live555还可以用来流化、接收、处理MPEG, H.265, H.264, H.263+, DV等视频编码格式以及若干音频编码格式。要支持其它音视频编码格式,你只需要简单的扩展。
下载Live555后,你得到以下几个组件,它们被位于不同的子目录:
组件 | 说明 |
UsageEnvironment |
包含类UsageEnvironment、TaskScheduler用于延迟事件的调度,例如异步读事件、输出错误/警告消息 包含类HashTable,一个哈希表实现 |
groupsock | 此组件中的类封装了网络接口和套接字。Groupsock对组播的收发行为进行了封装 |
liveMedia | 定义了一个类层次,其根是Medium。提供对多种流媒体类型、编码方式的支持 |
BasicUsageEnvironment | 定义了UsageEnvironment的一个具体化子类,主要在简单的、基于控制台的应用程序中使用。读事件和延迟操作在一个select()循环中处理 |
testProgs | 基于BasicUsageEnvironment实现了一些样例 |
Linux下安装Live555库的步骤如下:
1 2 3 4 5 6 |
wget http://www.live555.com/liveMedia/public/live.2017.07.18.tar.gz tar xzf live.2017.07.18.tar.gz pushd live # 如果需要保留调试信息,可以修改config.linux文件,添加编译参数-O0 -g3 ./genMakefiles linux make && make install PREFIX=/home/alex/CPP/lib/live555 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include "liveMedia.hh" #include "BasicUsageEnvironment.hh" #include <iostream> using namespace std; volatile char eventLoopWatchVariable = 0; int main() { TaskScheduler *scheduler = BasicTaskScheduler::createNew(); UsageEnvironment *env = BasicUsageEnvironment::createNew( *scheduler ); const char *url = "rtsp://admin:12345@192.168.0.196:554/ch1/sub/av_stream"; RTSPClient *client = RTSPClient::createNew( *env, url, 0 ); // 参数3传入1则控制台会打印调试信息 // 发送RTSP DESCRIBE命令,注意所有RTSP命令都是异步发送的,其应答后续在事件循环中被处理 client->sendDescribeCommand( []( RTSPClient *rtspClient, int resultCode, char *resultString ) -> void { // 如果resultCode不为0说明失败 // 打印SDP cout << resultString << endl; } ); // 直到eventLoopWatchVariable变为非零之前,下面的事件循环不会停止 env->taskScheduler().doEventLoop( &eventLoopWatchVariable ); return 0; } |
公司的IP摄像头给出如下应答:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
v=0 o=- 1505140878681876 1505140878681876 IN IP4 192.168.0.196 s=Media Presentation e=NONE b=AS:5050 t=0 0 a=control:rtsp://admin:12345@192.168.0.196:554/ch1/sub/av_stream/ # 视频基于RTP/VAP传输类型,编码方式H264 m=video 0 RTP/AVP 96 c=IN IP4 0.0.0.0 b=AS:5000 a=recvonly a=control:rtsp://admin:12345@192.168.0.196:554/ch1/sub/av_stream/trackID=1 # 90000表示时钟频率,即每秒内,有多少个时间戳tick,或者说每秒RTP时间戳增加多少 a=rtpmap:96 H264/90000 # H.264 Profile: baseline , constraints 0 , level-idc 4.1 # level-idc,用于提示自己的解码能力 —— 最大多大的分辨率、帧率、码率 a=fmtp:96 profile-level-id=420029; packetization-mode=1; sprop-parameter-sets=Z00AFJWoWCWm4CAgIEA=,aO48gA== a=Media_header:MEDIAINFO=494D4B48010100000400000100000000000000000000000000000000000000000000000000000000; a=appversion:1.0 |
上述内容和WebRTC的SDP Offer类似,都属于会话描述协议。SDP消息可以划分为三个主要的段,分别对会话、时间、媒体进行描述。SDP各字段的含义如下:
SDP字段 | 说明 | ||||
会话描述 | |||||
v | 协议版本号,总是0 | ||||
o | 发起者以及会话标识符 | ||||
s | 会话的名称 | ||||
i | 会话的描述和简短信息 | ||||
u |
描述(Description)的URI |
||||
e | 0-N个电子邮件地址,附加联系人名称 | ||||
p | 0-N个电话号码,附加联系人名称 | ||||
c | 连接信息 | ||||
b | 0-N个带宽信息行 | ||||
z | 时区调整 | ||||
k | 加密密钥 | ||||
a | 0-N个会话属性行 | ||||
时间描述(1-N) | |||||
t |
会话活动时间 其中的绝对时间基于网络时间协议(NTP)格式,即1900年到目前的秒数 开始时间为0表示会话是永久的;结束时间为零表示会话持续时间不限制 |
||||
r | 0-N个repeat times | ||||
媒体描述(0-N) | |||||
m |
媒体名称、传输地址,以及传输协议。示例:
载荷类型如果是96-127之间,则表示载荷类型是动态分配的,后面会出现a=rtpmap行来映射此载荷类型:
载荷类型可以声明若干个,表示这些类型在会话中都可能使用 |
||||
i | 媒体的标题或者信息 | ||||
c | 连接信息 | ||||
b | 带宽信息 | ||||
k | 加密密钥 | ||||
a | 0-N个媒体属性行,可以覆盖会话属性行同名属性 |
为简化开发,下面给出一个live555的RTSP客户端封装。
创建如下CMake项目:
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 |
cmake_minimum_required(VERSION 3.6) project(live5555) set(CMAKE_CXX_STANDARD 11) set(LIVE555_HOME /home/alex/CPP/lib/live555) include_directories(${LIVE555_HOME}/include/UsageEnvironment) include_directories(${LIVE555_HOME}/include/BasicUsageEnvironment) include_directories(${LIVE555_HOME}/include/liveMedia) include_directories(${LIVE555_HOME}/include/groupsock) include_directories(/home/alex/CPP/lib/spdlog/include) set(CMAKE_CXX_FLAGS "-w -pthread") set(LIVE5555_SRC SinkBase.cpp RTSPClientBase.cpp) add_library(live5555 ${LIVE5555_SRC}) target_link_libraries( live5555 ${LIVE555_HOME}/lib/libliveMedia.a ${LIVE555_HOME}/lib/libgroupsock.a ${LIVE555_HOME}/lib/libBasicUsageEnvironment.a ${LIVE555_HOME}/lib/libUsageEnvironment.a ) set(WS_PUSH_SRC wspush.cpp) add_executable(wspush ${WS_PUSH_SRC}) target_link_libraries( wspush live5555 ) |
基础头文件:
1 2 3 4 5 6 7 |
#ifndef LIVE5555_COMMON_H #define LIVE5555_COMMON_H #include "liveMedia.hh" #include "BasicUsageEnvironment.hh" #endif //LIVE5555_COMMON_H |
类RTSPClientBase,对RTSPClient进行扩展,将RTSP命令回调函数指针转换为成员函数,实现基本的取流逻辑:
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 |
#ifndef LIVE5555_RTSPCLIENTBASE_H #define LIVE5555_RTSPCLIENTBASE_H #include "common.h" class RTSPClientBase : public RTSPClient { private: MediaSession *session; // 事件循环监控变量,不为零时事件循环退出 volatile char eventLoopWatchVariable; char *rtspURL; volatile int acceptedSubSessionCount; volatile int preparedSubSessionCount; static void onDescribeResponse( RTSPClient *client, int resultCode, char *resultString ); static void onSetupResponse( RTSPClient *client, int resultCode, char *resultString ); static void onPlayResponse( RTSPClient *client, int resultCode, char *resultString ); static void onSubSessionClose( void *clientData ); protected: RTSPClientBase( UsageEnvironment &env, const char *rtspURL ); // 处理RTSP命令DESCRIBE的响应 virtual void onDescribeResponse( int resultCode, const char *sdp ); // 处理RTSP命令SETUP的响应 virtual void onSetupResponse( int resultCode, const char *resultString ); virtual void onPlayResponse( int resultCode, char *resultString ); // 是否初始化指定的子会话 virtual bool acceptSubSession( const char *mediumName, const char *codec )=0; virtual MediaSink *createSink( const char *mediumName, const char *codec, MediaSubsession *subSession )=0; // 处理子会话关闭事件 virtual void onSubSessionClose( MediaSubsession *subsess ); public: virtual void start(); virtual void stop(); }; #endif //LIVE5555_RTSPCLIENTBASE_H |
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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 |
#include "live5555/RTSPClientBase.h" #include "spdlog/spdlog.h" static auto LOGGER = spdlog::stdout_color_st( "RTSPClientBase" ); static const char *getResultString( char *resultString ) { return resultString ? resultString : "N/A"; } // 父构造函数的第三个参数是调试信息冗余级别 RTSPClientBase::RTSPClientBase( UsageEnvironment &env, const char *rtspURL ) : RTSPClient( env, rtspURL, 0, NULL, 0, -1 ) { } void RTSPClientBase::start() { LOGGER->trace( "Starting RTSP client..." ); this->rtspURL = rtspURL; LOGGER->trace( "Send RTSP command: DESCRIBE" ); sendDescribeCommand( onDescribeResponse ); LOGGER->trace( "Startup live555 eventloop" ); envir().taskScheduler().doEventLoop( &eventLoopWatchVariable ); } void RTSPClientBase::onDescribeResponse( RTSPClient *client, int resultCode, char *resultString ) { LOGGER->trace( "DESCRIBE response received, resultCode: {}", resultCode ); RTSPClientBase *clientBase = (RTSPClientBase *) client; bool ok = false; if ( resultCode == 0 ) { clientBase->onDescribeResponse( resultCode, resultString ); } else { LOGGER->trace( "Stopping due to DESCRIBE failure" ); clientBase->stop(); }; delete[] resultString; } void RTSPClientBase::onDescribeResponse( int resultCode, const char *sdp ) { LOGGER->debug( "SDP received: \n{}", sdp ); UsageEnvironment &env = envir(); LOGGER->trace( "Create new media session according to SDP" ); session = MediaSession::createNew( env, sdp ); if ( session && session->hasSubsessions()) { MediaSubsessionIterator *it = new MediaSubsessionIterator( *session ); // 遍历子会话,SDP中的每一个媒体行(m=***)对应一个子会话 while ( MediaSubsession *subsess = it->next()) { const char *mediumName = subsess->mediumName(); // 初始化子会话,导致相应的RTPSource被创建 LOGGER->trace( "Initialize sub session {}", mediumName ); if ( !acceptSubSession( mediumName, subsess->codecName())) { continue; } acceptedSubSessionCount++; bool ok = subsess->initiate(); if ( !ok ) { LOGGER->error( "Failed to initialize sub session: {}", mediumName ); stop(); break; } const Boolean muxed = subsess->rtcpIsMuxed(); const char *codec = subsess->codecName(); const int port = subsess->clientPortNum(); LOGGER->debug( "Initialized sub session... \nRTCP Muxed: {}\nPort: {}\nMedium : {}\nCodec: {}", muxed, port, mediumName, codec ); LOGGER->trace( "Send RTSP command: SETUP for subsession {}", mediumName ); sendSetupCommand( *subsess, onSetupResponse, False, False ); } } else { stop(); } } void RTSPClientBase::onSetupResponse( RTSPClient *client, int resultCode, char *resultString ) { LOGGER->trace( "SETUP response received, resultCode: {}, resultString: {}", resultCode, getResultString( resultString )); RTSPClientBase *clientBase = (RTSPClientBase *) client; if ( resultCode == 0 ) { clientBase->preparedSubSessionCount++; clientBase->onSetupResponse( resultCode, resultString ); } else { LOGGER->trace( "Stopping due to SETUP failure" ); clientBase->stop(); } delete[] resultString; } void RTSPClientBase::onSetupResponse( int resultCode, const char *resultString ) { if ( preparedSubSessionCount == acceptedSubSessionCount ) { MediaSubsessionIterator *it = new MediaSubsessionIterator( *session ); while ( MediaSubsession *subsess = it->next()) { const char *mediumName = subsess->mediumName(); const char *codec = subsess->codecName(); if ( acceptSubSession( mediumName, codec )) { MediaSink *sink = createSink( mediumName, codec, subsess ); // 让Sink回调能够感知Client对象 subsess->miscPtr = this; // 导致Sink的continuePlaying被调用,准备接受数据推送 sink->startPlaying( *subsess->readSource(), NULL, subsess ); // 此时数据推送不会立即开始,直到调用STSP命令PLAY RTCPInstance *rtcp = subsess->rtcpInstance(); if ( rtcp ) { // 正确处理针对此子会话的RTCP命令 rtcp->setByeHandler( onSubSessionClose, subsess ); } LOGGER->trace( "Send RTSP command: PLAY" ); // PLAY命令可以针对整个会话,也可以针对每个子会话 sendPlayCommand( *session, onPlayResponse ); } } } } void RTSPClientBase::onPlayResponse( RTSPClient *client, int resultCode, char *resultString ) { LOGGER->trace( "PLAY response received, resultCode: {}, resultString: {}", resultCode, getResultString( resultString )); RTSPClientBase *clientBase = (RTSPClientBase *) client; if ( resultCode == 0 ) { clientBase->onPlayResponse( resultCode, resultString ); } else { LOGGER->trace( "Stopping due to PLAY failure" ); clientBase->stop(); } delete[] resultString; } void RTSPClientBase::onPlayResponse( int resultCode, char *resultString ) { // 此时服务器应该开始推送流过来 // 如果播放的是定长的录像,这里应该注册回调,在时间到达后关闭客户端 double &startTime = session->playStartTime(); double &endTime = session->playEndTime(); LOGGER->debug_if( startTime == endTime, "Session is infinite" ); } void RTSPClientBase::onSubSessionClose( void *clientData ) { MediaSubsession *subsess = (MediaSubsession *) clientData; RTSPClientBase *clientBase = (RTSPClientBase *) subsess->miscPtr; clientBase->onSubSessionClose( subsess ); } void RTSPClientBase::onSubSessionClose( MediaSubsession *subsess ) { LOGGER->debug( "Stopping subsession..." ); // 首先关闭子会话的SINK Medium::close( subsess->sink ); subsess->sink = NULL; // 检查是否所有兄弟子会话均已经结束 MediaSession &session = subsess->parentSession(); MediaSubsessionIterator iter( session ); while (( subsess = iter.next()) != NULL ) { // 存在未结束的子会话,不能关闭当前客户端 if ( subsess->sink != NULL ) return; } // 关闭客户端 LOGGER->debug( "All subsession closed" ); stop(); } void RTSPClientBase::stop() { LOGGER->debug( "Stopping RTSP client..." ); // 修改事件循环监控变量 eventLoopWatchVariable = 0; UsageEnvironment &env = envir(); if ( session != NULL ) { Boolean someSubsessionsWereActive = False; MediaSubsessionIterator iter( *session ); MediaSubsession *subsession; // 检查是否存在需要处理的子会话 while (( subsession = iter.next()) != NULL ) { if ( subsession->sink != NULL ) { // 强制关闭子会话的SINK Medium::close( subsession->sink ); subsession->sink = NULL; if ( subsession->rtcpInstance() != NULL ) { // 服务器可能在处理TEARDOWN时发来RTCP包BYE subsession->rtcpInstance()->setByeHandler( NULL, NULL ); } someSubsessionsWereActive = True; } } if ( someSubsessionsWereActive ) { // 向服务器发送TEARDOWN命令,让服务器关闭输入流 sendTeardownCommand( *session, NULL ); } } // 关闭客户端 Medium::close( this ); } |
类SinkBase,一个基础的Sink实现,从流中获取帧:
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 |
#ifndef LIVE5555_SINKBASE_H #define LIVE5555_SINKBASE_H #include "common.h" class SinkBase : public MediaSink { private: static void afterGettingFrame( void *clientData, unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime, unsigned /*durationInMicroseconds*/ ); protected: unsigned recvBufSize; unsigned char *recvBuf; SinkBase( UsageEnvironment &env, unsigned recvBufSize ); // sink->startPlaying会调用continuePlaying,实现播放逻辑 virtual Boolean continuePlaying(); virtual ~SinkBase(); public: virtual void afterGettingFrame( unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime ); }; #endif //LIVE5555_SINKBASE_H |
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 |
#include "live5555/SinkBase.h" #include "spdlog/spdlog.h" static auto LOGGER = spdlog::stdout_color_st( "SinkBase" ); SinkBase::SinkBase( UsageEnvironment &env, unsigned recvBufSize ) : MediaSink( env ) { this->recvBufSize = recvBufSize; this->recvBuf = new unsigned char[recvBufSize]; } SinkBase::~SinkBase() { delete[] this->recvBuf; } // 缺省实现:保存已分帧源的下一帧到缓冲区中,然后执行回调 Boolean SinkBase::continuePlaying() { if ( fSource == NULL ) return False; fSource->getNextFrame( recvBuf, recvBufSize, afterGettingFrame, this, onSourceClosure, this ); return True; }; // 由于getNextFrame需要的是一个函数指针,因此这里用静态函数。此函数简单的转调对应的成员函数 void SinkBase::afterGettingFrame( void *clientData, unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime, unsigned /*durationInMicroseconds*/ ) { SinkBase *sink = (SinkBase *) clientData; sink->afterGettingFrame( frameSize, numTruncatedBytes, presentationTime ); } // 缺省实现:递归获取下一帧 void SinkBase::afterGettingFrame( unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime ) { LOGGER->trace( "Frame of {} bytes received",frameSize ); fSource->getNextFrame( recvBuf, recvBufSize, afterGettingFrame, this, onSourceClosure, this ); } |
下面的客户端基于上节的封装:
1 2 3 4 5 6 7 |
#ifndef LIVE5555_LIVE5555_H #define LIVE5555_LIVE5555_H #include "RTSPClientBase.h" #include "SinkBase.h" #endif //LIVE5555_LIVE5555_H |
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 |
#include <iostream> #include "live5555/client.h" #include "spdlog/spdlog.h" static auto LOGGER = spdlog::stdout_color_st( "wspush" ); class VideoSink : public SinkBase { public: VideoSink( UsageEnvironment &env, unsigned int recvBufSize ) : SinkBase( env, recvBufSize ) {} void afterGettingFrame( unsigned frameSize, unsigned numTruncatedBytes, struct timeval presentationTime ) override { unsigned naluHead = recvBuf[ 0 ]; unsigned nri = naluHead >> 5; unsigned f = nri >> 2; unsigned type = naluHead & 0b00011111; LOGGER->trace( "NALU info: nri {} type {}", nri, type ); SinkBase::afterGettingFrame( frameSize, numTruncatedBytes, presentationTime ); } }; class H264RTSPClient : public RTSPClientBase { private: MediaSink *videoSink; public: H264RTSPClient( UsageEnvironment &env, const char *rtspURL, MediaSink *videoSink ) : RTSPClientBase( env, rtspURL ), videoSink( videoSink ) {} protected: // 测试用的摄像头(RTSP源)仅仅有一个子会话,因此这里简化了实现: bool acceptSubSession( const char *mediumName, const char *codec ) override { return true; } MediaSink *createSink( const char *mediumName, const char *codec, MediaSubsession *subSession) override { return videoSink; } }; int main() { spdlog::set_pattern( "%Y-%m-%d %H:%M:%S.%e [%l] [%n] %v" ); spdlog::set_level( spdlog::level::trace ); TaskScheduler *scheduler = BasicTaskScheduler::createNew(); BasicUsageEnvironment *env = BasicUsageEnvironment::createNew( *scheduler ); VideoSink *sink = new VideoSink( *env, 1024 * 1024 ); H264RTSPClient *client = new H264RTSPClient( *env, "rtsp://admin:kingsmart123@192.168.0.196:554/ch1/sub/av_stream", sink ); client->start(); return 0; } |
此客户端很简单,它建立RTSP会话,然后依次执行DESCRIBE、SETUP、PLAY命令,最终建立RTP会话,开始视频流的传输。
live555的RTSP客户端中的所有网络事件 —— 包括RTSP请求响应、RTP推送 —— 都由BasicTaskScheduler的事件循环处理,该事件循环会不断的执行select()系统调用,监听网络事件的到达。事件循环由RTSPClientBase.start()启动。
在SETUP响应回调中,发送PLAY命令之前,我们调用了VideoSink.startPlaying()方法,此方法会转调VideoSink.continuePlaying()方法,后者则调用H264RTPSource.getNextFrame()注册下一帧的处理回调。getNextFrame()会将解析得到的帧存放到你指定的缓冲区中,测试时打印前几帧:
67 42 00 1F 95 A8 14 01 6E 9B 80 80 80 81
68 CE 3C 80
61 E4 A0 4F F3 7A 06 B9 36 39 80 07 4C 9A ...
可以看到,这些帧都是标准的H264 NALU格式(不带起始码),第一个是SPS,第二个是PPS,后续是普通切片。
RTP包到达后,事件循环委托H264RTPSource进行如下处理:
- 检查RTP包头的合法性
- 剔除RTP包头和Padding
- 抽取H264帧,然后针对自己调用afterGetting(this),并导致之前通过getNextFrame()注册的回调函数被调用
术语 | 说明 |
Jitter |
抖动(Jitter)是TCP/IP网络和组件天生具有的一种不被期望的“倾向”。它是数据报被接收到的延迟(Delay)时间的变化性(Variation) 发送方以固定的频率发送数据报,但是由于网络拥塞、不适当的数据报排队或者配置错误,接收者接收到数据报的频率可能会在较大范围变动 抖动会影响流媒体的回放体验,在等待延迟到达的数据报时,可能出现gap |
NPT |
正常播放时间(Normal Play Time),指示相对于展现(presentation)开始点的,流的当前位置(时间偏移) NPT使用一个浮点数表示,整数部分可能按秒数、或者小时+分钟+秒数来解释,小数部分则进行一秒内度量(例如.500表示当前秒过了一半) 展现的开始点的NPT定义为0.0,负数的意义没有定义 当x倍速播放时,NPT的增长速度增加为x倍 |
Presentation |
展现,一个或者多个被渲染到客户端流,它们共同组成了一个Media feed |
Leave a Reply