【开源项目】基于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()指定配置项 ->渲染数据 准备一个容器,例如: …...

select、poll、epoll 与 Reactor 模式
在高并发网络编程领域,高效处理大量连接和 I/O 事件是系统性能的关键。select、poll、epoll 作为 I/O 多路复用技术的代表,以及基于它们实现的 Reactor 模式,为开发者提供了强大的工具。本文将深入探讨这些技术的底层原理、优缺点。 一、I…...

10-Oracle 23 ai Vector Search 概述和参数
一、Oracle AI Vector Search 概述 企业和个人都在尝试各种AI,使用客户端或是内部自己搭建集成大模型的终端,加速与大型语言模型(LLM)的结合,同时使用检索增强生成(Retrieval Augmented Generation &#…...
【生成模型】视频生成论文调研
工作清单 上游应用方向:控制、速度、时长、高动态、多主体驱动 类型工作基础模型WAN / WAN-VACE / HunyuanVideo控制条件轨迹控制ATI~镜头控制ReCamMaster~多主体驱动Phantom~音频驱动Let Them Talk: Audio-Driven Multi-Person Conversational Video Generation速…...

JVM虚拟机:内存结构、垃圾回收、性能优化
1、JVM虚拟机的简介 Java 虚拟机(Java Virtual Machine 简称:JVM)是运行所有 Java 程序的抽象计算机,是 Java 语言的运行环境,实现了 Java 程序的跨平台特性。JVM 屏蔽了与具体操作系统平台相关的信息,使得 Java 程序只需生成在 JVM 上运行的目标代码(字节码),就可以…...

CVE-2020-17519源码分析与漏洞复现(Flink 任意文件读取)
漏洞概览 漏洞名称:Apache Flink REST API 任意文件读取漏洞CVE编号:CVE-2020-17519CVSS评分:7.5影响版本:Apache Flink 1.11.0、1.11.1、1.11.2修复版本:≥ 1.11.3 或 ≥ 1.12.0漏洞类型:路径遍历&#x…...

springboot整合VUE之在线教育管理系统简介
可以学习到的技能 学会常用技术栈的使用 独立开发项目 学会前端的开发流程 学会后端的开发流程 学会数据库的设计 学会前后端接口调用方式 学会多模块之间的关联 学会数据的处理 适用人群 在校学生,小白用户,想学习知识的 有点基础,想要通过项…...
Java求职者面试指南:计算机基础与源码原理深度解析
Java求职者面试指南:计算机基础与源码原理深度解析 第一轮提问:基础概念问题 1. 请解释什么是进程和线程的区别? 面试官:进程是程序的一次执行过程,是系统进行资源分配和调度的基本单位;而线程是进程中的…...

Windows安装Miniconda
一、下载 https://www.anaconda.com/download/success 二、安装 三、配置镜像源 Anaconda/Miniconda pip 配置清华镜像源_anaconda配置清华源-CSDN博客 四、常用操作命令 Anaconda/Miniconda 基本操作命令_miniconda创建环境命令-CSDN博客...

【 java 虚拟机知识 第一篇 】
目录 1.内存模型 1.1.JVM内存模型的介绍 1.2.堆和栈的区别 1.3.栈的存储细节 1.4.堆的部分 1.5.程序计数器的作用 1.6.方法区的内容 1.7.字符串池 1.8.引用类型 1.9.内存泄漏与内存溢出 1.10.会出现内存溢出的结构 1.内存模型 1.1.JVM内存模型的介绍 内存模型主要分…...