RTSP在不同FFmpeg版本中可能略有不同,本章主要介绍FFmpeg RTSP主要的相关代码流程,而涉及FFmpeg的一些结构并不会详细说明,FFmpeg详细的分析,有兴趣可以可以参考雷霄骅大神的博客https://my.csdn.net/leixiaohua1020。
AVInputFormat该结构被称为解复用模块,是音视频文件的一个解封装器,对RTSP这种媒体协议,FFmpeg将其当做一种封装格式来处理,主要的RTSP协议交互流程也在demux阶段处理。
AVInputFormat ff_rtsp_demuxer = {
"rtsp",
NULL_IF_CONFIG_SMALL("RTSP input format"),
sizeof(RTSPState),
rtsp_probe,
rtsp_read_header,
rtsp_read_packet,
rtsp_read_close,
rtsp_read_seek,
.flags = AVFMT_NOFILE,
.read_play = rtsp_read_play,
.read_pause = rtsp_read_pause,
.priv_class = &rtsp_demuxer_class,
};
当我们调用av_register_all接口时,便会将ff_rtsp_demuxer 注册进来,可以看到flags设置为AVFMT_NOFILE。
在FFmpeg中,当muxers和demuxers的flags有设AVFMT_NOFILE时,AVIOContext(表示字节流输入/输出的上下文)这个成员变量就不需要设置,因为muxers和demuxers会使用自己的方式处理输入/输出。
简单来说,就是RTSP的输入解析不是使用URLProtocol这种文件类型的I/O来进行,而是在demuxer中处理。
所以,我们最需要关注的是ff_rtsp_demuxer 对应的处理函数。
av_register_all:
REGISTER_MUXDEMUX(RTSP, rtsp);
当上层调用avformat_open_input函数打开一个rtsp播放地址时,会调用到read_header,代码如下:
avformat_open_input:
if (!(s->flags&AVFMT_FLAG_PRIV_OPT) && s->iformat->read_header)
if ((ret = s->iformat->read_header(s)) < 0)
goto fail;
调用协议对应demux的read_header函数,读取播放格式头并初始化。
在ff_rtsp_demuxer 中,rtsp_read_header函数主要代码如下:
rtsp_read_header:
{
RTSPState *rt = s->priv_data;
int ret;
redirect:
av_log(NULL, AV_LOG_INFO, "[%s:%d]\n", __FUNCTION__, __LINE__);
rt->send_keepalive=0;
ret = ff_rtsp_connect(s);
if (ret)
return ret;
......
if (rt->initial_pause) {
/* do not start immediately */
} else {
ret = rtsp_read_play(s);
if (ret < 0) {
ff_rtsp_close_streams(s);
ff_rtsp_close_connections(s);
if(ret <=-300&& ret > -400)//add by wusc for redirect
goto redirect;
return AVERROR_INVALIDDATA;
}
}
......
}
rtsp_read_header主要进行以下两个工作:
1、connect流程,包括OPTIONS ->DESCRIBE ->SETUP 过程。
2、根据配置是否进入PLAY 。
ff_rtsp_connect函数完成OPTIONS ->DESCRIBE ->SETUP 过程。
对于每一个命令前端的回复,FFmpeg会进行解析:
if ((ret = ff_rtsp_read_reply(s, reply, content_ptr, 0, method) ) < 0){
av_log(s, AV_LOG_INFO, "ff_rtsp_read_reply %s error status_code: %d\n", method,
reply->status_code);
return ret;
}
ff_rtsp_read_reply:
ff_rtsp_parse_line(reply, p, rt, method);
av_strlcat(rt->last_reply, p, sizeof(rt->last_reply));
av_strlcat(rt->last_reply, "\n", sizeof(rt->last_reply));
RTSP交互主要的代码流程如下:
1、解析播放地址url携带的参数,根据携带的"udp"/“tcp”/“http”,设置lower_transport_mask标志位 ,lower_transport_mask 代表setup阶段,底层RTP数据的传输,支持哪几种协议(UDP/TCP or 都支持)
ff_rtsp_connect:
if (!strcmp(option, "udp")) {
lower_transport_mask |= (1<< RTSP_LOWER_TRANSPORT_UDP);
} else if (!strcmp(option, "multicast")) {
lower_transport_mask |= (1<< RTSP_LOWER_TRANSPORT_UDP_MULTICAST);
} else if (!strcmp(option, "tcp")) {
lower_transport_mask |= (1<< RTSP_LOWER_TRANSPORT_TCP);
} else if(!strcmp(option, "http")) {
lower_transport_mask |= (1<< RTSP_LOWER_TRANSPORT_TCP);
rt->control_transport = RTSP_MODE_TUNNEL;
} else if (!strcmp(option, "filter_src")) {
rt->filter_source = 1;
2、打开RTSP交互的TCP链接,无论RTP数据流是基于UDP还是TCP,RTSP协议交互的部分都是基于TCP的,所以先把url打上"tcp"标签,打开tcp链接。如果没有指定端口号,则RTSP默认端口号是554。
ff_rtsp_connect:
/* open the tcp connection */
ff_url_join(tcpname, sizeof(tcpname), "tcp", NULL, host, port, NULL);
if(rt->use_protocol_mode){
av_strlcat(tcpname, "?rcvbuf_size=1024", sizeof(tcpname));
}
int ret=ffurl_open(&rt->rtsp_hd, tcpname, AVIO_FLAG_READ_WRITE);
3、发送OPTIONS请求
ff_rtsp_connect:
ff_rtsp_send_cmd(s, "OPTIONS", rt->control_uri, cmd, reply, NULL);
4、发送DESCRIBE请求,并解析前端反馈的SDP消息,SDP消息的解析可以参考上篇文章。
请求
ff_rtsp_connect:
if (s->iformat && CONFIG_RTSP_DEMUXER)
err = ff_rtsp_setup_input_streams(s, reply);
ff_rtsp_setup_input_streams:
ff_rtsp_send_cmd(s, "DESCRIBE", rt->control_uri, cmd, reply, &content);
解析SDP
ff_rtsp_setup_input_streams:
/* now we got the SDP description, we parse it */
ret = ff_sdp_parse(s, (const char *)content);
5、发送SETUP请求
ff_rtsp_make_setup_request是SETUP请求的核心代码,下面分析它的主要代码逻辑:
1)lower_transport 指定的是RTP传输的载流协议,根据上面设置的lower_transport_mask 来确定。
ff_rtsp_connect:
int lower_transport = ff_log2_tab[lower_transport_mask &
~(lower_transport_mask - 1)];
float value = 0.0;
int ret = -1;
av_log(NULL, AV_LOG_INFO, "[%s:%d]lowtrans=%d,lowtransm=%d,value=%d\n", __FUNCTION__,__LINE__, lower_transport,lower_transport_mask,value);
err = ff_rtsp_make_setup_request(s, host, port, lower_transport,
rt->server_type == RTSP_SERVER_REAL ?
real_challenge : NULL);
2)根据SDP消息指定的传输协议rt->transport,设置SETUP请求载流协议的前缀
ff_rtsp_make_setup_request:
if (rt->transport == RTSP_TRANSPORT_RDT)
trans_pref = "x-pn-tng";
else if (rt->transport == RTSP_TRANSPORT_RAW)
trans_pref = "RAW/RAW";
else if (NULL != strstr(rt->server, "ZXUS") || NULL != strstr(rt->server, "ZMSS"))//wusc add for ZXUS/ZMSS server
trans_pref = "MP2T/RTP";
else
trans_pref = "RTP/AVP";
3)根据载流协议的不同,后续的处理流程也有些不同,我们以最常见的TCP和UDP协议来分析。
TCP方式Transport: MP2T/RTP/TCP;unicast;interleaved=0-1
如果采用TCP方式传送,
a.设置传输方式,即前缀后加上/TCP(MP2T/RTP/TCP),同时,设置为单播unicast
b.设置interleaved参数,如interleaved=0-1。因为传送的RTP,RTCP包都在同一个链路上,需要区分,所以有了interleaved,0表示是RTP的通道,1表示是RTCP的通道,interleaved值有两个:0和1,0表示RTP包,1表示RTCP包,接收端根据interleaved的值来区别是哪种数据包。
ff_rtsp_make_setup_request:
else if (lower_transport == RTSP_LOWER_TRANSPORT_TCP) {
......
snprintf(transport, sizeof(transport) - 1,
"%s/TCP;", trans_pref);
if (rt->transport != RTSP_TRANSPORT_RDT)
av_strlcat(transport, "unicast;", sizeof(transport));
av_strlcatf(transport, sizeof(transport),
"interleaved=%d-%d",
interleave, interleave + 1);
interleave += 2;
}
UDP方式Transport: MP2T/RTP/UDP;unicast;client_port=5000-5001
如果采用UDP方式传送,
a.首先也需要一对RTSP端口来作为RTP通道及RTCP通道,ff_url_join打上"rtp"标签,ffurl_open打开rtp传送通道。
b.设置传输方式,即前缀后加上/UDP(MP2T/RTP/UDP)。设置为单播unicast。
c.UDP需要携带client_port参数,将自己使用的RTP和RTCP端口号发送给前端,这样前端才知道需要把RTP数据发送给哪个端口。
ff_rtsp_make_setup_request:
while (j <= RTSP_RTP_PORT_MAX) {
ff_url_join(buf, sizeof(buf), "rtp", NULL, host, -1,
"?localport=%d", j);
/* we will use two ports per rtp stream (rtp and rtcp) */
j += 2;
if (ffurl_open(&rtsp_st->rtp_handle, buf, AVIO_FLAG_READ_WRITE) == 0){
av_log(NULL, AV_LOG_INFO, "[%s]ffurl_open,handle=%d\n", __FUNCTION__, rtsp_st->rtp_handle);
goto rtp_opened;
}
......
rtp_opened:
port = rtp_get_local_rtp_port(rtsp_st->rtp_handle);
have_port:
snprintf(transport, sizeof(transport) - 1,
"%s/UDP;", trans_pref);
if (rt->server_type != RTSP_SERVER_REAL)
av_strlcat(transport, "unicast;", sizeof(transport));
av_strlcatf(transport, sizeof(transport),
"client_port=%d", port);
if (rt->transport == RTSP_TRANSPORT_RTP &&
!(rt->server_type == RTSP_SERVER_WMS && i > 0))
av_strlcatf(transport, sizeof(transport), "-%d", port + 1);
6.SETUP 回复消息解析
如果设置的载流协议不支持,前端会回复461
RTSP/1.0 461 Unsupported transport
Server: ZXUSS100 1.0
CSeq: 3
对于461,FFmpeg会取消失败的这种载流协议,设置新的lower_transport_mask ,直到lower_transport_mask == 0(即客户端设置的几种协议都已经试完)。
ff_rtsp_connect:
err = ff_rtsp_make_setup_request(s, host, port, lower_transport,
rt->server_type == RTSP_SERVER_REAL ?
real_challenge : NULL);
if (err < 0)
goto fail;
lower_transport_mask &= ~(1 << lower_transport);
if (lower_transport_mask == 0 && err == 1) {
err = AVERROR(EPROTONOSUPPORT);
goto fail;
}
} while (err);
SETUP 消息发送成功的情况下,
1)、对于TCP载流的方式,FFmpeg会将前端服务器回复的interleaved参数作为正式的RTP/RTCP通道编号。
ff_rtsp_make_setup_request:
case RTSP_LOWER_TRANSPORT_TCP:
rtsp_st->interleaved_min = reply->transports[0].interleaved_min;
rtsp_st->interleaved_max = reply->transports[0].interleaved_max;
break;
RTSP/1.0 200 OK
CSeq: 4
Server: Wowza Streaming Engine 4.7.5.01 build21752
Cache-Control: no-cache
Expires: Thu, 5 Jul 2018 03:56:01 UTC
Transport: RTP/AVP/TCP;unicast;interleaved=0-1
Date: Thu, 5 Jul 2018 03:56:01 UTC
Session: 1531975305;timeout=60
2)、对于UDP载流方式,前端服务器回复的source参数表明RTP服务器地址,server_port则是服务器端口。需要将RTP链接指向这个IP及端口。
ff_rtsp_make_setup_request:
if (reply->transports[0].source[0]) {
ff_url_join(url, sizeof(url), "rtp", NULL,
reply->transports[0].source,
reply->transports[0].server_port_min, "%s", options);
} else {
ff_url_join(url, sizeof(url), "rtp", NULL, host,
reply->transports[0].server_port_min, "%s", options);
}
if (!(rt->server_type == RTSP_SERVER_WMS && i > 1) &&
rtp_set_remote_url(rtsp_st->rtp_handle, url) < 0) {
err = AVERROR_INVALIDDATA;
goto fail;
}
RTSP/1.0 200 OK
Server: ZMSS_ChinaTelcom2.2/ZXMSSV3.00.23.13U10P01P07T01
CSeq: 4
Session: 791434909882685964
Date: Thu, 10 May 2018 12:41:40 GMT
Expires: Thu, 10 May 2018 12:41:40 GMT
Transport:
RTP/AVP/UDP;unicast;source=210.13.2.51;client_port=5000-5001;server_port=30146-30147
同时,对于UDP方式,我们最好发打洞包做NAT,即客户端需要根据source及server_port,传送一小段RTP包给服务器,以便服务器能成功推流到客户端。否则对于一些内网的组网情况,PLAY后无法播放。
ff_rtsp_make_setup_request:
if (!(rt->server_type == RTSP_SERVER_WMS && i > 1) && s->iformat &&
CONFIG_RTPDEC){
if(NULL != strstr(rt->server, "ZXUS") || NULL != strstr(rt->server, "ZMSS")){
av_log(s, AV_LOG_ERROR, "ZXUS/ZMSS server send NAT after PLAY\n");
}else{
rtp_send_zte_punch_packets(rtsp_st->rtp_handle,rt->rtsp_hd);//ZTE NAT
rtp_send_punch_packets(rtsp_st->rtp_handle);
}
}
break;
有些前端会回复destination参数,和client_port一起,这是前端推RTP数据时,客户端接收的IP及端口,从下面这例子destination=192.168.1.16;可以看出是内网组网方式,这种情况必须NAT,否则无法收到数据。
Transport:
MP2T/RTP/UDP;destination=192.168.1.16;client_port=5000-5001;server_port=8000-8001;source=239.2.1.242
7、根据SETUP阶段设置RTP载流方式、IP地址、端口,打开RTP交互的上下文,用于后续对RTP数据的解析。
ff_rtsp_make_setup_request:
if ((err = rtsp_open_transport_ctx(s, rtsp_st)))
至此ff_rtsp_connect流程基本完成。下一阶段,则又进入rtsp_read_play。
ff_rtsp_connect完成客户端与服务器已经建立好了连接,进入rtsp_read_play。
1、设置PLAY参数,主要是Range和Scale这两参数。
1)Range表示请求的播放数据的位置,根据上层设置值进行拼接,有几种:
a.绝对时间描述,使用clock=xxx-来表示需要请求哪个时间段的视频数据;
b.位置描述,根据上层seek操作设置下来的相对位置,向前播放的情况设置npt=xxx- ;先后播放的情况设置npt=xxx-0;如果起播则是ntp=0- ;
c.位置描述,使用字符串描述,npt=now-;npt=begin-xxx;npt=0-end;这些都是可以的;
以下实现了a和b两种,c其实和b一样,也是位置描述。
2)Scale表示播放的速率,也是根据上层操作设置下来。
rtsp_read_play:
设置Range
// time scale function
// seek, fast-forward/fast-rewind triggered seek
if (rt->playback_rate_permille != rt->playback_rate_permille_next)
rt->playback_rate_permille = rt->playback_rate_permille_next;
if(rt->playseekFlag == 1){
snprintf(cmd, sizeof(cmd),"Range: clock=%s.00Z-\r\n",rt->playseekTime);
}else{
if (rt->playback_rate_permille >= 0) {
// forward
snprintf(cmd, sizeof(cmd),
"Range: npt=%"PRId64".%03"PRId64"-\r\n",
rt->seek_timestamp / AV_TIME_BASE,
rt->seek_timestamp / (AV_TIME_BASE / 1000) % 1000);
} else {
// backward
snprintf(cmd, sizeof(cmd),
"Range: npt=%"PRId64".%03"PRId64"-0\r\n",
rt->seek_timestamp / AV_TIME_BASE,
rt->seek_timestamp / (AV_TIME_BASE / 1000) % 1000);
}
}
设置Scale
length = strlen (cmd);
snprintf(&cmd [length], sizeof(cmd) - length,
"Scale: %d.%d\r\n",
rt->playback_rate_permille / 1000,
rt->playback_rate_permille % 1000);
// length = strlen (cmd);
2、发送PLAY 请求
PLAY rtsp://61.149.64.212:554/live/ch11091521361097960208.sdp/
RTSP/1.0Range: clock=20180628T065101.00Z-
Scale: 1.0
CSeq: 4
Session: 65538918
RTSP/1.0 200 OK
Server: ZXUSS100 1.0
CSeq: 4
Range: clock=20180628T065101.00Z-20180628T065158.98Z
Scale: 1.0
Session: 65538918
RTP-Info:
url=rtsp://61.149.64.132:11842/live/ch11091521361097960208.sdp/trackID=2;seq=0;rtptime=453338458
代码如下:
rtsp_read_play:
ff_rtsp_send_cmd(s, "PLAY", rt->control_uri, cmd, reply, NULL);
发送PLAY后,需要对回复的消息进行解析,其中,RTP-Info:表示RTP的相关信息,会进行解析,解析函数rtsp_parse_rtp_info,比较简单,不详细展开。但是要关注的是RTP-Info:携带的url可能与SETUP时前端返回的source不一样,这是得以RTP-Info中的为准。
至此,PLAY也已完成。
整个rtsp_read_header也已结束,起播的协议交互流程完成。解下来就是拉流播放,我们看下rtsp_read_packet函数。
播放的大致流程就是获取流数据,解封装后注入解码,我们只关注RTSP相关部分,即是拉流部分。
上面rtsp_read_header已经完成起播RTSP协议交互部分,那现在需要的就是获取播放数据了。
当上层使用ff_read_packet拉流获取数据时,会调用对应demux的read_packet函数,在rtsp中,则对应 rtsp_read_packet。
.read_packet = rtsp_read_packet,
rtsp_read_packet里,核心部分是ff_rtsp_fetch_packet
rtsp_read_packet:
ret = ff_rtsp_fetch_packet(s, pkt);
载流方式为UDP时,读取数据超时,如果客户端支持TCP载流方式,则会切换成TCP载流。这相当于TEARDOWN 后重新走一遍协议交互的流程了。
rtsp_read_packet:
if (ret == AVERROR(ETIMEDOUT) && !rt->packets) {
if (rt->lower_transport == RTSP_LOWER_TRANSPORT_UDP &&
rt->lower_transport_mask & (1 << RTSP_LOWER_TRANSPORT_TCP)) {
RTSPMessageHeader reply1, *reply = &reply1;
av_log(s, AV_LOG_WARNING, "UDP timeout, retrying with TCP\n");
if (rtsp_read_pause(s) != 0)
return -1;
// TEARDOWN is required on Real-RTSP, but might make
// other servers close the connection.
if (rt->server_type == RTSP_SERVER_REAL)
ff_rtsp_send_cmd(s, "TEARDOWN", rt->control_uri, NULL,
reply, NULL);
rt->session_id[0] = '\0';
if (resetup_tcp(s) == 0) {
rt->state = RTSP_STATE_IDLE;
rt->need_subscription = 1;
if (rtsp_read_play(s) != 0)
return -1;
goto retry;
}
}
}
接下来,分析下载部分的核心函数ff_rtsp_fetch_packet,该函数完成媒体数据的下载及解析。
1、如果上次RTP包中数据还未完全解析完,则本次继续解析上次的RTP包。
根据RTP数据传输的封装,分别走三个解析函数:
1)RDT是real公司专有的传输rm格式文件的协议,调用ff_rdt_parse_packet解析
2)如果数据是以RTP封装,调用ff_rtp_parse_packet解析
3)如果数据是裸流形式传输,调用avpriv_mpegts_parse_packet解析
在这里不详细说明解析的流程,RTP封装这种比较常见。函数参数如下:
/**
ff_rtsp_fetch_packet:
/* get next frames from the same RTP packet */
if (rt->cur_transport_priv) {
if (rt->transport == RTSP_TRANSPORT_RDT) {
ret = ff_rdt_parse_packet(rt->cur_transport_priv, pkt, NULL, 0);
} else if (rt->transport == RTSP_TRANSPORT_RTP) {
ret = ff_rtp_parse_packet(rt->cur_transport_priv, pkt, NULL, 0);
} else if (CONFIG_RTPDEC && rt->ts) {
ret = avpriv_mpegts_parse_packet(rt->ts, pkt, rt->recvbuf + rt->recvbuf_pos, rt->recvbuf_len - rt->recvbuf_pos);
if (ret >= 0) {
rt->recvbuf_pos += ret;
ret = rt->recvbuf_pos < rt->recvbuf_len;
}
2、如果上个RTP包已经解析完,这次则会收新的的RTP包,并进行解析。
接收数据代码:
1)TCP载流调用ff_rtsp_tcp_read_packet
2)UDP载流调用udp_read_packet;对于UDP,每次接收完数据后,还需要给前端发送反馈包,本次接收了多少数据,调用函数ff_rtp_check_and_send_back_rr。
接收函数和反馈函数,底层实现均调用ffurl_read/ffurl_write等封装好的接口实现。会调用到相应的协议接口实现。
ff_rtsp_fetch_packet:
default:
#if CONFIG_RTSP_DEMUXER
case RTSP_LOWER_TRANSPORT_TCP:
len = ff_rtsp_tcp_read_packet(s, &rtsp_st, rt->recvbuf, RECVBUF_SIZE);
break;
#endif
case RTSP_LOWER_TRANSPORT_UDP:
case RTSP_LOWER_TRANSPORT_UDP_MULTICAST:
len = udp_read_packet(s, &rtsp_st, rt->recvbuf, RECVBUF_SIZE, wait_end);
if (len > 0 && rtsp_st->transport_priv && rt->transport == RTSP_TRANSPORT_RTP)
ff_rtp_check_and_send_back_rr(rtsp_st->transport_priv, rtsp_st->rtp_handle, NULL, len);
break;
接收完RTP数据后的解析过程则和上面描述的解析流程基本一样。
发送GET_PARAMETER作为心跳包上报
rtsp_read_packet:
if (!(rt->rtsp_flags & RTSP_FLAG_LISTEN)) {
/* send dummy request to keep TCP connection alive */
if ((av_gettime_relative() - rt->last_cmd_time) / 1000000 >= rt->timeout / 2 ||
rt->auth_state.stale) {
if (rt->server_type == RTSP_SERVER_WMS ||
(rt->server_type != RTSP_SERVER_REAL &&
rt->get_parameter_supported)) {
ff_rtsp_send_cmd_async(s, "GET_PARAMETER", rt->control_uri, NULL);
}
至此,播放部分RTSP流媒体拉流的分析就结束
主要流程有3个:收数据->RTP解析->发送心跳
上层会一直调用read函数收数据,并将其写入到解码器中,实现播放。
本文在上一章节的基础上,以FFmpeg中的RTSP代码为基础进行分析,主要分析协议交互部分和数据下载部分的相关代码流程。特别是协议交互部分,分析该部分代码,可以让我们更加清楚地了解RTSP协议。
FFmpeg给了我们一个非常好的框架蓝本,但是实际情况中前端情况各有不同,需要我们做一些适配工作。特别是对于UDP的情况,在后面的文章中,会涉及实际项目对于该部分代码的修改。