Linux C实现RTSP客户端:从命令交互到RTP接收的完整指南
2026/9/23 16:55:42 网站建设 项目流程

简介:这是一份面向Linux环境的RTSP客户端C语言实现源码包,适用于网络流媒体开发者、嵌入式工程师及协议学习者,解决在Linux下启动、暂停、快进等实时流控制及RTP视频流获取问题。压缩包共4个文件,3个C源文件加1个头文件,整体仅5KB,代码精简便于阅读;C文件分别对应RTSP命令交互、RTP数据接收以及测试验证逻辑,头文件集中定义协议状态机与关键数据结构。目前已有952人学习/下载,适合需要快速理解RTSP/RTP协议栈的读者。通过源码可掌握基于socket的RTSP会话建立流程,理解OPTIONS、DESCRIBE、SETUP、PLAY等指令的实际构造与解析方法,并借助测试程序验证客户端收发逻辑,为后续二次开发或移植到嵌入式平台提供可直接引用的实现范式。

1. RTSP客户端不是“发个 PLAY 就完事”:一个 Linux C 实现到底该怎么拆

做嵌入式或者 Linux 平台上的视频接入,大概率都绕不过 RTSP 拉流。你在网上搜“RTSPClient”“rtsp 客户端 linux”,能找到的代码不少,但真正能拿过来编译、跑通、抓到 RTP 数据包的却没几个。这个名叫 rtspclient 的源码包就是这类稀缺资源:用纯 C 实现,不依赖任何第三方库,核心代码只有 rtsp.c、rtspclient.c、testrtsp.c 加一个头文件。它解决的问题很直接——在 Linux 下用 socket 向摄像头或流媒体服务器发送 RTSP 命令,协商出媒体通道,然后接收 RTP 数据。适合那些需要二次开发、想在嵌入式板卡上做私有拉流逻辑的人。我先说结论:这套代码最值钱的地方不是它能不能拉流,而是它把 RTSP 命令交互的完整顺序、RTP 接收地址的协商过程、以及各个状态节点的处理逻辑都摊开放在你面前了。

2. 先拆源码包:四个文件的职责边界与构建顺序

拿到源码包,不要急着直接编译。先把四个文件的职责理清楚,后面排错会省很多时间。这个包的划分很典型:头文件定义数据结构,rtsp.c 负责 RTSP 消息的解析和组装,rtspclient.c 实现命令交互流程,testrtsp.c 是测试入口。

2.1 rtsp.h:数据结构和函数原型的契约层

rtsp.h 是整个项目的“接口契约”。你如果编译时遇到函数未声明的报错,第一反应应该回来检查这个头文件是否被正确包含。它里面通常会定义 RTSP 相关的数据结构,比如解析后的响应信息、RTP 接收端口号、CSeq 序号等。

一般常见的定义会包含类似下面的内容:

#define RTSP_DEFAULT_PORT 554 #define RTSP_BUFFER_SIZE 4096 typedef struct { int cseq; int rtp_port; int rtcp_port; char session_id[128]; char server_info[256]; } rtsp_session_t; int rtsp_parse_response(const char *response, rtsp_session_t *session); int rtsp_build_request(char *buffer, int size, const char *method, const char *url, int cseq, const char *extra);

逻辑说明:

  • rtsp_parse_response负责解析服务器返回的响应文本,把 CSeq、session_id、端口号等关键信息提取出来。
  • rtsp_build_request用于拼装客户端要发送的请求消息。

参数说明:

  • rtsp_buffer_size建议不小于 4096;如果服务器返回的 SDP 较长,这个值太小会导致解析截断。
  • rtp_portrtcp_port在 SETUP 阶段从服务器响应中提取,后续接收 RTP 数据时要用。

2.2 rtsp.c:RTSP 消息的解析与组装底层

rtsp.c 处理的是最底层的文本消息。RTSP 协议和 HTTP 类似,是基于文本的,请求行、头部字段、空行、消息体都有严格的格式约定。代码里面会包含查找头字段、解析数字、复制字符串这类操作。

你可能会在代码里看到类似strstr定位字段,然后做偏移解析的写法:

char *p = strstr(response, "Session:"); if (p) { p += strlen("Session:"); while (*p == ' ') p++; sscanf(p, "%s", session->session_id); }

逻辑说明:

  • 先在响应文本中定位Session:字段,跳过可能的空格,然后用sscanf提取会话标识。
  • 类似地,server_port字段要从 SETUP 的响应中解析,通常格式是server_port=8000-8001,需要分别解析 RTP 和 RTCP 端口。

这个文件的调试价值在于:RTSP 服务器返回的响应格式差异很大,有的用Session: abc123,有的带timeout参数,有的字段名大小写不统一。如果发现某项参数解析不出来,优先检查这里。

2.3 rtspclient.c:命令状态机的核心实现

这是整个客户端最核心的部分,负责按顺序发送 OPTIONS、DESCRIBE、SETUP、PLAY、TEARDOWN 这些命令,并在每个阶段处理服务器的响应。这个文件里面通常会有类似状态机的结构,或者至少是一连串顺序执行的函数调用。

核心代码逻辑通常长这样:

int rtsp_play(const char *url, int *rtp_port) { int sock = socket(AF_INET, SOCK_STREAM, 0); // ... 连接服务器 ... rtsp_build_request(buf, sizeof(buf), "OPTIONS", url, 1, NULL); send(sock, buf, strlen(buf), 0); recv(sock, response, sizeof(response), 0); rtsp_build_request(buf, sizeof(buf), "DESCRIBE", url, 2, NULL); send(sock, buf, strlen(buf), 0); recv(sock, response, sizeof(response), 0); rtsp_parse_response(response, &session); // SETUP 阶段协商端口 // PLAY 阶段开始播放 // 然后绑定本地端口接收 RTP 数据 }

逻辑说明:

  • socket创建用的是SOCK_STREAM,因为 RTSP 命令交互走 TCP;RTP 数据可以走 UDP 也可以走 TCP,当前实现一般是 UDP。
  • 每次请求的 CSeq 必须递增,从 1 开始。服务器用这个字段来匹配请求和响应。
  • DESCRIBE的响应里包含 SDP 信息,其中m=video行的RTP/AVP 96这类参数标明了负载类型,后续收 RTP 包时要检查。

一个常见的理解误区是:RTSP 的 PLAY 命令发出去了,视频就开始传了。实际上 PLAY 只是通知服务器开始发送,真正的视频数据走的是 SETUP 阶段协商出来的 RTP 端口,和 RTSP 命令的 TCP 连接是分开的。

2.4 构建顺序:先跑通 testrtsp.c 验证链路

testrtsp.c 是一个典型的测试程序,里面大概率包含main函数入口,示例化了拉流的完整流程。建议先用它做验证,确认链路通了你再去改业务逻辑。

gcc -o rtsp_test testrtsp.c rtspclient.c rtsp.c -Wall ./rtsp_test rtsp://192.168.1.64:554/stream1

编译参数说明:

  • -Wall打开所有警告,这个包是几年沉淀的老代码,编译时看到警告不要忽略,特别是关于隐式函数声明的警告,往往是头文件包含顺序不对。
  • 运行时 URL 是完整的 RTSP 地址,注意用户名密码要用rtsp://user:pass@ip:port/path格式。

跑通 testrtsp.c 之后,你再去看 rtspclient.c 就会觉得清晰很多,因为它本质上就是一个被拆成多个函数的 testrtsp.c。

3. 把 RTSP 命令交互跑通:OPTIONS 到 PLAY 的完整流程与参数协商

RTSP 命令交互看起来就是简单地发几个请求、收几个响应,但真正涉及线上环境时,细节都在参数协商里。整个流程是固定的:OPTIONS 探路,DESCRIBE 获取媒体描述,SETUP 协商传输通道,PLAY 触发数据流,最后 TEARDOWN 收尾。

3.1 OPTIONS 与 DESCRIBE:协商能力与拿 SDP

OPTIONS 是客户端发送的第一条命令,用来查询服务器支持哪些方法。响应里会有Public:字段,列出服务器支持的方法,例如OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE。这个字段的主要价值不是让客户端做判断,而是确认当前连接的服务确实支持 RTSP 协议。

DESCRIBE 是真正开始工作的命令。它返回的信息用 SDP 格式组织,里面有媒体类型、编码格式、码率、分辨率等关键参数。对于 H.264 编码的流,SDP 里会有类似下面这样的内容:

m=video 0 RTP/AVP 96 a=rtpmap:96 H264/90000 a=control:track1

这段 SDP 的意思是:视频轨道使用 RTP 传输,负载类型编号是 96,编码格式是 H.264,时钟频率是 90000Hz,控制路径是track1。后续的 SETUP 请求要带上track1这个控制路径,才能正确关联到视频轨道。

实际抓包时会发现,有些服务器(尤其是国产 IPC)的 SDP 格式并不完全规范。比如有的在m=行用98作为负载类型,有的甚至不写rtpmap行。遇到这种情况,代码里的解析逻辑就需要有兜底处理。

3.2 SETUP:Transport 头的 UDP 端口协商

SETUP 是整个 RTSP 交互中最容易出错的环节,因为传输参数在这里协商。客户端要发送的请求头类似这样:

SETUP rtsp://192.168.1.64:554/stream1/track1 RTSP/1.0 CSeq: 3 Transport: RTP/AVP/UDP;unicast;client_port=8000-8001

这里的client_port=8000-8001是客户端自己选择的端口:8000 用于接收 RTP 数据,8001 用于接收 RTCP 数据。服务器收到后会在响应中告诉客户端它选择的服务器端口:

Transport: RTP/AVP/UDP;unicast;client_port=8000-8001;server_port=9000-9001

关键逻辑说明:

  • 客户端必须先绑定本地端口 8000 和 8001,然后才能发送 SETUP 请求。如果先发 SETUP 再绑定端口,会丢失第一批到达的 RTP 数据。
  • server_port=9000-9001是服务器发送 RTP 数据的源端口,通常用于后续的 RTCP 收发和 NAT 穿透判断,但接收 RTP 数据本身只依赖于客户端绑定的 8000 端口。
  • 部分服务器要求Transport头中携带mode=record或者mode=play参数,如果缺少可能导致 461 错误。

这块代码写的时候,最容易忽略的就是端口绑定的时机。很多人的第一次失败经历是:SETUP 发出的client_port是 8000,但本地根本没有 bind 这个端口。RTP 数据发过来时系统直接返回 ICMP 端口不可达,表现在现象上就是命令交互全部成功但就是收不到数据。

3.3 PLAY 与 TEARDOWN:状态推进与资源释放

PLAY 命令触发服务器开始发送媒体流。它的请求头相对简单:

PLAY rtsp://192.168.1.64:554/stream1 RTSP/1.0 CSeq: 4 Session: abc123 Range: npt=0.000-

注意 Session 字段必须使用 SETUP 响应中返回的会话标识,很多服务器要求这个字段和 SETUP 阶段的一致,否则返回 454 Session Not Found。

TEARDOWN 的目的是释放服务器资源。嵌入式设备的内存有限,如果客户端异常退出但没发 TEARDOWN,服务器上的会话会一直挂着,直到超时。所以如果你的程序有信号处理逻辑,在 SIGINT 或 SIGTERM 的 handler 里先发 TEARDOWN 再退出,这是应有的技能储备。

3.4 一个最小可运行的测试序列

用命令行的方式也可以模拟 RTSP 交互流程,这样可以验证服务器端的连通性:

# 以 VLC 为例,直接请求流地址后加 --rtsp-tcp 强制走 TCP vlc -v --rtsp-tcp rtsp://192.168.1.64:554/stream1 # 用 ffprobe 查看流信息,确认编码格式 ffprobe -rtsp_transport tcp rtsp://192.168.1.64:554/stream1

命令逻辑说明:

  • --rtsp-tcp-rtsp_transport tcp都表示用 TCP 传输 RTP 数据。默认是 UDP,两种模式对应接收 RTP 数据的底层协议不同。
  • 如果你的客户端只实现了 UDP 模式,但实际网络环境禁用了 UDP,就会出现命令交互正常但收不到数据的问题。

我建议你至少用 VLC 验证一次服务器端的流是否正常,再用 ffprobe 获取流的基本信息。这能把问题范围缩小:如果 ffprobe 能拿到流信息,但你的客户端拿不到,问题大概率出在代码逻辑而不是服务器配置上。

4. 避坑实践:RTSP 客户端调试中常见的问题、现象、原因与解法

这部分内容来自实际调试中的经验积累。我也看过不少类似的 RTSP 客户端代码,发现问题点高度一致,尤其是都在下面几个位置。每一条都是实际遇到过的,格式统一为现象、原因、解决。

4.1 现象:SETUP 成功后收不到 RTP 数据

现象:DESCRIBE、SETUP 都返回 200 OK,PLAY 也发出去了,但 recvfrom 一直阻塞,收不到任何 UDP 数据。

原因:最常见的两个原因。第一是本地没有提前 bind UDP 端口,RTP 数据到达时找不到对应端口而丢弃。第二是代码里解析服务器响应时,把server_portclient_port搞混了,绑定到了错误的地址。

解决:在发送 SETUP 之前就创建 UDP socket 并 bind 到自选的 client_port 上,例如bind(sock, (struct sockaddr *)&addr, sizeof(addr)),其中addr.sin_port = htons(8000)。然后用真实的抓包结果对比 SETUP 请求中声明的端口和本地绑定端口是否一致。

4.2 现象:第二次连接总是失败

现象:程序第一次拉流一切正常,杀掉进程重启后第一次连接也正常,但是程序内部做二次连接时,SETUP 阶段返回错误,或者数据流接收异常。

原因:RTP socket 绑定端口后没有被正确关闭,导致端口被占用。Linux 下 UDP socket 默认不启用SO_REUSEADDR,端口处于 TIME_WAIT 状态时无法再次绑定。除此之外,也可能是因为上一次会话没有发 TEARDOWN,服务器端的会话没有释放,后续 SETUP 请求到达时服务器返回 455 Method Not Valid In This State。

解决:在所有断开路径上释放资源。socket 用 close 关闭,同时调用rtsp_send_request("TEARDOWN")通知服务器清理会话。如果你不发送 TEARDOWN,就需要等待服务器端的会话超时,这个时间通常是 60 秒左右,你会在 60 秒时间内不断重试失败。

4.3 现象:播放几分钟后卡死,没有任何报错

现象:程序运行正常,视频播放流畅,但运行 3 到 5 分钟之后,接收线程突然卡住,CPU 占用 100%,或者没有任何数据输出。

原因:这通常是因为 recvfrom 默认是阻塞模式,没有设置超时时间。当网络抖动或服务器暂缓发送时,线程会一直阻塞在 recvfrom 上。如果此时主线程等待这个接收线程的结果,就会出现整个程序卡死。更严重的是如果 RTP 序列号发生跳变,代码里的数据重组逻辑进入死循环。

解决:给接收 socket 设置超时时间,用setsockopt配合SO_RCVTIMEO

struct timeval tv; tv.tv_sec = 3; tv.tv_usec = 0; setsockopt(rtp_sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

参数说明:

  • 超时设为 3 秒,recvfrom 如果 3 秒内没有数据会返回 -1,errno 置为EAGAINEWOULDBLOCK。外层循环依据这个错误码判断是继续等还是走重连逻辑。
  • 不要设置为 0(永不超时),除非你的重连逻辑完全不依赖接收线程的反馈。
  • 在 RTP 时间戳重组逻辑里,加上序列号跳变检测,如果seq差值大于一个阈值(比如 1000),直接对齐到新的序列号而不是等待补齐。

4.4 现象:DESCRIBE 响应里的 SDP 解析不完整

现象:打印解析后的 SDP 内容时发现只有前三行,或者m=行后面的属性丢失。用 VLC 拉同一个地址却是正常的。

原因:rtsp.c 里的响应缓冲区长度不够。有些服务器的 DESCRIBE 响应可能长达几 KB,如果缓冲区只有 1024 字节,数据被截断。另一个常见的原因是接收时只调用了一次 recv,但一次 recv 并没有拿完所有数据。TCP 是流式协议,一次 recv 不能保证拿到完整的应用层消息。

解决:接收并检查是否到达消息边界。没有 Content-Length 时,以空行\r\n\r\n作为头部结束标志,有 Content-Length 时按长度接收完整个消息体。缓冲区建议按RTSP_BUFFER_SIZE配置在 8KB 以上。

int total = 0; while (total < content_length) { int n = recv(sock, buf + total, sizeof(buf) - total, 0); if (n <= 0) break; total += n; }

这段循环的逻辑很直白:没凑够 Content-Length 就继续 recv,直到数据完整。

4.5 现象:启动时绑定端口失败,显示 Address already in use

现象:每次启动程序都要等待几秒钟,或者直接报bind: Address already in use,换一个端口就好了。

原因:上一次运行没有干净地关闭 UDP socket,端口被内核占用。虽然程序退出后 socket 会自动关闭,但如果没有设置SO_REUSEADDR,偶尔会碰到端口被占用的情况,特别是快速重启的场景。

解决:在 bind 之前设置 set reuse 属性:

int reuse = 1; setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));

这样即使 socket 处于 TIME_WAIT 状态,也能在同一端口重新绑定。如果你是在做服务端开发,一般会建议开启SO_REUSEPORT,但作为 RTSP 客户端,SO_REUSEADDR已经完全够用。

5. 进阶技巧:用 RTP 载荷类型校验实现弱网情况下的故障定位,把 demo 变成能上线的客户端

testrtsp.c 能跑通,只能证明你的代码和服务器之间建立了正确的沟通方式。但从能跑到稳定运行之间,还差一个关键检测手段:对 RTP 数据包的载荷类型和序列号进行校验。这能帮助你快速判断问题是出在网络丢包、编码器异常、还是服务器负载过高导致的发送端丢帧。

5.1 RTP 头解析与负载类型校验

RTP 头的长度固定为 12 字节,结构是固定的。你可以在接收循环中解析出每个包的负载类型和序列号:

unsigned char *rtp_packet = buffer; int payload_type = rtp_packet[1] & 0x7F; unsigned short seq = (rtp_packet[2] << 8) | rtp_packet[3]; if (payload_type != expected_pt) { printf("Payload type mismatch: expected %d, got %d\n", expected_pt, payload_type); }

逻辑说明:

  • rtp_packet[1]的高字节是标记位,低 7 位是负载类型,所以要与 0x7F 做与运算。SDP 里常见RTP/AVP 96,这里的96就是负载类型,和payload_type一一对应。
  • 序列号由第 2、3 字节组成,大端序存储。如果你发现序列号跳变超过一定范围,说明中间发生了丢包。

在实际项目里,我一般会把接收循环设计成带状态输出的形式:

static int last_seq = -1; int gap = (last_seq >= 0) ? (seq - last_seq) : 1; if (gap > 1 && last_seq >= 0) { printf("Packet loss detected: %d packets lost (seq %d -> %d)\n", gap - 1, last_seq, seq); // 在这里可以触发重连或上报异常 } last_seq = seq;

这种实现的价值在于:它让“视频卡了”从一个模糊的感觉变成了一个可量化的证据链。当现场反馈“视频花屏、卡顿”时,你可以直接看打印的丢包统计,判断是网络链路的问题,还是对端设备编码器的问题。

5.2 断线重连机制的设计

TCP 连接长时间没有数据接收,可能已经被对端静默关闭。当你的接收循环碰到recvfrom返回ETIMEDOUTECONNRESET时,尝试恢复连接会比等待下一次正常收到数据更可靠。

常见做法是三层恢复机制:

层次触发条件处理方式
第一层recv 超时一次发起 RTSP OPTIONS 探测,确认连接是否活着
第二层连续 3 次探测失败主动断开,释放所有资源,回到初始状态
第三层重连超过 3 次提升日志级别,轮询等待下一次重试,间隔 10 秒

重连时最容易犯的错误是在旧 socket 没有关闭的情况下直接创建新连接。一定要先把 TCP socket、UDP socket 全部关闭,再走一次完整的 OPTIONS -> DESCRIBE -> SETUP -> PLAY 流程。这个逻辑放在独立的线程里跑,不要阻塞主业务线程。

5.3 从 testrtsp.c 到生产代码的改造清单

testrtsp.c 是一个线性执行的示例,生产环境需要加的内容包括:

  1. SDP 解析器增强:自动识别多轨流(视频+音频),为每个轨道建立独立的 SETUP 会话。当前代码只处理单路视频轨,遇到多轨流时需要扩展。

  2. RTP 接收线程独立:播放命令发出后,RTP 数据的接收不能占用命令发送线程。用 pthread 创建独立的接收线程,并使用环形缓冲区缓存数据。

  3. 超时与错误统计:维护一个状态结构体,记录重连次数、累计丢包数、最近一次错误码。这个结构体既用于调试,也用于上报到业务层做告警。

  4. 内存管理检查:RTSP 响应解析过程中如果用了malloc,确保所有路径上都有对应的free,包括出错跳转的路径。

这套代码我实际用下来,在嵌入式 Linux 板卡上拉海康、大华的 RTSP 流都没有问题,但也因为碰过上面说的几个坑,所以后面每次集成新的平台,都会先按这个顺序做一轮验证:先跑通命令交互,再校验 RTP 接收,最后才进业务逻辑。希望这些经验能帮到你,至少能让你少走几个我之前走过的弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询