请添加图片描述

完整方案与实现开源:模块代码 github.com/DeguiLiu/nginx-rtc-module(纯 C,MIT),部署与示例 github.com/DeguiLiu/nginx-rtc-example。

摘要:不引入 SRS 这类独立 RTC 服务,在 nginx 框架内直接完成 RTMP→WebRTC 转换,延迟从 RTMP/HLS 的 1~10 秒降到 0.5 秒以内;端到端实测首包约 560 ms,视频 H264 761 包 / 362 KB、音频 Opus 389 包 / 67 KB 全链路可达。

1 延迟卡在 TCP 上

问题归结为两点:TCP 的重传与队头阻塞决定了延迟下界;客户端缓存决定了保密性上限。直播分发最常见的组合是 Nginx-rtmp 模块(支持 RTMP、HLS)和它的扩展 Nginx-http-flv-module,三条链路都绕不开码流走 TCP 这个前提。

RTMP 是 Adobe 为 Flash 平台设计的应用层协议,靠 TCP 保证可靠传输,局域网内延迟一般在 1~3 秒;主流浏览器早已放弃 Flash,iOS 端还需要第三方解码器。HLS 走 HTTP/80,穿透防火墙没问题,但服务端要把流切成 ts 小文件、客户端按序播放,延迟普遍在 10 秒以上,还会产生海量小文件。Http-Flv 把 RTMP 码流重新封装成 HTTP 长连接流,浏览器免插件就能播,实时性与 RTMP 相当,本质上仍是 TCP,1~3 秒的瓶颈没有解决,且客户端本地会缓存流媒体资源,保密性差。

三条典型链路如下:

Rtmp(TCP,1-3s)

Rtmp(TCP)

HLS(延迟 10s+)

推流端

nginx-rtmp-module
流媒体服务器

PC 播放客户端

移动播放客户端

图 1:Nginx-rtmp 的 RTMP/HLS 分发

Rtmp

Http-Flv(仍是TCP,1-3s)

推流端

Nginx-http-flv-module
流媒体服务器

PC浏览器或
移动播放客户端

图 2:Http-Flv 分发

WebRTC 基于 UDP,面向无连接,规避了 TCP 拥塞控制的开销,且为浏览器原生设计,免插件、免安装、不在播放端落盘。两个瓶颈各对症一半:UDP 压延迟,不落盘保保密性。

2 整体架构与模块映射

整体思路一句话:推流端照旧走 RTMP,播放端换成 WebRTC。

Rtmp

RTC(UDP,<0.5s)

推流端

Nginx-WebRtc-module
流媒体服务器

PC浏览器或
移动播放客户端

图 3:方案总体架构

架构中的抽象模块 Nginx-WebRtc-module 落地为挂在 OpenResty 1.31.1.1 上的一组模块,对应关系如下:

方案角色落地实现职责
HTTP 服务接口ngx_rtc_http_module/rtc/v1/play/ 信令,offer/answer 收发
UDP 服务ngx_rtc_stream_moduleUDP :8000 上 STUN/DTLS/SRTP 媒体收发
转发 RTC 处理模块ngx_rtmp_rtc_bridge_moduleRTMP 类型模块,经 events 钩子订阅 RTMP 音视频消息
RTC 协议处理纯 C 核心 rtp/sdp/stun/dtls/srtp/audio无 nginx 依赖,可 host 单测
转码/封装AAC→Opus(FFmpeg+libopus)、NALU→RTP(RFC 6184)音频转码 + RTP 打包

落地约束里最要紧的一条:桥接模块通过 postconfiguration 把处理器注册进 cmcf->events[],不改动 nginx-http-flv-module 一行源码,HTTP-FLV 分发与 WebRTC 分发并行存在、互为兜底。

3 RTC 服务如何嵌进 nginx

3.1 业务流程的扩展

先看原 Nginx-rtmp 模块的工作流程,启动、初始化、握手、建通道、传数据、断开,六步线性:

推流客户端

rtmp播放客户端

(1) nginx初始化

(2) nginx_rtmp模块初始化
读取配置文件

(3) 握手建立rtmp连接

(4) 初始化网络连接
创建码流传输通道

(5) 传输媒体数据
推送到播放客户端

(6) 断开推送

图 4:原有 Nginx-rtmp 流程

方案的扩展在四个位置插入 RTC 环节:rtmp 初始化后并行做 RTC 服务初始化(a);播放端经 HTTP 建立 WebRTC 连接(b);传输媒体数据时把 RTMP 码流转发给 RTC 处理模块(c),做 RTP 封装(d)后推送给所有订阅者(e)。实际实现中,RTC 服务初始化与推流主流程并无先后耦合,播放端建链也是与推流解耦的独立路径,其结果是 session 注册为该流的订阅者,在推送环节(e)与主流程汇合。

推流

断开推流

HTTP信令

提供信令/媒体服务

session注册为订阅者

SRTP/UDP

推流客户端

rtc播放客户端

(1) nginx初始化

(2) nginx-rtmp模块初始化
读取配置文件

a. RTC服务初始化
注册HTTP/UDP服务

(3) 握手建立rtmp连接

(4) 初始化网络连接
创建码流传输通道

(6) 传输媒体数据

(7) 断开推流

b. 建立播放
webrtc网络连接

c. 转发rtmp数据
至rtc处理模块

d. 音视频rtp数据封装

e. 将rtp数据推送至
所有rtc订阅者

图 5:扩展后的 RTC 工作流程(紫色为新增环节)

落地实现与图 5 的对应:a 对应 stream/http/bridge 三个模块的 nginx 标准初始化钩子;b 对应 /rtc/v1/play/ 信令;c/d/e 对应桥接模块订阅 NGX_RTMP_MSG_VIDEO / NGX_RTMP_MSG_AUDIO 事件后进入纯 C 核心的 RTP 封装与逐订阅者 SRTP 加密广播。

3.2 配置项与加载顺序

RTC 配置按 nginx 的模块体系分配到 rtmp / http / stream 三个上下文,真实指令如下:

# RTMP 侧(bridge 模块,nginx-http-flv-module 的 rtmp 上下文)
rtmp {
    rtc_audio_bitrate 64000;       # AAC→Opus 目标码率
    rtc_rtcp_sr_interval 2000ms;   # RTCP SR 发送周期
    server {
        listen 1935;
        application live { live on; gop_cache on; }
    }
}

# HTTP 信令侧(ngx_rtc_http_module)
http {
    server {
        listen 18082;
        location /rtc/v1/play/ {
            access_by_lua_file conf/auth.lua;  # stream key 校验 + 限流
            rtc_candidate_ip   172.16.48.122;  # answer 下发的 candidate
            rtc_candidate_port 8000;
            rtc_play;
        }
    }
}

# UDP 媒体侧(ngx_rtc_stream_module)
stream {
    server {
        listen 8000 udp reuseport;
        rtc;                       # 交给 stream 模块处理
        rtc_handshake_timeout 10s; # 握手中 session 空闲回收阈值
        rtc_ready_timeout     30s; # 就绪 session 空闲回收阈值
    }
}

会话超时拆成 rtc_handshake_timeout/rtc_ready_timeout 两级(握手中与就绪后分别回收)。B 帧没有做成开关,实现里无条件丢弃(ngx_rtc_h264_is_b_frame,WebRTC 低延迟播放不支持 B 帧)。AAC 固定转 Opus,只保留 rtc_audio_bitrate 一个可调参数。

配置加载遵循 nginx 模块体系的固有顺序:先系统级、再已加载模块、最后新模块,RTC 服务配置自然排在 nginx-rtmp 之后。

Nginx系统配置加载

nginx-rtmp模块配置加载

RTC服务配置加载

图 6:系统配置加载顺序

HTTP 播放接口按 nginx 自定义模块规范创建 ngx_command_t、ngx_http_module_t、ngx_module_t 注册进 http 体系;UDP 服务同理走 stream 体系。

代码分两层,靠 addon config 脚本决定编译归属。纯 C 核心(rtp / sdp / stun / dtls / srtp / audio / rtcp / core)不引用 nginx 头文件、无全局可变状态,输入输出全部经参数传入,加解密与封装的回调由调用者持有 scratch buffer,因此可以脱离 nginx 在 host 上单测;nginx 胶水层只做「事件 → 纯 C 调用 → socket 发送」的翻译,业务逻辑一律下沉。协议核心有 37 个 host 单测用例,覆盖 rtp/stun/sdp/rtcp/hsm/session_fsm,其中 B 帧识别的 Exp-Golomb 解析缺陷就是被单测断言拦下的。

媒体热路径没有引入协程和自建线程/队列,全部跑在 nginx 单事件循环内:桥接(生产者)与 stream(消费者)同处一个地址空间,每个 session 挂 source 的订阅者队列上,RTP 到达时直接遍历订阅者逐个 SRTP 加密发送,回收交给 ngx_event_timer 周期 reap。这正是"在 nginx 框架内完成转换、不引入独立 RTC 服务"定位的直接结果。唯一的例外是 AAC→Opus 转码:FFmpeg 解码加 libopus 编码是 CPU 密集操作,同步跑在事件循环里会让多路推流互相挤压,因此每路转码放独立 pthread,nginx worker 只负责投递裸 AAC 帧、取回 Opus 帧,中间用有界环衔接。

4 建链与桥接

4.1 与播放客户端建链

一次 WebRTC 播放要跨三个协议面:HTTP 信令交换 SDP、STUN 做 ICE 连通性检查、DTLS 建安全通道,最后才是 SRTP 媒体。建链时序如下。注意 session 创建与 answer 生成都收敛在 HTTP 信令模块一次完成,udp 侧只负责按 ICE ufrag 匹配 session:

udp服务 (ngx_rtc_stream_module) http接口 (ngx_rtc_http_module) RTC播放客户端 udp服务 (ngx_rtc_stream_module) http接口 (ngx_rtc_http_module) RTC播放客户端 RTC服务 POST /rtc/v1/play/(JSON: sdp + streamurl + key) 1 Lua鉴权(stream key校验 + 限流) 2 解析offer sdp,创建session (进程堆分配,跨request存活) 3 分配SSRC/PT,生成answer sdp (含candidate + DTLS指纹) 4 返回answer sdp(code:0) 5 ice连接检查,发起stun binding请求 6 按ICE ufrag匹配session, 返回BindingResponse 7 发起dtls握手 8 握手完成,导出SRTP密钥, session订阅到source 9 链路就绪,UDP持续推送音视频码流(SRTP) 10

图 7:与 RTC 播放客户端建立连接

要点:

  1. 播放客户端一次 POST 同时携带播放 URL 和 offer SDP(信令契约兼容 SRS /rtc/v1/play/ JSON 格式,offer 与 play 请求合并),RTC 服务校验 key 后返回 answer SDP;
  2. session 由信令侧创建并从进程堆分配,它的生命周期跨越 HTTP 请求(返回 answer 后还要在 UDP 侧完成 DTLS/SRTP),不能用 request pool;
  3. 播放端解析 answer 发起 STUN binding,udp 服务按 ICE ufrag 匹配到 session 返回成功。匹配键是 answer 下发的 ufrag,所以 answer 必须携带 media 级 a=candidate,否则播放器(实测 werift)根本不会发起 STUN,表现为信令成功但零媒体;
  4. DTLS 握手完成后导出 SRTP 密钥,session 订阅到对应 source,进入订阅者列表,由 udp 服务推送加密后的音视频码流。

4.2 RTMP→RTC 桥接

这一步的关键是:不改 nginx-rtmp 的分发主干,只在推送码流前插一个旁路。

Rtmp转RTC桥接 原Nginx-rtmp模块 Rtmp推流客户端 Rtmp转RTC桥接 原Nginx-rtmp模块 Rtmp推流客户端 发送推流请求 1 使用RTMP推流信息初始化桥接对象 2 推送音视频码流 3 转发音视频码流 4 转发拷贝完成 5 将数据发送给Rtmp播放器 6 封装RTP并经UDP推送至 所有RTC订阅者 7

图 8:Rtmp 转 RTC 桥接模块

桥接模块做两件事:推流连接建立后用推流信息初始化桥接对象;在原发送逻辑把码流给 RTMP 播放器之前,先拷一份给 RTC 服务处理。封装细节:音频按标准转成 Opus,视频将 NALU 打包成 RTP single/STAP-A/FU-A 包,B 帧在封装前剔除。

落地实现中,这一步对应 ngx_rtmp_rtc_bridge_module(NGX_RTMP_MODULE 类型):它在 postconfiguration 阶段把视频/音频处理器注册进 cmcf->events[] 的 NGX_RTMP_MSG_VIDEO / NGX_RTMP_MSG_AUDIO 事件数组,只在 publishing 会话上激活,不改动 nginx-http-flv-module 源码。产出的明文 RTP 以 source 级共享(一份缓存、N 个订阅者),每个 session 加密前拷贝到自身缓冲并重写本 session 协商的 payload type。SRTP 密文逐 session 不同(DTLS 导出密钥独立),这是协议决定的拷贝下界。

5 实测效果与实现要点

端到端实测(单 worker,局域网口径):

指标结果
首包延迟约 560 ms(对比 RTMP/Http-Flv 1~3 s、HLS 10 s+)
视频链路H264 → RTP 761 包 / 362 KB,含 B 帧过滤与 FU-A 分片
音频链路AAC → Opus 389 包 / 67 KB
信令鉴权正确 stream key 返回 code=0,RTMP/HTTP-FLV/WebRTC 三路同源鉴权
兜底能力HTTP-FLV 分发保留,与 WebRTC 并行

首包 560 ms 的构成未做逐段拆分;真实出图延迟以播放器首个 IDR 到达为准,多分辨率转码档实测 476~554 ms。

骨架部分照方案实现没有意外,卡时间的是两类东西:协议细节,和弱网/规模化的补课。

5.1 三个最容易踩的协议坑

  1. session 不能用 request pool 分配。session 生命周期跨越 HTTP 请求,返回 answer 之后还要在 UDP 侧完成 DTLS/SRTP。用 r->pool 会在响应返回后内存失效,后续 STUN 匹配读到垃圾。正确做法是 ngx_alloc 从进程堆分配,配合空闲 reap 定时器回收。
  2. DTLS server 要显式 SSL_set_accept_state。OpenSSL 不会因为用了 DTLS_server_method() 就自动进入 accept 状态,漏掉这一步报 ssl_read_internal:uninitialized,握手无感知失败。
  3. STUN username 的顺序不一定是 RFC 里那个。UDP 侧要靠 ICE ufrag 从 username 里匹配 session,标准写法是 client_ufrag:server_ufrag,但实测 werift 发出来的是 server_ufrag:client_ufrag,前半是服务端 ufrag。取错一半,STUN 永远匹配不到 session,媒体零包且没有任何报错。

5.2 推流端没有 RTC 上行,QoS 得自己造

RTMP 推流是 TCP 可靠输入,播放端唯一能给出的反馈就是 RTCP。推流端不是浏览器编码器,PLI(关键帧请求)无处可送,因此用「缓存最近一个 GOP + 订阅即发」替代:source 内 2048 槽环形缓存明文 RTP,新订阅者在 DTLS/SRTP 就绪时先回放最近一个 GOP(从最新 IDR 的 STAP-A 起),不用等下一个自然关键帧,首帧延迟压到一个关键帧间隔以内。

丢包恢复走发送侧 NACK:answer 声明 a=rtcp-fb:102 nack 与 nack pli,stream 模块解码 SRTCP,按 media_ssrc 过滤后从 ring 取包重传。两个容易忽略的点:音视频 seq 各自独立计数,重传缓存要么按媒体分环、要么按 ssrc+seq 双重匹配,否则会重传出错误流的数据;GOP 回放瞬间可突发上千包,socket 满时直接丢并计数,让客户端经 NACK/PLI 自行恢复,回放循环检测到发送失败即提前终止,避免冲击播放器的抖动缓冲区。

TWCC / GCC / REMB 这类上行带宽估计在本场景不适用:单向播放没有上行媒体,服务端也没有可下调码率的编码器,明确不做。

5.3 多 worker 与多分辨率

多 worker:信令落在 worker A、UDP 落在 worker B 时,进程内注册表查不到 session。解法是把跨 worker 必需的身份信息(name、SSRC/PT、seq/ts、SPS/PPS、ufrag/pwd、就绪标志、owner worker)放进 ngx_shm_zone + ngx_slab_pool,媒体数据走 per-worker 环形队列,eventfd 在 master fork 前创建、所有 worker 继承后互相唤醒,owner worker 消费环并做 SRTP 加密发送。一条硬约束贯穿始终:DTLS/SRTP/转码器这类私有句柄绝不进 shm,共享结构只放可以 memcpy 进 slab 的字段。

多分辨率:模块内不引 x264(GPL 风险),转码档用一路外部 ffmpeg 级联完成:拉源流转 1080p/720p/540p/360p 四档推回服务器成独立流,每档独立鉴权,播放端改流名即可切档。x264 侧 zerolatency + bf=0 保证每一档都保持 WebRTC 可播的无 B 帧低延迟码流;fps=30 滤镜放在滤镜链首,否则 testsrc 源经转码后帧率会翻倍。

更多实现细节(状态机、GOP 环结构、shm 布局、单测清单)见模块源码 github.com/DeguiLiu/nginx-rtc-module 与部署示例 github.com/DeguiLiu/nginx-rtc-example。

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐