【开源项目】基于RTP协议的H264码流发送器和接收器
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 30 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; // 一共接收到多少个packetint rtp_buffer_size; // 当前buffer中的sizeint rtp_frame_cnt; // 这些packet一共是多少帧int packet_loc; // 当前packet所在位置,是否为一帧的起始packet或者最终packetuint8_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; // 1bitint nal_ref_idc = data0 & 0x60; // 2 bitint nal_unit_type = data0 & 0x1F; // 5bitif ((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; // Se = data1 & 0x40; // Etype = data1 & 0x1F; // typepos = data1 & 0xC0; // 1100 0000switch (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: // errorprintf("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 posuint8_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_typertp_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_DOWNLOADfwrite(rtp_ctx->rtp_buffer_data + rtp_ctx->rtp_buffer_size, 1, data_size, fp_in);
#endiffprintf(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_DOWNLOADfwrite(rtp_ctx->rtp_buffer_data + rtp_ctx->rtp_buffer_size, 1, real_data_size, fp_in);fflush(fp_in);
#endifrtp_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.2WORD 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// RFC3551rtp_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 parserprintf("MPEGTS type\n");break;case 96: // h264sprintf(payload_str, "H264");//h264_packet_parse(rtp_ctx); // TODO : add h264 parse and SDL playbreak;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 portret = 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_TYPErtp_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)= 1414ret = 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; // endrtp_pkt->rtp_data[1] |= buf[start_pos] & 0x1F; // NALU typememcpy(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 protocolif (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; // H264rtp_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 packetret = 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 itif (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_DOWNLOADfp_in = fopen("rtp_receive.h264", "wb");
#endif// 1.parse udp dataudp_parser("127.0.0.1", 8880);// 2.send udp data//udp_send("127.0.0.1", "output.h264", 8880);#if STREAM_DOWNLOADfclose(fp_in);
#endifreturn 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 zeroif (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 dataif (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
相关文章:
【开源项目】基于RTP协议的H264码流发送器和接收器
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.h3.1.2 utils.h 3.2 cpp文件3.2.1 rtp.cpp3.2.2 utils.cpp 4.测试5.小结 参考: 视音频数据处理入门:UD…...
【C++】4.类和对象(2)
文章目录 1.类的默认成员函数2.构造函数 1.类的默认成员函数 默认成员函数就是用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数。一个类,我们不写的情况下编译器会默认生成以下6个默认成员函数,需要注意的是这6个中最重要的是前…...
搭建基于树莓派的Linux学习环境(TODO)
主要是想学一下Linux内核,所以搭一套环境,其实有几个选择,好几个都是我买了板子的。 首先是正点原子的RK3568,最早是想弄安卓,但是SDK的大小真的把我劝退了,动不动几百个G的空间,还有就是保底1…...
《大电机技术》是什么级别的期刊?是正规期刊吗?能评职称吗?
问题解答 问:《大电机技术》是不是核心期刊? 答:不是,是知网收录的第一批认定学术期刊。 问:《机电产品开发与创新》级别? 答:省级。主管单位:哈尔滨电气集团公司 主办…...
Python 中使用 Split 忽略逗号
在 Python 中,split 方法可以用于将字符串分割成列表,默认情况下使用空格作为分隔符,但你也可以指定其他分隔符。若想使用 split 方法忽略逗号并按其他分隔符分割字符串,可以使用以下几种方法。 1、问题背景 在 Python 中&#x…...
YOLOv10改进 | 主干篇 | YOLOv10引入CVPR2023 顶会论文BiFormer用于主干修改
1. 使用之前用于注意力的BiFormer在这里用于主干修改。 YOLOv10改进 | 注意力篇 | YOLOv10引入BiFormer注意力机制 2. 核心代码 from collections import OrderedDict from functools import partial from typing import Optional, Union import torch import torch.nn as n…...
sql注入靶场搭建
1.安装小皮面板(PhpStudy) 1.从官网下载:http://www.xp.cn 2、Sqli-labs环境安装 准备好sqli-labs-php7-master文件 3.安装之前确保本地没有下载mysql服务器 如果电脑下载了MySQL可以把MySQL的服务停掉 此电脑>右键>管理>服务…...
【MySQL】MySQL的JSON特性
引言 MySQL从5.7版本开始引入了JSON数据类型,并在8.0版本中大大增强了JSON的支持,包括函数和索引功能。JSON数据类型允许你在MySQL表中存储JSON文档,这些文档可以是对象或数组,并且你可以使用SQL查询来检索、搜索、更新和修改这些…...
微信小程序 - 自定义计数器 - 优化(键盘输入校验)
微信小程序通过自定义组件,实现计数器值的增加、减少、清零、最大最小值限定、禁用等操作。通过按钮事件触发方式,更新计数器的值,并修改相关联的其它变量。通过提升用户体验,对计数器进行优化设计,使用户操作更加便捷…...
Nacos 容器化安装和代理配置指南
简介 Nacos(Dynamic Naming and Configuration Service)是阿里巴巴开源的一款动态服务发现、配置管理和服务管理平台。本文将介绍如何使用 Docker 容器化安装 Nacos 以及如何配置 Nacos 的代理。 前提条件 已安装 Docker 和 Docker Compose基本的 Doc…...
css水波浪动画效果
为缩小gif大小,动画效果做了加速,效果如下: <!DOCTYPE html> <html> <head> <style> *{padding:0;margin:0;}/*清除默认填充及边距*/.water{position:relative;width:100vw;height:100vh;overflow:hidden;background…...
SQL二次注入
目录 1.什么是二次注入? 2.二次注入过程 2.1寻找注入点 2.2注册admin#用户 2.3修改密码 1.什么是二次注入? 当用户提交的恶意数据被存入数据库后,因为被过滤函数过滤掉了,所以无法生效,但应用程序在从数据库中拿…...
深入学习小程序开发第二天:数据绑定与动态更新
一、概念 在小程序中,数据绑定是指将页面的数据和视图进行关联,使得数据的变化能够自动反映在视图上,而不需要手动操作DOM。这种绑定是双向的,即数据改变时视图更新,视图操作(如用户输入)也能改变数据。 二、用法 1.单向数据绑定与双向数据绑定: 在小程序中,数据绑定…...
【ai】 时间序列分析的python例子
时间序列分析 :分析和理解随时间变化的数据序列 在gcc的趋势滤波后,需要对排队延迟梯度进行检测及调整,参考的是一个阈值, 调整阈值时就使用了时间序列分析技术: 时间序列分析是统计学和数据分析中的一种技术,用于分析和理解随时间变化的数据序列。时间序列数据具有时间上…...
生成订单幂等性(防止订单重复提交)
订单唯一性(防止重复下单)方案 重复下单产生原因: 客户端原因: 比如下单的按键在点按之后,在没有收到服务器请求之前,按键的状态没有设为已禁用状态,还可以被按。又或者,在触摸屏下,用户手指…...
IDEA自定义注释模版
1.类(接口/枚举等同理) 2.方法模版 先自定义一个模版组,然后在里面添加模版名,触发快捷键(Tab/Enter),模版描述,哪些语言中应用 模版中的自定义参数params和returns可以自动展开参数…...
Spring Cloud Gateway实现API访问频率限制
Spring Cloud Gateway实现API访问频率限制 一、为什么需要访问频率限制?二、使用全局过滤器实现访问频率限制步骤:示例代码: 三、使用特定路由的过滤器实现访问频率限制步骤:示例代码: 四、总结 在微服务架构中&#x…...
单例模式:确保唯一实例的设计模式
前言 在学习框架和大型项目开发时,我们常常会遇到“单例模式”这个词。虽然它时常被提及,但往往没有详细讲解。为了搞懂单例模式的真正意义以及它在开发中的应用,我查阅了一些资料并总结了这篇博客。希望通过这篇文章,能够帮助大…...
MCU调试技巧-串口打印
1. 软件仿真printf 条件:MDK 效果:在软件仿真模式下,调试页面的串口终端中可以看到串口打印 教程:https://blog.csdn.net/ybhuangfugui/article/details/94378195 2. 串口重定向printf 条件:物理串口接线 效果&…...
VS+Qt+C++点云PCL三维显示编辑系统
程序示例精选 VSQtC点云PCL三维显示编辑系统 如需安装运行环境或远程调试,见文章底部个人QQ名片,由专业技术人员远程协助! 前言 这篇博客针对《VSQtC点云PCL三维显示编辑系统》编写代码,代码整洁,规则,易…...
Vue2 第一节_Vue2上手_插值表达式{{}}_访问数据和修改数据_Vue开发者工具
文章目录 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染2. 插值表达式{{}}3. 访问数据和修改数据4. vue响应式5. Vue开发者工具--方便调试 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染 准备容器引包创建Vue实例 new Vue()指定配置项 ->渲染数据 准备一个容器,例如: …...
高等数学(下)题型笔记(八)空间解析几何与向量代数
目录 0 前言 1 向量的点乘 1.1 基本公式 1.2 例题 2 向量的叉乘 2.1 基础知识 2.2 例题 3 空间平面方程 3.1 基础知识 3.2 例题 4 空间直线方程 4.1 基础知识 4.2 例题 5 旋转曲面及其方程 5.1 基础知识 5.2 例题 6 空间曲面的法线与切平面 6.1 基础知识 6.2…...
Java-41 深入浅出 Spring - 声明式事务的支持 事务配置 XML模式 XML+注解模式
点一下关注吧!!!非常感谢!!持续更新!!! 🚀 AI篇持续更新中!(长期更新) 目前2025年06月05日更新到: AI炼丹日志-28 - Aud…...
【python异步多线程】异步多线程爬虫代码示例
claude生成的python多线程、异步代码示例,模拟20个网页的爬取,每个网页假设要0.5-2秒完成。 代码 Python多线程爬虫教程 核心概念 多线程:允许程序同时执行多个任务,提高IO密集型任务(如网络请求)的效率…...
20个超级好用的 CSS 动画库
分享 20 个最佳 CSS 动画库。 它们中的大多数将生成纯 CSS 代码,而不需要任何外部库。 1.Animate.css 一个开箱即用型的跨浏览器动画库,可供你在项目中使用。 2.Magic Animations CSS3 一组简单的动画,可以包含在你的网页或应用项目中。 3.An…...
Selenium常用函数介绍
目录 一,元素定位 1.1 cssSeector 1.2 xpath 二,操作测试对象 三,窗口 3.1 案例 3.2 窗口切换 3.3 窗口大小 3.4 屏幕截图 3.5 关闭窗口 四,弹窗 五,等待 六,导航 七,文件上传 …...
如何应对敏捷转型中的团队阻力
应对敏捷转型中的团队阻力需要明确沟通敏捷转型目的、提升团队参与感、提供充分的培训与支持、逐步推进敏捷实践、建立清晰的奖励和反馈机制。其中,明确沟通敏捷转型目的尤为关键,团队成员只有清晰理解转型背后的原因和利益,才能降低对变化的…...
2025年- H71-Lc179--39.组合总和(回溯,组合)--Java版
1.题目描述 2.思路 当前的元素可以重复使用。 (1)确定回溯算法函数的参数和返回值(一般是void类型) (2)因为是用递归实现的,所以我们要确定终止条件 (3)单层搜索逻辑 二…...
Python爬虫实战:研究Restkit库相关技术
1. 引言 1.1 研究背景与意义 在当今信息爆炸的时代,互联网上存在着海量的有价值数据。如何高效地采集这些数据并将其应用于实际业务中,成为了许多企业和开发者关注的焦点。网络爬虫技术作为一种自动化的数据采集工具,可以帮助我们从网页中提取所需的信息。而 RESTful API …...
MeshGPT 笔记
[2311.15475] MeshGPT: Generating Triangle Meshes with Decoder-Only Transformers https://library.scholarcy.com/try 真正意义上的AI生成三维模型MESHGPT来袭!_哔哩哔哩_bilibili GitHub - lucidrains/meshgpt-pytorch: Implementation of MeshGPT, SOTA Me…...
