> 技术文档 > 【RTSP从零实践】12、TCP传输H264格式RTP包(RTP_over_TCP)的RTSP服务器(附带源码)

【RTSP从零实践】12、TCP传输H264格式RTP包(RTP_over_TCP)的RTSP服务器(附带源码)


😁博客主页😁:🚀https://blog.csdn.net/wkd_007🚀
🤑博客内容🤑:🍭嵌入式开发、Linux、C语言、C++、数据结构、音视频🍭
🤣本文内容🤣:🍭TCP传输H264格式RTP包(RTP_OVER_TCP)的RTSP服务器🍭
⏰发布时间⏰: 2025-07-16

本文未经允许,不得转发!!!

目录

  • 🎄一、概述
  • 🎄二、RTP over TCP(RTSP) 介绍
    • ✨2.1 RTP over TCP(RTSP) 相关概念
    • ✨2.2 怎么区分 RTSP包、RTP包、RTCP包
    • ✨2.3 RTP包封装代码
  • 🎄三、实现步骤、实现细节
    • ✨3.1 创建TCP服务端套接字
    • ✨3.2 处理RTSP命令
    • ✨3.3 读取H264文件
    • ✨3.4 封装成RTP包并发送
      • 🎈3.4.1、RTP包最开始的4个字节
      • 🎈3.4.2、RTP头(RTP Header)
      • 🎈3.4.3、H264 的 RTP负载(RTP Payload)
  • 🎄四、RTP_over_TCP的RTSP服务端源码
  • 🎄五、、总结

在这里插入图片描述

前面系列文章回顾:
【音视频 | RTSP】RTSP协议详解 及 抓包例子解析(详细而不赘述)
【音视频 | RTSP】SDP(会话描述协议)详解 及 抓包例子分析
【音视频 | RTP】RTP协议详解(H.264的RTP封包格式、AAC的RTP封包格式)
【RTSP从零实践】01、根据RTSP协议实现一个RTSP服务
【RTSP从零实践】02、使用RTP协议封装并传输H264
【RTSP从零实践】03、实现最简单的传输H264的RTSP服务器
【RTSP从零实践】04、使用RTP协议封装并传输AAC
【RTSP从零实践】05、实现最简单的传输AAC的RTSP服务器
【RTSP从零实践】06、实现最简单的同时传输H264、AAC的RTSP服务器
【RTSP从零实践】07、多播传输H264格式的RTP包(附带源码)
【RTSP从零实践】08、多播传输H264码流的RTSP服务器——最简单的实现例子(附带源码)
【RTSP从零实践】09、多播传输AAC格式的RTP包(附带源码)
【RTSP从零实践】10、多播传输AAC码流的RTSP服务器——最简单的实现例子(附带源码)
【RTSP从零实践】11、多播同时传输H264、AAC码流的RTSP服务器——最简单的实现例子(附带源码)

【RTSP从零实践】12、TCP传输H264格式RTP包(RTP_over_TCP)的RTSP服务器(附带源码)

🎄一、概述

前面文章介绍的文章都是由UDP作为传输层协议来发送RTP报文的(RTP over UDP),在RTSP服务器中,还有一种RTP包传输方式,即使用TCP协议作为传输层协议发送RTP报文的RTP over TCP。本文将介绍怎样实现一个最简单的使用TCP方式发送RTP包的RTSP服务器。

本文内容安排如下:

  • 1、介绍使用tcp协议发送RTP(RTP over TCP)的相关内容:
  • 2、使用tcp协议发送RTP的RTSP服务端实现步骤和细节。

【RTSP从零实践】12、TCP传输H264格式RTP包(RTP_over_TCP)的RTSP服务器(附带源码)

🎄二、RTP over TCP(RTSP) 介绍

✨2.1 RTP over TCP(RTSP) 相关概念

我们前面系列文章介绍过的rtsp服务器都是创建了一个TCP服务来处理RTSP协议,创建另一个UDP套接字来发送RTP包,这种方式就是 RTP over UDP。而RTP over TCP不单独建立一个UDP套接字去发送RTP包,而是利用处理RTSP协议的TCP套接字来发送RTP包的,所有有些资料也把这种方式称为RTP over RTSP

  • RTP over UDP:一个TCP套接字处理RTSP协议,另一个UDP套接字发送RTP包、RTCP包;
  • RTP over TCP(RTSP):一个TCP套接字处理RTSP协议、RTP包、RTCP包。

✨2.2 怎么区分 RTSP包、RTP包、RTCP包

如上面所说,我们复用发送RTSP交互的socket来发送RTP包和RTCP信息,那么对于客户端来说,如何区分这三种数据呢?

我们将这三个分为两类,一类是RTSP,一类是RTP、RTCP

发送RTSP信息的情况没有变化,还是更以前一样的方式

发送RTP、RTCP包,在每个包前面都加上四个字节,这四个字节解释如下:

字节 描述 第一个字节 字符\'$\',表示这个包是RTP包 或 RTCP包 第二个字节 通道号channel,用于区分RTP包 或 RTCP包 第三、四个字节 表示RTP包的大小

使用TCP协议传输的RTP包和RTCP包,第一个字节固定为$字符,第二字节的channel是在RTSP服务器处理的SETUP过程中,客户端发送给服务端的。

所以,使用TCP协议传输的RTP包结构如下:
【RTSP从零实践】12、TCP传输H264格式RTP包(RTP_over_TCP)的RTSP服务器(附带源码)


✨2.3 RTP包封装代码

在RTP包起始位置增加4个字节的数据:

struct RtpPacket{ char header[4]; struct RtpHeader rtpHeader; uint8_t payload[0];};

在发送RTP包前,是这样填写这4个字节:

rtpPacket->header[0] = \'$\';rtpPacket->header[1] = rtpChannel;rtpPacket->header[2] = ((dataSize+RTP_HEADER_SIZE) & 0xFF00 ) >> 8;rtpPacket->header[3] = (dataSize+RTP_HEADER_SIZE) & 0xFF;

【RTSP从零实践】12、TCP传输H264格式RTP包(RTP_over_TCP)的RTSP服务器(附带源码)

🎄三、实现步骤、实现细节

本文与之前的文章【RTSP从零实践】03、实现最简单的传输H264的RTSP服务器 有许多共同点,特点是代码,就是在这个基础去修改的,读者可以比较学习。

使用TCP协议发送RTP包的RTSP服务端实现步骤主要有下面几个:

  • 1、创建TCP服务端套接字;
  • 2、处理RTSP客户端发过来的命令;
  • 3、读取H264文件;
  • 4、封装成RTP包并发送

✨3.1 创建TCP服务端套接字

由于RTP包也是使用TCP套接字发送的,所以只需要创建一个TCP套接字即可,不需要再创建UDP套接字了。

int server_fd, client_fd;struct sockaddr_in address;int opt = 1;socklen_t addrlen = sizeof(address);// 创建套接字if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0){ perror(\"socket failed\"); return -1;}// 设置套接字选项if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))){ perror(\"setsockopt\"); return -1;}address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;address.sin_port = htons(RTSP_PORT);// 绑定端口if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0){ perror(\"bind failed\"); return -1;}// 开始监听if (listen(server_fd, MAX_CLIENTS) < 0){ perror(\"listen\"); return -1;}

✨3.2 处理RTSP命令

  • OPTION
    【RTSP从零实践】12、TCP传输H264格式RTP包(RTP_over_TCP)的RTSP服务器(附带源码)

  • DESCRIBE
    【RTSP从零实践】12、TCP传输H264格式RTP包(RTP_over_TCP)的RTSP服务器(附带源码)

  • SETUP
    RTP/AVP/TCP表示RTP流通过TCP传输,当此值出现时,报文没有client_port字段;
    interleaved=0-1表示streamid,标识RTP的streamid=0;RTCP的streamid=1;
    【RTSP从零实践】12、TCP传输H264格式RTP包(RTP_over_TCP)的RTSP服务器(附带源码)

  • PLAY
    【RTSP从零实践】12、TCP传输H264格式RTP包(RTP_over_TCP)的RTSP服务器(附带源码)

  • TEARDOWN
    【RTSP从零实践】12、TCP传输H264格式RTP包(RTP_over_TCP)的RTSP服务器(附带源码)


✨3.3 读取H264文件

在这里插入图片描述

H.264文件保存了h264编码的视频帧,每个视频帧之间以开始码00 00 0100 00 00 01分隔开。我们可以用下面代码判断是否为开始码。
在这里插入图片描述

在两个开始码之间的就是视频帧数据。h264视频帧数据的第一个字节是一个NAL头,内容如下图:
在这里插入图片描述
可以用下面代码读取NAL头:
在这里插入图片描述
以上H264文件结构需要了解的知识,完整代码可以到下个小节查看。


✨3.4 封装成RTP包并发送

本文介绍的RTP数据包是使用TCP协议发送的,主要由3部分组成,即 开始的4个字节、RTP头、RTP负载。
【RTSP从零实践】12、TCP传输H264格式RTP包(RTP_over_TCP)的RTSP服务器(附带源码)

🎈3.4.1、RTP包最开始的4个字节

RTP包最前面的4个字节在前文介绍过了,可以按照下面代码填写:

rtpPacket->header[0] = \'$\';rtpPacket->header[1] = rtpChannel;rtpPacket->header[2] = ((dataSize+RTP_HEADER_SIZE) & 0xFF00 ) >> 8;rtpPacket->header[3] = (dataSize+RTP_HEADER_SIZE) & 0xFF;

🎈3.4.2、RTP头(RTP Header)

在这里插入图片描述
上图是RTP头的结构图,包含了12个字节的内容,可以用代码定义成如下结构体:

struct RtpHeader{ /* byte 0 */ uint8_t csrcLen:4; uint8_t extension:1; uint8_t padding:1; uint8_t version:2; /* byte 1 */ uint8_t payloadType:7; uint8_t marker:1; /* bytes 2,3 */ uint16_t seq; /* bytes 4-7 */ uint32_t timestamp; /* bytes 8-11 */ uint32_t ssrc;};

RTP头这里涉及到一个 时间戳怎么计算 的问题,需要注意的是,这个时间戳是一个 时钟频率 为单位的,而不是具体的时间(秒、毫秒等)。
一般情况下,H264的时钟频率为90000Hz,假设帧率为25,那么每一帧的 时间间隔 就是1/25秒,每一帧的 时钟增量 就是(90000/25=3600)。那时间戳怎么算呢?举个例子,如果帧率为25的H264视频,第一帧的RTP时间戳为0的话,那么第二帧的RTP时间戳就是 3600,第三帧的RTP时间戳就是 7200,依次类推,后一帧的RTP时间戳在前一帧的RTP时间戳的值加上一个时钟增量


🎈3.4.3、H264 的 RTP负载(RTP Payload)

H264 的 RTP负载需要介绍两种方式,第一种是 单个NAL单元封包(Single NAL Unit Packet);第二种是 分片单元(Fragmentation Unit) 。如果H264的视频帧NALU(NAL Unit)总字节数小于 MTU(网络最大传输单元1500字节),就可以使用第一种方式,因为有一些TCP/UDP头数据,所以一般判断小于1400字节,就采用 单个NAL单元封包(Single NAL Unit Packet),否则使用分片单元(Fragmentation Unit)的方式封装RTP包。

单个NAL单元封包 的RTP负载结构如下图,相当于直接将整个NAL Unit 填入RTP负载即可:
在这里插入图片描述

分片单元的RTP负载方式也有两种,本文介绍的是FU-A的方式,RTP负载最开始由三部分组成:第一个字节是FU indicator,第二个字节是FU header,第三个字节开始就是NAL单元去掉NAL头之后的数据:
在这里插入图片描述

  • FU indicatorFU indicator的大小是一个字节,格式如下,跟NAL头的格式一样,但作为 分片RTP封包 ,并不能直接将H264的NAL头直接填上去。
    F:一般为0。为0表示此NAL单元不应包含bit错误或语法违规;为1表示此NAL单元可能包含bit错误或语法违规;
    NRI:直接将H264NAL头的NRI值填入即可;
    Type:FU-A格式的封包填28,FU-B格式的封包填29。

    +---------------+|0|1|2|3|4|5|6|7|+-+-+-+-+-+-+-+-+|F|NRI| Type |+---------------+
  • FU header FU header的大小也是一个字节,格式如下:
    S:start,NALU拆分多个分包后,第一个发送的分包,此bit位置1,其他分包为0;
    E:end,NALU拆分多个分包后,最后一个发送的分包,此bit位置1,其他分包为0;
    R:保留位,必须等于0;
    Type:将H264的NAL头的负载类型Type直接填入。

    +---------------+|0|1|2|3|4|5|6|7|+-+-+-+-+-+-+-+-+|S|E|R| Type |+---------------+

这部分可以结合下个小节代码,多看几篇去理解。


【RTSP从零实践】12、TCP传输H264格式RTP包(RTP_over_TCP)的RTSP服务器(附带源码)

🎄四、RTP_over_TCP的RTSP服务端源码

1、H264Reader.h

/** * @file H264Reader.h * @author https://blog.csdn.net/wkd_007 * @brief * @version 0.1 * @date 2025-06-24 * * @copyright Copyright (c) 2025 * */#ifndef __H264_READER_H__#define __H264_READER_H__#include #define MAX_STARTCODE_LEN (4)typedef enum{ FALSE, TRUE,} BOOL;typedef enum{ H264_NALU_TYPE_SLICE = 1, H264_NALU_TYPE_DPA = 2, H264_NALU_TYPE_DPB = 3, H264_NALU_TYPE_DPC = 4, H264_NALU_TYPE_IDR = 5, H264_NALU_TYPE_SEI = 6, H264_NALU_TYPE_SPS = 7, H264_NALU_TYPE_PPS = 8, H264_NALU_TYPE_AUD = 9, H264_NALU_TYPE_EOSEQ = 10, H264_NALU_TYPE_EOSTREAM = 11, H264_NALU_TYPE_FILL = 12,} H264NaluType;typedef enum{ H264_NALU_PRIORITY_DISPOSABLE = 0, H264_NALU_PRIRITY_LOW = 1, H264_NALU_PRIORITY_HIGH = 2, H264_NALU_PRIORITY_HIGHEST = 3} H264NaluPriority;typedef struct{ int startcode_len; //! 4 for parameter sets and first slice in picture, 3 for everything else (suggested) int forbidden_bit; //! should be always FALSE int nal_reference_idc; //! H264_NALU_PRIORITY_xxxx int nal_unit_type; //! H264_NALU_TYPE_xxxx BOOL isLastFrame; //! int frame_len; //! unsigned char *pFrameBuf; //!} H264Frame_t;typedef struct H264ReaderInfo_s{ FILE *pFileFd; int frameNum;} H264ReaderInfo_t;int H264_FileOpen(char *fileName, H264ReaderInfo_t *pH264Info);int H264_FileClose(H264ReaderInfo_t *pH264Info);int H264_GetFrame(H264Frame_t *pH264Frame, H264ReaderInfo_t *pH264Info);BOOL H264_IsEndOfFile(const H264ReaderInfo_t *pH264Info);void H264_SeekFile(H264ReaderInfo_t *pH264Info);#endif // __H264_READER_H__

2、H264Reader.c

/** * @file H264Reader.c * @author https://blog.csdn.net/wkd_007 * @brief * @version 0.1 * @date 2025-06-30 * * @copyright Copyright (c) 2025 * */#include \"H264Reader.h\"#include #define MAX_FRAME_LEN (1920 * 1080 * 1.5) // 一帧数据最大字节数static BOOL findStartCode_001(unsigned char *Buf){ // printf(\"[%d %d %d]\\n\", Buf[0], Buf[1], Buf[2]); return (Buf[0] == 0 && Buf[1] == 0 && Buf[2] == 1); // 0x000001 ?}static BOOL findStartCode_0001(unsigned char *Buf){ // printf(\"[%d %d %d %d]\\n\", Buf[0], Buf[1], Buf[2], Buf[3]); return (Buf[0] == 0 && Buf[1] == 0 && Buf[2] == 0 && Buf[3] == 1); // 0x00000001 ?}int H264_FileOpen(char *fileName, H264ReaderInfo_t *pH264Info){ pH264Info->pFileFd = fopen(fileName, \"rb+\"); if (pH264Info->pFileFd == NULL) { printf(\"[%s %d]Open file error\\n\", __FILE__, __LINE__); return -1; } pH264Info->frameNum = 0; return 0;}int H264_FileClose(H264ReaderInfo_t *pH264Info){ if (pH264Info->pFileFd != NULL) { fclose(pH264Info->pFileFd); pH264Info->pFileFd = NULL; } return 0;}BOOL H264_IsEndOfFile(const H264ReaderInfo_t *pH264Info){ return feof(pH264Info->pFileFd);}void H264_SeekFile(H264ReaderInfo_t *pH264Info){ fseek(pH264Info->pFileFd, 0, SEEK_SET); pH264Info->frameNum = 0;}/** * @brief 获取一阵h264视频帧 * * @param pH264Frame :输出参数,使用后 pH264Frame->pFrameBuf 需要free * @param pH264Info :输入参数 * @return int */int H264_GetFrame(H264Frame_t *pH264Frame, H264ReaderInfo_t *pH264Info){ int rewind = 0; if (pH264Info->pFileFd == NULL) { printf(\"[%s %d]pFileFd error\\n\", __FILE__, __LINE__); return -1; } // 1.读取帧数据 // unsigned char *pFrame = (unsigned char *)malloc(MAX_FRAME_LEN); unsigned char *pFrame = pH264Frame->pFrameBuf; int readLen = fread(pFrame, 1, MAX_FRAME_LEN, pH264Info->pFileFd); if (readLen <= 0) { printf(\"[%s %d]fread error\\n\", __FILE__, __LINE__); // free(pFrame); return -1; } // 2.查找当前帧开始码 int i = 0; for (; i < readLen - MAX_STARTCODE_LEN; i++) { if (!findStartCode_0001(&pFrame[i])) { if (!findStartCode_001(&pFrame[i])) { continue; } else { pH264Frame->startcode_len = 3; break; } } else { pH264Frame->startcode_len = 4; break; } } if (i != 0) // 不是帧开头,偏移到帧开头重新读 { printf(\"[%s %d]startcode error, i=%d\\n\", __FILE__, __LINE__, i); // free(pFrame); rewind = (-(readLen - i)); fseek(pH264Info->pFileFd, rewind, SEEK_CUR); return -1; } // 3.查找下一帧开始码 i += MAX_STARTCODE_LEN; for (; i < readLen - MAX_STARTCODE_LEN; i++) { if (!findStartCode_0001(&pFrame[i])) { if (!findStartCode_001(&pFrame[i])) { continue; } else { break; } } else { break; } } if (i == (readLen - MAX_STARTCODE_LEN)) { if (!feof(pH264Info->pFileFd)) { printf(\"[%s %d]MAX_FRAME_LEN too small\\n\", __FILE__, __LINE__); // free(pFrame); return -1; } else { pH264Frame->isLastFrame = TRUE; } } // 4.填数据 pH264Frame->forbidden_bit = pFrame[pH264Frame->startcode_len] & 0x80; // 1 bit pH264Frame->nal_reference_idc = pFrame[pH264Frame->startcode_len] & 0x60; // 2 bit pH264Frame->nal_unit_type = pFrame[pH264Frame->startcode_len] & 0x1f; // 5 bit, naluType 是开始码后一个字节的最后 5 位 // pH264Frame->pFrameBuf = pFrame; pH264Frame->frame_len = i; // 5.文件读取指针偏移到下一帧位置 rewind = (-(readLen - i)); fseek(pH264Info->pFileFd, rewind, SEEK_CUR); pH264Info->frameNum++; return pH264Frame->frame_len;}

3、tcp_rtp.h

#ifndef _RTP_H_#define _RTP_H_#include  #define RTP_VESION  2 #define RTP_PAYLOAD_TYPE_H264 96#define RTP_PAYLOAD_TYPE_AAC 97 #define RTP_HEADER_SIZE 12#define RTP_MAX_PKT_SIZE 1400 /* * * 0  1  2  3 * 7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0|7 6 5 4 3 2 1 0 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * |V=2|P|X| CC |M| PT | sequence number | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * | timestamp | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * |  synchronization source (SSRC) identifier | * +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ * | contributing source (CSRC) identifiers | * : .... : * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * */struct RtpHeader{ /* byte 0 */ uint8_t csrcLen:4; uint8_t extension:1; uint8_t padding:1; uint8_t version:2; /* byte 1 */ uint8_t payloadType:7; uint8_t marker:1; /* bytes 2,3 */ uint16_t seq; /* bytes 4-7 */ uint32_t timestamp; /* bytes 8-11 */ uint32_t ssrc;}; struct RtpPacket{ char header[4]; struct RtpHeader rtpHeader; uint8_t payload[0];}; void rtpHeaderInit(struct RtpPacket* rtpPacket, uint8_t csrcLen, uint8_t extension,  uint8_t padding, uint8_t version, uint8_t payloadType, uint8_t marker,  uint16_t seq, uint32_t timestamp, uint32_t ssrc);int rtpSendPacket(int socket, uint8_t rtpChannel, struct RtpPacket* rtpPacket, uint32_t dataSize); #endif //_RTP_H_

4、tcp_rtp.c

#include #include #include #include #include  #include \"tcp_rtp.h\" void rtpHeaderInit(struct RtpPacket* rtpPacket, uint8_t csrcLen, uint8_t extension,  uint8_t padding, uint8_t version, uint8_t payloadType, uint8_t marker,  uint16_t seq, uint32_t timestamp, uint32_t ssrc){ rtpPacket->rtpHeader.csrcLen = csrcLen; rtpPacket->rtpHeader.extension = extension; rtpPacket->rtpHeader.padding = padding; rtpPacket->rtpHeader.version = version; rtpPacket->rtpHeader.payloadType = payloadType; rtpPacket->rtpHeader.marker = marker; rtpPacket->rtpHeader.seq = seq; rtpPacket->rtpHeader.timestamp = timestamp; rtpPacket->rtpHeader.ssrc = ssrc;} int rtpSendPacket(int socket, uint8_t rtpChannel, struct RtpPacket* rtpPacket, uint32_t dataSize){ int ret; rtpPacket->header[0] = \'$\'; rtpPacket->header[1] = rtpChannel; rtpPacket->header[2] = ((dataSize+RTP_HEADER_SIZE) & 0xFF00 ) >> 8; rtpPacket->header[3] = (dataSize+RTP_HEADER_SIZE) & 0xFF; rtpPacket->rtpHeader.seq = htons(rtpPacket->rtpHeader.seq); rtpPacket->rtpHeader.timestamp = htonl(rtpPacket->rtpHeader.timestamp); rtpPacket->rtpHeader.ssrc = htonl(rtpPacket->rtpHeader.ssrc); ret = send(socket, (void*)rtpPacket, dataSize+RTP_HEADER_SIZE+4, 0); rtpPacket->rtpHeader.seq = ntohs(rtpPacket->rtpHeader.seq); rtpPacket->rtpHeader.timestamp = ntohl(rtpPacket->rtpHeader.timestamp); rtpPacket->rtpHeader.ssrc = ntohl(rtpPacket->rtpHeader.ssrc); return ret;}

5、rtsp_h264_tcp_main.c

/** * @file rtsp_h264_tcp_main.c * @author https://blog.csdn.net/wkd_007 * @brief * @version 0.1 * @date 2025-07-10 * * @copyright Copyright (c) 2025 * */#include #include #include #include #include #include #include #include #include #include #include #include \"tcp_rtp.h\"#include \"H264Reader.h\"#define H264_FILE_NAME \"test.h264\"#define FPS 25#define RTSP_PORT 8554#define MAX_CLIENTS 5#define SESSION_ID 10086001#define SESSION_TIMEOUT 60typedef struct{ int rtpSendFd; int rtpPort; int rtpChannel; int bPlayFlag; // 播放标志 char *cliIp;} RTP_Send_t;typedef enum{ RTP_NULL, RTP_PLAY, RTP_PLAYING, RTP_STOP,} RTP_PLAY_STATE;static int rtpSendH264Frame(int socket, int rtpChannel, struct RtpPacket *rtpPacket, uint8_t *frame, uint32_t frameSize){ uint8_t naluType; // nalu第一个字节 int sendBytes = 0; int ret; naluType = frame[0]; if (frameSize <= RTP_MAX_PKT_SIZE) // nalu长度小于最大包场:单一NALU单元模式 { /* * 0 1 2 3 4 5 6 7 8 9 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ * |F|NRI| Type | a single NAL unit ... | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */ memcpy(rtpPacket->payload, frame, frameSize); ret = rtpSendPacket(socket, rtpChannel, rtpPacket, frameSize); if (ret < 0) return -1; rtpPacket->rtpHeader.seq++; sendBytes += ret; if ((naluType & 0x1F) == 7 || (naluType & 0x1F) == 8) // 如果是SPS、PPS就不需要加时间戳 goto out; } else // nalu长度大于最大包场:分片模式 { /* * 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 pktNum = frameSize / RTP_MAX_PKT_SIZE; // 有几个完整的包 int remainPktSize = frameSize % RTP_MAX_PKT_SIZE; // 剩余不完整包的大小 int i, pos = 1; /* 发送完整的包 */ for (i = 0; i < pktNum; i++) { rtpPacket->payload[0] = (naluType & 0x60) | 28; rtpPacket->payload[1] = naluType & 0x1F; if (i == 0)  // 第一包数据 rtpPacket->payload[1] |= 0x80;  // start else if (remainPktSize == 0 && i == pktNum - 1) // 最后一包数据 rtpPacket->payload[1] |= 0x40;  // end memcpy(rtpPacket->payload + 2, frame + pos, RTP_MAX_PKT_SIZE); ret = rtpSendPacket(socket, rtpChannel, rtpPacket, RTP_MAX_PKT_SIZE + 2); if (ret < 0) return -1; rtpPacket->rtpHeader.seq++; sendBytes += ret; pos += RTP_MAX_PKT_SIZE; } /* 发送剩余的数据 */ if (remainPktSize > 0) { rtpPacket->payload[0] = (naluType & 0x60) | 28; rtpPacket->payload[1] = naluType & 0x1F; rtpPacket->payload[1] |= 0x40; // end memcpy(rtpPacket->payload + 2, frame + pos, remainPktSize + 2); ret = rtpSendPacket(socket, rtpChannel, rtpPacket, remainPktSize + 2); if (ret < 0) return -1; rtpPacket->rtpHeader.seq++; sendBytes += ret; } }out: return sendBytes;}void *sendRtp(void *arg){ RTP_Send_t *pRtpSend = (RTP_Send_t *)arg; int rtp_send_fd = pRtpSend->rtpSendFd; int rtpChannel = pRtpSend->rtpChannel; struct RtpPacket *rtpPacket = (struct RtpPacket *)malloc(sizeof(struct RtpPacket) + (1920 * 1080 * 4)); rtpHeaderInit(rtpPacket, 0, 0, 0, RTP_VESION, RTP_PAYLOAD_TYPE_H264, 0,  0, 0, 0x88923423); // h264 H264ReaderInfo_t h264Info; if (H264_FileOpen(H264_FILE_NAME, &h264Info) < 0) { printf(\"failed to open %s\\n\", H264_FILE_NAME); return NULL; } H264Frame_t h264Frame; h264Frame.pFrameBuf = (unsigned char *)malloc(1920 * 1080 * 4); while (pRtpSend->bPlayFlag) { if (!H264_IsEndOfFile(&h264Info)) { h264Frame.isLastFrame = 0; H264_GetFrame(&h264Frame, &h264Info); if (h264Frame.pFrameBuf != NULL) { if (h264Frame.isLastFrame) // 最后一帧,移到开头重新读 {  printf(\"warning SeekFile 1\\n\");  H264_SeekFile(&h264Info); } // printf(\"rtpSendH264Frame, frameNum=%d, time=%u\\n\", h264Info.frameNum, rtpPacket->rtpHeader.timestamp); rtpSendH264Frame(rtp_send_fd, rtpChannel, rtpPacket,  h264Frame.pFrameBuf + h264Frame.startcode_len,  h264Frame.frame_len - h264Frame.startcode_len); rtpPacket->rtpHeader.timestamp += 90000 / FPS; // RTP 传输视频每秒 90k HZ usleep(1000 * 1000 / FPS); } } else { printf(\"warning need SeekFile 1\\n\"); } } free(h264Frame.pFrameBuf); free(rtpPacket); H264_FileClose(&h264Info);}// 解析RTSP请求void rtsp_request_parse(char *buffer, char *method, char *url, int *cseq, int *pRtpChannel){ char *line = strtok(buffer, \"\\r\\n\"); sscanf(line, \"%s %s RTSP/1.0\", method, url); while ((line = strtok(NULL, \"\\r\\n\")) != NULL) { if (strncmp(line, \"CSeq:\", 5) == 0) { sscanf(line, \"CSeq: %d\", cseq); } char *pInterleaved = strstr(line, \"interleaved=\"); if (pInterleaved != NULL) { int rtcpChn = 0; sscanf(pInterleaved, \"interleaved=%d-%d\", pRtpChannel, &rtcpChn); // printf(\"rtpPort: %d-%d\\n\",*pRtpChannel, rtcpChn); } }}// 生成SDP描述const char *generate_sdp(){ return \"v=0\\r\\n\"  \"o=- 0 0 IN IP4 0.0.0.0\\r\\n\"  \"s=Example Stream\\r\\n\"  \"t=0 0\\r\\n\"  \"m=video 0 RTP/AVP 96\\r\\n\"  \"a=rtpmap:96 H264/90000\\r\\n\"  \"a=control:streamid=0\\r\\n\";}void rtsp_handle_OPTION(char *response, int cseq){ sprintf(response, \"RTSP/1.0 200 OK\\r\\n\" \"CSeq: %d\\r\\n\" \"Public: OPTIONS, DESCRIBE, SETUP, PLAY, TEARDOWN\\r\\n\\r\\n\", cseq);}static void rtsp_handle_DESCRIBE(char *response, int cseq){ sprintf(response, \"RTSP/1.0 200 OK\\r\\n\" \"CSeq: %d\\r\\n\" \"Content-Type: application/sdp\\r\\n\" \"Content-Length: %zu\\r\\n\\r\\n%s\", cseq, strlen(generate_sdp()), generate_sdp());}static void rtsp_handle_SETUP(char *response, int cseq, uint8_t rtpChannel){ sprintf(response, \"RTSP/1.0 200 OK\\r\\n\" \"CSeq: %d\\r\\n\" \"Session: %u; timeout=%d\\r\\n\" \"Transport: RTP/AVP/TCP;unicast;interleaved=%hhu-%hhu\\r\\n\\r\\n\", cseq, SESSION_ID, SESSION_TIMEOUT, rtpChannel, rtpChannel + 1);}static void rtsp_handle_PLAY(char *response, int cseq){ sprintf(response, \"RTSP/1.0 200 OK\\r\\n\" \"CSeq: %d\\r\\n\" \"Session: %u; timeout=%d\\r\\n\" \"Range: npt=0.000-\\r\\n\\r\\n\", cseq, SESSION_ID, SESSION_TIMEOUT);}static void rtsp_handle_TEARDOWN(char *response, int cseq){ sprintf(response, \"RTSP/1.0 200 OK\\r\\n\" \"CSeq: %d\\r\\n\" \"Session: %d; timeout=%d\\r\\n\\r\\n\", cseq, SESSION_ID, SESSION_TIMEOUT);}// 处理客户端连接int handle_client(int cli_fd, char *cli_ip){ int  client_sock = cli_fd; char buffer[1024] = {0}; int  cseq = 0; int  rtpChn = 0; unsigned char bSendFlag = RTP_NULL; RTP_Send_t rtpSend; pthread_t thread_id; while (1) { memset(buffer, 0, sizeof(buffer)); int len = read(client_sock, buffer, sizeof(buffer) - 1); if (len <= 0) break; printf(\"C->S [\\n%s]\\n\\n\", buffer); char method[16] = {0}; char url[128] = {0}; rtsp_request_parse(buffer, method, url, &cseq, &rtpChn); char response[1024] = {0}; // 构造响应 if (strcmp(method, \"OPTIONS\") == 0) { rtsp_handle_OPTION(response, cseq); } else if (strcmp(method, \"DESCRIBE\") == 0) { rtsp_handle_DESCRIBE(response, cseq); } else if (strcmp(method, \"SETUP\") == 0) { rtsp_handle_SETUP(response, cseq, rtpChn); } else if (strcmp(method, \"PLAY\") == 0) { rtsp_handle_PLAY(response, cseq); bSendFlag = RTP_PLAY; } else if (strcmp(method, \"TEARDOWN\") == 0) { rtsp_handle_TEARDOWN(response, cseq); bSendFlag = RTP_STOP; } else { snprintf(response, sizeof(response),  \"RTSP/1.0 501 Not Implemented\\r\\nCSeq: %d\\r\\n\\r\\n\", cseq); } write(client_sock, response, strlen(response)); printf(\"S->C [\\n%s]\\n\\n\", response); if (bSendFlag == RTP_PLAY) // PLAY { rtpSend.rtpSendFd = cli_fd; rtpSend.rtpPort = 0; rtpSend.rtpChannel = rtpChn; rtpSend.cliIp = NULL; rtpSend.bPlayFlag = 1; // 这里不使用线程的话,会一直无法处理 client_sock 发过来的 OPTION 消息,导致播放出问题 if (pthread_create(&thread_id, NULL, (void *)sendRtp, (void *)&rtpSend) < 0) { perror(\"pthread_create\"); } bSendFlag = RTP_PLAYING; } if (bSendFlag == RTP_STOP) // TEARDOWN { rtpSend.bPlayFlag = 0; pthread_join(thread_id); // 等待线程结束 bSendFlag = RTP_NULL; break; } } printf(\"close ip=[%s] fd=[%d]\\n\", cli_ip, client_sock); close(client_sock); return 0;}int main(){ int server_fd, client_fd; struct sockaddr_in address; int opt = 1; socklen_t addrlen = sizeof(address); // 创建套接字 if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { perror(\"socket failed\"); return -1; } // 设置套接字选项 if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) { perror(\"setsockopt\"); return -1; } address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(RTSP_PORT); // 绑定端口 if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) { perror(\"bind failed\"); return -1; } // 开始监听 if (listen(server_fd, MAX_CLIENTS) < 0) { perror(\"listen\"); return -1; } printf(\"RTSP Server listening on port %d\\n\", RTSP_PORT); // 主循环接受连接,目前处理一个客户端 while (1) { char cli_ip[40] = {0}; if ((client_fd = accept(server_fd, (struct sockaddr *)&address, &addrlen)) < 0) { perror(\"accept\"); return -1; } strncpy(cli_ip, inet_ntoa(address.sin_addr), sizeof(cli_ip)); printf(\"handle cliend [%s]\\n\", cli_ip); handle_client(client_fd, cli_ip); } return 0;}

首先设置一下VLC,工具->偏好设置->输入/编解码器,勾选如下图的RTP over RTSP(TCP)
【RTSP从零实践】12、TCP传输H264格式RTP包(RTP_over_TCP)的RTSP服务器(附带源码)

将上面代码保存在同一个目录后,并且在同目录里放一个.h264文件,然后运行 gcc *.c -lpthread 编译,再执行./a.out运行程序,下面是我运行的过程:
【RTSP从零实践】12、TCP传输H264格式RTP包(RTP_over_TCP)的RTSP服务器(附带源码)


【RTSP从零实践】12、TCP传输H264格式RTP包(RTP_over_TCP)的RTSP服务器(附带源码)

🎄五、、总结

本文介绍了RTP_over_TCP的一些概念,以及TCP传输H264RTP包的RTSP服务器实现的步骤和细节,最后提供了实现的源代码,帮助读者学习理解。

在这里插入图片描述
如果文章有帮助的话,点赞👍、收藏⭐,支持一波,谢谢 😁😁😁

参考:
https://blog.csdn.net/huabiaochen/article/details/104582872