【开源项目】基于RTP协议的H264码流发送器和接收器

news2024/9/24 3:26:21

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协议的特点包括:

  1. 无连接
    RTP协议本身不保证数据的可靠传输,它只是负责将数据包从发送端发送到接收端,而不关心数据包是否按顺序到达或者是丢失
  2. 面向数据包
    RTP协议适用于传输数据包,而不是连续的数据流。这意味着它可以处理任意大小的数据包,而不需要预先建立连接
  3. 时间戳
    每个RTP数据包都包含一个时间戳字段,表示该数据包的发送时间。这有助于接收端重新组装和播放数据包,以保持正确的播放顺序
  4. 序列号
    每个RTP数据包都有一个序列号,用于标识数据包的顺序。接收端可以根据序列号对数据包进行排序,以确保它们按照正确的顺序被处理
  5. 同步源(SSRC)
    每个RTP会话有唯一的同步源标识符(SSRC),用于区分不同的发送者。这有助于接收端识别并处理来自不同发送者的数据包
  6. 扩展头
    RTP协议支持扩展头,允许在数据包中添加额外的信息,如编解码器信息、载荷类型等

RTP头的格式为
在这里插入图片描述

名称表示内容占用比特备注
V版本号2表示RTP的版本
P填充标志1如果设置,表示在数据包尾部具有一定的填充字节
X扩展标志1如果设置,表示在固定数据头部之后还有一个扩展头部
CCCSRC计数4表示CSRC(贡献源)标识符的数量
M标记1用于特定的标识符,为1时表示一帧的结束
PT有效载荷类型(payload type)7表示数据包中的负载类型,例如H264格式,JPEG格式
Sequence number序列号16标识数据包的计数,可用于检测是否存在丢失或错序
timestamp时间戳321.时间同步:接收端要知道每个数据包的发送时间,以便正确地播放音频或视频。通过使用时间戳,接收端可以准确地将数据包按照到达的顺序进行排序和播放
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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1971739.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Arco Design 之Table表格

此篇文章为table表格示例&#xff0c;包含列、data数据、展开、选中、自定义等相关属性 基础表格 <a-table :columns"columns1" :data"tableData1" />const columns1 [{ title: "编号", dataIndex: "no"},{ title: "名称…

Linux线程2

线程相关函数 线程分离--pthread_detach&#xff08;后面会详细讲&#xff09; 函数原型&#xff1a;int pthread_datach(pthread_t thread); 调用该函数之后不需要 pthread_join 子线程会自动回收自己的PCB 杀死&#xff08;取消&#xff09;线程--pthread_cancel 取…

自动驾驶将驶向何方?大模型(World Models)自动驾驶综述

前言 自动驾驶系统的开发是一个技术与哲学的双重挑战&#xff0c;核心在于模拟人类的直觉推理和常识。尽管机器学习在模式识别上取得了进展&#xff0c;但在复杂情境下仍存在局限。人类决策基于感官感知&#xff0c;但能预见行动结果和预判变化&#xff0c;这是机器难以复制的…

欧科云链受邀参与EDCON 大会,听听OKLink为开发者带来哪些惊喜?

一年一度的 EDCON 大会于 7 月底在位于东京的联合国大学盛大举行。OKLink 与 Polygon 联手为来自全球各地的数千名开发者打造开放空间&#xff0c;带来多场精彩的主题分享&#xff0c;让开发者得以在上手体验的同时获取到关于最新开发工具的全面信息。 在论坛环节中&#xff0…

[Docker][Docker Container]详细讲解

目录 1.什么是容器&#xff1f;2.容器命令1.docker creatre2.docker run3.docker ps4.docker logs5.docker attach6.docker exec7.docker start8.docker stop9.docker restart10.docker kill11.docker top12.docker stats13.docker container inspect14.docker port15.docker c…

0730评价项目 实现数据库行转列查询

0730评价项目包-CSDN博客 数据库字段&#xff1a; 实现业务&#xff1a; 1&#xff09;查询对应部门&#xff0c;年份的员工季度评价信息&#xff1a; 对应sql语句&#xff1a; 使用 group by 和 GROUP_CONCAT 关键字进行行转列&#xff0c; case when 后接关联条件&#xf…

【Py/Java/C++三种语言详解】LeetCode 1334、LeetCode1334. 阈值距离内邻居最少的城市【全源最短路问题Floyd算法】

可上 欧弟OJ系统 练习华子OD、大厂真题 绿色聊天软件戳 od1441了解算法冲刺训练&#xff08;备注【CSDN】否则不通过&#xff09; 文章目录 相关推荐阅读**一、题目描述****二、题目解析****三、参考代码**PythonJavaC **四、时空复杂度**华为OD算法/大厂面试高频题算法练习冲刺…

webstorm配置项目Typescript编译环境

使用npm命令安装typeScript编译器 npm install typescript -g 安装好&#xff0c;在命令行可以查看编译器的版本 tsc --version 用Webstorm打开一个Typescript的项目。为TypeScript文件更改编译设置&#xff0c;File->Settings->toosl -> File Watchers->TypeScri…

【工具篇】华为VRP通用操作系统 —— 基础命令介绍

文章目录 视图切换命令命令报错误类型命令行快捷键 【工具篇】华为VRP通用操作系统 —— 基础知识 通过上一节的华为VRP通用操作系统介绍&#xff0c;掌握如何登入设备以及命令行架构。也通过eNSP虚拟器搭建拓扑成功登入华为VRP通用操作系统。 本文章介绍基础命令以及快捷键&am…

【数据结构】二叉树基本操作(孩子兄弟表示法 + Java详解 + 原码)

Hi~&#xff01;这里是奋斗的明志&#xff0c;很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~~ &#x1f331;&#x1f331;个人主页&#xff1a;奋斗的明志 &#x1f331;&#x1f331;所属专栏&#xff1a;数据结构 &#x1f4da;本系列文章为个人学…

OpenStack入门体验

一、云计算概述 1.1什么是云计算 云计算(cloud computing)是一种基于网络的超级计算模式,基于用户的不同需求&#xff0c;提供所需的资源&#xff0c;包括计算资源、存储资源、网络资源等。云计算服务运行在若干台高性能物理服务器之上&#xff0c;提供每秒 10万亿次的运算能力…

MSF回弹木马ubuntu测试

网站地址为192.168.104.128 web.php内容为&#xff1a; <?php eval($_POST[123]); ?>linux版本信息&#xff1a;20.04.1-Ubuntu nginx信息&#xff1a;nginx-1.21.6 php信息&#xff1a;PHP 7.3.33-19 php-fpm信息&#xff1a;/etc/php/7.3/fpm/php-fpm.conf 一、使用…

解决VisualVM下载插件失败(手动安装)

解决VisualVM下载插件失败 当前为jdk8自带的jvisualvm,出现以下情况,已经配置了对于java版本的url 点设置,再点编辑,对url进行修改 进入这里网址 检查url是否对应,复制到上面的url上面 例如: 我的jdk版本为: jdk-8u321-windows-x64 , 选131 - 351 再重新下载 还是不行, 可以…

嵌入式人工智能(38-基于树莓派4B的角速度和加速度传感器-MPU6050)

1、角速度传感器 角速度传感器是一种用于测量物体在空间中绕坐标轴旋转的速度的传感器。角速度是一个物体围绕某一轴旋转的速度大小&#xff0c;通常以角度/秒或弧度/秒的形式表示。 角速度传感器通常使用陀螺仪原理来测量角速度。陀螺仪是基于角动量守恒定律的物理原理&…

力扣高频SQL 50题(基础版)第三十八题

文章目录 力扣高频SQL 50题&#xff08;基础版&#xff09;第三十八题1484.按日期分组销售产品题目说明实现过程准备数据实现方式结果截图总结 力扣高频SQL 50题&#xff08;基础版&#xff09;第三十八题 1484.按日期分组销售产品 题目说明 表 Activities&#xff1a; ---…

类型推断技术及仓颉语言实践

史磊 仓颉语言类型推断技术专家 一、一种看待类型系统的方式 一门编程语言一定得包含类型系统吗&#xff1f; 这个问题今天看来可能显而易见&#xff0c;一个程序没有类型的话还能算是个完整、正确的程序吗&#xff1f;但是其实关于类型系统的作用&#xff0c;一直是存在两种…

PCB打板——usb扩展坞

采用一个typec模块&#xff0c;四个typea模块&#xff0c;以及保险丝&#xff0c;电源滤波部分&#xff0c;原理图如下 这里usb信号线为差分信号&#xff0c;要用差分导线&#xff08;长度相同&#xff0c;对称&#xff09;

RK3568笔记五十:SPI通信-回环测试

若该文为原创文章&#xff0c;转载请注明原文出处。 一、SPI引脚关系 其中SPI1的引脚关系如下表所示 SPI 引脚 功能 MOSI GPIO3_C1 主设备输出/从设备输入 MISO GPIO3_C2 主设备输入/从设备输出 CLOCK CPIO3_C3 时钟信号线 CS0 GPIO3_A1 片选信号线0 CS1 NC …

kettle从入门到精通 第八十二课 ETL之kettle kettle中的【阻塞数据直到步骤都完成】使用教程

1、在使用步骤【阻塞数据直到步骤都完成】&#xff08;英文为Block this step until steps finish&#xff09;之前&#xff0c;我们先来了解下什么是 Copy Nr&#xff1f; Copy Nr是指 “副本编号” 或 “拷贝编号”&#xff0c;也就是下图中的复制的记录行数&#xff0c;图中…

全网最强Nginx教程 | 万字长文爆肝Nginx(五)

Nginx实现服务器端集群搭建 Nginx与Tomcat部署 前面课程已经将Nginx的大部分内容进行了讲解&#xff0c;我们都知道了Nginx在高并发场景和处理静态资源是非常高性能的&#xff0c;但是在实际项目中除了静态资源还有就是后台业务代码模块&#xff0c;一般后台业务都会被部署在…