Android MediaPlayer中的RTSP(二):FFmpeg中RTSP代码流程分析

背景:

RTSP在不同FFmpeg版本中可能略有不同,本章主要介绍FFmpeg RTSP主要的相关代码流程,而涉及FFmpeg的一些结构并不会详细说明,FFmpeg详细的分析,有兴趣可以可以参考雷霄骅大神的博客https://my.csdn.net/leixiaohua1020。

1、关键结构ff_rtsp_demuxer :

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);

2、打开文件rtsp_read_header

当上层调用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 。

2.1 connect阶段流程

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。

2.2 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.0

Range: 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函数。

3、播放 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;
                }
            }
        }

3.1 核心函数ff_rtsp_fetch_packet

接下来,分析下载部分的核心函数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封装这种比较常见。函数参数如下:

/**

  • Parse an RTP or RTCP packet directly sent as a buffer.
  • @param s RTP parse context.
  • @param pkt returned packet
  • @param bufptr pointer to the input buffer or NULL to read the next packets
  • @param len buffer len
  • @return 0 if a packet is returned, 1 if a packet is returned and more can follow
  • (use buf as NULL to read the next). -1 if no packet (error or no more packet).
    */
    int ff_rtp_parse_packet(RTPDemuxContext *s, AVPacket *pkt,
    uint8_t **bufptr, int len)
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数据后的解析过程则和上面描述的解析流程基本一样。

3.2、心跳上报

发送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的情况,在后面的文章中,会涉及实际项目对于该部分代码的修改。

你可能感兴趣的:(FFmpeg,流媒体,Android)