背景:
前面两章节简单介绍了RTSP协议,FFmpeg中RTSP的简单交互流程。之前提到过,实际应用中,对于不同项目,对FFmpeg需要进行完善,以适应各种特殊情况。
之前列的情况如下:
1、SETUP阶段的协议选择:载流协议是UDP还是TCP,是否使用RTP承载,尝试一种载流协议不支持后的切换流程
2、重定向的完善:正常来说,一般在SETUP阶段后,就不会再有重定向了,因为这样需要重新断开再连接,但是实际使用中有些服务器是会在PLAY阶段去重定向的
3、NAT穿透: 使用UDP载流时,处于内网的客户端该如何进行NAT的打洞,以让服务器能将数据传送给客户端
4、媒体服务器的选择:RTP-Info给的服务器和DESCRIBE/SETUP给的服务器不一样时,如何切换为RTP-Info指定的服务器。
5、与MediaPlayer的整合:PLAY中使用绝对时间描述时,该如何与MediaPlayer整合
接下来进行相关修改的说明。
相关修改
1、SETUP阶段的协议选择
这个比较简单,上一章节说过,SETUP是在connect阶段进行的,解析播放地址url携带的参数,根据携带的”udp”/”tcp”/”http”,设置lower_transport_mask标志位,根据标志位会一个个进行尝试。
而对于已经确定的项目或者服务器,可以根据属性配置或者是服务器类型(服务器类型可以在ff_rtsp_parse_line函数中对"Server:"携带的参数进行判断),在ff_rtsp_make_setup_request指定用什么载流,这样可以避免多余的SETUP重试。
2、重定向的完善
原生FFmpeg并不支持在PLAY命令发送的时候进行重定向操作,一般的服务器也不会有这样的操作,因为如有重定向的需要,connect阶段(DESCRIBE->SETUP)可以进行重定向操作,而如果在PLAY阶段再进行重定向,则需要断开连接再重新连接,会加长播放的起播速度。
当然,实际情况下中兴的服务器是会PLAY进行重定向操作的,按照上面说的流程,需要将其进行断开再重新连接。
对于这种情况的修改,最初的修改是在rtsp_read_header函数内部进行修改,给302这类重定向返回特定错误码,断开连接后重新跳到ff_rtsp_connect函数开头,用新的URL重新连接。
修改如下:
+++ rtspdec.c (working copy)
@@ -237,7 +237,16 @@
}
ff_rtsp_send_cmd(s, "PLAY", rt->control_uri, cmd, reply, NULL);
if (reply->status_code != RTSP_STATUS_OK) {
- return -1;
+ if(reply->status_code >=300 && reply->status_code < 400 && s->iformat){
+ av_strlcpy(s->filename, reply->location, sizeof(s->filename));
+ if(rt->playseekFlag == 1){
+ av_strlcatf(s->filename, sizeof(s->filename),"&playseek=%sZ", rt->playseekTime);
+ }
+ av_log(s, AV_LOG_ERROR, "Redirecting to %s\n", s->filename);
+ return -(reply->status_code);
+ }
+ else
+ return -1;
}
if (rt->transport == RTSP_TRANSPORT_RTP &&
reply->range_start != AV_NOPTS_VALUE) {
@@ -365,6 +374,8 @@
{
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);
@@ -407,9 +418,12 @@
if (rt->initial_pause) {
/* do not start immediately */
} else {
- if (rtsp_read_play(s) < 0) {
+ 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;
}
}
原理是这样,但这样简单修改会导致很多地方崩溃,例如对于已经关闭的连接,stream等,在重新连接的时候会有重新free等操作造成崩溃,有些是已经alloc的成员多次alloc造成崩溃,有些涉及到了底层RTP/TCP层的修改,为了避免对其他模块造成影响,最后没有采取这种方式,而是在更上层进行修改。
上一章节说过rtsp_read_header的入口是在avformat_open_input中,当错误代码是RTSP 重定向的错误代码,则再次调用avformat_open_input,因为AVFormatContext在RTSP demux中就已经free,所以无法将重定向的URL带回去上层,只能用比较丑陋的方式(全局变量)将URL给到avformat_open_input。这样修改能完整地重新走一遍FFmpeg打开文件的流程,好处是修改的地方较少,比较可靠,较为简单,不好的地方则是代码比较混乱。
修改如下:
avformat_open_input:
if (!(s->flags&AVFMT_FLAG_PRIV_OPT) && s->iformat->read_header)
if ((ret = s->iformat->read_header(s)) < 0)
goto fail;
Index: rtsp.c
===================================================================
--- rtsp.c (revision 151774)
+++ rtsp.c (revision 151775)
@@ -538,8 +538,10 @@
avformat_free_context(rtpctx);
} else if (rt->transport == RTSP_TRANSPORT_RDT && CONFIG_RTPDEC)
ff_rdt_parse_close(rtsp_st->transport_priv);
- else if (CONFIG_RTPDEC)
+ else if (CONFIG_RTPDEC){
+ if(!rtsp_st->transport_priv)
rtp_parse_close(rtsp_st->transport_priv);
+ }
}
rtsp_st->transport_priv = NULL;
if (rtsp_st->rtp_handle)
Index: utils.c
===================================================================
--- utils.c (revision 151774)
+++ utils.c (revision 151775)
@@ -1018,6 +1018,7 @@
return av_probe_input_buffer(s->pb, &s->iformat, filename, s, 0, 0);
}
+extern char rtsp_play_redirect_url[1024] = {0};
int avformat_open_input_header(AVFormatContext **ps, const char *filename, AVInputFormat *fmt, AVDictionary **options, const char *headers)
{
@@ -1035,6 +1036,10 @@
if ((ret = av_opt_set_dict(s, &tmp)) < 0)
goto fail;
+ if(rtsp_play_redirect_url[0] ){
+ filename = av_strdup(rtsp_play_redirect_url);
+ av_log(NULL,NULL,"ugly code for RTSP PLAY 302 -%s---\n",filename);
+ memset(rtsp_play_redirect_url,0,sizeof(rtsp_play_redirect_url));}
av_log(NULL,NULL,"-%s---\n",__FUNCTION__);
if ((ret = init_input(s, filename, headers, &tmp)) < 0)
goto fail;
Index: avio.c
===================================================================
--- avio.c (revision 151774)
+++ avio.c (revision 151775)
@@ -530,8 +530,12 @@
int ret = 0;
if (!h) return 0; /* can happen when ffurl_open fails */
+ if(!h->prot)
+ return 0;
if (h->is_connected && h->prot->url_close)
+ {
ret = h->prot->url_close(h);
+ }
#if CONFIG_NETWORK
ff_network_close();
#endif
Index: rtspdec.c
===================================================================
--- rtspdec.c (revision 151774)
+++ rtspdec.c (revision 151775)
@@ -45,6 +45,7 @@
static int _rtsp_read_packet(AVFormatContext *s, AVPacket *pkt) ;
static int rtsp_read_flush(AVFormatContext *s);
void rtsp_invoke(void *para);
+char rtsp_play_redirect_url[1024] ;
static void *recv_buffer_task( void *_AVFormatContext)
{
@@ -386,7 +387,6 @@
RTSPState *rt = s->priv_data;
int ret;
av_log(NULL, AV_LOG_INFO, "[%s:%d]\n", __FUNCTION__, __LINE__);
rt->send_keepalive=0;
ret = ff_rtsp_connect(s);
@@ -431,26 +431,20 @@
} 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
+ av_log(NULL, AV_LOG_INFO, "play redirect to :%s",s->filename);
+ av_strlcpy(rtsp_play_redirect_url, s->filename, 1024);
+ rtsp_play_redirect_url[1023] = '\0';
+ return ret;
}
return AVERROR_INVALIDDATA;
}
}
av_log(s, AV_LOG_INFO, "[%s]transport=%d timeout=%d div=%d\n",__FUNCTION__,rt->transport,rt->timeout,rt->keeplive_div);
return 0;
}
3、NAT穿透
使用UDP载流时,根据组网的情况,处于内网的客户端可能需要进行NAT的打洞,以让服务器能将数据传送给客户端 。
FFmpeg中本身有这样的处理机制,不同版本函数名称不同,ff_rtp_send_punch_packets/rtp_send_punch_packets。原生代码中,打洞是在SETUP之后,PLAY之前,但对于中兴服务器,需要将其放在PLAY之后,而且打洞机制需要和心跳一样,间隔一段时间就发送一次。怎么发送,和第4点修改一起说明。
4、媒体服务器的选择
按照RTSP协议,媒体服务器在DESCRIBE/SETUP中都会给我们服务器地址,
RTP-Info是服务器回复我们的媒体服务器信息,但是对于实际情况RTP-Info给的服务器和DESCRIBE/SETUP给的服务器并不一样,而且按照标准RTSP流程接收DESCRIBE/SETUP指定地址并无流是,我们就得考虑使用RTP-Info给的服务器地址了,同样的,如果是UDP载流,需要发送NAT包。
RTP-Info字段的解析及媒体服务器地址切换修改如下:
Index: rtsp.c
===================================================================
--- rtsp.c (revision 151103)
+++ rtsp.c (revision 151104)
@@ -781,8 +781,17 @@
p++;
get_word_sep(value, sizeof(value), ";, ", &p);
read++;
- if (!strcmp(key, "url"))
+ if (!strcmp(key, "url")){
av_strlcpy(url, value, sizeof(url));
+
+ //wusc add for zte server rtpinfo
+ if(NULL != strstr(rt->server, "ZXUS") || NULL != strstr(rt->server, "ZMSS")){
+ memset(rt->rtpinfo_url,0,sizeof(rt->rtpinfo_url));
+ av_strlcpy(rt->rtpinfo_url, value, sizeof(rt->rtpinfo_url));
+ av_log(NULL, AV_LOG_ERROR, "ZXUS/ZMSS server should ignore transports source\n");
+ }
+ }
+
else if (!strcmp(key, "seq"))
seq = strtoul(value, NULL, 10);
else if (!strcmp(key, "rtptime"))
@@ -1173,7 +1182,7 @@
/* 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);
+ av_log(NULL, AV_LOG_INFO, "[%s]ffurl_open,handle=%d\n", __FUNCTION__, rtsp_st->rtp_handle);
goto rtp_opened;
}
}
@@ -1307,6 +1316,7 @@
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;
@@ -1318,8 +1328,12 @@
*/
if (!(rt->server_type == RTSP_SERVER_WMS && i > 1) && s->iformat &&
CONFIG_RTPDEC){
- rtp_send_zte_punch_packets(rtsp_st->rtp_handle,rt->rtsp_hd);//ZTE NAT
- rtp_send_punch_packets(rtsp_st->rtp_handle);
+ 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;
}
Index: rtsp.h
===================================================================
--- rtsp.h (revision 151103)
+++ rtsp.h (revision 151104)
@@ -373,6 +373,7 @@
//add by wusc for UT Apk eg:playseek=20180423T115645Z
int playseekFlag;
char playseekTime[128];
+ char rtpinfo_url[1024];//for zte server
} RTSPState;
/**
Index: rtspdec.c
===================================================================
--- rtspdec.c (revision 151103)
+++ rtspdec.c (revision 151104)
@@ -553,6 +568,27 @@
/* send dummy request to keep TCP connection alive */
if ((av_gettime() - rt->last_cmd_time) / 1000000 >= (rt->timeout / keeplive_div) || rt->send_keepalive == 0) {
+ //add by wusc for zte server rtpinfo
+ if(rt->lower_transport == RTSP_LOWER_TRANSPORT_UDP && rt->rtpinfo_url[0] != 0
+ && (NULL != strstr(rt->server, "ZXUS") || NULL != strstr(rt->server, "ZMSS"))){
+ char url[1024],host[20]="";
+ int port=0;
+ av_url_split(NULL, 0, NULL, 0, host, sizeof(host), &port,NULL, 0,rt->rtpinfo_url);
+ ff_url_join(url, sizeof(url), "rtp", NULL, host,port , "%s", "");
+ av_log(NULL, AV_LOG_ERROR, "rt->rtpinfo_url %s \nset rtp url[%s] \n",rt->rtpinfo_url,url);
+ for (int i = 0; i < rt->nb_rtsp_streams; i++){
+ RTSPStream *rtsp_st = rt->rtsp_streams[i];
+ RTPDemuxContext *rtpctx = rtsp_st->transport_priv;
+ if (!rtpctx || rtsp_st->stream_index < 0)
+ continue;
+
+ int ret = rtp_set_remote_url(rtsp_st->rtp_handle, url) ;
+ av_log(NULL, AV_LOG_ERROR, "ZTE NAT rtp_set_remote_url ret = %d\n",ret);
+ rtp_send_zte_punch_packets(rtsp_st->rtp_handle,rt->rtsp_hd);//ZTE NAT
+ rtp_send_punch_packets(rtsp_st->rtp_handle);
+ }
+ }
+
if (rt->server_type == RTSP_SERVER_WMS ||
(rt->server_type != RTSP_SERVER_REAL &&
rt->get_parameter_supported)) {
逻辑比较简单,即在PLAY命令成功后解析出RTP-Info携带URL,在PLAY命令返回正确后,重新设置RTP handle的URL(接收媒体数据的URL)为新的URL,调用rtp_send_punch_packets网新地址发送NAT穿透包。
需要注意的是,对于音视频分开的情况,则是nb_rtsp_streams大于1,RTP-Info携带的URL则也大于1,需要分开重新设置,不过本身这种流程并不符合RTSP协议的标准流程,所以该处修改并没考虑这种情况。
5、实时流播放,使用绝对时间描述发送PLAY命令
Android MediaPlayer中,RTSP的播放一般是用于点播,基本用的都是相对时间,而对于一些直播流的时移,则是使用绝对时间来发送PLAY命令,APK会在URL携带playseek字段,客户端则解析该字段进行PLAY操作。
修改如下:
Index: rtsp.c
===================================================================
--- rtsp.c (revision 149739)
+++ rtsp.c (revision 149740)
@@ -1374,7 +1387,9 @@
socklen_t peer_len = sizeof(peer);
int support_options =1;
int failed_to_retry =0 ;
+ rt->playseekFlag = 0;
+ memset(rt->playseekTime,0x00,sizeof(rt->playseekTime));
+
if (!ff_network_init())
return AVERROR(EIO);
redirect:
@@ -1413,7 +1428,22 @@
rt->control_transport = RTSP_MODE_TUNNEL;
} else if (!strcmp(option, "filter_src")) {
rt->filter_source = 1;
- } else {
+ } else if (strstr(option, "playseek=")) {//add by wusc for UT Apk eg:playseek=20180423T115645Z
+ lower_transport_mask |= (1<< RTSP_LOWER_TRANSPORT_UDP);
+ rt->playseekFlag = 1;
+ av_log(NULL, AV_LOG_INFO, "[%s:%d]playseek option:%s", __FUNCTION__,__LINE__, option);
+ char *start = strstr(option, "playseek=") + 9;
+ int len = 0;
+ if(strchr(start, '&')){
+ len = strchr(start, '&') - start;
+ }else{
+ len = strlen(start);
+ }
+ av_strlcpy(rt->playseekTime, start, len);
+ rt->playseekTime[len] = '\0';
+ av_log(NULL, AV_LOG_INFO, "[%s:%d]len is %d,set playseek:%s", __FUNCTION__,__LINE__,len, rt->playseekTime);
+ }
+ else {
/* Write options back into the buffer, using memmove instead
* of strcpy since the strings may overlap. */
int len = strlen(option);
Index: rtsp.h
===================================================================
--- rtsp.h (revision 149739)
+++ rtsp.h (revision 149740)
@@ -368,6 +368,9 @@
int use_protocol_mode;
void *priv_data;
int send_keepalive;
+ //add by wusc for eg:playseek=20180423T115645Z
+ int playseekFlag;
+ char playseekTime[128];
} RTSPState;
/**
Index: rtspdec.c
===================================================================
--- rtspdec.c (revision 149739)
+++ rtspdec.c (revision 149740)
@@ -186,6 +186,7 @@
for (i = 0; i < rt->nb_rtsp_streams; i++) {
RTSPStream *rtsp_st = rt->rtsp_streams[i];
RTPDemuxContext *rtpctx = rtsp_st->transport_priv;
if (!rtpctx)
continue;
ff_rtp_reset_packet_queue(rtpctx);
@@ -198,7 +199,7 @@
if (rt->state == RTSP_STATE_PAUSED) {
cmd[0] = 0;
} else {
@@ -208,20 +209,24 @@
// 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->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);
- }
+ 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);
+ }
+ }
+
length = strlen (cmd);
snprintf(&cmd [length], sizeof(cmd) - length,
"Scale: %d.%d\r\n",
修改也比较简单,在ff_rtsp_connect函数中解析URL options时,如果携带playseek=参数,则将其保存,在PLAY阶段发送时以Range: clock=%xxxxxxxxx.00Z-的形式发送即可。
需要注意的是,如果在PLAY返回重定向时,重定向的地址可能不会携带playseek字段,如果重定向前的有playseek字段,则新的URL也需要携带上。
总结
以上几种是针对特殊前端的一些整合修改,其实实际上还有一些小的点需要注意,例如对于有些服务器设置载流需要发送"MP2T/RTP"前缀,有些服务器不能那么快发送NAT穿透包,有些服务器请求的URL必须携带某些参数等等。
实际情况需要在熟悉RTSP协议的基础上,针对各种情况将其一步步完善,本文提供了一些修改参考,三篇文章对RTSP协议的入门及FFmpeg中RTSP的实现都能有初步了解。