ZLMediaKit 已返回 SDP,浏览器仍然无视频画面问题排查记录

一、引言

本文记录一次 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.mp420s_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.mp420s_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,并监听 connectionStateiceConnectionStatesignalingState。这不会替代 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阶段。

  1. 浏览器把 offer POST 到 ZLM。

  2. ZLM 收到请求,创建 play transport,并生成 code=0 的 SDP answer。

  3. ZLM 响应缺少允许业务前端 Origin 的 CORS 头。

  4. 浏览器网络层收到响应,但按同源策略禁止 JavaScript 读取。

  5. Axios 的成功回调不执行,客户端不会调用 setRemoteDescription(answer)

  6. 没有 remote description 就没有 remote ICE candidate,浏览器不会发 STUN 检测。

  7. 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 结果相互印证。

最终根因证据链:

  1. ZLM 收到 POST,并返回 code=0 和 SDP answer。
  2. 业务页面对应的 play 会话没有 setSelectedPair
  3. 同一浏览器打开 ZLM 内置页面立即成功。
  4. 两份 SDP 实质相同,局域网 RTC 路径已被成功使用。
  5. 失败页面来自跨域 Origin,成功页面与 ZLM 同源。
  6. 修复前 OPTIONS 响应没有 Access-Control-Allow-Origin
  7. 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/网络问题" 和"业务前端跨域/生命周期问题"。

十二、可复用排查流程

  1. **先看 HTTP 返回。**区分"连接 ZLM 失败""轨道未 ready"和"已返回播放 URL但浏览器无画面"。
  2. **确认输入。**检查当前是否有节目单、是否有视频帧、素材是否带音轨。
  3. **确认 publisher。**检查 Video RTP、Audio RTP、publisher ICE、DTLS 和 ZLM H.264/Opus ready。
  4. **确认 ZLM 服务。**不能只看 PID;必须检查 HTTP/UDP/TCP 监听和 API 响应。
  5. 确认 JavaScript 能读取 SDP answer。 检查 HTTP 状态和 CORS;跨域时用 OPTIONS 验证 Access-Control-Allow-Origin
  6. 确认 setRemoteDescription。 检查 Promise rejection、signalingState,再核对 codec、方向和 candidate。
  7. 确认浏览器是否开始 ICE。 查看 ZLM 是否出现 play setSelectedPair;必要时使用 edge://webrtc-internals
  8. **此时才抓包。**只有浏览器确实进入 ICE 后,抓包判断 UDP 是否到板。
  9. **用内置页面做 A/B。**同浏览器内置页面成功而业务页面失败,优先排查 CORS 和前端调用。
  10. **最后检查 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 逐层证明哪一段已经正常,再进入下一段。

  1. 服务端收到 WebRTC POST,不代表浏览器真的拿到了 SDP answer。
  2. 没有 play setSelectedPair 时,不要急着认定 UDP/NAT 有问题。
  3. setRemoteDescription() 是浏览器信令到 ICE 的关键分界点。
  4. 同一浏览器中的"业务页面失败 / ZLM 内置页面成功"是非常有价值的 A/B 实验。
  5. 抓包有前提:先证明浏览器已经进入 ICE,否则没有 UDP 包本身不能证明网络故障。

本案例最终用同浏览器 A/B、缺失的 play selected pair、OPTIONS 响应和配置空值形成闭环, 再用 allow_cross_domains=1 的最小修改恢复播放。前三类配套问题继续保留在文中,因为它们说明了 如何排除真实但非最终的故障,而不是为了把一次 CORS 修复写得更复杂。

相关推荐
YWamy2 小时前
远程医疗音视频系统技术选型与全场景落地架构
架构·音视频
Agudamu11613 小时前
B站学习视频怎么变笔记:用 Ai好记 + Obsidian 搭建个人知识库的完整教程
人工智能·笔记·学习·音视频
2601_962304913 小时前
2026年健康科普视频怎么制作:一条开源工具链从全手动到半自动的工程复盘
开源·音视频
举个栗子。4 小时前
OpenMontage:首个开源的 Agent 化视频制作系统,用自然语言一键出品影片
开源·音视频
小柯南敲键盘5 小时前
跨马翻译批量图片视频字幕翻译,跨境电商AI智能抠图一站式工具
人工智能·python·音视频·xcode
searchforAI6 小时前
AI自动总结YouTube视频:英文内容一键总结、智能时间戳、视频重点快速看
人工智能·ai·音视频
martindelophy7 小时前
开源 AI 视频编辑器实战:用 Manifest + Adapter 构建可扩展的生成插件系统
人工智能·开源·音视频
技灵AI7 小时前
Seedance 视频生成提示词怎么写?从镜头方法到 10 套可直接改的完整 Prompt
人工智能·prompt·aigc·音视频
RoboWizard7 小时前
拍4K视频手机内存不够用怎么办?
智能手机·音视频