【RTSP從零實踐】8、多播傳輸H264碼流的RTSP服務器——最簡單的實現例子(附帶源碼)

😁博客主頁😁:🚀https://blog.csdn.net/wkd_007🚀
🤑博客內容🤑:🍭嵌入式開發、Linux、C語言、C++、數據結構、音視頻🍭
🤣本文內容🤣:🍭介紹 🍭
😎金句分享😎:🍭你不能選擇最好的,但最好的會來選擇你——泰戈爾🍭
?發布時間?: 2025-07-07

本文未經允許,不得轉發!!!

目錄

  • 🎄一、概述
  • 🎄二、多播的概念
  • 🎄三、實現步驟、實現細節
  • 🎄四、多播傳輸H264格式的RTP包的實現源碼
  • 🎄五、總結


在這里插入圖片描述

前面系列文章回顧:
【音視頻 | RTSP】RTSP協議詳解 及 抓包例子解析(詳細而不贅述)
【音視頻 | RTSP】SDP(會話描述協議)詳解 及 抓包例子分析
【音視頻 | RTP】RTP協議詳解(H.264的RTP封包格式、AAC的RTP封包格式)
【RTSP從零實踐】1、根據RTSP協議實現一個RTSP服務
【RTSP從零實踐】2、使用RTP協議封裝并傳輸H264
【RTSP從零實踐】3、實現最簡單的傳輸H264的RTSP服務器
【RTSP從零實踐】4、使用RTP協議封裝并傳輸AAC
【RTSP從零實踐】5、實現最簡單的傳輸AAC的RTSP服務器
【RTSP從零實踐】6、實現最簡單的同時傳輸H264、AAC的RTSP服務器

在這里插入圖片描述

🎄一、概述

上篇文章介紹了使用多播來傳輸RTP包,這篇繼續介紹使用一個多播的方式來傳輸H264碼流的RTSP服務器,讀者閱讀的時候可以將這篇文章的代碼與之前的文章 【RTSP從零實踐】3、實現最簡單的傳輸H264的RTSP服務器 進行對比,主要改了兩點:

  • 1、將UDP單播發送的方式改為UDP多播發送;
  • 2、處理RTSP命令時,返回多播的相關信息。

下面章節先是介紹多播的一些簡單概念,然后介紹多播的方式處理RTSP命令時的注意點。


在這里插入圖片描述

🎄二、多播的概念

關于多播的概念可以參考這篇文章:多播的概念、多播地址、UDP實現多播的C語言例子。下面只簡單介紹一下多播。

IP 多播(也稱多址廣播或組播)技術,是允許一臺主機 向 多臺主機 發送消息的一種通信方式。單播只向單個IP接口發送數據,廣播是向子網內所有IP接口發送數據,多播則介于兩者之間,向一組IP接口發送數據。

多播地址:用來標識多播組,IPv4使用D類地址的某一個來表示一個多播組地址。IPv4的D類地址(從224.0.0.0239.255.255.255)是IPv4多播地址,見下圖:

多播發送端:用于發送多播數據報的程序,下面是一個簡單的多播發送端例子的代碼。基本上就是在發送數據包時,將目的地址設置為 多播地址

// multicastCli.c
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>int main()
{// 1、創建UDP套接字socketint sockfd = socket(AF_INET, SOCK_DGRAM, 0);if(sockfd<0)perror("socket error" );// 2、準備多播組地址和端口struct sockaddr_in servaddr;bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_port = htons (10086);if (inet_pton(AF_INET, "239.0.1.1", &servaddr.sin_addr) <= 0)perror("inet_pton error");// 4、使用 sendto 發送多播組數據報if(sendto(sockfd, "Hello,I am udp client", strlen("Hello,I am udp client"), 0, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)perror("sendto error" );// 5、處理應答char recvline[256];int n = 0;struct sockaddr_in tmpAddr;bzero(&tmpAddr, sizeof(tmpAddr));socklen_t addrLen=sizeof(tmpAddr);while ( (n = recvfrom (sockfd, recvline, sizeof(recvline), 0, (struct sockaddr*)&tmpAddr, &addrLen)) > 0){recvline[n] = 0 ;/*null terminate */printf("recvfrom ip=[%s], [%s]\n",inet_ntoa(tmpAddr.sin_addr), recvline);bzero(&tmpAddr, sizeof(tmpAddr));}if (n < 0)perror("read error" );// 6、關閉close(sockfd);return 0;
}

多播接收端:接收端是使用UDP服務端代碼修改,需要在交互數據之前,將套接字加入多播組239.0.1.1,讓鏈路層接口接收該多播組的數據報,使用完需要離開多播組。下面是一個多播接收端代碼:

// multicastSer.c
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>int main()
{// 1、創建UDP套接字socketint sockfd = socket(AF_INET, SOCK_DGRAM, 0);if(sockfd<0)perror("socket error" );// 2、準備本地ip接口和多播組端口struct sockaddr_in servaddr;bzero(&servaddr, sizeof(servaddr));servaddr.sin_family = AF_INET;servaddr.sin_port = htons (10086);servaddr.sin_addr.s_addr = INADDR_ANY; // 指定ip地址為 INADDR_ANY,這樣要是服務器主機有多個網絡接口,服務器進程就可以在任一網絡接口上接受客戶端的連接// 3、綁定多播組端口 bindif (bind(sockfd,(struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)perror("bind error" );// 4、加入多播組 239.0.1.1struct ip_mreq mreq;mreq.imr_multiaddr.s_addr = inet_addr("239.0.1.1");	// 多播組的IP地址mreq.imr_interface.s_addr = htonl(INADDR_ANY);		// 加入的客服端主機IP地址if (setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) == -1)    {perror("setsockopt");return -1;}// 5、使用 sendto、recvfrom 交互數據printf("UdpSer sockfd=%d, start \n",sockfd);char recvline[256];while(1){struct sockaddr_in cliaddr;bzero(&cliaddr, sizeof(cliaddr));socklen_t addrLen=sizeof(cliaddr);int n = recvfrom(sockfd, recvline, sizeof(recvline), 0, (struct sockaddr*)&cliaddr, &addrLen);if(n>0){recvline[n] = 0 ;/*null terminate */printf("recv sockfd=%d %d byte, [%s] addrLen=%d, cliIp=%s, cliPort=%d\n",sockfd, n, recvline, addrLen, inet_ntoa(cliaddr.sin_addr),cliaddr.sin_port);sendto(sockfd, "Hello,I am udp server", strlen("Hello,I am udp server"), 0, (struct sockaddr*)&cliaddr, addrLen);}}// 6、離開多播組setsockopt(sockfd, IPPROTO_IP, IP_DROP_MEMBERSHIP,&mreq, sizeof(mreq)); // 7、關閉close(sockfd);return 0;
}

在這里插入圖片描述

🎄三、實現步驟、實現細節

多播傳輸H264格式的RTSP服務器的實現步驟如下:
1、實現 H264文件讀取器。
2、實現 H264 的 RTP 數據包封裝。
3、實現多播發送RTP包。
4、處理RTSP命令并發送碼流。

前兩點在文章 使用RTP協議封裝并傳輸H264 介紹得很詳細,需要看的童鞋,留言一下,我給你復制過來。😑

實現多播發送RTP包,我們只需要在UDP單播發送的代碼里將UDP的目的地址改為我們要使用的多播組地址即可,我們這里要使用的是"239.0.0.1"

下面內容是多播方式處理RTSP命令的過程:

  • OPTION
    處理OPTION消息時,直接返回本服務器支持的方法即可,沒什么特別的:
    在這里插入圖片描述

  • DESCRIBE
    在這里插入圖片描述

  • SETUP
    在這里插入圖片描述

  • PLAY
    在這里插入圖片描述

  • TEARDOWN
    在這里插入圖片描述


在這里插入圖片描述

🎄四、多播傳輸H264格式的RTP包的實現源碼

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

/*** @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 <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);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 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、multicast_rtp_h264_main

/*** @file multicast_rtsp_h264_main.c* @author : https://blog.csdn.net/wkd_007* @brief* @version 0.1* @date 2025-07-07** @copyright Copyright (c) 2025**/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <fcntl.h>#include "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 60#define MULTICAST_IP    "239.0.0.1"
#define MULTICAST_PORT  55666typedef struct
{int   rtpSendFd;int   rtpPort;int   bPlayFlag; // 播放標志char *cliIp;
} RTP_Send_t;typedef enum
{RTP_NULL,RTP_PLAY,RTP_PLAYING,RTP_STOP,
} RTP_PLAY_STATE;static 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;
}void *sendRtp(void *arg)
{RTP_Send_t *pRtpSend    = (RTP_Send_t *)arg;int         rtp_send_fd = pRtpSend->rtpSendFd;int         rtpPort     = pRtpSend->rtpPort;char       *cli_ip      = pRtpSend->cliIp;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 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, cli_ip, rtpPort, rtpPacket,h264Frame.pFrameBuf + h264Frame.startcode_len,h264Frame.frame_len - h264Frame.startcode_len);rtpPacket->rtpHeader.timestamp += 90000 / FPS; // RTP 傳輸視頻每秒 90k HZusleep(1000 * 1000 / FPS);}}else{printf("warning need SeekFile 1\n");}}free(h264Frame.pFrameBuf);free(rtpPacket);H264_FileClose(&h264Info);
}// 解析RTSP請求
static void rtsp_request_parse(char *buffer, char *method, char *url, int *cseq, int *pRtpPort)
{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 *pCliPort = strstr(line, "client_port=");if (pCliPort != NULL){int rtcpPort = 0;sscanf(pCliPort, "client_port=%d-%d", pRtpPort, &rtcpPort);// printf("rtpPort: %d-%d\n",*pRtpPort, rtcpPort);}}
}// 生成SDP描述
char        g_sdp[512] = {0};
const char *generate_sdp()
{memset(g_sdp, 0, sizeof(g_sdp));sprintf(g_sdp,"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""a=type:broadcast\r\n""a=rtcp-unicast: reflection\r\n""m=video %d RTP/AVP 96\r\n""c=IN IP4 %s/255\r\n""a=rtpmap:96 H264/90000\r\n""a=control:streamid=0\r\n",MULTICAST_PORT, MULTICAST_IP);return g_sdp;
}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, char *url)
{char localIp[32];sscanf(url, "rtsp://%[^:]:", localIp);sprintf(response,"RTSP/1.0 200 OK\r\n""CSeq: %d\r\n""Session: %u; timeout=%d\r\n""Transport: RTP/AVP;multicast;destination=%s;source=%s;port=%d-%d;ttl=255\r\n\r\n",cseq, SESSION_ID, SESSION_TIMEOUT, MULTICAST_IP, localIp, MULTICAST_PORT, MULTICAST_PORT + 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, int rtp_send_fd, char *cli_ip)
{int           client_sock  = cli_fd;char          buffer[1024] = {0};int           cseq         = 0;int           rtpPort      = 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, &rtpPort);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, url);}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 = rtp_send_fd;rtpSend.rtpPort   = MULTICAST_PORT;rtpSend.cliIp     = MULTICAST_IP;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;}// 用于發送 rtp 包的udp套接字,不需要綁定端口int rtp_send_fd = createUdpSocket();if (rtp_send_fd < 0){printf("failed to create socket\n");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, rtp_send_fd, cli_ip);}return 0;
}

將上面代碼保存在同一個目錄后,并且在同目錄里放一個.h264文件,然后運行 gcc *.c -lpthread 編譯,再執行./a.out運行程序,下面是我運行的過程:

在這里插入圖片描述


在這里插入圖片描述

🎄五、總結

本文介紹了多播的一些概念,以及多播傳輸H264碼流的RTSP服務器實現的步驟和細節,最后提供了實現的源代碼,幫助讀者學習理解。

在這里插入圖片描述
如果文章有幫助的話,點贊👍、收藏?,支持一波,謝謝 😁😁😁

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/90319.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/90319.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/90319.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【Linux】基礎開發工具(3)

1. 版本控制器Git1.1 Git的簡史Git 的歷史可以追溯到 2005 年1。當時 Linux 內核項目的開發團隊一直使用 BitKeeper 進行版本管理&#xff0c;但由于一位 Linux 開發成員寫了一個連接 BitKeeper 倉庫的外掛&#xff0c;BitMover 公司決定中止 Linux 免費使用 BitKeeper 的授權1…

synchronized 的使用和特性

synchronized 鎖對象 普通方法 synchronized 鎖普通方法時&#xff0c;其鎖的對象是調用該方法的實例 public synchronized void method() { // 方法體 } 靜態方法 靜態方法的鎖對象是所屬的 class&#xff0c;全局只有一個。 public static synchronized void staticMetho…

Gin Web 層集成 Viper 配置文件和 Zap 日志文件指南(下)

在微服務架構中&#xff0c;Gin 常被用作 Web 層框架&#xff0c;而 Viper 用于管理配置文件&#xff0c;Zap 則提供高性能的日志記錄功能。下面將詳細介紹如何在 Gin Web 層集成 Viper 配置文件和 Zap 日志文件。 1. 項目概述 假設我們有一個基于 Go 語言的微服務項目&#…

IoTDB:專為物聯網場景設計的高性能時序數據庫

什么是IoTDB&#xff1f;IoTDB&#xff08;Internet of Things Database&#xff09;是一款開源的時序數據庫管理系統&#xff0c;專為物聯網&#xff08;IoT&#xff09;場景設計&#xff0c;由清華大學軟件學院團隊自研&#xff0c;天謀科技團隊負責維護。它針對物聯網數據的…

[netty5: MessageAggregator HttpObjectAggregator]-源碼解析

在閱讀這篇文章前&#xff0c;推薦先閱讀 [netty5: ByteToMessageCodec & MessageToByteEncoder & ByteToMessageDecoder]-源碼分析[netty5: HttpObject]-源碼解析 100-continue 100-continue 是 HTTP/1.1 協議中的一種機制&#xff0c;用于客戶端在發送大體積請求體…

前端學習1--行內元素 vs 塊級元素(基礎概念+案例實操)

一、內外邊距學習&#xff1a;&#xff08;1&#xff09;簡單理解&#xff1a;padding為內邊距。padding不會影響元素的位置&#xff0c;只會調整元素的內容&#xff08;文字&#xff09;與邊框之間的間距。margin為外邊距。margin會影響元素在流式布局中的位置&#xff0c;改變…

Express + mysql2 + jwt 實現簡單的登錄鑒權

目前項目中使用Express 實現簡單API功能&#xff0c;需要提供一套登錄鑒權方案。這邊是API側實現 相關路由的登錄鑒權。大體思路&#xff1a;就是&#xff0c;登錄接口中通過jwt加密 token返回前端&#xff0c;前端其他接口把加密好的放入請求頭Authorization中。中間件通過請求…

ReAct (Reason and Act) OR 強化學習(Reinforcement Learning, RL)

這個問題觸及了現代AI智能體&#xff08;Agent&#xff09;構建的兩種核心思想。 簡單來說&#xff0c;ReAct 是一種“調用專家”的模式&#xff0c;而強化學習 (RL) 是一種“從零試錯”的模式。 為了讓你更清晰地理解&#xff0c;我們從一個生動的比喻開始&#xff0c;然后進行…

iTwinjs 4.10-4.11 更新

撤銷更改 目前&#xff0c;撤銷一個有缺陷的變更集的唯一方法是從 iModel Hub 中移除它&#xff0c;這可能會導致許多副作用&#xff08;無法撤銷&#xff09;。一個更好的方法是在時間線中撤銷變更集&#xff0c;并將其作為新的變更集引入。盡管這種方法仍然具有侵入性&#…

【CSS-15】深入理解CSS transition-duration:掌握過渡動畫的時長控制

在現代網頁設計中&#xff0c;平滑的過渡效果是提升用戶體驗的關鍵因素之一。CSS transitions 為我們提供了一種簡單而強大的方式來實現元素在不同狀態之間的平滑過渡&#xff0c;而 transition-duration 屬性則是控制這些過渡效果時長的核心工具。本文將全面探討 transition-d…

mysql-筆記

1. 安裝mysql # 使用brew安裝 brew install mysql# 查看是否安裝成功 mysql -V 相關文檔&#xff1a; mac&#xff1a;macOS下MySQL 8.0 安裝與配置教程 - KenTalk - 博客園 Linux安裝&#xff1a;linux安裝mysql客戶端_linux mysql 客戶端-CSDN博客 2. 啟動mysql 每次使…

Spring Boot啟動優化7板斧(延遲初始化、組件掃描精準打擊、JVM參數調優):砍掉70%啟動時間的魔鬼實踐

Spring Boot啟動優化7板斧&#xff1a;砍掉70%啟動時間的魔鬼實踐1. 延遲初始化&#xff1a;按需加載的智慧2. 組件掃描精準打擊&#xff1a;告別無差別掃描3. JVM參數調優&#xff1a;啟動加速的隱藏開關4. 自動配置瘦身&#xff1a;砍掉Spring Boot的"贅肉"5. 類加…

從0開始學習計算機視覺--Day08--卷積神經網絡

之前我們提到&#xff0c;神經網絡是通過全連接層對輸入做降維處理&#xff0c;將輸入的向量通過矩陣和激活函數進行降維&#xff0c;在神經元上輸出激活值。而卷積神經網絡中&#xff0c;用卷積層代替了全連接層。 不同的是&#xff0c;這里的輸入不再需要降維&#xff0c;而…

解決阿里云ubuntu內存溢出導致vps死機無法訪問 - 永久性增加ubuntu的swap空間 - 阿里云Linux實例內存溢出(OOM)問題修復方案

效果圖報錯通過對實例當前截屏的分析發現&#xff0c;實例因 Linux實例內存空間不足&#xff0c;導致操作系統出現內存溢出&#xff08;OOM&#xff09; 無法正常啟動。請您根據 Code&#xff1a;1684829582&#xff0c;在文檔中查詢該問題對應的修復方案&#xff0c;并通過VNC…

Serverless JManus: 企業生產級通用智能體運行時

作者&#xff1a;叢霄、陸龜 概述&#xff1a;本文介紹如何使用 JManus 框架構建通用智能體應用&#xff0c;部署并運行在 Serverless 運行時&#xff0c;構建企業級高可用智能體應用的實踐經驗。基于阿里云 Serverless 應用引擎SAE 運行穩定高可用的智能體應用&#xff0c; 基…

MySQL的數據目錄

導讀&#xff1a;根據前面的所學知識&#xff0c;我們知道了InnoDB存儲引擎存儲數據的數據結構、存儲過程&#xff0c;而被組織好的數據則被存儲在操作系統的磁盤上&#xff0c;當我們在對表數據進行增刪改查時&#xff0c;其實就是InnoDB存儲引擎與磁盤的交互。此外&#xff0…

Web前端開發: :has功能性偽類選擇器

:has功能性偽類選擇器::has() 是 CSS 中的一個功能性偽類選擇器&#xff0c;它允許開發者根據元素的后代元素、兄弟元素或后續元素的存在或狀態來選擇目標元素。它本質上是一個“父選擇器”或“關系選擇器”&#xff0c;解決了 CSS 長期以來無法根據子元素反向選擇父元素的痛點…

深度學習8(梯度下降算法改進2)

目錄 RMSProp 算法 Adam算法 學習率衰減 RMSProp 算法 RMSProp(Root Mean Square Prop)算法是在對梯度進行指數加權平均的基礎上&#xff0c;引入平方和平方根。 其中e是一個非常小的數&#xff0c;防止分母太小導致不穩定,當 dw 或 db 較大時&#xff0c;(du)2,(db)2會較大&…

JAVA面試寶典 -《網絡編程核心:NIO 與 Netty 線程模型詳解》

網絡編程核心&#xff1a;NIO 與 Netty 線程模型詳解 文章目錄網絡編程核心&#xff1a;NIO 與 Netty 線程模型詳解一、傳統 BIO 模型&#xff1a;排隊買奶茶的阻塞模式 &#x1f964;1.1 專業解釋1.2 簡單點比喻1.3 簡單示例二、NIO 模型&#xff1a;智能叫號餐廳系統 &#x…

藍橋杯 第十六屆(2025)真題思路復盤解析

本文以洛谷平臺所提供的題目描述及評測數據為基礎進行講解。 前言&#xff1a;這是本人的藍橋杯試卷&#xff0c;大概排省一前40%的位置&#xff0c;實際上這屆題目偏難&#xff0c;我沒有做出太多的有效得分。我把當時的思路和現在學習的思路都復盤進來&#xff0c;希望給大家…