RTP协议
- 1. 概述
- 1.1 RTP协议
- 1.2 RTP和UDP的关系
- 2. RTP打包H264码流
- 2.1 RTP单一传输
- 2.2 RTP分片传输
- 2.3 RTP多片合入传输
- 3.工程
- 3.1 头文件
- 3.1.1 rtp.h
- 3.1.2 utils.h
- 3.2 cpp文件
- 3.2.1 rtp.cpp
- 3.2.2 utils.cpp
- 4.测试
- 5.小结
参考:
视音频数据处理入门:UDP-RTP协议解析
从零开始写一个RTSP服务器(三)RTP传输H.264
1. 概述
1.1 RTP协议
RTP(Real-time Transport Protocol,实时传输协议)是一种网络协议,用于在IP网络上传输实时数据,如音频、视频等。它的主要目的是提供一种可靠的、面向数据包的传输机制,以支持实时多媒体应用。
RTP协议的特点包括:
- 无连接
RTP协议本身不保证数据的可靠传输,它只是负责将数据包从发送端发送到接收端,而不关心数据包是否按顺序到达或者是丢失 - 面向数据包
RTP协议适用于传输数据包,而不是连续的数据流。这意味着它可以处理任意大小的数据包,而不需要预先建立连接 - 时间戳
每个RTP数据包都包含一个时间戳字段,表示该数据包的发送时间。这有助于接收端重新组装和播放数据包,以保持正确的播放顺序 - 序列号
每个RTP数据包都有一个序列号,用于标识数据包的顺序。接收端可以根据序列号对数据包进行排序,以确保它们按照正确的顺序被处理 - 同步源(SSRC)
每个RTP会话有唯一的同步源标识符(SSRC),用于区分不同的发送者。这有助于接收端识别并处理来自不同发送者的数据包 - 扩展头
RTP协议支持扩展头,允许在数据包中添加额外的信息,如编解码器信息、载荷类型等
RTP头的格式为
名称 | 表示内容 | 占用比特 | 备注 |
---|---|---|---|
V | 版本号 | 2 | 表示RTP的版本 |
P | 填充标志 | 1 | 如果设置,表示在数据包尾部具有一定的填充字节 |
X | 扩展标志 | 1 | 如果设置,表示在固定数据头部之后还有一个扩展头部 |
CC | CSRC计数 | 4 | 表示CSRC(贡献源)标识符的数量 |
M | 标记 | 1 | 用于特定的标识符,为1时表示一帧的结束 |
PT | 有效载荷类型(payload type) | 7 | 表示数据包中的负载类型,例如H264格式,JPEG格式 |
Sequence number | 序列号 | 16 | 标识数据包的计数,可用于检测是否存在丢失或错序 |
timestamp | 时间戳 | 32 | 1.时间同步:接收端要知道每个数据包的发送时间,以便正确地播放音频或视频。通过使用时间戳,接收端可以准确地将数据包按照到达的顺序进行排序和播放 2.抖动控制:如果收到乱序的数据包,时间戳能够帮助接收端识别并处理这些乱序的数据包,从而减少播放时的延时 |
SSRC | 同步源标识符 | 32 | 用于唯一地表示一个RTP会话中的发送端。每个RTP流都有一个唯一的SSRC值,从而区分不同的发送端,防止冲突 |
CSRC | 贡献源标识符列表 | (0~15)*32 | 用于标识参与多传播的源。一个数据包可能由多个源发送,CSRC字段允许接收端知道有哪些源参与了该数据包的生成,也能够统计相关的信息,检查这个数据包的来源是否是合法的 |
RTP协议通常与RTCP(Real-time Transport Control Protocol,实时传输控制协议)一起使用。RTCP用于监控RTP会话的质量,收集统计信息,并提供反馈给发送端。RTCP报告包括发送方报告(SR)、接收方报告(RR)、源描述(SDES)和应用程序特定功能(APP)
1.2 RTP和UDP的关系
RTP和UDP(User Datagram Protocol,用户数据报协议)是两个不同的网络协议,但它们之间存在密切的关系。RTP负责定义音视频数据包的格式、顺序、时间戳等参数,以保证音视频数据在网络中的实时传输。RTP本身不提供任何传输可靠性,只负责数据的封装和传输。UDP是一种无连接的传输层协议,它提供了一种简单地、不可靠的数据传输服务。UDP协议将数据打包成数据报,通过IP网络进行传输。由于UDP没有建立连接的过程,所以它的传输速度比较快,但同时也无法保证数据传输的可靠性
RTP和UDP之间的关系在于,RTP通常使用UDP作为其底层传输协议。RTP数据包被封装在UDP数据报中进行传输,以利用UDP的高效传输特性。同时,RTP本身不关心数据传输的可靠性和顺序,这些由下层的UDP和IP协议来处理。借用一下其他文中的图片,流媒体协议栈如下所示
RTP之所以会使用UDP而不是TCP,是因为RTP主要用于实时音视频传输,这种应用场景对传输延迟非常敏感。TCP是一种面向连接的可靠性传输,它通过重传机制来保证数据的完整性和可靠性,但这也引入了额外的延迟,相对比而言,UDP速度更快,更适合音视频传输的需求。所以,RTP和UDP之间是互相配合的关系。
综上所述,RTP负责音视频数据的封装和传输,而UDP则提供了一种高效的传输服务。通过将RTP数据包封装在UDP数据包中,可以实现音视频数据在网络中的实时传输
2. RTP打包H264码流
在进行RTP打包H264码流时,因为是传输协议,需要考虑每个数据包的大小。在网络传输中,一般的最大传输单元(Maximum Transmission Unit,MTU)的大小是1500字节,TCP/IP协议栈(如IP头)占20字节,UDP头占8字节,RTP头占12字节,所以RTP数据包最大为1460字节,但是为了适应网络条件或避免分片,可能也会使用相对较小的数据包,例如1400字节。概括来说,RTP打包的格式如下
// 使用最大的RTP Payload情况,即RTP Payload=1460 Bytes
+------------+------------+------------+-------------+
| IP Header | UDP Header | RTP Header | RTP Payload |
+------------+------------+------------+-------------+
| 20 Bytes | 8 Bytes | 12 Bytes | 1460 Bytes |
+------------+------------+------------+-------------+
这里不记录IP Header和UDP Header的信息,仅考虑RTP的处理。其中,RTP Header就是前面记录的Header,RTP Payload就是具体的信息。这里会引入一个新的问题,传输的数据量(pkt_size)和载荷大小(rtp_payload)之间的关系:
(1)如果pkt_size等于设置的rtp_payload大小,则一个RTP包携带一份数据
(2)如果pkt_size大于了rtp_payload,需要将pkt分成片段来进行传输
(3)如果pkt_size小于了rtp_payload,可以将若干个pkt合并到一个RTP当中
2.1 RTP单一传输
RTP单一传输最为简单,其传输的格式为
+------------+------------+------------+--------------+
| IP Header | UDP Header | RTP Header | RTP Payload |
+------------+------------+------------+--------------+
| 20 Bytes | 8 Bytes | 12 Bytes | payload_size |
+------------+------------+------------+--------------+
对于将H264码流打包的情况,这里的RTP Payload就是H264码流的NALU。其中,RTP Payload的格式为
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|F|NRI| type | |
+-+-+-+-+-+-+-+-+ |
| |
| Bytes 2..n of a Single NAL unit |
| |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| :...OPTIONAL RTP padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
第一个字节描述了这个NALU的总体信息:
(1)F:forbidden_zero_bit,默认为0
(2)NRI: 表示NALU的重要程度,越大则越重要,最大为3
(3)type:表示NALU的类型
这里的type的描述类型包括
#define NAL_UNIT_TYPE_UNSPECIFIED 0 // Unspecified
#define NAL_UNIT_TYPE_CODED_SLICE_NON_IDR 1 // Coded slice of a non-IDR picture
#define NAL_UNIT_TYPE_CODED_SLICE_DATA_PARTITION_A 2 // Coded slice data partition A
#define NAL_UNIT_TYPE_CODED_SLICE_DATA_PARTITION_B 3 // Coded slice data partition B
#define NAL_UNIT_TYPE_CODED_SLICE_DATA_PARTITION_C 4 // Coded slice data partition C
#define NAL_UNIT_TYPE_CODED_SLICE_IDR 5 // Coded slice of an IDR picture
#define NAL_UNIT_TYPE_SEI 6 // Supplemental enhancement information (SEI)
#define NAL_UNIT_TYPE_SPS 7 // Sequence parameter set
#define NAL_UNIT_TYPE_PPS 8 // Picture parameter set
#define NAL_UNIT_TYPE_AUD 9 // Access unit delimiter
#define NAL_UNIT_TYPE_END_OF_SEQUENCE 10 // End of sequence
#define NAL_UNIT_TYPE_END_OF_STREAM 11 // End of stream
#define NAL_UNIT_TYPE_FILLER 12 // Filler data
#define NAL_UNIT_TYPE_SPS_EXT 13 // Sequence parameter set extension
// 14..18 // Reserved
#define NAL_UNIT_TYPE_CODED_SLICE_AUX 19 // Coded slice of an auxiliary coded picture without partitioning
// 20..23 // Reserved
// 24..31 // Unspecified
2.2 RTP分片传输
如果需要将pkt分成若干个片段进行传输,需要在RTP Header之后增加两个标识字段,分别是FU Indicator(Fragment Unit)和FU Header,其位置表示为
// 使用最大的RTP Payload情况,即RTP Payload=1460 Bytes
+------------+------------+------------+--------------+-----------+--------------+
| IP Header | UDP Header | RTP Header | FU Indicator | FU Header | RTP Payload |
+------------+------------+------------+--------------+-----------+--------------+
| 20 Bytes | 8 Bytes | 12 Bytes | 1 Bytes | 1 Bytes | payload_size |
+------------+------------+------------+--------------+-----------+--------------+
FU Indicator的字段为
// FU Indicator
+---------------+
|0|1|2|3|4|5|6|7|
+---------------+
|F|NRI| Type |
+---------------+
其中F和NRI与前面的一样:
(1)F:forbidden_zero_bit,默认为0
(2)NRI: 表示NALU的重要程度,越大则越重要,最大为3
(3)Type:NALU的类型,如果是H264格式,则为28,表示H264的第一个分片
FU Header的字段为
// FU Header
+---------------+
|0|1|2|3|4|5|6|7|
+---------------+
|S|E|R| Type |
+---------------+
其中:
(1)S:如果为1,则标识为第一个分片;否则,不是第一个分片
(2)E:如果为1,则表示为最后一个分片;否则,不是最后一个分片
(3)R:保留位,必须为0
(4)Type:NALU的类型,如果是H264格式,就是H264的NALU类型
2.3 RTP多片合入传输
这种情况比较少见,暂时不做记录
3.工程
基于前面对于RTP协议的理解,写一个简易的发送器和接收器,执行的大约流程是:
(1)发送端:将本地已有的h264码流文件,按照RTP格式进行打包,推送到本机地址127.0.0.1,端口号为8880
(2)接收端:接收传输过来的数据包,进行解析并且存储,要求可以正常进行解析(播放功能后续再做)
工程中的代码结构为:
发送端的核心函数是udp_send_packet(),其中调用了rtp_send_packet,最后会调用Winsock函数sendto将数据包传输到对应的IP和端口;接收端的核心函数是recvfrom()和check_fragment(),recvfrom获取远端传输过来的数据包,check_fragment()检查获取到的数据包是否是分片的,如果是分片的,还会进行拼接。在接收端,通过控制宏来决定是否要存储传输过来的数据包
3.1 头文件
3.1.1 rtp.h
rtp.h定义了RTP协议的Header,packet和上下文
#pragma once
#include <stdio.h>
#include <stdint.h>
#include <WinSock2.h>
extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
};
#define RECV_DATA_SIZE 10000
#define MAX_BUFF_SIZE 32 * 1024 * 1024
#define RTP_MAX_PKT_SIZE 1400 // RTP数据包最大尺寸,一般1400左右
#define RTP_HEADER_SIZE 12
#define RTP_PADDING_SIZE 64
#define RTP_PACKET_START 1
#define RTP_PACKET_FRAGMENT 2
#define RTP_PACKET_END 3
#define STREAM_DOWNLOAD 0
#define YUV_DOWNLOAD 0
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
//+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//|V=2|P|X| CC |M| PT | sequence number |
//+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//| timestamp |
//+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//| synchronization source(SSRC) identifier |
//+ =+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+= +
//| contributing source(CSRC) identifiers |
//| .... |
//+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
typedef struct rtp_header
{
// 存储时高位存储的是version
/* byte 0 */
uint8_t csrc_len : 4; /* expect 0 */
uint8_t extension : 1; /* expect 1 */
uint8_t padding : 1; /* expect 0 */
uint8_t version : 2; /* expect 2 */
/* byte 1 */
uint8_t payload_type : 7;
uint8_t marker : 1; /* expect 1 */
/* bytes 2, 3 */
uint16_t seq_num;
/* bytes 4-7 */
uint32_t timestamp;
/* bytes 8-11 */
uint32_t ssrc; /* stream number is used here. */
}rtp_header_t;
typedef struct rtp_packet
{
rtp_header_t rtp_h;
uint8_t rtp_data[RTP_MAX_PKT_SIZE + RTP_PADDING_SIZE];
}rtp_packet_t;
typedef struct rtp_context
{
int rtp_packet_cnt; // 一共接收到多少个packet
int rtp_buffer_size; // 当前buffer中的size
int rtp_frame_cnt; // 这些packet一共是多少帧
int packet_loc; // 当前packet所在位置,是否为一帧的起始packet或者最终packet
uint8_t* rtp_buffer_data; // 这个data会将之前接收到的数据存储起来
}rtp_context_t;
int udp_parser(const char* in_url, int port);
3.1.2 utils.h
utils.h中定义了一些辅助性检查的工具,以及一个用于分开NALU的函数find_nal_unit
#pragma once
#include "rtp.h"
int find_nal_unit(uint8_t* buf, int size, int* nal_start, int* nal_end);
void debug_byte(uint8_t* p, int size);
void debug_rtp_header(rtp_header_t* rtp_h);
void debug_rtp(rtp_packet_t* rtp_pkt, int pkt_size);
void debug_fragment_header(uint8_t* data, int size);
3.2 cpp文件
3.2.1 rtp.cpp
rtp.cpp文件中定义了RTP协议的实现方式,包括发送和接收,实现细节用注释表明,代码结构上可能还有不足的地方,不过功能正常
#pragma warning(disable : 4996)
#pragma comment(lib, "ws2_32.lib")
#include "./include/rtp.h"
#include "./include/parse.h"
#include "./include/utils.h"
FILE* fp_in;
void set_default_rtp_context(rtp_context_t* rtp_ctx)
{
memset(rtp_ctx->rtp_buffer_data, 0, sizeof(rtp_ctx->rtp_buffer_size));
rtp_ctx->rtp_packet_cnt = 0;
rtp_ctx->rtp_buffer_size = 0;
rtp_ctx->packet_loc = 0;
}
int check_nalu_header(uint8_t data0)
{
int forbidden_zero_bit = data0 & 0x80; // 1bit
int nal_ref_idc = data0 & 0x60; // 2 bit
int nal_unit_type = data0 & 0x1F; // 5bit
if ((data0 & 0x80) == 1)
{
printf("forbidden zero bit should be 0\n");
return -1;
}
return nal_unit_type;
}
int check_fragment_nalu_header(rtp_context_t* rtp_ctx, uint8_t data0, uint8_t data1)
{
int nal_unit_type = check_nalu_header(data0);
int s, e, type;
int pos;
if (nal_unit_type == 28) // H264
{
s = data1 & 0x80; // S
e = data1 & 0x40; // E
type = data1 & 0x1F; // type
pos = data1 & 0xC0; // 1100 0000
switch (pos)
{
case 0x80:
rtp_ctx->packet_loc = RTP_PACKET_START;
break;
case 0x40:
rtp_ctx->packet_loc = RTP_PACKET_END;
break;
case 0x00:
rtp_ctx->packet_loc = RTP_PACKET_FRAGMENT;
break;
default: // error
printf("invalid packet loc\n");
return -1;
break;
}
}
return 0;
}
// Check the data is fragment or not, if fragment, try to concate
int check_fragment(rtp_context_t* rtp_ctx, rtp_packet_t* rtp_pkt, uint8_t* data, int size)
{
int nal_start, nal_end;
int ret = 0;
int data_size = size - RTP_HEADER_SIZE;
find_nal_unit(data, data_size, &nal_start, &nal_end); // check NALU split pos
uint8_t data0 = data[nal_start];
uint8_t data1 = data[nal_start + 1];
uint8_t fu_indicator, fu_header;
if (nal_start > 0 && nal_start < 5) // single-fragment, maybe SPS, PPS or small size frame
{
fu_indicator = 0;
fu_header = 0;
ret = check_nalu_header(data0); // update nalu_type
rtp_ctx->rtp_buffer_data = (uint8_t*)realloc(rtp_ctx->rtp_buffer_data, (rtp_ctx->rtp_buffer_size + data_size) * sizeof(uint8_t));
memcpy(rtp_ctx->rtp_buffer_data + rtp_ctx->rtp_buffer_size, data, data_size);
#if STREAM_DOWNLOAD
fwrite(rtp_ctx->rtp_buffer_data + rtp_ctx->rtp_buffer_size, 1, data_size, fp_in);
#endif
fprintf(stdout, "rtp_ctx frame cnt:%d, frame_size:%d\n", rtp_ctx->rtp_frame_cnt, data_size);
rtp_ctx->rtp_frame_cnt++;
rtp_ctx->rtp_buffer_size += data_size;
}
else // multi-fragment
{
fu_indicator = data[0];
fu_header = data[1];
ret = check_fragment_nalu_header(rtp_ctx, fu_indicator, fu_header);
if (ret < 0)
{
printf("invalid nalu header\n");
return -1;
}
int real_data_size = data_size - 2;
rtp_ctx->rtp_buffer_data = (uint8_t*)realloc(rtp_ctx->rtp_buffer_data, (rtp_ctx->rtp_buffer_size + real_data_size) * sizeof(uint8_t));
if (!rtp_ctx->rtp_buffer_data)
{
printf("realloc rtp_buffer_data failed\n");
return -1;
}
memcpy(rtp_ctx->rtp_buffer_data + rtp_ctx->rtp_buffer_size, data + 2, real_data_size); // plus 2 to skip fu_indicator and fu_header
#if STREAM_DOWNLOAD
fwrite(rtp_ctx->rtp_buffer_data + rtp_ctx->rtp_buffer_size, 1, real_data_size, fp_in);
fflush(fp_in);
#endif
rtp_ctx->rtp_packet_cnt++;
rtp_ctx->rtp_buffer_size += real_data_size;
if (rtp_ctx->packet_loc == RTP_PACKET_END) // end of packet
{
fprintf(stdout, "rtp_ctx frame cnt:%d, frame_size:%d\n", rtp_ctx->rtp_frame_cnt, rtp_ctx->rtp_buffer_size);
rtp_ctx->rtp_frame_cnt++;
}
}
return 0;
}
int udp_parser(const char* in_url, int port)
{
WSADATA wsaData;
// 指定应用程序希望使用的Windows sockets规范版本,最高有效字节指定了主版本号,最低有效字节指定了次版本号
// 如果是 MAKEWORD(2, 2),则对应于 Winsock 2.2
WORD sockVersion = MAKEWORD(2, 2);
int cnt = 0;
// 初始化Windows Sockets DLL
// wsaData用于接收关于Winsock DLL的详细信息,包括实际的Windows Sockets版本号
if (WSAStartup(sockVersion, &wsaData) != 0)
{
return 0;
}
// 创建套接字,就像是网络通信的一条通道
// 类似于对普通文件的fopen操作,这个SOCKET描述符唯一标识了一个socket,后续的网络操作都围绕着这个socket描述符进行
SOCKET ser_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (ser_socket == INVALID_SOCKET)
{
ERROR("Invalid socket");
return -1;
}
int on = 1;
setsockopt(ser_socket, SOL_SOCKET, SO_REUSEADDR, (const char*)& on, sizeof(on));
sockaddr_in ser_addr;
// 地址簇标识符,用于指明地址的类型,以便系统能够正确地处理该地址
ser_addr.sin_family = AF_INET;
// 表示端口号(16位无符号整数),端口号在网络中以网络字节序(big-endian)存储
// 通常需要使用htons()函数,即Host to Network Short将其从主机字节序转换为网络字节序
ser_addr.sin_port = htons(port);
// sin_addr之中存储IPv4地址(32位无符号整数),按照网络字节序存储,通常会使用inet_addr()函数将
// 点分十进制的IP地址字符串9(如192.168.1.1)转换为这个整数,
// ser_addr.sin_addr.S_un.S_addr = INADDR_ANY;
ser_addr.sin_addr.s_addr = inet_addr(in_url);
// 将一个本地地址(包括主机地址和端口号)与一个未连接的socket相关联,从而建立起套接字的本地连接
if (bind(ser_socket, (sockaddr*)& ser_addr, sizeof(ser_addr)) == SOCKET_ERROR)
{
printf("Bind socket addr error\n");
closesocket(ser_socket);
return -1;
}
sockaddr_in remote_addr;
int addr_len = sizeof(remote_addr);
int parse_rtp = 1;
int parse_mpegts = 1;
fprintf(stdout, "Listening on port:%d\n", port);
char recv_data[RECV_DATA_SIZE];
rtp_context_t* rtp_ctx = (rtp_context_t*)calloc(1, sizeof(rtp_context_t));
if (!rtp_ctx)
{
printf("alloc rtp_ctx failed\n");
return -1;
}
rtp_packet_t* rtp_pkt = (rtp_packet_t*)calloc(1, sizeof(rtp_packet_t));
if (!rtp_pkt)
{
printf("alloc rtp_pkt failed\n");
return -1;
}
int nal_start, nal_end;
int data_size = 0;
while (1)
{
int pkt_size = recvfrom(ser_socket, recv_data, RECV_DATA_SIZE, 0, (sockaddr*)& remote_addr, &addr_len);
if (pkt_size > 0)
{
if (parse_rtp != 0)
{
char payload_str[10] = { 0 };
memcpy(rtp_pkt, recv_data, pkt_size);
check_fragment(rtp_ctx, rtp_pkt, rtp_pkt->rtp_data, pkt_size); // check pkt data is fragment or not
// RFC3551
rtp_header_t rtp_h = rtp_pkt->rtp_h;
unsigned int timestamp = ntohl(rtp_h.timestamp);
unsigned int seq_num = ntohs(rtp_h.seq_num);
char payload = rtp_h.payload_type;
if (rtp_ctx->packet_loc == RTP_PACKET_END) // parse data
{
switch (payload)
{
case 33: // mpegts
// mpegts_packet_parse((uint8_t*)rtp_data, parse_mpegts, payload, rtp_data_size); // TODO: add mpegts parser
printf("MPEGTS type\n");
break;
case 96: // h264
sprintf(payload_str, "H264");
//h264_packet_parse(rtp_ctx); // TODO : add h264 parse and SDL play
break;
default:
printf("Unknown type\n");
break;
}
// printf("[RTP PKT] %5d| %5s | %10u| %5d| %5d\n", cnt, payload_str, timestamp, seq_num, pkt_size);
set_default_rtp_context(rtp_ctx); // set default rtp ctx value
}
}
cnt++;
}
}
free(rtp_ctx->rtp_buffer_data);
free(rtp_ctx);
free(rtp_pkt);
closesocket(ser_socket);
WSACleanup();
return 0;
}
int rtp_send_packet(SOCKET * socket, rtp_packet_t * rtp_pkt, int port, const char* out_url, int size)
{
int ret;
sockaddr_in ser_addr;
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(port);
ser_addr.sin_addr.s_addr = inet_addr(out_url);
rtp_pkt->rtp_h.seq_num = htons(rtp_pkt->rtp_h.seq_num);
rtp_pkt->rtp_h.timestamp = htonl(rtp_pkt->rtp_h.timestamp);
rtp_pkt->rtp_h.ssrc = htonl(rtp_pkt->rtp_h.ssrc);
// send packet to specified IP and port
ret = sendto(*socket, (const char*)rtp_pkt, size, 0, (struct sockaddr*) & ser_addr, sizeof(ser_addr));
rtp_pkt->rtp_h.seq_num = ntohs(rtp_pkt->rtp_h.seq_num);
rtp_pkt->rtp_h.timestamp = ntohl(rtp_pkt->rtp_h.timestamp);
rtp_pkt->rtp_h.ssrc = ntohl(rtp_pkt->rtp_h.ssrc);
return ret;
}
int udp_send_packet(rtp_packet_t * rtp_pkt, uint8_t * buf, int start_pos, int size, SOCKET * socket, int port, const char* out_url)
{
int ret = 0;
int send_bytes = 0;
int fps = 25;
int nal_type = buf[0];
if (size <= RTP_MAX_PKT_SIZE) // single fragment
{
memcpy(rtp_pkt->rtp_data, buf, size);
ret = rtp_send_packet(socket, rtp_pkt, port, out_url, size + RTP_HEADER_SIZE);
if (ret < 0)
{
printf("rtp send packet failed\n");
return -1;
}
send_bytes += ret;
}
else // multi-fragment
{
/*
* 0 1 2
* 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
* | FU indicator | FU header | FU payload ... |
* +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
/*
* FU Indicator
* 0 1 2 3 4 5 6 7
* +-+-+-+-+-+-+-+-+
* |F|NRI| Type |
* +---------------+
*/
/*
* FU Header
* 0 1 2 3 4 5 6 7
* +-+-+-+-+-+-+-+-+
* |S|E|R| Type |
* +---------------+
*/
// 一共需要传输几个数据包
int pkt_num = size / RTP_MAX_PKT_SIZE; // RTP_MAX_PKT_SIZE=1400
// 最后一个数据包的大小
int pkt_left = size % RTP_MAX_PKT_SIZE;
int i, pos = 0;
// 发送完整的包
for (int i = 0; i < pkt_num; i++)
{
// 0x60 : 0110 0000, F=0, NRI=11, Type=0000
// 28 : Type=28,H264标准的定义
rtp_pkt->rtp_data[0] = (nal_type & 0x60) | 28;
// 0x1F : 0001 1111, S=0, E=0, R=0, Type & 11111
// 去掉前面3位,只保留NALU_TYPE
rtp_pkt->rtp_data[1] = nal_type & 0x1F;
if (i == 0) // 第一个包数据
{
// 0x80 : 1000 0000, S=1, 表示第一个包
rtp_pkt->rtp_data[1] |= 0x80; // start
}
else if (pkt_left == 0 && i == pkt_num - 1)
{
// 0x40 : 0100 0000, S=0, E=1,表示最后一个包
rtp_pkt->rtp_data[1] |= 0x40; // end
}
rtp_pkt->rtp_data[1] |= buf[start_pos] & 0x1F; // NALU type
// 从第三个字节开始,将数据填充到rtp_data中,填充1400个字节
memcpy(rtp_pkt->rtp_data + 2, buf + pos, RTP_MAX_PKT_SIZE);
// 发送数据包,一共是1400(data) + 2(FU Indicator + FU Header) + 12(RTP Header)= 1414
ret = rtp_send_packet(socket, rtp_pkt, port, out_url, RTP_MAX_PKT_SIZE + 2 + RTP_HEADER_SIZE);
if (ret < 0)
{
printf("rtp send packet failed\n");
return -1;
}
send_bytes += ret;
rtp_pkt->rtp_h.seq_num++;
pos += RTP_MAX_PKT_SIZE;
}
// 发送最后剩余的数据
if (pkt_left > 0)
{
rtp_pkt->rtp_data[0] = (nal_type & 0x60) | 28;
rtp_pkt->rtp_data[1] = nal_type & 0x1F;
rtp_pkt->rtp_data[1] |= 0x40; // end
rtp_pkt->rtp_data[1] |= buf[start_pos] & 0x1F; // NALU type
memcpy(rtp_pkt->rtp_data + 2, buf + pos, pkt_left);
ret = rtp_send_packet(socket, rtp_pkt, port, out_url, pkt_left + 2 + RTP_HEADER_SIZE);
if (ret < 0)
{
printf("rtp send packet failed\n");
return -1;
}
send_bytes += ret;
rtp_pkt->rtp_h.seq_num++;
}
}
rtp_pkt->rtp_h.timestamp += 90000 / fps;
return send_bytes;
}
int udp_send(const char* output_url, const char* output_file, int port)
{
FILE* out_file = fopen(output_file, "rb");
WSADATA wsaData;
WORD sockVersion = MAKEWORD(2, 2);
if (WSAStartup(sockVersion, &wsaData) != 0)
{
return 0;
}
SOCKET ser_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); // UDP protocol
if (ser_socket == INVALID_SOCKET)
{
printf("Invalid socket\n");
return -1;
}
int on = 1;
setsockopt(ser_socket, SOL_SOCKET, SO_REUSEADDR, (const char*)& on, sizeof(on));
rtp_packet_t* rtp_pkt = (rtp_packet_t*)malloc(sizeof(rtp_packet_t));
if (!rtp_pkt)
{
printf("calloc rtp pkt failed\n");
return -1;
}
memset(rtp_pkt, 0, sizeof(rtp_pkt));
rtp_pkt->rtp_h.payload_type = 96; // H264
rtp_pkt->rtp_h.version = 2;
rtp_pkt->rtp_h.ssrc = 0x88923423;
int frame_size = 0;
int ret = 0;
int send_bytes = 0;
int nal_type = 0;
int fps = 25;
static int cnt = 0;
uint8_t* buf = (uint8_t*)malloc(MAX_BUFF_SIZE);
if (!buf)
{
printf("malloc buf failed\n");
return -1;
}
size_t rsz = 0;
size_t sz = 0;
int64_t off = 0;
uint8_t* p = buf;
size_t send_byte_total = 0;
size_t read_byte_total = 0;
int nal_start = 0;
int nal_end = 0;
while (1)
{
rsz = fread(buf + sz, 1, MAX_BUFF_SIZE - sz, out_file);
read_byte_total += rsz;
if (rsz == 0)
{
if (ferror(out_file)) { fprintf(stderr, "!! Error: read failed: %s \n", strerror(errno)); break; }
printf("end of file, flush buffer if necessary \n");
int diff = read_byte_total - send_byte_total;
if (diff > 0 && diff == nal_end) // flush remaining data
{
ret = udp_send_packet(rtp_pkt, p, nal_start, nal_end, &ser_socket, port, output_url);
printf("flush remaining data, data size:%d, send_bytes:%d", nal_end, ret);
}
break; // if (feof(infile))
}
sz += rsz;
while (find_nal_unit(p, sz, &nal_start, &nal_end) > 0) // find nal unit pos
{
// send udp packet
ret = udp_send_packet(rtp_pkt, p, nal_start, nal_end, &ser_socket, port, output_url);
printf("start_size:%d, pkt_num:%d, size=%d, send_bytes=%d\n", nal_start, cnt++, nal_end, ret);
if (ret < 0)
{
printf("send pkt failed\n");
break;
}
p += nal_end;
sz -= nal_end;
send_byte_total += nal_end;
Sleep(1000 / fps);
}
// if no NALs found in buffer, discard it
if (p == buf)
{
fprintf(stderr, "!! Did not find any NALs between offset %lld (0x%04llX), size %lld (0x%04llX), discarding \n",
(long long int)off,
(long long int)off,
(long long int)off + sz,
(long long int)off + sz);
p = buf + sz;
sz = 0;
}
memmove(buf, p, sz);
off += p - buf;
p = buf;
}
free(rtp_pkt);
fclose(out_file);
free(p);
closesocket(ser_socket);
WSACleanup();
return 0;
}
int main()
{
#if STREAM_DOWNLOAD
fp_in = fopen("rtp_receive.h264", "wb");
#endif
// 1.parse udp data
udp_parser("127.0.0.1", 8880);
// 2.send udp data
//udp_send("127.0.0.1", "output.h264", 8880);
#if STREAM_DOWNLOAD
fclose(fp_in);
#endif
return 0;
}
3.2.2 utils.cpp
utils.cpp中定义了一些用于检查数据的工具,比较简单
#include "utils.h"
void debug_byte(uint8_t* p, int size)
{
int i;
for (i = 0; i < size; i++)
{
printf("data[%d]=%x ", i, p[i]);
if (i != 0 && (i % 10 == 0 || i == size - 1))
{
printf("\n");
}
}
printf("\n");
}
void debug_rtp_header(rtp_header_t* rtp_h)
{
printf("rtp_header->version:%d\n", rtp_h->version);
printf("rtp_header->padding:%d\n", rtp_h->padding);
printf("rtp_header->extension:%d\n", rtp_h->extension);
printf("rtp_header->csrc_len:%d\n", rtp_h->csrc_len);
printf("rtp_header->payload_type:%d\n", rtp_h->payload_type);
printf("rtp_header->marker:%d\n", rtp_h->marker);
printf("rtp_header->seq_num:%d\n", rtp_h->seq_num);
printf("rtp_header->timestamp:%d\n", rtp_h->timestamp);
printf("rtp_header->ssrc:%x\n", rtp_h->ssrc);
}
void debug_rtp(rtp_packet_t* rtp_pkt, int pkt_size)
{
debug_rtp_header(&rtp_pkt->rtp_h);
debug_byte(rtp_pkt->rtp_data, pkt_size);
}
int find_nal_unit(uint8_t* buf, int size, int* nal_start, int* nal_end)
{
int i;
// find start
*nal_start = 0;
*nal_end = 0;
i = 0;
while ( //( next_bits( 24 ) != 0x000001 && next_bits( 32 ) != 0x00000001 )
(buf[i] != 0 || buf[i + 1] != 0 || buf[i + 2] != 0x01) &&
(buf[i] != 0 || buf[i + 1] != 0 || buf[i + 2] != 0 || buf[i + 3] != 0x01)
)
{
i++; // skip leading zero
if (i + 4 >= size) { return 0; } // did not find nal start
}
if (buf[i] != 0 || buf[i + 1] != 0 || buf[i + 2] != 0x01) // ( next_bits( 24 ) != 0x000001 )
{
i++;
}
if (buf[i] != 0 || buf[i + 1] != 0 || buf[i + 2] != 0x01) { /* error, should never happen */ return 0; }
i += 3;
*nal_start = i;
while ( //( next_bits( 24 ) != 0x000000 && next_bits( 24 ) != 0x000001 )
(buf[i] != 0 || buf[i + 1] != 0 || buf[i + 2] != 0) &&
(buf[i] != 0 || buf[i + 1] != 0 || buf[i + 2] != 0x01)
)
{
i++;
// FIXME the next line fails when reading a nal that ends exactly at the end of the data
if (i + 3 >= size) { *nal_end = size; return -1; } // did not find nal end, stream ended first
}
*nal_end = i;
return (*nal_end - *nal_start);
}
4.测试
传输使用的文件为output.h264,这是一个Crew_1280x720.yuv编码而来的码流文件,一共有600帧
在使用的时候,可以先将发送端打包成exe,在cmd中执行send.exe,然后进行码流的接收receive.exe,这样就可以实现测试
发送端:
由于find_nal_unit无法分析出来最后一帧的nal_start和nal_end,所以还需要额外flush一次
接收端:
也可以用码流分析器正常打开,粗略的对比了一下,没有发现有误差,并且ffplay也可以正常播放
5.小结
在做这样一些工作的时候,其实还思考了如何从摄像头获取数据送入到发送端,以及如何在解码端进行解码和SDL播放,不过这里的代码结构修改起来比较麻烦,后续会继续做解码和SDL播放的工作。另外,在这些处理这些数据的时候,需要进行良好的内存管理,否则非常容易出现内存崩溃的问题,这一点应该多参考FFmpeg当中的内存管理机制,尤其是AVRefBuf这个变量的定义,比较重要
CSDN : https://blog.csdn.net/weixin_42877471
Github : https://github.com/DoFulangChen