😁博客主頁😁:🚀https://blog.csdn.net/wkd_007🚀
🤑博客內容🤑:🍭嵌入式開發、Linux、C語言、C++、數據結構、音視頻🍭
🤣本文內容🤣:🍭介紹怎么使用RTP協議封裝并傳輸H264🍭
😎金句分享😎:🍭你不能選擇最好的,但最好的會來選擇你——泰戈爾🍭
?發布時間?: 2025-06-07
本文未經允許,不得轉發!!!
目錄
- 🎄一、概述
- 🎄二、實現步驟、實現細節
- ?2.1、實現H.264文件讀取器
- ?2.2、實現 H264 的 RTP 數據包封裝
- 🎈2.2.1、RTP頭(RTP Header)
- 🎈2.2.2、H264 的 RTP負載(RTP Payload)
- ?2.3、SDP協議介紹
- 🎄三、RTP協議封裝并傳輸H264的實現源碼
- 🎄四、總結
🎄一、概述
上篇文章 根據RTSP協議實現一個RTSP服務,介紹了怎么寫一個RTSP服務端,但處理的過程中,沒有介紹怎么發送RTP包。這篇文章主要就是介紹怎么將H264視頻數據封裝成RTP包并發送的。
首先,需要對 H264的文件結構 有了解,我們要從一個H264文件讀取視頻幀,再進行RTP封包發送。本文會介紹需要用到的h264知識,需要了解更詳細的可以參考以前文章:H.264視頻編碼及NALU詳解。
然后,需要對 RTP包結構 有個了解,本文介紹的RTP包由2部分組成:RTP頭、RTP負載。需要了解RTP頭數據結構、RTP負載數據結構,下個小節會介紹怎樣用代碼實現。對RTP數據包格式需要了解更多的可以參考這篇文章:RTP協議詳解 。
最后,需要對 SDP協議 有了解,后面代碼運行后,需要使用vlc打開.sdp
文件來驗證功能,所以需要知道一個sdp文件的內容是什么意思,這個將會在2.3小節介紹,如果需要了解更多sdp協議知識,可以看這篇文章:SDP(會話描述協議)詳解 及 抓包例子分析。
🎄二、實現步驟、實現細節
這個小寫介紹一些基礎知識點,下個小節會提供源碼,可以結合著源碼看幫助理解消化。
?2.1、實現H.264文件讀取器
H.264文件保存了h264編碼的視頻幀,每個視頻幀之間以開始碼00 00 01
或00 00 00 01
分隔開。我們可以用下面代碼判斷是否為開始碼。
在兩個開始碼之間的就是視頻幀數據。h264視頻幀數據的第一個字節是一個NAL頭,內容如下圖:
可以用下面代碼讀取NAL頭:
以上H264文件結構需要了解的知識,完整代碼可以到第三節查看。
?2.2、實現 H264 的 RTP 數據包封裝
本文介紹的RTP數據包主要由2部分組成,即 RTP頭、RTP負載。
🎈2.2.1、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時間戳的值加上一個時鐘增量。
🎈2.2.2、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 indicator
:FU 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 | +---------------+
這部分可以結合下個小節代碼,多看幾篇去理解。
?2.3、SDP協議介紹
本文是使用VLC打開.sdp
文件來驗證功能的,所以也需要知道SDP協議的一些知識,下面是本文使用的sdp文件:
m=video 9832 RTP/AVP 96
a=rtpmap:96 H264/90000
a=framerate:25
c=IN IP4 192.168.2.180
-
m=video 9832 RTP/AVP 96
:表示這是一個媒體描述,媒體類型是video;接收媒體流的端口號是9832;傳輸協議是RTP/AVP
(通過UDP傳輸);RTP負載類型為96。 -
a=rtpmap:96 H264/90000
:RTP負載類型的具體編碼是 H264,時鐘頻率為 90000Hz。 -
a=framerate:25
:幀率為25。 -
c=IN IP4 192.168.2.180
:表示連接信息,網絡類型為IN,表示Internet。地址類型為IP4,表示IPv4地址。接收端地址為192.168.2.180。
🎄三、RTP協議封裝并傳輸H264的實現源碼
總共有5個源代碼文件,1個sdp文件,具體如下:
1、H264Reader.h
#ifndef __H264_READER_H__
#define __H264_READER_H__#include <stdio.h>#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 FALSEint nal_reference_idc; //! H264_NALU_PRIORITY_xxxxint nal_unit_type; //! H264_NALU_TYPE_xxxxBOOL 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
#include "H264Reader.h"
#include <stdlib.h>#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);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 bitpH264Frame->nal_reference_idc = pFrame[pH264Frame->startcode_len] & 0x60; // 2 bitpH264Frame->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、rtp.h
#ifndef _RTP_H_
#define _RTP_H_
#include <stdint.h>#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
{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, char* ip, int16_t port, struct RtpPacket* rtpPacket, uint32_t dataSize);#endif //_RTP_H_
4、rtp.c
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "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, char* ip, int16_t port, struct RtpPacket* rtpPacket, uint32_t dataSize)
{struct sockaddr_in addr;int ret;addr.sin_family = AF_INET;addr.sin_port = htons(port);addr.sin_addr.s_addr = inet_addr(ip);rtpPacket->rtpHeader.seq = htons(rtpPacket->rtpHeader.seq);rtpPacket->rtpHeader.timestamp = htonl(rtpPacket->rtpHeader.timestamp);rtpPacket->rtpHeader.ssrc = htonl(rtpPacket->rtpHeader.ssrc);ret = sendto(socket, (void*)rtpPacket, dataSize+RTP_HEADER_SIZE, 0,(struct sockaddr*)&addr, sizeof(addr));rtpPacket->rtpHeader.seq = ntohs(rtpPacket->rtpHeader.seq);rtpPacket->rtpHeader.timestamp = ntohl(rtpPacket->rtpHeader.timestamp);rtpPacket->rtpHeader.ssrc = ntohl(rtpPacket->rtpHeader.ssrc);return ret;
}
5、rtp_h264_main.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>#include "rtp.h"
#include "H264Reader.h"#define H264_FILE_NAME "test.h264"
#define CLIENT_IP "192.168.2.180" // 運行vlc打開sdp文件的電腦IP
#define CLIENT_PORT 9832
#define FPS 25static int createUdpSocket()
{int fd = socket(AF_INET, SOCK_DGRAM, 0);if (fd < 0)return -1;int on = 1;setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (const char *)&on, sizeof(on));return fd;
}static int rtpSendH264Frame(int socket, char *ip, int16_t port,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, ip, port, 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; // startelse if (remainPktSize == 0 && i == pktNum - 1) // 最后一包數據rtpPacket->payload[1] |= 0x40; // endmemcpy(rtpPacket->payload + 2, frame + pos, RTP_MAX_PKT_SIZE);ret = rtpSendPacket(socket, ip, port, 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; // endmemcpy(rtpPacket->payload + 2, frame + pos, remainPktSize + 2);ret = rtpSendPacket(socket, ip, port, rtpPacket, remainPktSize + 2);if (ret < 0)return -1;rtpPacket->rtpHeader.seq++;sendBytes += ret;}}out:return sendBytes;
}int main(int argc, char *argv[])
{int socket = createUdpSocket();if (socket < 0){printf("failed to create socket\n");return -1;}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);// h264H264ReaderInfo_t h264Info;if (H264_FileOpen(H264_FILE_NAME, &h264Info) < 0){printf("failed to open %s\n", H264_FILE_NAME);return -1;}while (1){if (!H264_IsEndOfFile(&h264Info)){H264Frame_t h264Frame;memset(&h264Frame, 0, sizeof(h264Frame));H264_GetFrame(&h264Frame, &h264Info);if (h264Frame.pFrameBuf != NULL){if (h264Frame.isLastFrame) // 最后一幀,移到開頭重新讀{printf("warning SeekFile 1\n");H264_SeekFile(&h264Info);}// printf("rtpSendH264Frame\n");rtpSendH264Frame(socket, CLIENT_IP, CLIENT_PORT, rtpPacket,h264Frame.pFrameBuf + h264Frame.startcode_len,h264Frame.frame_len - h264Frame.startcode_len);free(h264Frame.pFrameBuf);rtpPacket->rtpHeader.timestamp += 90000 / FPS; // RTP 傳輸視頻每秒 90k HZusleep(1000 * 1000 / FPS);}}}free(rtpPacket);return 0;
}
6、rtp_h264.sdp
m=video 9832 RTP/AVP 96
a=rtpmap:96 H264/90000
a=framerate:25
c=IN IP4 192.168.2.180
將上面代碼保存在同一個目錄后,并且在同目錄里放一個.h264文件,然后運行 gcc *.c
編譯,再執行./a.out
運行程序,下面是我運行的過程:
🎄四、總結
本文介紹了實現 使用RTP協議封裝并傳輸H264 的一些步驟和細節,介紹了RTP封包格式,sdp相關知識等,也提供了實現源碼和運行結果,可以幫助讀者快速了解RT協議。
如果文章有幫助的話,點贊👍、收藏?,支持一波,謝謝 😁😁😁
參考:
https://blog.csdn.net/huabiaochen/article/details/104550107