一、引言
本文记录一次 RK3588、GStreamer、WebRTC 与 ZLMediaKit 的分层排除故障的经历:publisher 已经 CONNECTED,ZLM(ZLMediaKit) 也生成了 code=0 的 SDP answer,业务页面却一直停在"连接中"。真正的突破口 不是继续猜端口和 NAT,而是找到"服务端返回 answer"与"浏览器开始 ICE"之间缺失的状态。
本文基于启动优化后的系统,重点记录无法播放故障排查。
最终结论: 192.168.31.13的rk3588板子已恢复播放。 最后一次"ZLM 已收到播放请求、浏览器仍无画面"的直接根因,是 ZLMediaKit 两个重复 [http] 段中的 allow_cross_domains 为空,浏览器因此拦截 SDP answer,未执行 setRemoteDescription(),后续 ICE 检测完全没有开始。
现场很容易让人先猜:RTC 端口是不是没开放?公网 IP 或 NAT 回环是否异常?candidate 是否不可达? H.264/Opus 是否不兼容?是 Edge 的问题,还是前端没有正确处理 answer?真正有价值的排查不是挑一个 "最像"的答案,而是沿 WebRTC 状态机逐层证明。
1. 媒体输入无音轨素材导致 Opus 永远不 ready
2. ZLM 服务API/配置异常导致 publisher 无法建立
3. ICE 可达性LAN/公网 candidate 与 NAT 回环需要验证
4. 最终直接根因CORS 阻断业务前端读取 SDP answer
这不是一个 bug,而是一条实时流链路上先后暴露出的多个问题。前三项是系统稳定工作的 必要条件,也都值得修复;但它们不是最后一次浏览器无画面的直接根因。全文将保留这些排除过程,同时把 CORS 证据链作为主线。
二、故障现场:服务端成功,页面却卡在"连接中"
最具迷惑性的现场不是接口报错,而是每一条服务端日志都"看起来成功":播放器(我们开发的程序)已经发布 H.264 和 Opus, publisher 出现 selected pair 和 DTLS connected;业务页面向 ZLM 发起 play POST,ZLM 返回 code=0 和完整 SDP answer。然而页面始终没有画面,ZLM 的 play 会话最终超时。
ZLM 日志中的 selected_pair 表示 ICE 已经从候选地址中选出一对实际使用的本地端点和远端端点。
publisher: setSelectedPair → OnDtlsTransportConnected
ZLM play API: POST → code=0 + SDP answer
browser page: 一直"连接中"
ZLM play transport: 没有 setSelectedPair → 接受rtp/rtcp/datachannel超时
这个组合把问题限定在一条很窄的边界:发布和媒体源已经正常,ZLM 也处理了信令;下一步不是重做整个 GStreamer pipeline,而是确认 answer 是否真正进入浏览器的 RTCPeerConnection。
三、实时流链路与分层边界
素材 / HDMI 播放管线
│
├─ H.264 视频桥接
└─ PCM → Opus;无音轨时持续生成静音 Opus
│
▼
rk3588_gst_player publisher
│
│ WebRTC 链路 1:publisher → ZLM
│ SDP API: 127.0.0.1:ZLM_HTTP_PORT
│ ICE / DTLS / H.264 + Opus
▼
ZLMediaKit
│
│ WebRTC 链路 2:ZLM → browser
│ SDP answer: HTTP
│ ICE / DTLS / H.264 + Opus
▼
浏览器
(一)WebRTC 基础概念
- **SDP:**描述双方准备收发的音视频格式、方向和连接参数,本身不承载媒体。
- **ICE:**从多个 candidate 中寻找可达路径,并选出实际通信的地址对。
- **DTLS:**ICE 建立路径后进行密钥协商,为后续加密媒体传输建立安全上下文。
readerCount 是 ZLM 当前媒体源的读者数量;大于 0 表示至少有一个播放端正在消费该流。
| 层 | 成功标志 | 典型失败 |
|---|---|---|
| 播放器输入 | 存在视频帧;音频为真实 PCM 或静音 PCM | 无节目单时没有 H.264;无声素材没有真实 PCM |
| 编码与发布 | Video RTP、Audio RTP 持续增长 | 编码器、桥接或 GStreamer 管线失败 |
| 播放器→ZLM | publisher 出现 setSelectedPair 和 DTLS connected |
ZLM 未启动、端口错误、板内 ICE 绕行公网 |
| ZLM 媒体注册 | H.264 和 Opus 均 ready,流已注册 | 只有一个轨道、轨道没有帧 |
| 浏览器信令 | 浏览器能读取 SDP answer 并调用 setRemoteDescription |
CORS、Mixed Content、SDP 解析错误 |
| 浏览器 ICE | play 会话出现 setSelectedPair |
candidate 不可达、UDP/NAT/防火墙问题 |
| 浏览器媒体 | DTLS connected、readerCount > 0 |
编解码兼容、前端 video 元素或自动播放问题 |
四、先建立排障状态机
每一层成功后再进入下一层。这样能避免把"信令没完成"误判成"UDP 不通",也不会在输入为空时先查浏览器。
streamControl 是业务侧发给播放器的启流命令,用于启动指定通道的实时流并等待媒体真正 ready。
1. streamControl / 播放器 请求是否成功?目标通道是否有视频输入?成功:pipeline 与 RTP 开始增长
2. ZLM API HTTP 端口是否监听?API 是否真实响应?成功:进入 publisher 协商
3. publisher 是否出现 selected pair 与 DTLS connected?成功:播放器到 ZLM 已通
4. 媒体注册 H.264 / Opus 是否 ready 且有帧?成功:ZLM 媒体源可播放
5. SDP answer 浏览器 JavaScript 是否真正读取响应?成功:调用 setRemoteDescription
6. 浏览器 signaling setRemoteDescription Promise 是否成功?成功:remote candidate 生效
7. 浏览器 ICE ZLM play 是否出现 setSelectedPair?成功:再验证网络路径
8. DTLS play 会话是否 OnDtlsTransportConnected?成功:安全媒体通道建立
9. readerCount 读者数是否大于 0?成功:媒体正在被消费
10. video 仍无画面才查 codec、srcObject、autoplay这是最后一层
不要把不同层的问题混在一起
- publisher 正常,不代表浏览器播放链路正常。
- ZLM 返回
code=0,不代表浏览器 JavaScript 已经拿到 answer。 - 浏览器收到 answer,不代表
setRemoteDescription()一定成功。 - remote description 成功,不代表 ICE、DTLS 一定成功。
- 只有 play selected pair、DTLS connected 和
readerCount > 0已成立,才应该检查 video 元素和自动播放。
五、阶段一:无音轨素材与 Opus 不就绪
(一)原始错误
Failed to make ZLMediaKit stream ready within 10 seconds:
Opus track is not ready
失败通道的日志持续显示:
Audio RTP: packets=0, bytes=0
Audio bridge input: samples=0, bytes=0
使用 gst-discoverer-1.0 检查 10s_blue.mp4 和 20s_green.mp4,两者只有 H.264 视频,没有音频流。同期另一个播放有声素材的通道约 5.2 秒后成功:
ZLMediaKit stream ready: H264 and Opus tracks are ready
(二)根因
streamControl 的"回复即可以播放"语义要求 ZLM 中 H.264 和 Opus 都 ready。也就是说,业务侧把"启流成功"的完成条件定义在 ZLM 媒体源的双轨就绪上,而不是"视频 pipeline 已启动"或"publisher 已连接"。这个语义本身是合理的:只有 H.264 和 Opus 两个轨道都在 ZLM 中注册并持续产出帧,浏览器端才能拿到一份完整、可播放的 SDP,后续的 ICE 与 DTLS 协商才有意义。
问题出在素材侧。无音轨素材(例如 10s_blue.mp4、20s_green.mp4)只包含 H.264 视频流,没有任何音频流。播放器在解码这类素材时,音频桥接的输入始终为空:既没有真实 PCM 采样,也没有任何可编码为 Opus 的音频数据。于是音频编码链路虽然存在,却永远产不出一个 Opus RTP 包,ZLM 侧的 Opus track 也就永远不会进入 ready 状态。
更关键的是,这个状态不会自行恢复。只要当前播放的素材没有音轨,音频桥接就会一直处于"有管线、无输入"的空转状态,Opus track 的 ready 标志永远不会被置位。业务侧 streamControl 在发出启流命令后,会等待 ZLM 双轨 ready,默认超时窗口为 10 秒。在这 10 秒内,如果 Opus track 始终不就绪,等待逻辑就会超时,随后把刚刚建立起来的 publisher 会话整体清理掉,并向上层返回"Opus track is not ready"的错误。
这解释了一个看似矛盾的现象:为什么一个与浏览器、与网络完全无关的素材问题,会让整个启流接口失败。因为业务完成条件定义在 ZLM 双轨 ready,而不是"视频 pipeline 已启动"。视频链路可能已经完全正常、RTP 也在持续增长,但只要音频轨缺失,整个启流流程就会被判定为失败。换句话说,素材的音轨缺失被"放大"成了整条实时流链路的故障,这正是分层排查时容易在第一层就被卡住的原因。
无音轨 MP4
→ 没有 PCM
→ 没有 Opus RTP
→ ZLM 的 Opus track 永远不 ready
→ streamControl 等待 10s
→ 本次 publisher 被清理
这解释了为什么一个看似与浏览器无关的素材问题,会让整个启流接口失败:业务完成条件定义在 ZLM 双轨 ready,而不是"视频 pipeline 已启动"。
(三)修复
无音轨素材播放期间,由网页流音频管线持续生成静音 PCM 并编码为 Opus,保持固定的 H.264 + Opus 双轨拓扑。 这样从无声素材切换到有声素材时可以直接接入真实音频,不需要重建 ZLM 流。
单纯把就绪条件放宽为"只有 H.264 也返回成功"虽然更简单,但后续从无声素材切换到有声素材时, ZLM 音轨拓扑可能变化,浏览器端需要重新协商,稳定性较差。
六、阶段二:ZLM API 无法连接
(一)原始错误
Failed to connect to ZLMediaKit WebRTC API at http://127.0.0.1:65518
(二)排查方法
**看到 MediaServer PID 不等于服务可用。**最低限度要同时确认:PID、HTTP API 端口、RTC UDP、 RTC TCP 和一次真实 API 响应。缺少任何一项,都不能把这一层标记为"已通过"。对应检查命令集中在附录。
该问题阻断启流,但不是最终浏览器无画面的根因。
(三)永久处理
- 使用健康检查型 supervisor 管理 MediaServer,异常退出后自动拉起。
- 每次启动前从播放器
zlm_api_base_url读取期望 HTTP 端口。 - 把 ZLM 配置中所有重复
[http]段的port统一,避免后一个空值导致重启后不监听。 - 健康状态同时检查 PID 和 HTTP API,不再只检查进程存在。
七、阶段三:公网地址与双 ICE candidate
(一)为什么增加局域网 candidate
浏览器位于局域网时,如果 SDP 只公布公网地址,就依赖路由器 UDP NAT 回环。为同时支持局域网和公网播放, 最终让 ZLM 同时公布局域网与公网地址:
# 192.168.31.13
externIP=192.168.31.13,<PUBLIC_IP>
192.168.31.163
externIP=192.168.31.163,<PUBLIC_IP>
播放器配置:
[stream]
backend = zlmediakit
zlm_api_base_url = http://127.0.0.1:ZLM_HTTP_PORT
zlm_publisher_ice_ip = 127.0.0.1
zlm_lan_ice_ip = auto
zlm_public_base_url = http://<PUBLIC_IP>:ZLM_HTTP_PORT
zlm_public_ip_auto_detect = true
正常启动日志:
Resolved ZLMediaKit LAN ICE IP from eth1: 192.168.31.163
[WebRTC-ZLM] Public IP synchronized: <PUBLIC_IP>,
rtc.externIP=192.168.31.163,<PUBLIC_IP>,
streamBaseUrl=http://<PUBLIC_IP>:65521
(二)publisher 与浏览器 candidate 必须分开
| 用途 | 地址 | 原因 |
|---|---|---|
| 播放器发布到同机 ZLM | 127.0.0.1 |
避免板内流量绕行公网 NAT |
| 局域网浏览器播放 | 板卡 LAN IP | 直接走局域网,不依赖 NAT 回环 |
| 公网浏览器播放 | 公网 IP | 通过路由器端口映射进入 ZLM |
(三)网络验证
在浏览器发起播放的同时按 RTC 端口抓包(命令见附录)。本次测试 UDP 包验证了局域网和公网 NAT 回环:
# .13 公网回环到达板端
192.168.31.1:40000 > 192.168.31.13:65519 UDP
.163 公网回环到达板端
192.168.31.1:40010 > 192.168.31.163:65522 UDP
该结果证明当前公网 UDP 端口映射和局域网 NAT 回环正常。路由器不是最终故障根因。
双 candidate 是一个真实存在、而且必须验证的问题,但最终证明它不是最后一个故障。测试 UDP 包已经到板, ZLM 内置播放器随后也能通过同一 RTC 服务播放。此时继续盯着 NAT 已经没有意义,排查方向应转向浏览器 信令层。这一步体现了一个重要习惯:证据排除一层后,就及时移动到下一条状态边界。
八、阶段四:播放会话始终不建立 ICE
(一)发布链路已经健康
192.168.31.13 的板子上两路 publisher 的证据:
Initial selected_pair: tcp 127.0.0.1:65519 <-> 192.168.31.13:临时端口
OnDtlsTransportConnected
codec info: opus[48000/2/16] H264[1920/1080/30]
codec info: opus[48000/2/16] H264[1280/720/30]
视频和音频 RTP 持续增长,说明以下部分均正常:
- 素材解码和 HDMI 播放管线
- H.264 编码或压缩流复用
- 静音 Opus 回退
- 播放器到 ZLM 的 SDP、ICE、DTLS 和媒体注册
(二)失败播放会话的特征
POST /index/api/webrtc?app=live&stream=rk3588-channel-1&type=play
Host: <PUBLIC_IP>:65518
Origin: http://192.168.31.145
ZLM 返回 code=0 和完整 SDP answer,但之后没有:
setSelectedPair
OnDtlsTransportConnected
readerCount: 0
totalReaderCount: 0
接受rtp/rtcp/datachannel超时
这说明浏览器没有向任何 ZLM candidate 发出可用 STUN/DTLS 流量。故障发生在"ZLM 返回 answer"与 "浏览器开始 ICE"之间。
(三) 一个非常有价值的分界线:play setSelectedPair
publisher 有 setSelectedPair
→ 说明播放器到 ZLM 的发布链路正常
play 有 setSelectedPair
→ 说明浏览器已经应用 remote SDP 并开始 WebRTC ICE
如果 publisher 有、play 没有:
优先检查信令 / SDP / CORS / setRemoteDescription
不要直接把结论写成 UDP 或 NAT 故障
setSelectedPair 是信令层与网络层之间的实用分界。连 play selected pair 都没有时,浏览器可能 根本没有进入 ICE;此时抓不到 UDP 是结果,不是网络故障的证据。
(四)检查前端客户端:关键错误被隐藏
业务前端使用 ZLMRTCClient 1.1.2。代码流程为:
createOffer()
→ setLocalDescription(offer)
→ axios POST offer.sdp
→ setRemoteDescription(answer)
前端当时使用 debug:false,并且没有监听连接状态。setRemoteDescription() 的异常只进入 被禁用的内部 logger,因此页面只能停留在"连接中",无法显示真正错误。这个可观测性缺口增加了排查难度。
后续改进建议,不是本次已验证修复: 业务前端至少应显式记录 setRemoteDescription() 的 Promise rejection,并监听 connectionState、 iceConnectionState 和 signalingState。这不会替代 CORS 修复,但能让故障停在哪个状态 立即可见。
九、决定性 A/B 实验:同一个 Edge,内置页面为什么能播?
这是本次定位过程中最关键的实验,它把问题范围从 ZLM/网络缩小到了浏览器信令层。
在同一台 Windows 电脑、同一个 Edge 中打开 ZLM 内置播放器:
http://192.168.31.13:65518/webrtc/index.html?app=live&stream=rk3588-channel-1&type=play
内置页面立即成功:
2026-08-24 10:21:32
Initial selected_pair: udp 192.168.31.13:65519 <-> 192.168.31.108:58878
OnDtlsTransportConnected
这一步排除了:
- Edge 151 本身不支持当前 ZLM SDP
ZLMRTCClient 1.1.2与当前 ZLM 完全不兼容- 局域网 UDP、ZLM RTC 端口或 H.264/Opus 流不可用
| 对比项 | 业务前端失败 | ZLM 内置页面成功 |
|---|---|---|
| 浏览器和版本 | Edge 151 | Edge 151 |
| offer | a=recvonly,H.264/Opus |
相同 |
| answer | 局域网+公网四个 candidate | 相同 |
| 页面 Origin | http://192.168.31.145 |
http://192.168.31.13:65518 |
| API Host | <PUBLIC_IP>:65518 |
192.168.31.13:65518 |
| 是否跨域 | 是 | 否 |
两个页面使用同一台 Windows、同一个 Edge、相同流、相同 ZLM 和实质相同的 SDP/candidate。业务页面 Origin 是 192.168.31.145,ZLM 内置页面与 ZLM 同源;前者失败、后者成功。因此问题从 "ZLM / 网络 / 浏览器能力"进一步缩小到业务页面信令层,同源与跨域成为最关键变量。
十、最终根因:跨域 SDP answer 被拦截
(一)配置证据
ZLM 配置中存在两个重复的 [http] 段,两处 CORS 值均为空:
[http]
port=65518
allow_cross_domains=
文件后部又出现一次
[http]
port=65518
allow_cross_domains=
OPTIONS 验证:
curl -i -X OPTIONS \
'http://192.168.31.13:65518/index/api/webrtc?app=live&stream=rk3588-channel-1&type=play' \
-H 'Origin: http://192.168.31.145' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: content-type'
修复前响应没有 Access-Control-Allow-Origin。
(二)为什么 CORS 会伪装成 WebRTC 网络故障?
CORS导致的问题看起来像WebRTC网络失败,但实际上浏览器甚至没有进入ICE阶段。
-
浏览器把 offer POST 到 ZLM。
-
ZLM 收到请求,创建 play transport,并生成
code=0的 SDP answer。 -
ZLM 响应缺少允许业务前端 Origin 的 CORS 头。
-
浏览器网络层收到响应,但按同源策略禁止 JavaScript 读取。
-
Axios 的成功回调不执行,客户端不会调用
setRemoteDescription(answer)。 -
没有 remote description 就没有 remote ICE candidate,浏览器不会发 STUN 检测。
-
ZLM 等待超时,最终记录"接受 rtp/rtcp/datachannel 超时"。
业务前端 POST /webrtc
→ ZLM 正常处理
→ 返回 code=0 + SDP answer
→ CORS 阻止 JavaScript 读取响应
→ setRemoteDescription() 没有执行
→ 浏览器没有开始 ICE
→ ZLM 永远等不到 play setSelectedPair
因此下面四句话不能画等号:
HTTP 服务端成功
≠ 浏览器 JavaScript 成功读取 HTTP 响应
≠ setRemoteDescription() 成功
≠ ICE 已经开始
关键认知: 服务端日志里出现 WebRTC POST 和 code=0,只代表服务端完成了处理, 不代表浏览器 JavaScript 有权读取响应。跨域响应被拦截时,服务端会留下一个最终超时的 play transport, 这与网络 candidate 不可达的表面现象非常相似。
(三)与故障时间的对应
历史备份中的第一处 allow_cross_domains=1,而故障配置的两处值均为空;配置变化时间与播放 失败开始时间一致。这条时间证据与 A/B 和 OPTIONS 结果相互印证。
最终根因证据链:
- ZLM 收到 POST,并返回
code=0和 SDP answer。 - 业务页面对应的 play 会话没有
setSelectedPair。 - 同一浏览器打开 ZLM 内置页面立即成功。
- 两份 SDP 实质相同,局域网 RTC 路径已被成功使用。
- 失败页面来自跨域 Origin,成功页面与 ZLM 同源。
- 修复前 OPTIONS 响应没有
Access-Control-Allow-Origin。 - ZLM 两个
[http]段的allow_cross_domains都是空值。
修复 allow_cross_domains=1 后,浏览器开始 ICE,随后出现 DTLS connected、 readerCount > 0 并正常播放。
十一、最终修复与验证
(一)核心修复只有一项
恢复播放所需的直接修复是启用 ZLM HTTP 跨域响应:
[http]
allow_cross_domains=1
故障配置当时有两个重复 [http] 段。即时恢复时两处都设置为 1,避免后一个空值覆盖; 永久处理则把配置合并成单一 [http],同时修正安装包模板,防止重新部署后复发。修改后重启并检查服务, 具体命令见附录。
其余改动是系统稳定运行所需的配套处理,不应与最终 CORS 根因混为一谈:
| 配套处理 | 解决的问题 | 是否为最后一次无画面的直接根因 |
|---|---|---|
| 无音轨时生成静音 Opus | 保证 H.264 + Opus 双轨 ready | 否 |
| ZLM supervisor 与端口修复 | 确保进程、HTTP API 和 RTC 服务可用 | 否 |
| publisher 使用板内地址 | 避免发布媒体绕行公网 NAT | 否 |
| LAN + 公网 candidate | 兼顾局域网直连和公网映射 | 否,网络已经过抓包和 A/B 验证 |
| 合并重复配置 section | 避免后部空值覆盖并防止重新部署复发 | 它保证 CORS 修复持久有效 |
(二)分层验收
进程和端口
使用附录中的健康检查和端口命令,期望:
- 播放器健康检查返回
status: ok。 - ZLM status 同时显示 supervisor、MediaServer 和 HTTP API 健康。
- HTTP、RTC UDP、RTC TCP 均处于监听状态。
CORS
重复前文的 OPTIONS 检查,修复后局域网和公网入口均返回:
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: *
Access-Control-Allow-Methods: GET, POST, PUT, HEAD, OPTIONS, DELETE
普通 API 响应会回显具体 Origin:
Access-Control-Allow-Origin: http://192.168.31.145
publisher
发起 streamControl 后检查播放器和 ZLM 日志,期望依次出现:
Shared hybrid H.264/Opus pipeline started
Initial selected_pair: tcp 127.0.0.1:RTC_PORT <-> BOARD_IP:临时端口
OnDtlsTransportConnected
codec info: opus[48000/2/16] H264[宽/高/帧率]
ZLMediaKit stream ready: H264 and Opus tracks are ready
浏览器 play
期望 ZLM 日志出现不同于 publisher 的浏览器 selected pair:
Initial selected_pair: udp BOARD_IP:RTC_PORT <-> BROWSER_IP:临时端口
OnDtlsTransportConnected
readerCount: 1
内置页面 A/B
重复第八节的同浏览器 A/B。内置页面和业务页面使用同一个浏览器测试,可以快速区分"ZLM/网络问题" 和"业务前端跨域/生命周期问题"。
十二、可复用排查流程
- **先看 HTTP 返回。**区分"连接 ZLM 失败""轨道未 ready"和"已返回播放 URL但浏览器无画面"。
- **确认输入。**检查当前是否有节目单、是否有视频帧、素材是否带音轨。
- **确认 publisher。**检查 Video RTP、Audio RTP、publisher ICE、DTLS 和 ZLM H.264/Opus ready。
- **确认 ZLM 服务。**不能只看 PID;必须检查 HTTP/UDP/TCP 监听和 API 响应。
- 确认 JavaScript 能读取 SDP answer。 检查 HTTP 状态和 CORS;跨域时用 OPTIONS 验证
Access-Control-Allow-Origin。 - 确认 setRemoteDescription。 检查 Promise rejection、
signalingState,再核对 codec、方向和 candidate。 - 确认浏览器是否开始 ICE。 查看 ZLM 是否出现 play
setSelectedPair;必要时使用edge://webrtc-internals。 - **此时才抓包。**只有浏览器确实进入 ICE 后,抓包判断 UDP 是否到板。
- **用内置页面做 A/B。**同浏览器内置页面成功而业务页面失败,优先排查 CORS 和前端调用。
- **最后检查 video 元素。**只有 readerCount 已增长、DTLS 已连接后,才排查自动播放、muted、srcObject 等 UI 问题。
(一)快速判断矩阵
| 日志/现象 | 最可能的层 | 下一步 |
|---|---|---|
Failed to connect to ZLMediaKit API |
ZLM 进程、HTTP 端口 | 检查 status、ss、config 中重复 http.port |
Opus track is not ready |
素材/音频桥接 | 用 gst-discoverer 检查音轨,确认静音 Opus |
H264 track is not ready |
当前没有视频输入 | 检查节目单、视频 RTP 和编码器 |
| publisher 有 selected pair,play 没有 | 浏览器信令/CORS/ICE | 检查 OPTIONS、DevTools、内置页面 |
| play 有 selected pair,无 DTLS | DTLS/证书/包丢失 | 抓包并检查 fingerprint、角色和防火墙 |
| DTLS 已连接、readerCount 增长但无画面 | codec 或前端 video | 检查 H.264 profile、track、srcObject、autoplay |
接受rtp/rtcp/datachannel超时 且无 selected pair |
remote SDP 未应用或 ICE 未开始 | 先查 CORS/setRemoteDescription,不要直接认定路由器故障 |
十三、容易误判的现象
(一)看到 WebRTC 超时,不要立刻抓包
先确认 API 是否返回、JavaScript 是否有权读取 answer、setRemoteDescription() 是否成功,以及 play 会话是否出现 selected pair。如果 remote description 根本没执行,浏览器就不会开始 ICE;这时抓不到 UDP 是必然结果,继续分析 NAT 只会把排查带偏。
(二)服务端收到 POST,不等于浏览器拿到 answer
CORS 发生在浏览器读取响应阶段。服务端会正常打印请求并创建 transport,因此日志看起来像"信令成功"。
(三)同时公布 LAN candidate 不能修复 CORS
双 candidate 只影响 ICE 选路。如果浏览器根本没有执行 setRemoteDescription(),candidate 再正确也不会被使用。
(四)TCP 端口可达不能证明 UDP WebRTC 正常
HTTP API 和 WebRTC 媒体是两条路径。应分别验证 HTTP TCP、RTC UDP、RTC TCP。
(五)无节目单时不能把音频单轨错误归因于 ZLM
.163 的板子部署后的自动测试发生在"两个通道均无节目单"状态,publisher offer 只有静音 Opus,没有 H.264, 因而返回"必须同时接受 H264 和 Opus"。推送节目单后实时流正常。这说明验收前必须先确认当前确实有视频输入。
(六)重复配置段必须全部处理
ZLM 的运行时配置持久化可能在文件后部追加重复 section。只改第一处可能被后面的空值覆盖。 修改后应使用附录中的配置核对命令检查所有 section。
(七)公网 IP 会变化
历史成功日志与后续故障期间使用过不同的公网地址。本文统一脱敏为 <PUBLIC_IP>。 启用自动检测可同步 ZLM rtc.externIP 和裸 IP 形式的 zlm_public_base_url,但路由器映射仍需保持正确。
十四、附录:环境与命令速查
(一)测试环境与端口
| 项目 | 板子一 192.168.31.13 | 板子二 192.168.31.163 |
|---|---|---|
| 播放器 HTTP | 8050/tcp |
8050/tcp |
| ZLM HTTP/WebRTC API | 65518/tcp |
65521/tcp |
| ZLM RTC 媒体 | 65519/udp,tcp |
65522/udp,tcp |
| 局域网 ICE 地址 | 192.168.31.13 |
192.168.31.163 |
| 公网地址 | <PUBLIC_IP>(公开版已脱敏) |
|
| 业务页面 | http://192.168.31.145,由 Nginx 提供 |
|
| 浏览器 | Windows 10,Microsoft Edge/Chromium 151 |
(二)素材轨道
gst-discoverer-1.0 /path/to/material.mp4
ffprobe -hide_banner /path/to/material.mp4
(三)播放器
ps -ef | grep rk3588_gst_player
curl http://127.0.0.1:8050/health
tail -f /root/Test/gst-test/player.log
tail -f /root/Test/gst-test/player-supervisor.log
(四)ZLMediaKit
/root/ZLMediaKit/run-zlmediakit.sh status
/root/ZLMediaKit/run-zlmediakit.sh repair-config
/root/ZLMediaKit/run-zlmediakit.sh restart
tail -f /root/ZLMediaKit/log/MediaServer.log
grep -R 'setSelectedPair|OnDtlsTransportConnected|接受rtp' /root/ZLMediaKit/log
(五)网络
ip -br addr
ip route
ss -lntup | grep -E '65518|65519|65521|65522'
tcpdump -ni eth1 'udp port RTC_PORT or tcp port RTC_PORT'
(六)配置核对
grep -nE '^\[stream\]|^zlm_' /root/config.ini
grep -nE '^\[http\]|^allow_cross_domains|^\[rtc\]|^externIP|^port=|^tcpPort=' \
/root/ZLMediaKit/config.ini
(七)构建与部署
cd /home/cuijc/xiangmu/RK3588-GStreamer-Player
bash scripts/build.sh
bash scripts/upload.sh
安全要求: ZLM [api].secret 不应出现在文档、命令历史、前端代码或公开日志中。 本文所有 API 管理命令均省略实际 secret。公网只映射播放所需端口,不要直接暴露播放器 8050 或无鉴权的管理 API。
十五、结论
这次故障真正值得记录的不是一行 CORS 配置
更可迁移的经验是:WebRTC 故障要沿状态机边界推进,而不是看到超时就直接归因于网络。按 输入 → publisher → ZLM → SDP → setRemoteDescription → ICE → DTLS → RTP → video 逐层证明哪一段已经正常,再进入下一段。
- 服务端收到 WebRTC POST,不代表浏览器真的拿到了 SDP answer。
- 没有 play
setSelectedPair时,不要急着认定 UDP/NAT 有问题。 setRemoteDescription()是浏览器信令到 ICE 的关键分界点。- 同一浏览器中的"业务页面失败 / ZLM 内置页面成功"是非常有价值的 A/B 实验。
- 抓包有前提:先证明浏览器已经进入 ICE,否则没有 UDP 包本身不能证明网络故障。
本案例最终用同浏览器 A/B、缺失的 play selected pair、OPTIONS 响应和配置空值形成闭环, 再用 allow_cross_domains=1 的最小修改恢复播放。前三类配套问题继续保留在文中,因为它们说明了 如何排除真实但非最终的故障,而不是为了把一次 CORS 修复写得更复杂。