97、音视频通话:WebRTC入门、信令服务器、P2P连接、音视频编解码
说实话,WebRTC 这玩意儿,我第一次接触的时候也觉得挺玄乎的。两个浏览器之间直接传视频?不用插件?不用中间服务器转发数据?听起来像魔术。但等你真正把流程跑通一遍,就会发现它其实是一套非常精巧的「打洞 + 加密 + 自适应」协议栈。今天我就带你把这套东西拆开揉碎了讲清楚。
WebRTC 到底解决了什么问题?
传统的视频通话,比如早期的 Skype,数据流都要经过服务器中转。服务器压力大,延迟也高。WebRTC 的核心思想是 P2P(点对点)——两个客户端之间直接建立连接,数据不经过中间服务器。服务器只负责「牵线搭桥」,也就是我们常说的 信令服务器。
你想想看,如果全世界所有的视频通话流量都走你的服务器,那得烧多少钱?P2P 不仅省钱,延迟还低。但问题来了:两个设备都在 NAT 后面(比如家里的路由器),怎么找到对方?这就是 WebRTC 要解决的核心难题。
信令服务器:牵线搭桥的「红娘」
信令服务器本身不传音视频数据,它只交换三种信息:
- Session Description(SDP):描述本端支持的音视频编码格式、分辨率、网络端口等。
- ICE Candidate:本端可能的网络地址(包括内网地址、公网地址、TURN 中继地址)。
- 用户状态:比如谁加入了房间、谁离开了房间。
我在项目中遇到过一种情况:信令服务器用的是 WebSocket,结果客户端频繁断连。后来发现是心跳机制没做好。WebSocket 长时间无数据会被运营商掐断,所以一定要加心跳包,比如每 30 秒发一个 ping。
下面是一个最简单的信令服务器核心逻辑(Node.js + ws 库):
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
ws.on('message', (message) => {
const data = JSON.parse(message);
// 转发给房间内的其他用户
wss.clients.forEach(client => {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(data));
}
});
});
});
嗯,这里要注意:生产环境绝对不能这么写。你需要管理房间、用户 ID、消息去重。否则一个用户发消息,全服务器的人都收到了,那不乱套了?
P2P 连接:ICE 与 NAT 穿透
WebRTC 建立 P2P 连接的过程,说白了就是一场「找路」游戏。它用了 ICE(Interactive Connectivity Establishment)框架,按优先级尝试三种方式:
- Host Candidate:本机内网地址(比如 192.168.1.100)。如果两个设备在同一个局域网,这一步就搞定了。
- Srflx Candidate:通过 STUN 服务器获取的公网地址。大部分情况下,路由器会分配一个公网 IP + 端口映射。
- Relay Candidate:如果前两种都失败了(比如对称 NAT),就通过 TURN 服务器中继。这是兜底方案,延迟最高。
我曾经调试过一个诡异的 Bug:两个设备在同一个 WiFi 下,居然走了 TURN 中继。查了半天发现是 STUN 服务器地址配错了,导致 ICE 直接跳过了第二步。所以测试时一定要先确认 STUN 服务器能正常返回公网地址。
ICE 的交互流程可以用下面这张图来理解:
音视频编解码:VP8、H.264 与自适应码率
WebRTC 默认支持 VP8 和 H.264 两种编码。VP8 是 Google 亲儿子,开源免费,兼容性好。H.264 硬件编码支持更广,很多手机芯片直接硬编硬解,省电又流畅。
我个人习惯优先用 H.264,因为大部分 Android 设备都有硬件编码器。但要注意:不同厂商的 H.264 实现有细微差异,偶尔会出现解码花屏的情况。我在项目中遇到过一台老款小米手机,H.264 硬编出来的流在 iPhone 上解码全是绿块。最后解决方案是降级到 VP8 软编,虽然 CPU 占用高了点,但兼容性没问题。
WebRTC 还有一个很聪明的机制叫 自适应码率。它会根据网络状况动态调整视频分辨率、帧率和码率。网络差的时候自动降分辨率,网络好了再升回来。你不需要手动干预,WebRTC 内部有一套拥塞控制算法(GCC)在做这件事。
下面是一个典型的 SDP 片段,里面包含了编解码信息:
v=0
o=- 123456 2 IN IP4 0.0.0.0
s=-
t=0 0
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98
c=IN IP4 0.0.0.0
a=rtpmap:96 H264/90000
a=rtpmap:97 VP8/90000
a=rtpmap:98 VP9/90000
a=fmtp:96 profile-level-id=42e01f; packetization-mode=1
看到 profile-level-id=42e01f 了吗?这是 H.264 的约束基线配置。如果两端配置不匹配,就会协商失败。我曾经调试过一个跨平台项目,Android 端配了 42e01f,Web 端配了 64001f,结果视频一直黑屏。后来统一成 42e01f 才解决。
实战:一个完整的 WebRTC 通话流程
把上面这些知识点串起来,一个完整的通话流程大概是这样的:
- 设备 A 创建
RTCPeerConnection对象。 - 设备 A 创建 Offer(SDP),通过信令服务器发给设备 B。
- 设备 B 收到 Offer,创建 Answer(SDP),发回给 A。
- 双方开始交换 ICE Candidate。
- ICE 协商成功,P2P 连接建立。
- 音视频流开始传输,编解码器自动协商。
- 网络变化时,自适应码率自动调整。
核心要点:WebRTC 的难点不在 API 调用,而在信令服务器的设计、ICE 穿透的成功率、以及编解码兼容性。这三个坑踩过去,你的音视频通话就稳了。
我的小建议:刚开始学习时,不要一上来就搞复杂的多人会议。先跑通一个 1v1 的 Demo,用 Chrome 的 chrome://webrtc-internals 查看 ICE 状态和码率变化。亲眼看到 P2P 连接建立的那一刻,你会对 WebRTC 有更直观的理解。
注意:生产环境一定要部署 TURN 服务器。不要以为 STUN 就够了。我见过太多项目上线后才发现 30% 的用户无法建立 P2P 连接,最后紧急补 TURN。推荐用 coturn,开源稳定,配置也不复杂。
好了,关于 WebRTC 的入门就讲到这里。记住:信令是骨架,ICE 是血肉,编解码是灵魂。把这三块吃透,你就能在 Android 上做出靠谱的音视频通话功能了。
公众号:蓝海资料掘金营,微信 deep3321