实现音视频同步!RTCP协议解析及代码实现

RTCP是实时控制协议(Real-Time Control Protocol)的缩写。RTCP由RFC 3550定义(取代作废的RFC 1889)。

实时传输协议(RTP)和实时控制协议(RTCP)结合使用,可以监视大型多播网络的数据传递。RTP承载媒体流,而RTCP用于监视传输统计信息和服务质量。监视使接收器能够检测是否有任何丢包并补偿任何延迟抖动。

两种协议都独立于基础传输层协议和网络层协议工作。RTP标头中的信息告诉接收器如何重建数据,并描述编解码器比特流的打包方式。

下面我们重点讲解RTCP功能、RTCP信息包等。最后RTCP协议解析实现。

RTCP有哪些功能

1、RTCP主要功能是提供有关质量的反馈数据分发。这是RTP角色不可或缺的一部分,传输协议,与流量和拥塞有关其他传输协议的控制功能。

2、RTCP带有RTP的持久性传输级别标识符源称为规范名称或CNAME。自从如果发现冲突或程序重新启动,接收方要求CNAME跟踪每个参与者。

接收者也可能要求CNAME将来自给定参与者的多个数据流关联到集合中相关RTP会话的数量,例如同步音频和视频。媒体间同步还需要NTP和RTP数据发送方在RTCP数据包中包含的时间戳。

3、前两个功能要求所有参与者发送RTCP数据包,因此必须控制速率以使RTP能够扩大到大量参与者。通过让每个参与者将其控制包发送给所有其他人,每个人都可以独立观察参与者的数量。

4、这项功能对于参加者可以任意进入和离开的松散会话进程十分有用,参加者可以*进入或离开,没有成员控制或参数协调。

功能1-3应该在所有环境中使用,尤其是在IP多播环境。RTP应用程序设计师应避免只能在单播模式下工作且无法扩展到的机制更大的数字。RTCP的传输可以单独控制对于发送者和接收者,适用于例如单向链接,而接收者没有反馈可能的。

RTCP协议的端口

RTSP通常使用RTP协议来传送实时流,RTP一般使用偶数端口,而RTCP使用相邻的奇数端口,即RTP端口号+1。

RTP端口
实现音视频同步!RTCP协议解析及代码实现

RTCP端口

实现音视频同步!RTCP协议解析及代码实现

RTCP信息包有哪些

在RTCP通信控制中,RTCP协议的功能是通过不同类型的RTCP包来实现的。RTCP也是基于UDP包来传送的,主要有五种类型的封包:

  • SR:发送端报告,由发送RTP数据报的应用程序或中端发出的。
  • RR:接收端报告,由接受但不发送RTP数据报的应用程序或中端发出。
  • SDES: 源描述,传递与会话成员有关的标识信息的载体,如用户名、邮件、电话等。
  • BYE: 通知离开,通知回话中的其他成员将退出会话。
  • APP: 由应用程序自己定义,作为RTCP协议的扩展。
#define RTCP_SR      200
#define RTCP_RR      201
#define RTCP_SDES    202
#define RTCP_BYE     203
#define RTCP_APP     204

我们可以根据这五种类型包判断RTCP头部

static bool dissect_rtcp_heur(u_char *rtcp_info,int PayloadLen)
{
    unsigned int offset = 0;
    unsigned int first_byte = 0;
    unsigned int packet_type = 0;


    /* 查看第一个字节 */
    first_byte = rtcp_info[offset];

    /* 版本位是否设置为2*/
	//printf("version: %d\n",((first_byte & 0xC0) >> 6));
    if (((first_byte & 0xC0) >> 6) != 2)
    {
        return false;
    }

    /* 看包类型 */
	offset += 1;
    packet_type = rtcp_info[offset];
	//printf("packet_type: %d\n",packet_type);
    /* 复合数据包中的第一个数据包应该是发送方或接收者报告 */
    if (!((packet_type == RTCP_SR)  || (packet_type == RTCP_RR) ||
          (packet_type == RTCP_BYE) || (packet_type == RTCP_APP) ||
          (packet_type == RTCP_PSFB)))
    {
        return false;
    }

    /*总长度必须是4个字节的倍数*/
	//printf("PayloadLen: %d\n",PayloadLen);
    if (PayloadLen % 4)
    {
        return false;
    }

    /* OK, dissect as RTCP */
    dissect_rtcp(rtcp_info,packet_type,offset,PayloadLen);
    return true;
}

RTCP协议报文格式

SR:发送端报告
实现音视频同步!RTCP协议解析及代码实现

版本(V):同RTP包头部

填充 ( P ) :同RTP包头部。

接收报告计数器(RC):5b 该SR包中接收的报告块的数目。

包类型(PT): 8bit SR包类型为200

长度(length):SR包以32bit为1单位的长度减1

同步源(SSRC):SR包发送的同步源标识符。与对应RTP包中的SSRC一样。

NTP时间戳(Network Time Protocol):SR包发送时的绝对时间。用于同步不同的流。

RTP时间戳:与NTP时间戳对应,与RTP包中的时间戳具有相同的初始值。

Send’s Packet count:从开始发包到产生这个SR包的这段时间内发送者发送的有效数据的总字节数,不包括头部和填充,发送者改变SSRC时,该域要清零。

同步源n的SSRC标识符:该报告中包含的是从该源接收到的包的统计信息。

丢失率:表明从上一个SR或RR包发出依来从同步源n发送的RTP包的丢失率。

累计丢失数据:从开始接受SSRC_n的包到发送SR这个时间段内SSRC_n发送的RTP丢失的总数目。

收到的扩展最大序列号:从SSRC_n收到的从RTP数据包中的最大序列号。

接收抖动(Interarrival jitter):RTP数据包接收时间的统计方差估计。

上次SR时间戳(Last SR):取最近从SSRC_n收到的SR包中的NTP时间戳中的中间32bit。如果还未收到SR包,则为0。

上次依赖SR延迟(Delay since Last SR):从上次SSRC_n收到SR包到发送本包的延迟

Wireshark抓包:
实现音视频同步!RTCP协议解析及代码实现

活动会话的参与者在发送和接收RTP分组时使用SR。SR有三个不同的部分:报头信息、发送方信息和许多接收方报告块。SR也可以有一个与大纲相关的扩展域。

RR:接收端报告

RR是由一个会话参与者发送的,它接收到了RTP分组,但还没有发送任何它自己的RTP分组给其他的会话参与者。RR主要用于接收非活动站的统计信息。
实现音视频同步!RTCP协议解析及代码实现

Wireshark抓包:
实现音视频同步!RTCP协议解析及代码实现

SDES: 源描述

SDES提供了传递与会话成员有关的标识信息的载体,如用户名、邮件、电话等。每个RTCP混合分组中必须有SDES分组。
实现音视频同步!RTCP协议解析及代码实现

报头包含一个长度域、一个净荷类型域(PT=202)和一个源计数(RC)域。RC域5个bit,表示分组中信息块的数量。

每个信息块包含一个SSRC或CSRC值,其后跟着一个或多个的标识符和一些可用于SSRC或CSRC的信息。

CNAME 项的SDES包必须包含在每个组合RTCP包中。SDES包可能包括其他源描述项,这要根据特别的应用需要,并同时考虑带宽限制。

Wireshark抓包:
实现音视频同步!RTCP协议解析及代码实现

SDES源描述包提供了直观的文本信息来描述会话的参加者,包括CNAME、NAME、EMAIL、PHONE、LOC等源描述项。

这些为接收方获取发送方的有关信息提供了方便。SDES 包由包头与数据块组成,数据块可以没有,也可有多个。包头由版本(V)、填充(P)、长度指示、包类型(PT)和源计数(SC)组成。

PT占8位,用于识别RTCP的SDES包,SC占5位,指示包含在SDES包中的SSRC/CSRC块数量,零值有效,但没有意义。

BYE: 通知离开

BYE分组用于表示一个或多个媒体源不再是处于激活状态。

实现音视频同步!RTCP协议解析及代码实现

Wireshark抓包:
实现音视频同步!RTCP协议解析及代码实现作为可选项,BYE包可包括一个8位八进制计数,后跟文本信息,表示离开原因。

最后,组合包中每个RTCP包可独立处理,而不需要按照包组合的先后顺序处理。

在组合包中有以下几条强制约束

  • 只要带宽允许,在SR包或RR包中的接收统计应该经常发送,因此每个周期发送的组合RTCP 包中应包含报告包。
  • 每个组合包中都应该包含SDES CNAME,因为新接收者需要通过接收CNAME来识别源,并与媒体联系进行同步。
  • 组合包前面是包类型数量,其增长应该受到限制。

RTCP协议如何实现媒体流的同步

通过抓包分析RTCP发送端报告,RTP的同步其实就靠这三个域:

sender SSRC :SR包发送的同步源标识符。与对应RTP包中的SSRC一样
NTP timestamp:SR包发送时的绝对时间。用于同步不同的流。
RTP timestamp:与NTP时间戳对应,与RTP包中的时间戳具有相同的初始值。

实现音视频同步!RTCP协议解析及代码实现那怎么计算NTP时间呢?在RTCP中NTP时间存放在8个字节中,分为:MSW和LSW,分别占用4个字节。

const char *tvb_ntp_fmt_ts_sec(u_char *rtcp_info, int offset)
{
	uint32_t tempstmp = 0;
	time_t temptime = 0;
	struct tm *bd;
	char *buff = NULL;
	
	tempstmp = ntohl(*(uint32_t*)(rtcp_info + offset));
	if (tempstmp == 0){
		return "NULL";
	}

	/* We need a temporary variable here so the unsigned math
	* works correctly (for years > 2036 according to RFC 2030
	* chapter 3).
	*/
	temptime = (time_t)(tempstmp - NTP_BASETIME);
	bd = gmtime(&temptime);
	if (!bd){
		return "Not representable";
	}

	buff = (char *)malloc(NTP_TS_SIZE);
	snprintf(buff, NTP_TS_SIZE,
		"%s %2d, %d %02d:%02d:%02d UTC",
		mon_names[bd->tm_mon],
		bd->tm_mday,
		bd->tm_year + 1900,
		bd->tm_hour,
		bd->tm_min,
		bd->tm_sec);
	return buff;
}

    /* NTP timestamp */
    ts_msw = ntohl(*(uint32_t*)(rtcp_info + offset));
	printf("ts_msw: 0x%x\n",ts_msw);
    ts_lsw = ntohl(*(uint32_t*)(rtcp_info + offset + 4));
	printf("ts_lsw: 0x%x\n",ts_lsw);
	printf("MSW: %s\n",tvb_ntp_fmt_ts_sec(rtcp_info,offset));
    offset += 8;

RTCP协议实现

#include <sys/stat.h>
#include <sys/types.h>
#include <netinet/tcp.h>
#include <netinet/udp.h>
#include <netinet/ip.h>
#include <netinet/ip6.h>
#include <net/ethernet.h>
#include <pcap.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <math.h>


/* 接收者/发送者计数是最后5位   */
#define RTCP_COUNT(octet)   ((octet) & 0x1F)


#define RTCP_PT_MIN  192
/* Supplemental H.261 specific RTCP packet types according to Section C.3.5 */
#define RTCP_FIR     192
#define RTCP_NACK    193
#define RTCP_SMPTETC 194
#define RTCP_IJ      195
/* RTCP packet types according to Section A.11.1 */
/* And https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml */
#define RTCP_SR      200
#define RTCP_RR      201
#define RTCP_SDES    202
#define RTCP_BYE     203
#define RTCP_APP     204
#define RTCP_RTPFB   205
#define RTCP_PSFB    206
#define RTCP_XR      207
#define RTCP_AVB     208
#define RTCP_RSI     209
#define RTCP_TOKEN   210

#define RTCP_PT_MAX  210


static const char mon_names[12][4] = {
	"Jan",
	"Feb",
	"Mar",
	"Apr",
	"May",
	"Jun",
	"Jul",
	"Aug",
	"Sep",
	"Oct",
	"Nov",
	"Dec"
};

/** data structure to hold time values with nanosecond resolution*/
typedef struct {
	time_t	secs;
	int	nsecs;
} nstime_t;


/*
 * 1900-01-01 00:00:00 (proleptic?) UTC.
 * Used by a number of time formats.
 */
#define EPOCH_DELTA_1900_01_01_00_00_00_UTC 2208988800U

/*
 * NTP_BASETIME is in fact epoch - ntp_start_time; ntp_start_time
 * is January 1, 2036, 00:00:00 UTC.
 */
#define NTP_BASETIME EPOCH_DELTA_1900_01_01_00_00_00_UTC
#define NTP_FLOAT_DENOM 4294967296.0
#define NTP_TS_SIZE 100


/* 解剖长度字段。附加到此字段的文字表示转换为的实际字节数 (即 (原始值 + 1) * 4) */
static int dissect_rtcp_length_field(u_char *rtcp_info, int offset)
{

    uint16_t  raw_length = ntohs(*(uint16_t*)(rtcp_info + offset));
    printf("(%u bytes)\n", (raw_length+1)*4);
    offset += 2;
    return offset;
}
static int dissect_rtcp_rr(u_char *rtcp_info, int offset,int count, int packet_length )
{
    int counter = 0;
    uint8_t  rr_flt = 0;
    int    rr_offset = offset;
	
    counter = 1;
    while ( counter <= count ) {
        uint32_t lsr = 0, dlsr = 0;

        /* Create a new subtree for a length of 24 bytes */

        /* SSRC_n source identifier, 32 bits */

        offset += 4;

        /* Fraction lost, 8bits */
        rr_flt = rtcp_info[offset];

        offset++;

        /* Cumulative number of packets lost, 24 bits */
        offset += 3;


        /* Sequence number cycles */

        offset += 2;
        /* highest sequence number received */

        offset += 2;

        /* Interarrival jitter */

        offset += 4;

        /* Last SR timestamp */
        lsr = ntohl(*(uint32_t*)(rtcp_info + offset));
		printf("Last SR timestamp: 0x%x\n",lsr);
        offset += 4;

        /* Delay since last SR timestamp */
        dlsr = ntohl(*(uint32_t*)(rtcp_info + offset));

        printf("(%d milliseconds)\n",(int)(((double)dlsr/(double)65536) * 1000.0));
        offset += 4;

        counter++;
    }

    return offset;
}

const char *tvb_ntp_fmt_ts_sec(u_char *rtcp_info, int offset)
{
	uint32_t tempstmp = 0;
	time_t temptime = 0;
	struct tm *bd;
	char *buff = NULL;
	
	tempstmp = ntohl(*(uint32_t*)(rtcp_info + offset));
	if (tempstmp == 0){
		return "NULL";
	}

	/* We need a temporary variable here so the unsigned math
	* works correctly (for years > 2036 according to RFC 2030
	* chapter 3).
	*/
	temptime = (time_t)(tempstmp - NTP_BASETIME);
	bd = gmtime(&temptime);
	if (!bd){
		return "Not representable";
	}

	buff = (char *)malloc(NTP_TS_SIZE);
	snprintf(buff, NTP_TS_SIZE,
		"%s %2d, %d %02d:%02d:%02d UTC",
		mon_names[bd->tm_mon],
		bd->tm_mday,
		bd->tm_year + 1900,
		bd->tm_hour,
		bd->tm_min,
		bd->tm_sec);
	return buff;
}

static int dissect_rtcp_sr(u_char *rtcp_info, int offset,int count, int packet_length)
{

    uint32_t     ts_msw = 0, ts_lsw = 0;
    int         sr_offset = offset;

    /* NTP timestamp */
    ts_msw = ntohl(*(uint32_t*)(rtcp_info + offset));
	printf("ts_msw: 0x%x\n",ts_msw);
    ts_lsw = ntohl(*(uint32_t*)(rtcp_info + offset + 4));
	printf("ts_lsw: 0x%x\n",ts_lsw);

	//printf("offset: 0x%x 0x%x 0x%x 0x%x\n",rtcp_info[offset],rtcp_info[offset + 1],rtcp_info[offset + 2],rtcp_info[offset + 3]);
	printf("MSW: %s\n",tvb_ntp_fmt_ts_sec(rtcp_info,offset));
    offset += 8;

    /* RTP timestamp, 32 bits */
	
    offset += 4;
    /* Sender's packet count, 32 bits */

    offset += 4;
    /* Sender's octet count, 32 bits */

    offset += 4;


    /* The rest of the packet is equal to the RR packet */
    if ( count != 0 )
        offset = dissect_rtcp_rr(rtcp_info, offset, count, packet_length-(offset-sr_offset));
    else
    {
        /* If length remaining, assume profile-specific extension bytes */
        if ((offset-sr_offset) < packet_length)
        {

            offset = sr_offset + packet_length;
        }
    }

    return offset;
}

static int dissect_rtcp_sdes(u_char *rtcp_info, int offset, int count)
{
    int           chunk = 0;

    int           start_offset = 0;
    int           items_start_offset = 0;
    uint32_t       ssrc = 0;
    unsigned int  item_len = 0;
    unsigned int  sdes_type = 0;
    unsigned int  prefix_len = 0;

    chunk = 1;
    while ( chunk <= count ) 
	{
		/* Create a subtree for this chunk; we don't yet know
		   the length. */
		start_offset = offset;

		ssrc = ntohl(*(uint32_t*)(rtcp_info + offset));
		printf("Chunk %u, SSRC/CSRC 0x%X\n", chunk, ssrc);

		/* SSRC_n source identifier, 32 bits */
		offset += 4;

		/* Create a subtree for the SDES items; we don't yet know
		   the length */


		/*
		 * Not every message is ended with "null" bytes, so check for
		 * end of frame as well.
		 */
		 
		  /* ID, 8 bits */
		sdes_type = rtcp_info[offset];
		printf("Type: %d\n",sdes_type);
		if (sdes_type == 0)
			break;
		offset++;
		/* Item length, 8 bits */
		item_len = rtcp_info[offset];
		printf("Length: %d\n",item_len);
		offset++;
		
		char *pszText = (char*)malloc(item_len);
		if (pszText != 0)
		{
			memcpy(pszText, rtcp_info + offset,item_len);
			pszText[item_len] = '\0';
			printf("Text = %s\n",pszText);
		}	

		chunk++;
    }

    return offset;
}

static void dissect_rtcp(u_char *rtcp_info,int packet_type, int offset,int PayloadLen)
{
	unsigned int  temp_byte = 0;
	int  elem_count = 0;
	int  packet_length = 0;
	int  total_packet_length = 0;
	int loop = 2;
	bool flag_rtcp = false;

		/*检查是否为有效类型*/
		if ( ( packet_type < RTCP_PT_MIN ) || ( packet_type >  RTCP_PT_MAX ) )
			exit(-1);

		/*
		 * 获取完整的RTCP数据包的长度
		 */
		 
		packet_length = (ntohs(*(uint16_t*)(rtcp_info + offset + 1)) + 1) * 4 ;
		//printf("packet_length: %d\n",packet_length);

		
		temp_byte = rtcp_info[offset-1];
		elem_count = RTCP_COUNT( temp_byte );/* Source count, 5 bits */
		printf("Reception report count: %d\n",elem_count);  

		switch ( packet_type ) 
		{
			
			case RTCP_SR:
			case RTCP_RR:
				/*
					Real-time Transport Control Protocol (Receiver Report)
					10.. .... = Version: RFC 1889 Version (2)
					..0. .... = Padding: False
					...0 0001 = Reception report count: 1
					Packet type: Receiver Report (201)
					Length: 7 (32 bytes)
					Sender SSRC: 0xb584b03e (3045371966)
					Source 1
				*/

				/* Packet type, 8 bits */
				offset++;
				/* Packet length in 32 bit words MINUS one, 16 bits */
				offset = dissect_rtcp_length_field(rtcp_info, offset);
				/* Sender Synchronization source, 32 bits */
				offset += 4;

				if ( packet_type == RTCP_SR )
				{
					offset = dissect_rtcp_sr(rtcp_info, offset, elem_count, packet_length-8 );
					printf("dissect_rtcp_sr\n");
				}
				else
				{	
					offset = dissect_rtcp_rr(rtcp_info, offset, elem_count, packet_length-8 );							
				}
				
				//uint16_t second_packet_type = ntohs(*(uint16_t*)(rtcp_info + offset));

				//printf("111offset: 0x%x 0x%x 0x%x 0x%x\n",rtcp_info[offset],rtcp_info[offset + 1],rtcp_info[offset + 2],rtcp_info[offset + 3]);
				
				if (rtcp_info[offset + 1] == RTCP_SDES)
				{
					
					/* Source count, 5 bits */
					offset++;
					/* Packet type, 8 bits */
					offset++;
					/* Packet length in 32 bit words MINUS one, 16 bits */
					offset = dissect_rtcp_length_field(rtcp_info, offset);
					offset = dissect_rtcp_sdes(rtcp_info,offset,elem_count);

				}

			break;

			default:
				/*
				 * To prevent endless loops in case of an unknown message type
				 * increase offset. Some time the while will end :-)
				 */
				offset++;
				break;

		}
		
}


static bool dissect_rtcp_heur(u_char *rtcp_info,int PayloadLen)
{
    unsigned int offset = 0;
    unsigned int first_byte = 0;
    unsigned int packet_type = 0;


    /* 查看第一个字节 */
    first_byte = rtcp_info[offset];

    /* 版本位是否设置为2*/
	//printf("version: %d\n",((first_byte & 0xC0) >> 6));
    if (((first_byte & 0xC0) >> 6) != 2)
    {
        return false;
    }

    /* 看包类型 */
	offset += 1;
    packet_type = rtcp_info[offset];
	//printf("packet_type: %d\n",packet_type);
    /* 复合数据包中的第一个数据包应该是发送方或接收者报告 */
    if (!((packet_type == RTCP_SR)  || (packet_type == RTCP_RR) ||
          (packet_type == RTCP_BYE) || (packet_type == RTCP_APP) ||
          (packet_type == RTCP_PSFB)))
    {
        return false;
    }

    /*总长度必须是4个字节的倍数*/
	//printf("PayloadLen: %d\n",PayloadLen);
    if (PayloadLen % 4)
    {
        return false;
    }

    /* OK, dissect as RTCP */
    dissect_rtcp(rtcp_info,packet_type,offset,PayloadLen);
    return true;
}


static void confirm_rtcp_packet(struct ip *pIp)
{
	int iIpTotalLen = ntohs(pIp->ip_len);
	int offset = 0;
	int nFragSeq = 0;
	struct udphdr* pUdpHdr = (struct udphdr*)((char*)pIp + (pIp->ip_hl<<2));
	if (pIp->ip_p == IPPROTO_UDP) 
	{
		printf("\n");
		
		int iPayloadLen = iIpTotalLen - (pIp->ip_hl<<2) - 8;
		printf("UDP Payload Len %d\n", iPayloadLen);
		
		u_char *pDnsHdr = (u_char*)(pUdpHdr+1);
		dissect_rtcp_heur(pDnsHdr,iPayloadLen);
		
	}	
}

编译运行:

实现音视频同步!RTCP协议解析及代码实现

总结

RTCP协议是流媒体通信的基石。RTCP协议则负责可靠传输、流量控制和拥塞控制等服务质量保证。上面讲解了RTCP功能、RTCP数据包格式及代码实现。最后,学习一个新的协议,最好还是研究学习官方文档,因为这是最权威的资料。

欢迎关注微信公众号【程序猿编码】,欢迎添加本人微信号(17865354792),欢迎进入技术交流群。我们一起学习进步!

参考:https://tools.ietf.org/html/rfc3550

上一篇:UDP聊天实现


下一篇:Cisio Packet Tracer 用三层交换机作为路由器实现静态路由、RIP、OSPF和单臂路由