ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

C语言实现RTSP客户端:信令交互、RTP接收与H264组装实战

C语言实现RTSP客户端:信令交互、RTP接收与H264组装实战

简介:一份使用C语言实现的实时流协议(RTSP)客户端源码包,面向流媒体开发者和网络协议学习者,主要用于理解客户端与服务器之间的请求响应交互、会话建立以及播放、暂停、快进等控制流程。压缩包内共有28个文件,其中包含10个C源程序文件、6个头文件、8个编译生成的目标文件,以及静态库、工程构建文件与文本说明,整体体积仅157KB,结构紧凑,便于快速阅读和二次开发。核心功能覆盖了协议请求构造、响应报文解析、媒体会话描述解析、基于传输层协议的网络通信等模块,通过源码与头文件的配合呈现了一个最小可运行的客户端框架,能够帮助读者掌握套接字编程、协议状态机管理以及实时音视频数据交互的完整流程。文件分类清晰,源文件承载具体协议逻辑,头文件定义数据结构和接口,编译中间产物方便直接链接验证,为学习者提供了从代码阅读到实际编译的一体化参考。目前已有349人学习或下载,适合具备一定C语言基础、希望深入网络协议开发或构建视频监控、直播采集类应用的开发者参考学习。

1. rtsp.rar 这个标题,本质是每个嵌入式工程师都绕不开的 RTSP 客户端

rtsp.rar 这个压缩包名字,几乎是安防行业里 C 语言 RTSP 客户端的代名词:RTSP 客户端、信令交互、RTP 接收、H264 拼接全塞在一个包里,谁做嵌入式拉流都得先从它起步。我最早解压这类包是在做海康取流适配的时候,跑通 DESCRIBE 很简单,但真正让它连续播放不花屏、断网能重连,折腾了三个晚上。这篇文章就把 RTSP 客户端怎么选型、信令怎么写、RTP 怎么拼、平台对接在哪翻车,一次性讲透。适合要接萤石取流地址或者海康威视 RTSP 接口的嵌入式开发者,也适合想摆脱 FFmpeg 体积、自己掌控每一帧数据的 C 语言客户端方向的从业者。

2. RTSP 客户端的组成与选型:为什么说信令交互是 C 客户端的命门

2.1 RTSP 会话的本质:OPTIONS/DESCRIBE/SETUP/PLAY 四步信令

RTSP(Real Time Streaming Protocol)是基于文本的协议,默认端口 554,结构和 HTTP/1.1 相似:请求行加头字段加空行,服务器返回状态码和头。很多初写 RTSP 客户端的人把精力放在 RTP 包解析上,结果信令没走通,后面全是白费。客户端要做的第一件事不是收流,是“先说话,再听戏”——OPTIONS、DESCRIBE、SETUP、PLAY 四步走完,RTP 数据才开始往你这边来。

RTSP 是带状态的协议,服务器端为每个会话维护一个状态机。OPTIONS 询问服务器支持哪些方法,DESCRIBE 拿回 SDP 描述(有几路媒体、什么编码、控制 URL 是什么),SETUP 协商 RTP 传输方式并绑定端口和 session id,PLAY 才让媒体真正流动。反过来,停止播放可以发送 TEARDOWN,但实际生产我更倾向直接断 TCP 连接再重连,避免挂在半死不活的会话状态上。

方法用途关键头字段
OPTIONS获取服务器支持的方法列表CSeq、User-Agent
DESCRIBE获取 SDP 媒体描述Accept: application/sdp
SETUP协商传输方式与端口Transport、Session
PLAY开始推流Session、Range

SDP 里最关键的是这几行:m=video 0 RTP/AVP 96,数字 96 是 RTP 负载类型,要和你 RTP 解析里的 payload type 对上;a=control:trackID=1给出单轨控制 URL,SETUP 的请求 URI 不是最初的rtsp://ip/stream,而是 base URL 和 control 字段拼接出来的完整 track URL。拼错就返回 404,这是新手掉进去的第一个坑。

2.2 选型:裸写 socket、live555、FFmpeg、GStreamer 怎么选

标题里写着 C 语言 RTSP 客户端,常见做法有两派:基于 POSIX socket 自研信令和接收层,或者基于 live555 裁剪。我的建议是信令部分自己写,RTP 接收看场景;下面是这几条路线的真实边界。

方案体积上手成本适合场景
自研 socket小高RV1106 这类 Flash 受限的嵌入式平台
live555中中要兼容大量摄像头、但要啃古老代码
FFmpeg大低不介意体积、还要解码服务
GStreamer大低项目里已经依赖 GStreamer 插件体系

如果目标芯片只有 32MB Flash,FFmpeg 直接出局;如果要做多路并发拉流,live555 的 openRTSP 模型改造起来比自研更费劲,它的回调模型和阻塞 socket 混在一起,多路扩展要自己补线程池。自研虽然要处理 Digest 认证、SDP 解析、FU-A 重组这些细节,但每一帧数据都在你手里,线上出问题能直接抓包下结论。本文后面代码全按自研方向写,遇到细节点我会标注“这里可以参考 live555 的验证逻辑”。

2.3 最小可用的 C 语言信令骨架:状态机与请求函数划分

先给骨架,后面每节再填充细节。一次完整的 RTSP 交互被拆成 5 个操作函数,配合状态机枚举,目的是让信令流和媒体流在代码层面分离。多路并发时每个客户端实例持有自己的 socket 和 cseq,互不干扰。

typedef enum { RTSP_STATE_INIT = 0, RTSP_STATE_OPTIONS, RTSP_STATE_DESCRIBE, RTSP_STATE_SETUP, RTSP_STATE_PLAY, RTSP_STATE_TEARDOWN } rtsp_state_t; typedef struct rtsp_client { int sock; /* TCP 连接句柄 */ uint32_t cseq; /* 请求序号,每次请求自增 */ char session[64]; /* SETUP 应答带回的会话 ID */ char realm[128]; /* Digest 认证域 */ char nonce[64]; /* Digest 随机数 */ rtsp_state_t state; int rtp_port; /* 本地 RTP 端口 */ int rtcp_port; /* 本地 RTCP 端口 */ uint8_t payload_type; /* SDP 里协商的 RTP 负载类型 */ } rtsp_client_t; int rtsp_send_request(rtsp_client_t *c, const char *method, const char *uri, const char *extra_headers); int rtsp_read_response(rtsp_client_t *c, char *buf, int buf_len);

cseq 从 1 开始,每次请求递增,服务器靠它匹配请求和响应。sock 用阻塞 socket 就够,但底层必须设置SO_RCVTIMEO,否则服务器不响应时整个线程卡死。session 字段在 SETUP 的应答里返回,PLAY 和后续保活请求都必须带上这个头,不带会被服务器视为非法请求。

这里还缺一个统一读响应函数,常见做法是把“读一行”和“读空行结束的头区”拆开:

static int rtsp_read_line(rtsp_client_t *c, char *line, int cap) { int n = 0; char ch; while (n < cap - 1) { int r = recv(c->sock, &ch, 1, 0); if (r <= 0) return -1; if (ch == '\n') break; if (ch != '\r') line[n++] = ch; } line[n] = '\0'; return n; }

recv一次一个字节看起来低效,但 RTSP 是低频控制信令,每次会话只有几十条请求,瓶颈不在信令而在 RTP 媒体流,所以这种写法足够。注意\r\n是协议要求的行结束符,按行解析时把\r剥掉再存字符串,后面做 Digest 运算时要拿干净的字符串去拼。

3. 用 C 实现 RTSP 信令交互:从 URL 解析到 PLAY 的可复现代码

3.1 解析 rtsp:// 地址并建立 TCP 连接

摄像头 RTSP 地址常见格式是rtsp://user:pass@ip:554/Streaming/Channels/101,用户名密码可能带@、:、%这些特殊字符,直接跑strchr找最后一个@会出错。解析顺序应该是:先找://,再找路径起始的/,再看主机段里有没有@,有的话分离用户名密码。

static int rtsp_parse_url(const char *url, rtsp_url_t *u) { const char *p = url; const char *path, *host, *at, *colon; if (strncmp(p, "rtsp://", 7) != 0) return -1; p += 7; path = strchr(p, '/'); if (!path) return -1; host = p; at = memchr(p, '@', path - p); if (at) { const char *pw = memchr(p, ':', at - p); if (pw) { size_t ulen = pw - p; size_t plen = at - pw - 1; memcpy(u->user, p, ulen); u->user[ulen] = '\0'; memcpy(u->pass, pw + 1, plen); u->pass[plen] = '\0'; } host = at + 1; } colon = memchr(host, ':', path - host); if (colon) { u->port = atoi(colon + 1); memcpy(u->host, host, colon - host); } else { u->port = 554; memcpy(u->host, host, path - host); } u->host[path - host] = '\0'; snprintf(u->path, sizeof(u->path), "%s", path); return 0; }

参数说明:路径从第一个/开始复制,因为 SETUP 时需要在路径后面拼接trackID之类的 control 字段;端口没写默认 554。用户名密码分离后,DESCRIBE 请求可以暂时不带头,等服务器回 401 再带认证信息,这样兼容性最好——有些摄像头直接在 URL 里带用户名密码,有些则强制走 Digest 流程。

建连部分没有特殊技巧,socket + connect + SO_RCVTIMEO三件套。设置 5 秒超时比较稳妥:连接超时、读响应超时都按 5 秒算。嵌入式平台如果走的是蜂窝网络,首次建连可能超过 5 秒,可以放宽到 8 秒,但读响应不要放宽,因为信令是同步问答,服务器正常情况下几毫秒就回。

3.2 DESCRIBE 与 SDP 解析:拿到媒体轨道和控制 URL

建完连接,第一步发 DESCRIBE。请求行里必须带Accept: application/sdp,很多服务器不认裸的 DESCRIBE。

static int rtsp_describe(rtsp_client_t *c, const char *uri) { char req[512]; c->cseq++; snprintf(req, sizeof(req), "DESCRIBE %s RTSP/1.0\r\n" "CSeq: %u\r\n" "Accept: application/sdp\r\n" "User-Agent: TinyRtspClient/1.0\r\n" "\r\n", uri, c->cseq); if (send(c->sock, req, strlen(req), 0) < 0) return -1; return rtsp_read_response(c, c->resp_buf, sizeof(c->resp_buf)); }

服务器应答 200 OK 后,正文就是 SDP。解析 SDP 的常见做法是逐行扫描,找m=video和紧随其后的a=control、a=rtpmap、a=fmtp。rtpmap 里会写H264/90000,这个 90000 是 RTP 时间戳频率;fmtp 里是sprop-parameter-sets,里面是 Base64 编码的 SPS/PPS,它们在关键帧到达前就可以提前存好,是播放器秒开的关键。

static void sdp_parse_video(const char *sdp, rtsp_media_t *m) { const char *line = sdp; while ((line = strstr(line, "\r\n")) != NULL) { line += 2; if (strncmp(line, "m=video ", 8) == 0) { sscanf(line, "m=video %*d RTP/AVP %hhu", &m->payload_type); } else if (strncmp(line, "a=control:", 10) == 0) { snprintf(m->control_url, sizeof(m->control_url), "%s", line + 10); } else if (strncmp(line, "a=rtpmap:", 9) == 0) { char codec[32]; sscanf(line, "a=rtpmap:%*d %31s/%*d", codec); strncpy(m->codec, codec, sizeof(m->codec) - 1); } } }

这里有个细节:control_url 可能是相对路径,也可能是完整 URL。实际处理时先判断它是不是以rtsp://开头,不是就用 DESCRIBE 应答里的Content-Base头拼接。Content-Base 和 control_url 的拼接规则各家摄像头不统一,最稳妥的办法是看 SETUP 用哪个 URL 服务器不返回 404。这部分没有捷径,拿 Wireshark 抓一次正常播放的包,照着请求格式对齐即可。

3.3 SETUP / PLAY:协商传输方式并开始拉流

SETUP 是在为 RTP 流“定契约”。UDP 模式要写清楚client_port,TCP 模式要带interleaved。

static int rtsp_setup_udp(rtsp_client_t *c, const char *control_url) { char req[512]; c->cseq++; snprintf(req, sizeof(req), "SETUP %s RTSP/1.0\r\n" "CSeq: %u\r\n" "Transport: RTP/AVP/UDP;unicast;client_port=%d-%d\r\n" "\r\n", control_url, c->cseq, c->rtp_port, c->rtcp_port); if (send(c->sock, req, strlen(req), 0) < 0) return -1; if (rtsp_read_response(c, c->resp_buf, sizeof(c->resp_buf)) < 0) return -1; const char *session_hdr = strstr(c->resp_buf, "Session:"); if (session_hdr) { sscanf(session_hdr, "Session: %63[^;]", c->session); } return 0; }

Transport 应答里会带回server_port,那是服务器的 RTP 端口。客户端绑定的本地 UDP 端口要提前创建好,否则数据到了内核但没进程接收,表现为“SETUP 成功但收不到任何 RTP”,这个坑后面避坑章节还会展开。

PLAY 请求带上 Session 头,Range 用npt=0.000-表示从零开始:

static int rtsp_play(rtsp_client_t *c, const char *uri) { char req[512]; c->cseq++; snprintf(req, sizeof(req), "PLAY %s RTSP/1.0\r\n" "CSeq: %u\r\n" "Session: %s\r\n" "Range: npt=0.000-\r\n" "\r\n", uri, c->cseq, c->session); if (send(c->sock, req, strlen(req), 0) < 0) return -1; return rtsp_read_response(c, c->resp_buf, sizeof(c->resp_buf)); }

PLAY 返回 200 OK 后,UDP 模式下 RTP 包会从服务器端口发到client_port,TCP 模式下则复用同一个 TCP 连接,以$开头加通道号和两字节长度的封装格式混在信令流里。区分这两种模式,是接收层的第一件事。

4. RTP 接收与 H264 数据组装:画面能出来才是真客户端

4.1 RTP 头解析:先读对 12 字节的固定头

RTP 头固定 12 字节,网络字节序。解析时最常翻车的不是结构体定义,而是没做字节序转换就用了memcpy。RTP 的序号和时间戳都是大端序,在 x86 上必须ntohs/ntohl。

typedef struct { uint8_t version; /* 2 */ uint8_t padding; uint8_t extension; uint8_t csrc_count; uint8_t marker; /* 关键帧的标志,在 H264 里通常是 IDR 帧起始 */ uint8_t payload_type; uint16_t seq; /* RTP 序号,可能回绕 */ uint32_t timestamp; /* 时间戳,90kHz 时钟 */ uint32_t ssrc; } rtp_hdr_t; static void rtp_parse_header(const uint8_t *buf, rtp_hdr_t *h) { h->version = (buf[0] >> 6) & 0x03; h->padding = (buf[0] >> 5) & 0x01; h->extension = (buf[0] >> 4) & 0x01; h->csrc_count = buf[0] & 0x0F; h->marker = (buf[1] >> 7) & 0x01; h->payload_type= buf[1] & 0x7F; h->seq = (buf[2] << 8) | buf[3]; h->timestamp = (buf[4] << 24) | (buf[5] << 16) | (buf[6] << 8) | buf[7]; h->ssrc = (buf[8] << 24) | (buf[9] << 16) | (buf[10] << 8) | buf[11]; }

marker 位在 H264 over RTP 里的含义是一帧的最后一个 RTP 包。很多新手以为 marker 等于关键帧,其实它只表示“这个 RTP 包完成后,一个完整的接入单元可以送解码器”。H264 单帧数据如果被拆成 5 个 RTP 包,只有第 5 个包 marker 为 1。识别 IDR 要看 NAL 头,不是看 marker。

4.2 H264 的 FU-A 分片重组与 RBSP 拼接

H264 NAL 单元超过 MTU 后会被 RTP 分片。负载类型 28 表示 FU-A 分片,这是最容易写错的地方。FU 指示字节的低 5 位是原始 NAL 类型,FU 头字节的 bit7 是起始位 S,bit6 是结束位 E。重组时要把 SPS/PPS、IDR 这些 NAL 前拼上 00 00 00 01 起始码,输出给解码器或封装器。

static int rtp_h264_reassemble(uint8_t *out, int out_cap, const uint8_t *payload, int payload_len, uint8_t nal_header, uint8_t fu_header) { int nal_type = nal_header & 0x1F; int start = (fu_header >> 7) & 1; int end = (fu_header >> 6) & 1; int offset = 0; if (start) { if (out_cap < payload_len - 1 + 5) return -1; out[0] = 0x00; out[1] = 0x00; out[2] = 0x00; out[3] = 0x01; /* 重建 NAL 头:类型位取 FU 头低 5 位,F/NRI 保持 */ out[4] = (nal_header & 0xE0) | (fu_header & 0x1F); memcpy(out + 5, payload + 2, payload_len - 2); offset = payload_len + 3; } else { if (out_cap < payload_len - 2) return -1; memcpy(out, payload + 2, payload_len - 2); offset = payload_len - 2; } return end ? offset : 0; /* 返回 0 表示分片未结束 */ }

逻辑说明:起始分片要重建 NAL 头,非起始分片直接拷负载就行。函数返回 0 代表这个分片还不是完整的 NAL,返回正值代表一个 NAL 组装完毕。调用方要自己维护一个跨包缓冲区,收到的每个 FU-A 分片先判断 S 位是否和缓冲区为空的状态匹配——S 位为 1 时有未消费的数据,是分片丢失,应该直接丢弃这一帧。

这段代码只覆盖了最简单的单分片和 FU-A。实际流里还有 STAP-A 聚合包、H265 的 VPS/SPS/PPS 多参数集,以及 RTP 扩展头可能占掉负载头之外 4 字节。扩展头要不要跳过,看 RTP 固定头的 X 位,处理不当会出现“最后几帧偶尔花屏”的玄学问题。

4.3 时间戳与抖动:别让缓冲越积越大

RTP 时间戳是 90kHz 的采样时钟,25fps 视频相邻两帧差 3600。不能拿它直接当 PTS 用,要按帧率换算成毫秒:

static uint32_t rtp_ts_to_ms(uint32_t rtp_ts) { return (uint32_t)((uint64_t)rtp_ts * 1000 / 90000); }

换算本身简单,真正的坑在时间戳回绕。RTP 时间戳是 32 位无符号数,大约 13 小时回绕一次,直接相减会算出负数。常见做法是定义差值时把结果强转成有符号 32 位,再判断正负:

static int32_t ts_diff(uint32_t a, uint32_t b) { return (int32_t)(a - b); /* 负值表示 b 比 a 晚 */ }

解码器的 PTS 增量,建议用“收到的帧计数”而不是绝对时间戳对齐。做法是记录当前帧时间戳和上一帧时间戳的差值,差值在 3000~4200 之间就认为是一帧正常间隔;差值过大说明丢帧,过小说明重复帧或乱序,这两种都按上一帧加 3600 处理。网络抖动严重时,可以在解码前挂一个能装 5 帧的小缓冲,超过 5 帧就把最旧的帧送解码器,防止内存越积越大——这是最容易在长时间运行后被业务方投诉“内存涨了”的地方。

5. 接萤石、海康这类平台时的 5 个血泪坑:现象、原因、解法

5.1 401 认证循环:Digest 摘要没带对重放字段

现象:DESCRIBE 返回 401,带Authorization头重发还是 401,Wireshark 里看到客户端反复发 DESCRIBE。

原因:海康和不少流媒体服务器用的是 Digest 认证,response计算要求 H(A1)、nonce、H(A2) 三部分,其中 H(A2) 里的 URI 必须和请求行里的请求 URI 完全一致。很多实现把路径写成了不带参数的部分,或者把 cnonce、nc 两个字段漏掉,服务器直接拒绝。

解决:先不看库代码,用 OpenSSL 的 MD5 函数重算一遍。Digest 摘要公式是MD5(HA1:nonce:nc:cnonce:qop:HA2),HA1 是md5(user:realm:pass),HA2 是md5(method:uri)。uri 必须和请求行一致,realm 和 nonce 取自服务器 401 应答的WWW-Authenticate头。

5.2 UDP 拉流花屏:默认传输方式不等于最优传输方式

现象:SETUP 用RTP/AVP/UDP,画面大面积花屏,同一台设备用 VLC 却正常。

原因:VLC 默认会用 TCP 拉流。UDP 模式丢包没有重传,RTP 序号断档后解码器等不到后续分片,画面就花了。跨网段拉取,或者 Wi-Fi 环境,丢包超过 1% 就会肉眼可见。

解决:把 SETUP 的 Transport 改成RTP/AVP/TCP;unicast;interleaved=0-1。TCP 模式下媒体数据和信令复用同一个 TCP 连接,客户端接收层要识别0x24开头的封装:通道号 + 两字节长度 + RTP 数据。代价是带宽开销略大,但丢包时 TCP 的重传保住了解码连续性,安防场景优先 TCP。

static int rtsp_on_tcp_data(const uint8_t *buf, int len, rtp_hdr_t *hdr) { if (len < 4 || buf[0] != '$') return -1; int channel = buf[1]; int rtp_len = (buf[2] << 8) | buf[3]; if (channel != 0 || 4 + rtp_len > len) return -1; rtp_parse_header(buf + 4, hdr); return 4 + rtp_len; }

5.3 会话超时静默掉线:没保活,服务器以为你走了

现象:拉流两分钟后画面卡住,服务器日志显示会话已关闭,客户端没有任何报错。

原因:SETUP 应答里Session: 12345;timeout=60,60 秒是服务器端的会话超时。客户端没解析这个字段,也没发任何保活请求,服务器到期后直接回收会话资源,后续 RTP 自然断流。

解决:解析 Session 头拿到 timeout,起一个保活定时器,周期设为 timeout 的三分之一,发送GET_PARAMETER带 Session 头。GET_PARAMETER 的 URI 用会话的 base URL 就行,不需要完整轨道地址。如果连续三次保活都没收到响应,判定会话死亡,主动触发重连。

5.4 H265 源的 SPS/PPS 不在关键帧里:解码器等参数等到超时

现象:拉到 H265 的流,ffprobe能看到轨道信息,但自己的播放器黑屏,日志里全是“解码器无参数集”。

原因:海康威视 RTSP 接口默认把 SPS/PPS 放在关键帧前面,但部分新固件和萤石取流地址走的是带外参数集,SPS/PPS 通过 SETUP 阶段或者单独的 RTP 包发送。客户端没缓存参数集,关键帧到了也无法解码。

解决:接收层单独维护 SPS/PPS 缓存区,无论是从 SDP 的 fmtp 字段、STAP-A 聚合包还是单包 NAL 里发现参数集,都覆盖式缓存。每帧输出给解码器前,先检查参数集是否存在,不存在就拼接在 IDR 帧前面。注意 H265 有 VPS、SPS、PPS 三类参数集,缺一不可。

5.5 收不到 RTP 数据:先按顺序排除这四个位置

现象:SETUP 返回 200 OK,PLAY 也 200 OK,但接收线程一个 RTP 包都没等到。

原因:可能是本地端口没绑定、服务器走 TCP interleaved 但客户端只处理 UDP、防火墙丢包、或者 client_port 和服务器实际发送端口不一致。最隐蔽的是第四种:客户端绑定的 UDP 端口和 SETUP 里写的 client_port 不是同一个端口。

解决:按顺序查。第一步tcpdump -i eth0 port 8000 or port 8554看有没有包到本机;第二步确认本地 socket 绑定端口和 client_port 一致,getsockname打出来对比;第三步看服务器应答的 Transport 头里 server_port 是哪个,如果服务器用 server_port 发 RTP,那就是服务器端配置问题,换一台设备对比;第四步检查防火墙,临时iptables -I INPUT -p udp --dport 8000 -j ACCEPT验证。这个过程里 80% 的“收不到数据”最终都落在第一步和第二步。

6. 从能播放到能上线:测试流验证、断流重连与最后的封装技巧

6.1 用 gstreamer 起本地测试流,再用 ffprobe 验证你的客户端

自研客户端最怕的是没法和参考实现对比。常见做法是用 gstreamer 的 rtsp-server 示例起一路测试流,这样你既能控制编码参数,又能随时抓包看正确行为。命令行起一个 H264 测试源:

test-launch "( videotestsrc ! video/x-raw,width=640,height=480 ! x264enc ! rtph264pay name=pay0 )"

起完之后,你的客户端去拉rtsp://127.0.0.1:8554/test。如果客户端不正常,先用ffprobe -rtsp_transport tcp rtsp://127.0.0.1:8554/test确认参考实现能通,再用 Wireshark 对比自己和 ffprobe 发出的请求差异。这个对比法能解决 90% 的“服务器不搭理我”类问题——你缺了哪个头,参考实现里有,照着补就行。

6.2 断流重连与日志埋点:上线前必须补的两层防护

重连策略我一般用指数退避,第一次断线立即重连,之后 1 秒、2 秒、4 秒,封顶 30 秒。重连时整个 rtsp_client 结构体要重新走一遍 INIT 状态,session、cseq、SPS/PPS 缓存全部清空。日志埋点至少要有:每次信令请求和应答的状态码、SETUP 协商出来的传输模式、RTP 序号跳跃次数、重连触发原因。这些日志在线上排查“为什么这台设备黑屏”“为什么那台设备半小时断一次”时,比任何调试器都管用。

我自己吃过的最大的亏是只测局域网、不测跨网段。局域网里 UDP 拉流怎么都不花屏,一上客户现场就翻车;后来所有新设备验收都先过一遍 TCP 传输模式的用例。另一个教训是,H265 和 H264 不要共用同一套参数集缓存逻辑,H265 的 VPS 丢了同样黑屏。这两个习惯保留到现在,帮我少值了不少夜班。希望帮到你。

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

返回列表