写点什么

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

用户头像
声网Agora
关注
发布于: 刚刚

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 也是基于 UDP 包来传送的,主要有五种类型的封包:


  1. SR:发送端报告,由发送 RTP 数据报的应用程序或中端发出的。

  2. RR:接收端报告,由接受但不发送 RTP 数据报的应用程序或中端发出。

  3. SDES:源描述,传递与会话成员有关的标识信息的载体,如用户名、邮件、电话等。

  4. BYE:通知离开,通知回话中的其他成员将退出会话。

  5. 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:发送端报告



版本(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 抓包:



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



Wireshark 抓包:



SDES:源描述


SDES 提供了传递与会话成员有关的标识信息的载体,如用户名、邮件、电话等。每个 RTCP 混合分组中必须有 SDES 分组。



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


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


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


Wireshark 抓包:



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


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


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


BYE: 通知离开


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



Wireshark 抓包:



作为可选项,BYE 包可包括一个 8 位八进制计数,后跟文本信息,表示离开原因。


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


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


  1. 只要带宽允许,在 SR 包或 RR 包中的接收统计应该经常发送,因此每个周期发送的组合 RTCP 包中应包含报告包。

  2. 每个组合包中都应该包含 SDES CNAME,因为新接收者需要通过接收 CNAME 来识别源,并与媒体联系进行同步。

  3. 组合包前面是包类型数量,其增长应该受到限制。


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


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


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

  2. NTP timestamp:SR 包发送时的绝对时间。用于同步不同的流。

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



那怎么计算 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


      /* 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 协议实现


下面我给出对 RTCP 协议解析实现的代码,根据回放报文,解析字段信息。


/* 接收者/发送者计数是最后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 数据包格式及代码实现。最后,学习一个新的协议,最好还是研究学习官方文档,因为这是最权威的资料。

用户头像

声网Agora

关注

还未添加个人签名 2021.02.05 加入

声网 Agora 是实时互动 API 平台行业开创者,实时互动技术服务覆盖全球 200 多个国家和地区。开发者只需简单调用 API,即可在应用内构建多种实时音视频互动场景。

评论

发布
暂无评论
音视频同步!RTCP 协议解析及代码实现