RTSP学习

RTSP 协议交互流程(基础示例)

RTSP(Real Time Streaming Protocol,实时流传输协议)是一个应用层控制协议,主要用于实时音视频流的控制(如播放、暂停、快进等),常用于视频监控、IP摄像头、流媒体点播/直播场景。

  • RTSP 本身不传输媒体数据 ,只负责信令交互(请求与响应)。
  • 实际的音视频数据由 RTP 承载传输。
  • RTCP 负责对 RTP 传输质量进行监控和反馈。

RTSP 客户端与服务器之间的标准交互过程如下:

1. OPTIONS - 查询服务器能力

html 复制代码
OPTIONS rtsp://example.com/media.mp4 RTSP/1.0
CSeq: 1

服务器响应:

html 复制代码
RTSP/1.0 200 OK
CSeq: 1
Public: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE

2. DESCRIBE - 获取媒体描述(SDP)

html 复制代码
DESCRIBE rtsp://example.com/media.mp4 RTSP/1.0
CSeq: 2
Accept: application/sdp

服务器响应(SDP 信息):

html 复制代码
RTSP/1.0 200 OK
CSeq: 2
Content-Type: application/sdp
Content-Length: ...

v=0
o=- 123456 123456 IN IP4 192.168.1.1
s=Example Stream
t=0 0
a=control:*
m=video 49170 RTP/AVP 96
c=IN IP4 239.255.255.255
a=rtpmap:96 H264/90000

3. SETUP - 建立传输通道

html 复制代码
SETUP rtsp://example.com/media.mp4/trackID=0 RTSP/1.0
CSeq: 3
Transport: RTP/AVP;unicast;client_port=49170-49171

服务器响应:

html 复制代码
RTSP/1.0 200 OK
CSeq: 3
Session: 12345678
Transport: RTP/AVP;unicast;server_port=49172-49173

4. PLAY - 开始播放

html 复制代码
PLAY rtsp://example.com/media.mp4 RTSP/1.0
CSeq: 4
Session: 12345678
Range: npt=0.000-

服务器响应:

html 复制代码
RTSP/1.0 200 OK
CSeq: 4
Session: 12345678
RTP-Info: url=rtsp://example.com/media.mp4/trackID=0;seq=1;rtptime=0

5. PAUSE - 暂停(可选)

html 复制代码
PAUSE rtsp://example.com/media.mp4 RTSP/1.0
CSeq: 5
Session: 12345678

6. TEARDOWN - 结束会话

html 复制代码
TEARDOWN rtsp://example.com/media.mp4 RTSP/1.0
CSeq: 6
Session: 12345678

基本的RTSP服务端

cpp 复制代码
//
// Created by bxc on 2022/11/30.
//

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <windows.h>
#include <string>
#pragma comment(lib, "ws2_32.lib")
#include <stdint.h>

#pragma warning( disable : 4996 )

#define SERVER_PORT      8554

#define SERVER_RTP_PORT  55532
#define SERVER_RTCP_PORT 55533

static int createTcpSocket()
{
    int sockfd;
    int on = 1;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
        return -1;

    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (const char*)&on, sizeof(on));

    return sockfd;
}

static int bindSocketAddr(int sockfd, const char* ip, int port)
{
    struct sockaddr_in addr;

    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = inet_addr(ip);

    if (bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr)) < 0)
        return -1;

    return 0;
}

static int acceptClient(int sockfd, char* ip, int* port)
{
    int clientfd;
    socklen_t len = 0;
    struct sockaddr_in addr;

    memset(&addr, 0, sizeof(addr));
    len = sizeof(addr);

    clientfd = accept(sockfd, (struct sockaddr*)&addr, &len);
    if (clientfd < 0)
        return -1;

    strcpy(ip, inet_ntoa(addr.sin_addr));
    *port = ntohs(addr.sin_port);

    return clientfd;
}

static int handleCmd_OPTIONS(char* result, int cseq)
{
    sprintf(result, "RTSP/1.0 200 OK\r\n"
        "CSeq: %d\r\n"
        "Public: OPTIONS, DESCRIBE, SETUP, PLAY\r\n"
        "\r\n",
        cseq);

    return 0;
}

static int handleCmd_DESCRIBE(char* result, int cseq, char* url)
{
    char sdp[500];
    char localIp[100];

    sscanf(url, "rtsp://%[^:]:", localIp);

    sprintf(sdp, "v=0\r\n"
        "o=- 9%ld 1 IN IP4 %s\r\n"
        "t=0 0\r\n"
        "a=control:*\r\n"
        "m=video 0 RTP/AVP 96\r\n"
        "a=rtpmap:96 H264/90000\r\n"
        "a=control:track0\r\n",
        time(NULL), localIp);

    sprintf(result, "RTSP/1.0 200 OK\r\nCSeq: %d\r\n"
        "Content-Base: %s\r\n"
        "Content-type: application/sdp\r\n"
        "Content-length: %zu\r\n\r\n"
        "%s",
        cseq,
        url,
        strlen(sdp),
        sdp);

    return 0;
}

static int handleCmd_SETUP(char* result, int cseq, int clientRtpPort)
{
    sprintf(result, "RTSP/1.0 200 OK\r\n"
        "CSeq: %d\r\n"
        "Transport: RTP/AVP;unicast;client_port=%d-%d;server_port=%d-%d\r\n"
        "Session: 66334873\r\n"
        "\r\n",
        cseq,
        clientRtpPort,
        clientRtpPort + 1,
        SERVER_RTP_PORT,
        SERVER_RTCP_PORT);

    return 0;
}

static int handleCmd_PLAY(char* result, int cseq)
{
    sprintf(result, "RTSP/1.0 200 OK\r\n"
        "CSeq: %d\r\n"
        "Range: npt=0.000-\r\n"
        "Session: 66334873; timeout=10\r\n\r\n",
        cseq);

    return 0;
}

static void doClient(int clientSockfd, const char* clientIP, int clientPort) {

    char method[40];
    char url[100];
    char version[40];
    int CSeq;

    int clientRtpPort, clientRtcpPort;
    char* rBuf = (char*)malloc(10000);
    char* sBuf = (char*)malloc(10000);

    while (true) {
        int recvLen;

        recvLen = recv(clientSockfd, rBuf, 2000, 0);//从客户端socket读取数据(最多2000字节)
        if (recvLen <= 0) {
            break;
        }

        rBuf[recvLen] = '\0';
        std::string recvStr = rBuf;
        printf(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n");
        printf("%s rBuf = %s \n",__FUNCTION__,rBuf);

        const char* sep = "\n";
        char* line = strtok(rBuf, sep);
        while (line) {
         
            if (strstr(line, "OPTIONS") ||
                strstr(line, "DESCRIBE") ||
                strstr(line, "SETUP") ||
                strstr(line, "PLAY")) {

                if (sscanf(line, "%s %s %s\r\n", method, url, version) != 3) {
                    // error
                }
            }
            else if (strstr(line, "CSeq")) {
                if (sscanf(line, "CSeq: %d\r\n", &CSeq) != 1) {
                    // error
                }
            }
            else if (!strncmp(line, "Transport:", strlen("Transport:"))) {
                // Transport: RTP/AVP/UDP;unicast;client_port=13358-13359
                // Transport: RTP/AVP;unicast;client_port=13358-13359

                if (sscanf(line, "Transport: RTP/AVP/UDP;unicast;client_port=%d-%d\r\n",
                    &clientRtpPort, &clientRtcpPort) != 2) {
                    // error
                    printf("parse Transport error \n");
                }
            }
            line = strtok(NULL, sep);
        }

        if (!strcmp(method, "OPTIONS")) {
            if (handleCmd_OPTIONS(sBuf, CSeq))
            {
                printf("failed to handle options\n");
                break;
            }
        }
        else if (!strcmp(method, "DESCRIBE")) {
            if (handleCmd_DESCRIBE(sBuf, CSeq, url))
            {
                printf("failed to handle describe\n");
                break;
            }
       
        }
        else if (!strcmp(method, "SETUP")) {
            if (handleCmd_SETUP(sBuf, CSeq, clientRtpPort))
            {
                printf("failed to handle setup\n");
                break;
            }
        }
        else if (!strcmp(method, "PLAY")) {
            if (handleCmd_PLAY(sBuf, CSeq))
            {
                printf("failed to handle play\n");
                break;
            }
        }
        else {
            printf("未定义的method = %s \n", method);
            break;
        }
        printf("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n");
        printf("%s sBuf = %s \n", __FUNCTION__, sBuf);

        send(clientSockfd, sBuf, strlen(sBuf), 0);


        //开始播放,发送RTP包
        if (!strcmp(method, "PLAY")) {
 
            printf("start play\n");
            printf("client ip:%s\n", clientIP);
            printf("client port:%d\n", clientRtpPort);

            while (true) {


                Sleep(40);
                //usleep(40000);//1000/25 * 1000
            }
   
            break;
        }

        memset(method,0,sizeof(method)/sizeof(char));
        memset(url,0,sizeof(url)/sizeof(char));
        CSeq = 0;

    }

    closesocket(clientSockfd);
    free(rBuf);
    free(sBuf);

}


int main(int argc, char* argv[])
{
    // 启动windows socket start
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    {
        printf("PC Server Socket Start Up Error \n");
        return -1;
    }
    // 启动windows socket end

    int serverSockfd;

    serverSockfd = createTcpSocket();
    if (serverSockfd < 0)
    {
        WSACleanup();
        printf("failed to create tcp socket\n");
        return -1;
    }

    if (bindSocketAddr(serverSockfd, "0.0.0.0", SERVER_PORT) < 0)
    {
        printf("failed to bind addr\n");
        return -1;
    }

    if (listen(serverSockfd, 10) < 0)
    {
        printf("failed to listen\n");
        return -1;
    }

    printf("%s rtsp://127.0.0.1:%d\n", __FILE__, SERVER_PORT);

    while (true) {
        int clientSockfd;
        char clientIp[40];
        int clientPort;

        clientSockfd = acceptClient(serverSockfd, clientIp, &clientPort);
        if (clientSockfd < 0)
        {
            printf("failed to accept client\n");
            return -1;
        }

        printf("accept client;client ip:%s,client port:%d\n", clientIp, clientPort);

        doClient(clientSockfd, clientIp, clientPort);
    }
    closesocket(serverSockfd);
    return 0;
}

关于 RTSP/RTP中 大端小端问题的总结:

为了适配小端序机器(Little-Endian,如 Windows/x86)的内存填充规则,我们在定义 RTP 结构体时必须**"反向"书写**。

这是因为网络标准(大端序)要求关键字段(如 Version)位于字节的高位 (Bit 7-6),而小端序编译器 是从低位 (Bit 0)开始填充位域的。因此,我们需要在代码中先定义低位字段(如 CSRC Count),让它们先占领低位,最后定义 Version,将其"挤"到剩下的高位去,从而在物理内存中拼凑出符合网络标准的数据格式。

ZLMediaKit

如果你把摄像头产生的视频流比作"原油",那么 ZLMediaKit 就是一个功能极其强大的**"炼油厂"兼"物流中心"**。它能接收各种协议的视频流,并实时把它们转化成几乎所有主流终端(网页、手机、监控软件)都能播放的格式。

1. 它能"听懂"并"翻译"几乎所有流媒体协议

ZLMediaKit 最强大的地方在于它的多协议转换能力(转封装)。你只要给它输入一个流,它就能自动生成多种格式的输出:

  • 输入(推流/拉流): RTSP、RTMP、HLS、GB28181、HTTP-FLV、WebRTC、SRT。

  • 输出(播放): 同上。

举个例子: 你把摄像头的 RTSP 流接入 ZLMediaKit,它会立刻为你生成:

  • 一个 RTMP 地址(发给直播平台)。

  • 一个 HTTP-FLVWebSocket-FLV 地址(网页低延迟播放)。

  • 一个 HLS 地址(手机浏览器播放)。

  • 一个 WebRTC 地址(实现无插件、极低延迟的网页通话级体验)。

2. 为什么它在安防监控(GB28181)中是标配?

GB28181 视频网关 架构中,ZLMediaKit 通常充当 "流媒体服务器 (Media Server)" 的角色:

  • 收流: 当 WVP(信令服务器)下发指令给摄像头后,摄像头会通过 国标 GB28181 (PS流) 把视频数据推送到 ZLMediaKit。

  • 解析: ZLMediaKit 负责把复杂的 PS 流拆解,提取出 H.264/H.265 视频和音频。

  • 对外分发: 你的前端网页只需要请求 ZLMediaKit 的 HTTP-FLV 接口,就能看到画面了。

3. ZLMediaKit 的核心优势

  • 高性能: 采用 C++ 开发,并发能力极强,单机可以支撑数千路视频流。

  • 功能全面: 自带录制(MP4/HLS)、截图、鉴权(防止别人偷看流)、多机级联(分布式部署)。

    • 轻量级: 代码结构清晰,不依赖复杂的第三方库,非常适合部署在 Linux 服务器甚至一些高性能的嵌入式设备(如瑞芯微 RK3588)上。

RTSP拉流传输H264服务器

第一阶段:RTSP 握手(建立控制连接)

客户端(如 VLC)先和服务器通过 TCP 端口(常见 554 或你代码里的 8554)完成 RTSP 会话:

  1. OPTIONS:客户端问"支持哪些方法",服务器返回支持 OPTIONS、DESCRIBE、SETUP、PLAY。
  2. DESCRIBE:客户端请求流描述,服务器返回 SDP。
    关键点:SDP 会声明视频编码是 H264,通常会有 payload type(如 96),以及 sprop-parameter-sets(SPS/PPS,供解码器初始化)。
  3. SETUP:客户端告诉服务器"我用 UDP 收流,RTP/RTCP 端口是多少"。服务器返回会话 ID(Session)和传输参数。
  4. PLAY:客户端下发"开始发送",服务器返回 200 OK,然后开始发 RTP 视频包。

第二阶段:H264 帧读取与拆解(生产者)

服务器进入循环,从本地 .h264 文件中按 NALU 边界取数据:

  1. 识别起始码:00 00 01 或 00 00 00 01。
  2. 截取一个完整 NALU(从当前起始码到下一个起始码前)。
  3. 解析 NALU 类型:
  • 1 到 23:普通单 NALU(如 P/B/I 片)
  • 5:IDR 关键帧
  • 7/8:SPS/PPS(解码参数,通常在播放初期或关键帧前发送)

第三阶段:RTP 封装(包装快递)

服务器把 NALU 封装为 RTP 后再发 UDP,核心有两种情况:

RTP Header(固定 12 字节)
  • Version = 2
  • Payload Type = SDP 协商值(常见 96)
  • Sequence Number:每发一个 RTP 包递增
  • Timestamp:按视频时钟递增(H264 常用 90000 时钟)
    • 例如 25fps 时,每帧增量约 90000/25 = 3600
  • SSRC:流标识,固定一个随机值即可
1.单 NALU 模式(Small NALU),当一个 NALU 小于 MTU 可承载大小时:
  • RTP 负载直接放完整 NALU(不含起始码)
  • 通常一个 NALU 对应一个 RTP 包FU-A 分片模式(Large NALU)
2.当一个 NALU 太大时拆包发送:
  • 第一个分片:S=1,E=0
  • 中间分片:S=0,E=0
  • 最后分片:S=0,E=1
  • 接收端按序重组后恢复原始 NALU
    这就是你客户端里看到的 FU-A 重组逻辑来源。

第四阶段:发送与定时(运输)

  1. UDP 发送:服务器用 sendto 发到客户端在 SETUP 指定的 RTP 端口。
  2. 节奏控制(Pacing):按帧率或时间戳节奏发送,不能一口气打满。
  • 不控速会导致接收端抖动、缓存异常、卡顿
  • 常见做法:每帧 Sleep 对应帧间隔(如 40ms 对应 25fps),或按时间戳调度更精细发送

H264在RTP中的包装

每个分片 RTP payload 开头 2 字节:

  • 第 1 字节 FU indicator = (原 NALU 的 F 和 NRI) + Type=28
  • 代码:
    • naluType = frame[0];//frame是该片段H264开头指针
    • payload[0] = (naluType & 0x60) | 28
  • 第 2 字节 FU header = S/E/R + 原始 NALU Type
    代码先写 payload[1] = naluType & 0x1F
    • 第一片置 S=1:rtpPacket->payload[1] |= 0x80
    • 最后一片置 E=1:rtpPacket->payload[1] |= 0x40

RTSP拉流传输AAC服务器

第一阶段:RTSP 握手(建立连接)

客户端(如 VLC)与服务器通过 TCP 端口(默认 554)进行交互:

  1. OPTIONS: 客户端问:"你能做什么?" 服务器回:"我支持 OPTIONS, DESCRIBE, SETUP, PLAY"。
  2. DESCRIBE : 客户端问:"流的信息是什么?" 服务器发送 SDP (Session Description Protocol)
    • 关键点 :SDP 里会告诉客户端这是 AAC 音频,采样率是多少(如 44100),以及 config 字符串(用于解码初始化)。
  3. SETUP: 客户端说:"我想通过 UDP 接收音频,我的 RTP 端口是 1234,RTCP 端口是 1235"。服务器说:"收到,我的 RTP 端口是 6789,会话 ID 是 XXX"。
  4. PLAY: 客户端说:"开始发货吧!" 服务器回:"OK,马上发送"。

第二阶段:AAC 数据读取与拆解(生产者)

服务器进入一个 while(true) 循环,开始处理本地 .aac 文件:

  1. 读取 ADTS 头 :从文件取 7 字节。
    • 通过 parseAdtsHeader 解析出 aacFrameLength(本帧总长度)。
  2. 提取载荷 :根据 aacFrameLength - 7 计算出纯音频数据的长度。
  3. 读取数据体 :调用 fread 把这部分"纯肉"读入内存缓冲区 frame

第三阶段:RTP 包封装(包装快递)

这是最核心的一步,服务器构造一个 RTP 报文,由三部分拼接而成:

1. 构造 RTP Header (12 字节)
  • Version (V): 固定为 2。
  • Payload Type (PT): 设为 97(刚才 DESCRIBE 里商量好的)。
  • Sequence Number : 每发一包 +1(接收端靠它检测是否丢包)。
  • Timestamp : 时间戳。由于一帧 AAC 对应 1024 个采样点,所以每发一帧,时间戳增加 1024
  • SSRC: 一个随机的唯一标识符。
2. 构造 AAC 封装层 (4 字节 - AU Header Section)

为了让接收端知道 AAC 帧的边界,额外包了一层:

  • AU-headers-length (2 字节) : 固定填 0x0010 (即 16 bits)。
  • AU-Header (2 字节) :
    • 高 13 bits:填入刚才得到的音频数据长度。
    • 低 3 bits:填入 0 (Index 字段)。
3. 填充数据体 (Payload)
  • 将读取的纯 AAC 数据紧跟在上面这 4 字节后面。

第四阶段:发送与定时(运输)

  1. UDP 发送 :调用 sendto 函数,目的地是客户端在 SETUP 阶段提供的 IP 和 RTP 端口。
  2. 控制节奏 (Pacing)
    • 发送完一帧后,服务器必须 Sleep 一段时间(比如 23ms)。
    • 原因:如果不休眠,服务器会在几毫秒内把整首歌发完,导致接收端缓冲区溢出,播放器直接崩溃或卡死。

完整代码

cpp 复制代码
//
// Created by sun on 2022/12/9.
//

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <windows.h>
#include "rtp.h"

#define SERVER_PORT     8554
#define SERVER_RTP_PORT  55532
#define SERVER_RTCP_PORT 55533
#define BUF_MAX_SIZE    (1024*1024)
#define AAC_FILE_NAME   "../data/test-long.aac"

static int createTcpSocket() {
    int sockfd;
    int on = 1;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
        return -1;

    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (const char*)&on, sizeof(on));

    return sockfd;
}

static int createUdpSocket() {
    int sockfd;
    int on = 1;

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
        return -1;

    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (const char*)&on, sizeof(on));

    return sockfd;
}

static int bindSocketAddr(int sockfd, const char* ip, int port) {
    struct sockaddr_in addr;

    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = inet_addr(ip);

    if (bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr)) < 0)
        return -1;

    return 0;
}

struct AdtsHeader {
    unsigned int syncword;  // 12 bit 同步字,固定为 '1111 1111 1111',表示 ADTS 帧开始
    uint8_t id;             // 1 bit,0 表示 MPEG-4,1 表示 MPEG-2
    uint8_t layer;          // 2 bit,固定为 0
    uint8_t protectionAbsent;  // 1 bit,1 表示无 CRC,0 表示有 CRC
    uint8_t profile;           // AAC 配置(MPEG-2 与 MPEG-4 下定义略有差异)
    uint8_t samplingFreqIndex; // 4 bit,采样率索引
    uint8_t privateBit;        // 1 bit,通常置 0
    uint8_t channelCfg;        // 3 bit,声道配置
    uint8_t originalCopy;      // 1 bit,通常置 0
    uint8_t home;              // 1 bit,通常置 0

    uint8_t copyrightIdentificationBit;   // 1 bit,通常置 0
    uint8_t copyrightIdentificationStart; // 1 bit,通常置 0
    unsigned int aacFrameLength;          // 13 bit,ADTS 帧总长度(头 + AAC 原始数据)
    unsigned int adtsBufferFullness;      // 11 bit,缓冲区满度,0x7FF 常用于 VBR

    /* number_of_raw_data_blocks_in_frame
        * 表示一个 ADTS 帧中包含的 AAC 原始帧数 = number_of_raw_data_blocks_in_frame + 1
        * 通常 number_of_raw_data_blocks_in_frame == 0
        * 即一个 ADTS 帧仅包含一个 AAC 原始帧(一般对应 1024 个采样点)
     */
    uint8_t numberOfRawDataBlockInFrame; //2 bit
};

static int parseAdtsHeader(uint8_t* in, struct AdtsHeader* res) {
    static int frame_number = 0;
    memset(res, 0, sizeof(*res));

    if ((in[0] == 0xFF) && ((in[1] & 0xF0) == 0xF0))
    {
        res->id = ((uint8_t)in[1] & 0x08) >> 3;      // 取第 2 字节中的 id 位
        res->layer = ((uint8_t)in[1] & 0x06) >> 1;   // 取第 2 字节中的 layer 位
        res->protectionAbsent = (uint8_t)in[1] & 0x01;
        res->profile = ((uint8_t)in[2] & 0xc0) >> 6;
        res->samplingFreqIndex = ((uint8_t)in[2] & 0x3c) >> 2;
        res->privateBit = ((uint8_t)in[2] & 0x02) >> 1;
        res->channelCfg = ((((uint8_t)in[2] & 0x01) << 2) | (((unsigned int)in[3] & 0xc0) >> 6));
        res->originalCopy = ((uint8_t)in[3] & 0x20) >> 5;
        res->home = ((uint8_t)in[3] & 0x10) >> 4;
        res->copyrightIdentificationBit = ((uint8_t)in[3] & 0x08) >> 3;
        res->copyrightIdentificationStart = (uint8_t)in[3] & 0x04 >> 2;
        
        res->aacFrameLength = (((((unsigned int)in[3]) & 0x03) << 11) |
            (((unsigned int)in[4] & 0xFF) << 3) |
            ((unsigned int)in[5] & 0xE0) >> 5);

        res->adtsBufferFullness = (((unsigned int)in[5] & 0x1f) << 6 |
            ((unsigned int)in[6] & 0xfc) >> 2);
        res->numberOfRawDataBlockInFrame = ((uint8_t)in[6] & 0x03);

        return 0;
    }
    else
    {
        printf("failed to parse adts header\n");
        return -1;
    }
}

static int rtpSendAACFrame(int socket, const char* ip, int16_t port,
    struct RtpPacket* rtpPacket, uint8_t* frame, uint32_t frameSize) {
    // 参考:AAC over RTP 打包格式说明
    int ret;

    rtpPacket->payload[0] = 0x00;
    rtpPacket->payload[1] = 0x10;
    rtpPacket->payload[2] = (frameSize & 0x1FE0) >> 5; // frameSize 的高 8 位
    rtpPacket->payload[3] = (frameSize & 0x1F) << 3;   // frameSize 的低 5 位

    memcpy(rtpPacket->payload + 4, frame, frameSize);

    ret = rtpSendPacketOverUdp(socket, ip, port, rtpPacket, frameSize + 4);
    if (ret < 0)
    {
        printf("failed to send rtp packet\n");
        return -1;
    }

    rtpPacket->rtpHeader.seq++;

    /*
        * 采样率为 44100Hz
        * AAC-LC 通常每帧 1024 个采样点
        * 每秒约 44100 / 1024 ≈ 43 帧
        * RTP 时间戳每帧增量约为 1025
        * 每帧时长约 23ms
     */
    rtpPacket->rtpHeader.timestamp += 1025;

    return 0;
}

static int acceptClient(int sockfd, char* ip, int* port) {
    int clientfd;
    socklen_t len = 0;
    struct sockaddr_in addr;

    memset(&addr, 0, sizeof(addr));
    len = sizeof(addr);

    clientfd = accept(sockfd, (struct sockaddr*)&addr, &len);
    if (clientfd < 0)
        return -1;

    strcpy(ip, inet_ntoa(addr.sin_addr));
    *port = ntohs(addr.sin_port);

    return clientfd;
}

static char* getLineFromBuf(char* buf, char* line) {
    while (*buf != '\n')
    {
        *line = *buf;
        line++;
        buf++;
    }

    *line = '\n';
    ++line;
    *line = '\0';

    ++buf;
    return buf;
}

static int handleCmd_OPTIONS(char* result, int cseq) {
    sprintf(result, "RTSP/1.0 200 OK\r\n"
        "CSeq: %d\r\n"
        "Public: OPTIONS, DESCRIBE, SETUP, PLAY\r\n"
        "\r\n",
        cseq);

    return 0;
}

static int handleCmd_DESCRIBE(char* result, int cseq, char* url) {
    char sdp[500];
    char localIp[100];

    sscanf(url, "rtsp://%[^:]:", localIp);

    sprintf(sdp, "v=0\r\n"
        "o=- 9%ld 1 IN IP4 %s\r\n"
        "t=0 0\r\n"
        "a=control:*\r\n"
        "m=audio 0 RTP/AVP 97\r\n"
        "a=rtpmap:97 mpeg4-generic/44100/2\r\n"
        "a=fmtp:97 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1210;\r\n"
   
        //"a=fmtp:97 SizeLength=13;\r\n"
        "a=control:track0\r\n",
        time(NULL), localIp);

    sprintf(result, "RTSP/1.0 200 OK\r\nCSeq: %d\r\n"
        "Content-Base: %s\r\n"
        "Content-type: application/sdp\r\n"
        "Content-length: %d\r\n\r\n"
        "%s",
        cseq,
        url,
        strlen(sdp),
        sdp);

    return 0;
}

static int handleCmd_SETUP(char* result, int cseq, int clientRtpPort) {
    sprintf(result, "RTSP/1.0 200 OK\r\n"
        "CSeq: %d\r\n"
        "Transport: RTP/AVP;unicast;client_port=%d-%d;server_port=%d-%d\r\n"
        "Session: 66334873\r\n"
        "\r\n",
        cseq,
        clientRtpPort,
        clientRtpPort + 1,
        SERVER_RTP_PORT,
        SERVER_RTCP_PORT
    );

    return 0;
}

static int handleCmd_PLAY(char* result, int cseq) {
    sprintf(result, "RTSP/1.0 200 OK\r\n"
        "CSeq: %d\r\n"
        "Range: npt=0.000-\r\n"
        "Session: 66334873; timeout=10\r\n\r\n",
        cseq);

    return 0;
}


static void doClient(int clientSockfd, const char* clientIP, int clientPort) {

    int serverRtpSockfd = -1, serverRtcpSockfd = -1;

    char method[40];
    char url[100];
    char version[40];
    int CSeq;

    int clientRtpPort, clientRtcpPort;
    char* rBuf = (char*)malloc(BUF_MAX_SIZE);
    char* sBuf = (char*)malloc(BUF_MAX_SIZE);

    while (true) {
        int recvLen;

        recvLen = recv(clientSockfd, rBuf, BUF_MAX_SIZE, 0);
        if (recvLen <= 0) {
            break;
        }

        rBuf[recvLen] = '\0';
        printf("%s rBuf = %s \n", __FUNCTION__, rBuf);

        const char* sep = "\n";
        char* line = strtok(rBuf, sep);
        while (line) {
            if (strstr(line, "OPTIONS") ||
                strstr(line, "DESCRIBE") ||
                strstr(line, "SETUP") ||
                strstr(line, "PLAY")) {

                if (sscanf(line, "%s %s %s\r\n", method, url, version) != 3) {
                    // error
                }
            }
            else if (strstr(line, "CSeq")) {
                if (sscanf(line, "CSeq: %d\r\n", &CSeq) != 1) {
                    // error
                }
            }
            else if (!strncmp(line, "Transport:", strlen("Transport:"))) {
                // Transport: RTP/AVP/UDP;unicast;client_port=13358-13359
                // Transport: RTP/AVP;unicast;client_port=13358-13359

                if (sscanf(line, "Transport: RTP/AVP/UDP;unicast;client_port=%d-%d\r\n",
                    &clientRtpPort, &clientRtcpPort) != 2) {
                    // error
                    printf("parse Transport error \n");
                }
            }
            line = strtok(NULL, sep);
        }

        if (!strcmp(method, "OPTIONS")) {
            if (handleCmd_OPTIONS(sBuf, CSeq))
            {
                printf("failed to handle options\n");
                break;
            }
        }
        else if (!strcmp(method, "DESCRIBE")) {
            if (handleCmd_DESCRIBE(sBuf, CSeq, url))
            {
                printf("failed to handle describe\n");
                break;
            }
        }
        else if (!strcmp(method, "SETUP")) {
            if (handleCmd_SETUP(sBuf, CSeq, clientRtpPort))
            {
                printf("failed to handle setup\n");
                break;
            }

            serverRtpSockfd = createUdpSocket();
            serverRtcpSockfd = createUdpSocket();
            if (serverRtpSockfd < 0 || serverRtcpSockfd < 0)
            {
                printf("failed to create udp socket\n");
                break;
            }

            if (bindSocketAddr(serverRtpSockfd, "0.0.0.0", SERVER_RTP_PORT) < 0 ||
                bindSocketAddr(serverRtcpSockfd, "0.0.0.0", SERVER_RTCP_PORT) < 0)
            {
                printf("failed to bind addr\n");
                break;
            }

        }
        else if (!strcmp(method, "PLAY")) {
            if (handleCmd_PLAY(sBuf, CSeq))
            {
                printf("failed to handle play\n");
                break;
            }
        }
        else {
            printf("����method = %s \n", method);
            break;
        }

        printf("%s sBuf = %s \n", __FUNCTION__, sBuf);

        send(clientSockfd, sBuf, strlen(sBuf), 0);


        // 开始推流:发送 RTP 包
        if (!strcmp(method, "PLAY")) {

            struct AdtsHeader adtsHeader;
            struct RtpPacket* rtpPacket;
            uint8_t* frame;
            int ret;

            FILE* fp = fopen(AAC_FILE_NAME, "rb");
            if (!fp) {
                printf("��ȡ %s ʧ��\n", AAC_FILE_NAME);
                break;
            }

            frame = (uint8_t*)malloc(5000);
            rtpPacket = (struct RtpPacket*)malloc(5000);

            rtpHeaderInit(rtpPacket, 0, 0, 0, RTP_VESION, RTP_PAYLOAD_TYPE_AAC, 1, 0, 0, 0x32411);

            while (true)
            {
                ret = fread(frame, 1, 7, fp);
                if (ret <= 0)
                {
                    printf("fread err\n");
                    break;
                }
                printf("fread ret=%d \n",ret);

                if (parseAdtsHeader(frame, &adtsHeader) < 0)
                {
                    printf("parseAdtsHeader err\n");
                    break;
                }
                ret = fread(frame, 1, adtsHeader.aacFrameLength - 7, fp);
                if (ret <= 0)
                {
                    printf("fread err\n"); 
                    break;
                }

                //先包装成 RTP 包,再发送
                rtpSendAACFrame(serverRtpSockfd, clientIP, clientRtpPort,
                    rtpPacket, frame, adtsHeader.aacFrameLength - 7);

                Sleep(1);
                //usleep(23223);//1000/43.06 * 1000
            }

            free(frame);
            free(rtpPacket);

            break;

        }

        memset(method, 0, sizeof(method) / sizeof(char));
        memset(url, 0, sizeof(url) / sizeof(char));
        CSeq = 0;
    }

    closesocket(clientSockfd);
    if (serverRtpSockfd) {
        closesocket(serverRtpSockfd);
    }
    if (serverRtcpSockfd > 0) {
        closesocket(serverRtcpSockfd);
    }

    free(rBuf);
    free(sBuf);

}

int main() {
    // 初始化 Windows Socket
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    {
        printf("PC Server Socket Start Up Error \n");
        return -1;
    }
    // Windows Socket 初始化结束

    int rtspServerSockfd;

    int ret;

    rtspServerSockfd = createTcpSocket();
    if (rtspServerSockfd < 0)
    {
        printf("failed to create tcp socket\n");
        return -1;
    }

    ret = bindSocketAddr(rtspServerSockfd, "0.0.0.0", SERVER_PORT);
    if (ret < 0)
    {
        printf("failed to bind addr\n");
        return -1;
    }

    ret = listen(rtspServerSockfd, 10);
    if (ret < 0)
    {
        printf("failed to listen\n");
        return -1;
    }

    printf("%s rtsp://127.0.0.1:%d\n", __FILE__, SERVER_PORT);

    while (1)
    {
        int clientSockfd;
        char clientIp[40];
        int clientPort;

        clientSockfd = acceptClient(rtspServerSockfd, clientIp, &clientPort);
        if (clientSockfd < 0)
        {
            printf("failed to accept client\n");
            return -1;
        }

        printf("accept client;client ip:%s,client port:%d\n", clientIp, clientPort);

        doClient(clientSockfd, clientIp, clientPort);
    }

    closesocket(rtspServerSockfd);

    return 0;
}

AAC在RTP中的包装(当前代码)

cpp 复制代码
rtpPacket->payload[0] = 0x00;
rtpPacket->payload[1] = 0x10;
rtpPacket->payload[2] = (frameSize & 0x1FE0) >> 5;  // 帧大小高8位
rtpPacket->payload[3] = (frameSize & 0x1F) << 3;    // 帧大小低5位
memcpy(rtpPacket->payload + 4, frame, frameSize);   // 实际AAC数据

格式(遵循RFC 3640):

  • Payload前4字节是AAC特定的头
  • 后面跟实际的AAC编码数据

MP3在RTP中的包装(RFC 3119)

cpp 复制代码
// MP3 RTP Payload 结构
rtpPacket->payload[0] = 0x00;           // MBZ (Must Be Zero)
rtpPacket->payload[1] = 0x00;           // MBZ
rtpPacket->payload[2] = (offset >> 5);  // 偏移量高位
rtpPacket->payload[3] = (offset << 3) | mbd; // 偏移量低位 + 边界标志
memcpy(rtpPacket->payload + 4, frame, frameSize); // 实际MP3数据

关键差异

特性 AAC MP3
标准 RFC 3640 RFC 3119
Payload类型 97 14
头部结构 4字节特定头 4字节头部
采样率处理 固定44100Hz 支持多种采样率
时间戳增量 ~1025 根据样本数计算
帧长度 1024采样点 1152采样点

心得:

很多涉及字符搬运的操作,基础变量要设置为uint8/char*

cpp 复制代码
memcpy(rtpPacket->payload+2, frame+pos, RTP_MAX_PKT_SIZE);

比如这里payload是设置为uint8[0]的,frame是char*

相关推荐
北顾笙9802 小时前
LLM学习-day04
学习
lzj_pxxw4 小时前
W25Q64存储芯片 软件设计刚需常识
stm32·单片机·嵌入式硬件·mcu·学习
Slow菜鸟4 小时前
AI学习篇(四) | AI设计类Skills推荐清单(2026年)
人工智能·学习
念恒123064 小时前
Python(列表进阶)
python·学习
QYQ_11276 小时前
嵌入式学习——杂项设备、Platform总线和设备树源文件
学习
wuxinyan1237 小时前
大模型学习之路03:提示工程从入门到精通(第三篇)
人工智能·python·学习
十安_数学好题速析7 小时前
【多选】曲线方程:四步避坑判断曲线类型
笔记·学习·高考
千寻girling8 小时前
五一劳动节快乐 [特殊字符][特殊字符][特殊字符]
java·c++·git·python·学习·github·php
波特率1152009 小时前
git指令学习
git·学习