gortmplib vs go2rtc vs FFmpeg:RTMP 推流源码对比与国内直播平台对接
在 MiBee NVR 项目里要把摄像头画面推到国内某直播平台,候选方案有三个:直接调 FFmpeg、用纯 Go 的 gortmplib、用 go2rtc。三个都能「推 RTMP」,但实际对接国内直播平台时表现差异巨大——有的秒断、有的几秒后断、有的稳如老狗。这篇从源码层面把三者拆开对比,并梳理对接国内 FMS 兼容平台时的坑和解法。
三个库的所有结论都来自源码逐行核对(go2rtc master / gortmplib main / FFmpeg master),不是文档推测。
先跑通:FFmpeg 已经能把画面推到直播间
在纠结用哪个库之前,先用最稳妥的方案把链路打通——在 MiBeeNVR 的「转推输出」里配一个 RTMP 目标,后端走 FFmpeg 子进程。配置界面长这样:

配上之后,桌面端 MiBeeNVR 推流、某直播平台桌面伴侣接收,两边各跑各的:

最终在手机端直播间里能看到画面——链路是通的:

所以用 FFmpeg 是能成的。但 FFmpeg 是外部进程,要单独装、单独管生命周期,部署和资源开销都不理想。我更希望整个推流链路纯 Go 实现——不依赖外部进程、能嵌进 NVR 进程统一管理。这才有了后面三个库(FFmpeg 作为参考、gortmplib、go2rtc)的源码对比:到底哪个 Go 库能把这条纯 Go 路走通。
三个库的定位
先把三个选手分清楚——它们根本不是一个量级的东西:
| 维度 | FFmpeg | gortmplib | go2rtc |
|---|---|---|---|
| 定位 | 媒体处理瑞士军刀 | 纯 RTMP 客户端/服务器库 | 终极摄像头流媒体应用 |
| 形态 | 命令行/C 库 | Go 库(可 import) | 独立二进制/Docker |
| RTMP 角色 | 20+ 协议之一 | 唯一核心 | 20+ 协议之一 |
| 代码规模 | 巨型 | 推流核心 ~1000 行 | RTMP 核心 ~580 行 |
| 典型用法 | ffmpeg -i ... -f flv rtmp://... | 嵌进自家 Go 程序 | 开箱即用的转分发 |
简单说:FFmpeg 是「能推流的全能选手」,gortmplib 是「专为 RTMP 生的库」,go2rtc 是「主打多协议互转的应用」。理解了定位,后面的差异就顺理成章。
RTMP 握手对比
RTMP 连接第一步是握手:C0/C1/C2(客户端→服务端)和 S0/S1/S2。握手分简单和复杂两种,区别在 C1 的 1536 字节里有没有 digest:
| 特性 | 简单握手 | 复杂握手 |
|---|---|---|
| C1 内容 | time+version+1528 随机字节 | 同上,但含 HMAC-SHA256 digest |
| digest key | 无 | Genuine Adobe Flash Player 001 |
| 服务端验证 | 仅 echo C1→S2 | 验证 C1 digest |
三个库的握手实现(源码核对):
go2rtc——最简,函数名在源码里就拼错了(clienHandshake,少个 t):
| |
C1 全零、无 digest、不校验服务端返回。
gortmplib——按版本号分两条路:
| |
普通模式(v3)走 fillPlain() 只填随机数、不算 digest;只有加密模式(v6/RTMPE)才走 fill() 算 HMAC-SHA256 digest。
FFmpeg——实现复杂握手,C1 含 digest,发送前用 rtmp_player_key 计算 HMAC-SHA256 写入。
注意:握手差异是不是卡点,取决于具体后端。部分云 FMS 接受简单握手;但也有严格的后端(如实测中的某直播平台桌面伴侣)在 version=3 下强制验证 digest——后文实战记录里就先栽在这上面。
客户端推流时序:控制消息才是关键
握手过后是 connect 命令,然后是一套精密的协议控制消息流。FMS 标准(某直播平台/虎牙/B站都是 FMS 兼容后端)的完整流程:
sequenceDiagram
participant C as 客户端
participant S as 该平台 FMS
C->>S: connect(app,flashVer,tcUrl)
S-->>C: WindowAckSize(2500000)
S-->>C: SetPeerBandwidth(2500000,2)
C->>S: WindowAckSize(2500000) 回应
S-->>C: _result(Connect.Success)
C->>S: createStream / FCPublish / publish
S-->>C: onStatus(Publish.Start)
Note over C,S: ⏱️ 5秒计时开始<br/>每收 2.5MB 必须回 ACK
C-->>S: ❌ 无 ACK → 5秒超时
S->>C: StreamReset(RST) 断开关键在 WindowAcknowledgementSize:服务端声明一个 2.5MB 窗口,客户端每累计收到 2.5MB 就必须回一个 Acknowledgement(type 3)。5 秒内没等到 ACK,服务端直接 RST。三个库在这里分出了高下:
| 控制消息 | FFmpeg | gortmplib | go2rtc |
|---|---|---|---|
| 发 WindowAckSize | ✅ | ✅ 主动发(2500000) | ❌ 不发 |
| 处理收到的 WindowAckSize | ✅ | ✅ | ❌ 忽略 |
| 发 SetPeerBandwidth | ✅ | ✅ 主动发 | ❌ 不发 |
| 周期发 Acknowledgement | ✅ 每1MB | ✅ 自动回调 | ❌ 从不发 |
go2rtc 的 readResponse() 是问题根源——它只认两类消息:
| |
这是 go2rtc 在强制 ACK 的云 FMS 上的一种典型失败模式:握手和推流命令都成功,几秒后因从不发 ACK 被 RST。但必须强调:这不是唯一的失败点——后文实战记录里,直播伴侣在握手层(digest)和帧层都有独立的坑,它 74ms 就 RST 的现象对不上 ACK 超时那种 5 秒断开。FFmpeg 和 gortmplib 都做了周期 ACK,至少不踩这一个。
推流数据写入机制
握手和控制消息之外,数据怎么写进 RTMP 连接,两套 Go 库哲学完全不同。
gortmplib——强类型消息:每个编解码器有专属方法,字段都是强类型:
| |
go2rtc——FLV 转换层:把 RTMP 当 FLV 的传输层,你喂 FLV 字节流,它翻译成 RTMP chunk:
| |
gortmplib 给你协议级精细控制,go2rtc 代码量少但牺牲了控制力。各有所长。
编解码器与多轨道支持
这是 gortmplib 的核心卖点——它实现了 Enhanced RTMP v2,支持标准 RTMP 装不下的新编解码器和多轨道:
| 编解码器 | gortmplib | go2rtc | FFmpeg |
|---|---|---|---|
| H.264/AVC | ✅ | ✅ | ✅ |
| H.265/HEVC | ✅ 原生 | ❌ | ⚠️ 需配置 |
| AV1 | ✅ 原生 | ❌ | ⚠️ |
| AAC/MP3/G.711 | ✅ | ✅ | ✅ |
| Opus/FLAC/AC-3 | ✅ 原生 | ❌ | ✅ |
| 多视频/音频轨道 | ✅ 原生 | ❌ | ❌ |
要推 H.265/AV1 或多视角多语言,gortmplib 是唯一选择。标准 H.264+AAC 三家都行。
认证与加密支持
| 能力 | gortmplib | go2rtc | FFmpeg |
|---|---|---|---|
| RTMPS(TLS) | ✅ 自动填 SNI | 注册了 scheme 但无实现 | ✅ |
| RTMPE 加密 | ✅ DH+RC4 | ❌ | ❌ |
| Adobe 鉴权(challenge-response) | ✅ 两阶段 HMAC-MD5 | ❌ | ❌ |
| URL query 鉴权 | ✅ | 仅明文参数 | ✅ |
对接需要 Adobe 鉴权的 Wowza/strict FMS,gortmplib 是唯一能打的。
Chunk 协议细节
| Chunk 特性 | gortmplib | go2rtc | FFmpeg |
|---|---|---|---|
| 写 Chunk Size | 65536 | 4096 | 4096 |
| 读 Chunk Size 初始值 | 128 | 128(偏小) | 4096 |
| 大 Chunk ID(>63) | ✅ 支持 | ❌ 部分不支持 | ✅ |
| 并发写保护 | ReadWriter 保证 | sync.Mutex 手动 | — |
| 视频数据 CSID | 6 | 4 | 6 |
go2rtc 的 rdPacketSize 初始值 128 偏小,虽然收到服务端 SetChunkSize 后会更新,但首轮交互效率低;且它注释明说不支持 chunkID>63。
connect 参数与元数据
connect 命令的 flashVer(源码核对,容易踩坑):
| 库 | flashVer |
|---|---|
| FFmpeg | FMLE/3.0 (compatible; FMSc/1.0) |
| go2rtc | FMLE/3.0 (compatible; FMSc/1.0) |
| gortmplib | LNX 9,0,124,2(Linux Flash Player 9) |
部分 FMS 会校验 flashVer,gortmplib 的 LNX 值可能被拒——这是 gortmplib 对接国内平台时一个值得优先排查的点。
元数据(@setDataFrame/onMetaData):gortmplib 的 onMetaData 只有 4 个字段(缺 width/height/framerate),而 FFmpeg 有 15+ 字段。FMS 用宽高初始化解码器,字段缺失可能导致服务端拒绝初始化。
国内直播平台对接:问题与解决思路
国内的某直播平台、虎牙、B站、战旗、龙珠等,后端基本都是 Adobe FMS 兼容实现。它们有一套共性要求,三个库踩坑各不相同。下面按问题分层梳理。
问题 1:ACK 超时(go2rtc 在云 FMS 上的典型坑)
现象:推流命令都成功了,几秒后连接被 RST,日志里看到「第二帧断开」。
根因:平台要求客户端每收 2.5MB 回一个 Acknowledgement,5 秒没等到就 RST。go2rtc 完全没实现这个机制。
解决:
- go2rtc:在
readMessage()里加字节计数,达窗口就发 ACK(type 3),并处理 type 5/6。 - gortmplib/FFmpeg:已实现,无需处理。
问题 2:握手 digest(是否强制,因后端而异)
现象:推流失败时,有人一口咬定是 digest 没算,也有人一口咬定 digest 无关。
真相:取决于后端。部分云 FMS 接受简单握手;但也有严格的——实测某直播平台桌面伴侣在 version=3 下强制验证 digest,补上 digest 后握手才通过(见后文实战记录)。所以别一刀切,用抓包确认握手是否真的失败。
解决:握手过不了就考虑 digest;握手能过就跳过这条。
问题 3:flashVer 被拒
现象:connect 后服务端不回 Connect.Success 或直接拒绝。
根因:部分 FMS 校验 flashVer,非 FMLE 格式可能被拒。gortmplib 默认发 LNX 9,0,124,2。
解决:把 flashVer 改成 FMLE/3.0 (compatible; FMSc/1.0) 重试。
问题 4:鉴权参数(wsSecret/wsTime)
现象:某直播平台推流地址形如 rtmp://<平台推流域名>/live/{推流码}?wsSecret=xxx&wsTime=xxx,鉴权信息在 URL query 里。
根因:query 参数必须原样保留在 tcUrl 里传给 connect,丢掉或错误解析都会鉴权失败。
解决:确认 URL 解析逻辑保留了 query;go2rtc 默认把 ?wsSecret=... 拼进 stream,gortmplib 的 splitPath 也保留 RawQuery,一般没问题,但要核对。
解决思路总结:分层排查
flowchart TD
A[推流失败] --> B{握手过没?}
B -->|没过| C[查 digest/网络]
B -->|过了| D{connect 成功?}
D -->|没| E[查 flashVer/鉴权参数]
D -->|成功| F{publish 后几秒断?}
F -->|是| G[ACK 超时-补控制消息]
F -->|否| H[查元数据/CSID]
style A fill:#2196F3,color:#fff
style G fill:#f44336,color:#fff
style E fill:#FF9800,color:#fff图示:按「握手→connect→publish 后是否秒断」三层定位,能快速锁定是 ACK、flashVer 还是 digest 问题。最终定论靠抓包,别靠猜。
选型建议
选 FFmpeg:要最快对接国内平台、不在乎外部进程依赖。它的控制消息和兼容性最全,是 OBS/Streamlabs 底层用的方案。
选 gortmplib:要推 H.265/AV1/多轨道、对接需要 Adobe 鉴权或 RTMPS 的服务器、想把 RTMP 嵌进 Go 程序做精细控制。注意它 flashVer 默认是 LNX,对接国内平台可能要改。
选 go2rtc:要在 RTMP/RTSP/WebRTC/HLS 之间互转、要开箱即用的多协议转发、推流编解码在 H.264+AAC 标准范围内。但对接国内 FMS 平台前必须先补齐控制消息,否则必被 ACK 超时 RST。
调试方法:抓包定论
源码分析能定位方向,最终靠 tcpdump 钉死:
| |
三步就能分清是 ACK 超时、flashVer 被拒、还是握手 digest。
实战记录:对接某直播平台桌面伴侣(进行中)
这是我们项目里真实踩坑的记录,问题尚未完全解决——握手层已攻克,视频帧传输仍失败。放在这里作为前文理论的实证对照,也提醒一句:源码分析能给方向,但「这个后端到底卡哪」只有抓包说了算。
目标架构:直播伴侣 ≠ 该平台云端
一个容易搞错的点:我们对接的不是该平台云端(<平台推流域名>),而是装在主播电脑上的某直播平台桌面伴侣——一个本地 RTMP 接收器 + 转码器,它收到流后再解码→重编码→推到该平台云端:
| |
本地推流地址形如 rtmp://<伴侣局域网IP>:1955/live/<推流码>,推流码由伴侣动态生成。伴侣本身实现了一套 Adobe FMS 兼容的 RTMP 服务端,行为和该平台云端不完全一样——这直接影响了下面的结论。
实测推翻了「digest 无关论」
前文按源码推断「部分云 FMS 接受简单握手」。但实测打脸:直播伴侣在 version=3 下强制验证 C1 digest。我们给 relay 补上复杂握手 digest(用 Genuine Adobe Flash Player 001 算 HMAC-SHA256 写入 C1)后,握手立刻通过、NetStream.Publish.Start 也收到了。
教训很直接:「这个后端要不要 digest」只有实测说了算。直播伴侣比一般云 FMS 更严格。
五层问题模型
把整个推流链路拆成五层,逐层定位:
| 层 | 问题 | FFmpeg | gortmplib | go2rtc | 我们的状态 |
|---|---|---|---|---|---|
| L1 握手 digest | C1 的 HMAC-SHA256 | ✅ | ❌ fillPlain 跳过 | ❌ 无 | ✅ 已补 |
| L2 chunk size | 写入分块大小 | 128 | 65536 | 4096 | ✅ 改 4096 |
| L3 chunk header | Type 0/1/2/3 选择 | Type 0 | Type 1/2/3 优化 | Type 0 | ✅ 强制 Type 0 |
| L4 后台读消息 | 消费服务端控制消息 | 内置 | 无 | goroutine | ✅ 加 goroutine |
| L5 视频帧格式 | 未知 | 正常 | 未知 | 正常 | ❌ 未解决 |
已攻克的四层
- L1 digest:自建握手函数,按 scheme 0 位置算 HMAC-SHA256 写入 C1;握手通过。
- L2 chunk size:在
connect之前发SetChunkSize=4096(对齐 OBS),减少续包。 - L3 chunk header:覆盖写入逻辑,视频/音频 AU 强制走 Type 0 + Type 3,绕开 gortmplib 的 Type 1/2/3(怀疑伴侣解析不了)。
- L4 后台读取:
publish后起 goroutine 持续读服务端消息,防接收缓冲区撑爆被 RST。
仍未解决:第一帧后 RST
修完 L1–L4 后,握手、connect、publish、Publish.Start、onMetaData、AVC seq header 全部成功,但第一个视频帧写入后约 74–252ms,伴侣就 RST。
注意这个时序:74–252ms 远快于 ACK 超时(5 秒 / 2.5MB)。所以前文说的「ACK 超时」在这里对不上——伴侣是在帧级别就拒绝了,不是连接维持层的超时。这说明 ACK 超时只是 go2rtc 在云 FMS 上的一种失败模式,不是直播伴侣的卡点。
已排除:digest、chunk header 类型、chunk size、关键帧/P 帧、后台读取——都不是。当前怀疑(均未证实):
- MessageStreamID:gortmplib 硬编码
0x1000000,go2rtc 用 createStream 返回值。 - onMetaData 字段不全:gortmplib 只有 4 个字段,FFmpeg 有 15+(含 width/height/framerate)。
- 缺音频轨道:源摄像头无声,relay 不发音频;伴侣可能要求至少一路音频。
- AVCC 字节级差异:gortmplib 的 AVCC 封装可能与 FFmpeg 略有不同。
- 伴侣端帧格式校验:收到首帧后做 SPS/PPS、profile 校验,不符则 RST。
- 限流残留:调试期快速重连(每 2 秒几百次)可能触发持久 IP 黑名单——当时连 FFmpeg 也被 RST,重启伴侣 + 等待后才恢复。
下一步与现状
最有价值的下一步是 pcap 逐字节对比:同条件下分别抓一次成功的 FFmpeg 推流和失败的 Go relay 推流,用 Wireshark 的 RTMP dissector 找第一个差异点。
现状(诚实交代):原生 Go relay 直推直播伴侣还没跑通;目前用 FFmpeg 子进程作为某直播平台的推流方式(项目里有开关),这条路稳定。理论分析帮我们排除了握手层,但视频帧层的问题还得靠抓包继续查。后续定位到会更新这篇。
全景对比总表
| 维度 | FFmpeg | gortmplib | go2rtc | 国内平台要求 |
|---|---|---|---|---|
| 握手 digest | ✅ 复杂 | ❌ 简单(v3) | ❌ 全零简单 | 多数接受简单 |
| WindowAckSize | ✅ | ✅ 发 | ❌ 不发 | 必需 |
| Acknowledgement | ✅ 周期 | ✅ 周期 | ❌ 从不 | 必需 |
| SetPeerBandwidth | ✅ | ✅ 发 | ❌ 不发 | 必需 |
| flashVer | FMLE | LNX | FMLE | 偏好 FMLE |
| releaseStream/FCPublish | ✅ | ✅ | ✅ 合并发 | 可选 |
| Chunk Size | 4096 | 65536 | 4096 | 无强要求 |
| H.265/AV1/多轨道 | ⚠️ | ✅ | ❌ | N/A |
| RTMPS/RTMPE/Adobe鉴权 | 部分 | ✅ 全 | ❌ | 视平台 |
| 国内 FMS 推流 | ✅ 稳 | ⚠️ 需补 digest + 改 flashVer,帧层待解 | ❌ 需补 digest + 控制消息 | — |
总结
三个库各有定位:FFmpeg 兼容性最全、是对接国内平台最稳的「保险方案」;gortmplib 协议实现最完整、是新编解码和精细控制的唯一选择,但默认 flashVer 要留意、握手 digest 也得自己补;go2rtc 多协议互转最省事,但握手 digest 和控制消息都得补齐,才推得动国内严格后端。
对接国内直播平台的坑是分层的:握手(digest,是否强制因后端而异——直播伴侣要、部分云 FMS 不要)、connect(flashVer / 鉴权参数)、连接维持(ACK 超时,主要咬 go2rtc 这种不发 ACK 的实现)、甚至帧格式(我们项目就卡在这层,未解决)。所以别信任何一刀切的结论——按「握手→connect→publish 后多久断」分层排查,配合 tcpdump 逐字节对比,坑才会现形。理论给方向,抓包定结论。