
午夜 1:40,我戴着工卡刷进香港葵涌的机房。白板上画着一张“老师在深圳、学生散落华南/东南亚”的拓扑,旁边贴着今天的事故:5 节互动课掉线 17 次,P2P 房间在 10+ 人时崩盘。
我当场拍板:把房间切到 Janus SFU(VideoRoom 插件),服务器放在香港(对内地/东南亚时延都能接受),客户端一律强制带上自建 TURN 兜底。SFU 的路由转发 + 浏览器侧 simulcast,我赌它能稳住。事实证明,这一把赌对了(后文有完整配置与对比数据)。
(VideoRoom 是 Janus 的 SFU 插件,负责“多人房间音视频转发”,不是混流 MCU。官方文档说得很清楚。)
1)硬件与带宽怎么挑:别被“核多=能打”骗了
场景目标:单房 12–20 人互动、双师/助教轮换、常态 720p、大图+小窗切换顺滑,在网络不太好的家庭宽带也能稳住 15–20fps 的流畅感。
我的机器清单(香港本地机房,单台)
| 项目 | 规格 | 选择理由 |
|---|---|---|
| CPU | 16 vCPU(AMD EPYC 或 Intel Xeon 同级),单核 ≥ 3.0GHz | SFU 不转码但要做大量包转发、NACK/FEC/PLI 处理,主频比“核多”更值钱 |
| 内存 | 32–64 GB | 足够的缓冲与多房并发 |
| 磁盘 | 1TB NVMe | 主要放日志/录制/转储,IO 快排障友好 |
| 网卡 | 1×1/10 Gbps,支持多队列+RSS | 让软中断压力分散到多核 |
| 线路 | 香港本地多线(CMI/CTG/CU 直连优先) | 低 RTT对互动课堂比带宽更关键 |
- 发布端(主讲 1.2–1.6 Mbps、学员 0.2–0.4 Mbps);
- 订阅端(每人平均拉 2–4 路,合计 1–3 Mbps)。
SFU 汇总带宽:下行 ≈ Σ订阅 ≈ 15–30 Mbps/房,上行 ≈ Σ发布 ≈ 5–10 Mbps/房。
单机 1Gbps 能跑 20–30 个房是常态(保守算,留足突发/NACK)。
2)系统与网络底座:Ubuntu + UDP 友好参数
系统:Ubuntu 22.04/24.04 LTS 都测过。先把内核与时钟打牢,否则后面全白忙。
# 基础
apt update && apt -y upgrade
apt -y install build-essential curl git htop iftop iotop ca-certificates chrony jq
# UDP 友好内核参数(/etc/sysctl.d/99-janus.conf)
cat >/etc/sysctl.d/99-janus.conf <<'EOF'
fs.file-max = 1048576
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
net.core.netdev_max_backlog = 250000
net.ipv4.udp_rmem_min = 4096
net.ipv4.udp_wmem_min = 4096
net.ipv4.ip_local_port_range = 10000 65535
net.ipv4.tcp_fastopen = 3
net.ipv4.tcp_congestion_control = bbr
EOF
sysctl --system
# 打开文件句柄(/etc/security/limits.conf)
echo -e "* soft nofile 1048576\n* hard nofile 1048576" >> /etc/security/limits.conf
时钟:chrony 同步到就近香港源,Jitter 明显更稳。
防火墙:只放行 Janus API(8088/8089/8188/8989,后面会反代)、以及 Janus 媒体 UDP 端口段(我们用 20000–24000)。Janus 的 UDP 端口段可在配置里指定(rtp_port_range 属于 WebRTC 连接所用端口段)。
3)编译 Janus(别偷懒,关键库要新)
官方 README 已经明确:libnice 建议用较新的版本(最好自行编译 master),libsrtp 也建议 2.x;否则各种 ICE/DTLS 小坑会折磨你。
# 依赖
apt -y install libmicrohttpd-dev libjansson-dev libssl-dev libsofia-sip-ua-dev \
libglib2.0-dev libopus-dev libogg-dev libcurl4-openssl-dev liblua5.3-dev \
libconfig-dev pkg-config libtool automake cmake autoconf libwebsockets-dev
# libnice(强烈建议从源构建)
git clone https://gitlab.freedesktop.org/libnice/libnice.git
cd libnice && meson --prefix=/usr build && ninja -C build && ninja -C build install
ldconfig && cd ..
# libsrtp2(若系统版本不新)
wget https://github.com/cisco/libsrtp/archive/refs/tags/v2.2.0.tar.gz
tar xf v2.2.0.tar.gz && cd libsrtp-2.2.0
./configure --prefix=/usr --enable-openssl && make -j && make install
ldconfig && cd ..
# 可选:usrsctp(要用 DataChannel 的话)
git clone https://github.com/sctplab/usrsctp.git
cd usrsctp && mkdir build && cd build && cmake .. && make -j && make install
ldconfig && cd ../..
# Janus
git clone https://github.com/meetecho/janus-gateway.git
cd janus-gateway && sh autogen.sh
./configure --prefix=/opt/janus --enable-websockets --enable-data-channels \
--disable-docs
make -j && make install && make configs
Systemd(/etc/systemd/system/janus.service):
[Unit]
Description=Janus WebRTC Gateway
After=network-online.target
[Service]
User=nobody
Group=nogroup
LimitNOFILE=1048576
ExecStart=/opt/janus/bin/janus -F /opt/janus/etc/janus
Restart=always
[Install]
WantedBy=multi-user.target
4)Janus 关键配置:把“连得上、跑得稳”放第一位
- 核心文件:/opt/janus/etc/janus/janus.jcfg(核心)
- 传输:janus.transport.http.jcfg、janus.transport.websockets.jcfg
- 插件:janus.plugin.videoroom.jcfg
4.1 janus.jcfg(核心片段)
general: {
configs_folder = "/opt/janus/etc/janus"
plugins_folder = "/opt/janus/lib/janus/plugins"
log_to_file = true
debug_level = 4
}
nat: {
ice_lite = true
# 1:1 NAT 或多公网 IP 时启用;单公网主机也建议显式写上,减少候选混乱
nat_1_1_mapping = "X.X.X.X" # 这里填你的香港服务器公网 IP
}
media: {
# WebRTC 媒体的 UDP 端口范围(记得配防火墙)
rtp_port_range = "20000-24000"
# 根据实际网卡 MTU 决定是否调低(TURN/TLS 会增加开销)
# mtu = 1200
}
certificates: {
# 如你走 WSS/HTTPS 直连而不是 Nginx 反代,则在此放置有效证书
cert_pem = "/opt/janus/etc/ssl/fullchain.pem"
cert_key = "/opt/janus/etc/ssl/privkey.pem"
}
提示:nat_1_1_mapping 适合静态公网 IP,会把这个地址加入本地候选,避免内网地址泄露或被选为候选导致 ICE 失败。
4.2 传输层(HTTP/WS)
HTTP(默认 8088/8089),WebSocket(默认 8188/8989)。这是 Janus 的 API/信令接口,课堂业务前端会连它。
janus.transport.websockets.jcfg(片段):
general: {
ws = true
ws_port = 8188
wss = false # 若走 Nginx 反代,这里可用 ws;由 Nginx 终止 TLS
# wss = true
# wss_port = 8989
# wss_certificates = "/opt/janus/etc/janus/certificates.jcfg"
pingpong_trigger = 30
pingpong_timeout = 50
}
Nginx 反代 WSS(推荐,把证书与 TLS 交给 Nginx):
server {
listen 443 ssl http2;
server_name janus.example.com;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
location /janus {
proxy_pass http://127.0.0.1:8088/janus;
}
location /ws {
proxy_pass http://127.0.0.1:8188;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 600s;
}
}
(Nginx 反代 WS/WSS 是常见部署做法,社区里也有大量实践讨论。)
4.3 VideoRoom 插件(SFU 的“心脏”)
janus.plugin.videoroom.jcfg(片段,按互动课优化):
room-10001: {
description = "Grade-6 Math - HK Edge"
is_private = false
publishers = 16 # 允许同时发流的人数
bitrate = 1200000 # 每个发布者默认上限
bitrate_cap = true # 严格限速,防止突刺拖垮弱网
fir_freq = 0 # 不周期性强制关键帧,靠 PLI 触发
opus_fec = true # 音频 FEC
audiocodec = opus
videocodec = vp8,vp9,h264 # 客户端按能力选择
notify_joining = true
}
这些字段(publishers/bitrate/bitrate_cap/fir_freq/opus_fec/...)都来自 VideoRoom 正式文档,那里还写了 simulcast/SVC、rtp_forward 等高级用法。
5)千万别忘 TURN:Janus 是 ICE-Lite,真正需要 TURN 的是客户端
这是很多人踩的坑:你给 Janus 配 TURN 没意义,应该给浏览器客户端配 TURN。官方社区里也多次强调——防火墙/对称 NAT 的场景,用 客户端侧 TURN 才能打通。
5.1 Coturn 安装与配置
apt -y install coturn
/etc/turnserver.conf(要点):
listening-port=3478
tls-listening-port=5349
fingerprint
lt-cred-mech
use-auth-secret
static-auth-secret=CHANGE_ME_TO_A_LONG_RANDOM_SECRET
realm=turn.example.com
total-quota=1000
bps-capacity=0
stale-nonce
no-cli
no-multicast-peers
# 公网/私网都写(若有)
external-ip=<PUBLIC_IP>
min-port=30000
max-port=34999
cert=/etc/letsencrypt/live/turn.example.com/fullchain.pem
pkey=/etc/letsencrypt/live/turn.example.com/privkey.pem
lt-cred-mech + use-auth-secret + static-auth-secret 组合,用 TURN REST 机制为每位学生发放短期凭证(用户名为到期时间戳,密码为 HMAC(username, secret))。Coturn 文档与 IETF 幻灯都有说明。
生成短期凭证(后端示例,Node.js/Python 任一语言都行):
# Python 版
import base64, hmac, hashlib, time
def gen_turn_cred(secret: str, ttl: int = 3600):
username = str(int(time.time()) + ttl)
pwd = base64.b64encode(hmac.new(secret.encode(), username.encode(), hashlib.sha1).digest()).decode()
return username, pwd
前端 Janus 初始化(强制带 TURN):
const iceServers = [{
urls: [
"turn:turn.example.com:3478?transport=udp",
"turns:turn.example.com:5349?transport=tcp"
],
username: TURN_USERNAME,
credential: TURN_PASSWORD
}];
const janus = new Janus({
server: "wss://janus.example.com/ws",
iceServers,
// 建议打开 trickle,弱网更快收敛
iceTransportPolicy: "all"
});
6)课堂关键体验优化:simulcast、带宽自适应、缩略图降级
发布端(老师端)强制 simulcast(例如 VP8 三层 180p/360p/720p),学生端订阅时按可视窗口选择层级。Janus 的 VideoRoom 完全支持这套逻辑,带宽一紧,切低层依然稳定说话不卡。
主讲大窗:订 720p;缩略图:订 180p/360p;
移动端:优先低层 + 限帧(15–20fps),保语音优先(Opus FEC 开启)。
动态限速:用 Janus 的 configure API 下发 bitrate 调整,网络劣化时发 PLI 触发关键帧;
WS 心跳:pingpong_trigger/pingpong_timeout 合理调大,防止移动端链路抖动被误判断线。
7)端口与连通性:一条条打通,别想当然
连通性自查清单
wss://janus.example.com/ws(或 HTTP)能握手;
服务器 udp/20000-24000 能被公网直连;
TURN 3478/5349 外网可达,min/max-port 放开;
nat_1_1_mapping 写明公网 IP,避免内网地址被浏览器 ICE 选中导致失败(尤其容器/双网卡)。
8)我真的踩过的坑 & 复盘
| 现象 | 根因 | 处理 |
|---|---|---|
| 晚上 8:10 大量“ICE failed for component 1” | 宿主双网卡 + Docker 虚拟网卡,浏览器被送了内网候选 | nat_1_1_mapping=公网IP 后问题立刻消失;容器改 host 网络模式更稳(或把接口绑死) |
| 有用户家里路由器防火墙很“毒”,直连 UDP 失败 | 客户端没带 TURN,或凭证过期 | 课堂入口强制下发 TURN REST 凭证,有效期 1 小时,过期自动刷新;Coturn 监控 401/438 激增告警 |
| 大并发时偶发花屏/卡帧 | 发布端突发码率 + NACK 洪水 | bitrate_cap=true + 缩略层限帧 + 业务层做“单画面放大”而非“订多路高层” |
| 端口段不生效 | 某版本/编译选项下 rtp_port_range 未正确加载或被覆盖 |
升级到较新 Janus & libnice,自查日志是否打印端口段;必要时二分回退配置验证(社区也有相关讨论) |
| WSS 偶发连接失败(自签证书) | 浏览器拒绝不受信根 | 一律走 Nginx 反代并部署正规证书,Janus 仅走 ws/http 内网回环 |
9)观测与压测:让“体感顺”落到数字上
Janus Admin/Monitor API 打开后,能看到会话、handle、房间、丢包/重传等指标;我用一个小脚本把它们抓到 Prometheus,再在 Grafana 做“房间看板”。
我们一次上线前 A/B(同一批学生):
| 指标 | 切 Janus 前(P2P/信令房) | 切 Janus 后(香港 SFU + TURN) |
|---|---|---|
| 平均首帧时间(秒) | 3.8 | 1.9 |
| 课堂 30 分钟内掉线率 | 8.1% | 1.7% |
| 视频卡顿(>1s 卡帧/每人·次) | 2.6 | 0.7 |
| 主讲平均端到端时延(ms) | 230–400 | 120–220 |
| 平均下行带宽/人(Mbps) | 2.8 | 1.6(simulcast 分层起效) |
课堂端“顺滑”的主观感受,和首帧时间 + 抖动 + 丢包重传的曲线高度一致。Hong Kong 边缘 + 客户端 TURN,是组合拳。
10)扩容与高可用:多 Janus 分房 + 反亲和
多 Janus 分片:用 L4/L7 网关按房号 Hash 到固定 Janus,避免房内跨机;
远端发布者(房级级联):VideoRoom 支持把本机房发布者远程化到另一台 Janus,实现跨实例“同房订阅”(需要你用 API 驱动),大房/跨地域时非常实用。
Coturn 多边缘:在香港/新加坡各放一台,前端按地理选择最近 TURN。
11)完整“能跑”的最小闭环(你可以直接抄)
- Ubuntu 安装与内核参数(见 §2)。
- 编译 Janus(见 §3,务必新 libnice/libsrtp)。
- 配置 Janus:
- janus.jcfg: ice_lite=true、nat_1_1_mapping=公网IP、rtp_port_range=20000-24000;
- websockets.jcfg: 开 ws,配 Nginx 反代做 wss;
- videoroom.jcfg: 设置 publishers/bitrate/opus_fec/...。
- Coturn:lt-cred-mech + use-auth-secret + static-auth-secret,开放 3478/5349 与中继端口段,后端发放 TURN REST 短期凭证。
- 前端:Janus 初始化强制 iceServers 带自家 TURN;发布端开 simulcast;弱网时用 configure 动态降码率。
- 连通自检:WSS 能连、UDP 20000–24000 可达、TURN 可用(trickle-ice/自检页)、房间首帧 < 2s。
- 监控:开启 Admin/Monitor,拉核心指标做告警。
上线那晚,香港暴雨,二期教室刚开,客服群里静得出奇。凌晨 2:30 我把风扇往机柜缝里又对了对,合上笔记本。
后来老师跟我说:“同样的网,同样的孩子,今天课堂顺得出奇。”
其实没什么魔法:把媒体面向“弱网”设计,让服务器做该做的转发/拥塞/分层,把“能打通”放在第一优先级,剩下的交给人和时间