ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux C实现RTSP客户端:从命令交互到RTP接收的完整指南

Linux C实现RTSP客户端:从命令交互到RTP接收的完整指南 简介这是一份面向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_port和rtcp_port在 SETUP 阶段从服务器响应中提取后续接收 RTP 数据时要用。2.2 rtsp.cRTSP 消息的解析与组装底层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_port8000-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 命令交互走 TCPRTP 数据可以走 UDP 也可以走 TCP当前实现一般是 UDP。每次请求的 CSeq 必须递增从 1 开始。服务器用这个字段来匹配请求和响应。DESCRIBE的响应里包含 SDP 信息其中mvideo行的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:passip:port/path格式。跑通 testrtsp.c 之后你再去看 rtspclient.c 就会觉得清晰很多因为它本质上就是一个被拆成多个函数的 testrtsp.c。3. 把 RTSP 命令交互跑通OPTIONS 到 PLAY 的完整流程与参数协商RTSP 命令交互看起来就是简单地发几个请求、收几个响应但真正涉及线上环境时细节都在参数协商里。整个流程是固定的OPTIONS 探路DESCRIBE 获取媒体描述SETUP 协商传输通道PLAY 触发数据流最后 TEARDOWN 收尾。3.1 OPTIONS 与 DESCRIBE协商能力与拿 SDPOPTIONS 是客户端发送的第一条命令用来查询服务器支持哪些方法。响应里会有Public:字段列出服务器支持的方法例如OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE。这个字段的主要价值不是让客户端做判断而是确认当前连接的服务确实支持 RTSP 协议。DESCRIBE 是真正开始工作的命令。它返回的信息用 SDP 格式组织里面有媒体类型、编码格式、码率、分辨率等关键参数。对于 H.264 编码的流SDP 里会有类似下面这样的内容mvideo 0 RTP/AVP 96 artpmap:96 H264/90000 acontrol:track1这段 SDP 的意思是视频轨道使用 RTP 传输负载类型编号是 96编码格式是 H.264时钟频率是 90000Hz控制路径是track1。后续的 SETUP 请求要带上track1这个控制路径才能正确关联到视频轨道。实际抓包时会发现有些服务器尤其是国产 IPC的 SDP 格式并不完全规范。比如有的在m行用98作为负载类型有的甚至不写rtpmap行。遇到这种情况代码里的解析逻辑就需要有兜底处理。3.2 SETUPTransport 头的 UDP 端口协商SETUP 是整个 RTSP 交互中最容易出错的环节因为传输参数在这里协商。客户端要发送的请求头类似这样SETUP rtsp://192.168.1.64:554/stream1/track1 RTSP/1.0 CSeq: 3 Transport: RTP/AVP/UDP;unicast;client_port8000-8001这里的client_port8000-8001是客户端自己选择的端口8000 用于接收 RTP 数据8001 用于接收 RTCP 数据。服务器收到后会在响应中告诉客户端它选择的服务器端口Transport: RTP/AVP/UDP;unicast;client_port8000-8001;server_port9000-9001关键逻辑说明客户端必须先绑定本地端口 8000 和 8001然后才能发送 SETUP 请求。如果先发 SETUP 再绑定端口会丢失第一批到达的 RTP 数据。server_port9000-9001是服务器发送 RTP 数据的源端口通常用于后续的 RTCP 收发和 NAT 穿透判断但接收 RTP 数据本身只依赖于客户端绑定的 8000 端口。部分服务器要求Transport头中携带moderecord或者modeplay参数如果缺少可能导致 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: npt0.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 OKPLAY 也发出去了但 recvfrom 一直阻塞收不到任何 UDP 数据。原因最常见的两个原因。第一是本地没有提前 bind UDP 端口RTP 数据到达时找不到对应端口而丢弃。第二是代码里解析服务器响应时把server_port和client_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_RCVTIMEOstruct timeval tv; tv.tv_sec 3; tv.tv_usec 0; setsockopt(rtp_sock, SOL_SOCKET, SO_RCVTIMEO, tv, sizeof(tv));参数说明超时设为 3 秒recvfrom 如果 3 秒内没有数据会返回 -1errno 置为EAGAIN或EWOULDBLOCK。外层循环依据这个错误码判断是继续等还是走重连逻辑。不要设置为 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返回ETIMEDOUT或ECONNRESET时尝试恢复连接会比等待下一次正常收到数据更可靠。常见做法是三层恢复机制层次触发条件处理方式第一层recv 超时一次发起 RTSP OPTIONS 探测确认连接是否活着第二层连续 3 次探测失败主动断开释放所有资源回到初始状态第三层重连超过 3 次提升日志级别轮询等待下一次重试间隔 10 秒重连时最容易犯的错误是在旧 socket 没有关闭的情况下直接创建新连接。一定要先把 TCP socket、UDP socket 全部关闭再走一次完整的 OPTIONS - DESCRIBE - SETUP - PLAY 流程。这个逻辑放在独立的线程里跑不要阻塞主业务线程。5.3 从 testrtsp.c 到生产代码的改造清单testrtsp.c 是一个线性执行的示例生产环境需要加的内容包括SDP 解析器增强自动识别多轨流视频音频为每个轨道建立独立的 SETUP 会话。当前代码只处理单路视频轨遇到多轨流时需要扩展。RTP 接收线程独立播放命令发出后RTP 数据的接收不能占用命令发送线程。用 pthread 创建独立的接收线程并使用环形缓冲区缓存数据。超时与错误统计维护一个状态结构体记录重连次数、累计丢包数、最近一次错误码。这个结构体既用于调试也用于上报到业务层做告警。内存管理检查RTSP 响应解析过程中如果用了malloc确保所有路径上都有对应的free包括出错跳转的路径。这套代码我实际用下来在嵌入式 Linux 板卡上拉海康、大华的 RTSP 流都没有问题但也因为碰过上面说的几个坑所以后面每次集成新的平台都会先按这个顺序做一轮验证先跑通命令交互再校验 RTP 接收最后才进业务逻辑。希望这些经验能帮到你至少能让你少走几个我之前走过的弯路。本文还有配套的精品资源点击获取
返回列表