15、WebRTC 入门:信令交换、NAT穿透、P2P连接、音视频通话实现。
说实话,WebRTC 是我这几年研究得最过瘾的技术之一。你想想看,浏览器之间直接传视频、传音频,不需要中间服务器转发数据——这在十年前简直是天方夜谭。但 WebRTC 做到了。
这一章,我会带你从零开始,把 WebRTC 的核心机制拆开揉碎。信令交换、NAT 穿透、P2P 连接,再到最终的音视频通话实现。嗯,咱们一步步来。
15.1 什么是 WebRTC?
WebRTC,全称 Web Real-Time Communication。说白了,就是让浏览器、Android 应用、iOS 应用之间能直接进行音视频通话的技术。
它不是一个单一协议,而是一整套框架。包括媒体捕获、编码解码、网络传输、安全加密等等。我当年第一次接触时,也被它的复杂度吓了一跳。但后来发现,核心就三个东西:
- 获取媒体流:从摄像头、麦克风拿到音视频数据
- 建立连接:通过信令和 NAT 穿透,找到对方设备
- 传输数据:用 SRTP/SCTP 协议安全传输音视频
核心要点:WebRTC 的终极目标,是让两个设备之间建立一条直接的、加密的、低延迟的音视频通道。
15.2 信令交换——建立连接的“握手”
信令交换,是 WebRTC 的第一步。两个设备要建立连接,总得先知道对方在哪吧?
信令的作用就是交换“元数据”。包括:
- 媒体能力(支持什么编码、什么分辨率)
- 网络信息(IP 地址、端口号)
- 会话描述(SDP 协议)
WebRTC 本身没有规定信令怎么传。你可以用 WebSocket、HTTP 轮询、甚至用鸽子送信都行。我个人习惯用 WebSocket,实时性好,双向通信方便。
15.2.1 SDP 协议
SDP(Session Description Protocol)是信令交换的核心。它描述了一个多媒体会话的所有参数。
举个例子,一个典型的 SDP 长这样:
v=0
o=- 123456 2 IN IP4 192.168.1.100
s=-
t=0 0
m=audio 49170 RTP/AVP 0
a=rtpmap:0 PCMU/8000
m=video 51372 RTP/AVP 96
a=rtpmap:96 H264/90000
你看,它告诉对方:我支持音频(PCMU 编码),支持视频(H264 编码),我的 IP 是 192.168.1.100,端口是 49170 和 51372。
信令交换的过程,就是双方互相发送 SDP 的过程。发起方创建 Offer,接收方回复 Answer。
我的经验:SDP 里有很多细节参数,比如 a=ice-ufrag、a=ice-pwd 这些。刚开始看不懂没关系,你只要知道它们是用来做 NAT 穿透的就行。
15.3 NAT 穿透——找到对方真正的 IP
这里有个大问题。你的设备在局域网里,IP 是 192.168.x.x。对方的设备也在另一个局域网里。你们怎么找到对方?
这就是 NAT(网络地址转换)穿透要解决的问题。
WebRTC 使用 ICE(Interactive Connectivity Establishment)框架来解决。ICE 会收集所有可能的候选地址(Candidate),然后尝试连接。
15.3.1 候选地址类型
| 类型 | 说明 | 优先级 |
|---|---|---|
| Host | 本机 IP,比如 192.168.1.100 | 最高 |
| Srflx | 通过 STUN 服务器获取的公网 IP | 中等 |
| Relay | 通过 TURN 服务器中转的地址 | 最低 |
ICE 会先尝试 Host 地址。如果不行(比如双方不在同一个局域网),就尝试 Srflx 地址。还不行?那就只能用 Relay 地址了。
我曾经在一个项目中遇到过极端情况:用户的网络环境极其复杂,多层 NAT 嵌套,STUN 都拿不到正确的公网 IP。最后只能靠 TURN 中转,延迟虽然高了点,但至少能通。
避坑指南:STUN 服务器只能帮你拿到公网 IP,但有些对称 NAT 环境下,STUN 也搞不定。这时候必须上 TURN。我曾经因为没部署 TURN 服务器,导致 30% 的用户连不上——血的教训。
15.4 P2P 连接——直连通道的建立
信令交换完了,候选地址也收集好了。接下来就是真正的 P2P 连接建立。
ICE 会按照优先级,依次尝试每一对候选地址组合。这个过程叫“连通性检查”。
举个例子:
- 你的 Host 地址 + 对方的 Host 地址 → 尝试
- 你的 Srflx 地址 + 对方的 Srflx 地址 → 尝试
- 你的 Relay 地址 + 对方的 Relay 地址 → 尝试
一旦某对地址连接成功,ICE 就会选择这条路径作为最终的传输通道。
这里有个细节:连接成功后,双方还会通过 STUN 定期发送心跳包,确保连接还活着。如果网络变化了(比如从 Wi-Fi 切到 4G),ICE 会重新协商,切换到新的路径。
关键点:P2P 连接不是一次性的。它会根据网络状况动态调整。这也是 WebRTC 能适应复杂网络环境的原因。
15.5 音视频通话实现——从理论到代码
好了,理论讲完了。咱们来看看 Android 上怎么实现一个简单的音视频通话。
15.5.1 引入 WebRTC 库
在 Android 项目里,我推荐用 Google 官方的 Maven 依赖:
implementation 'org.webrtc:google-webrtc:1.0.32006'
注意版本号。不同版本 API 可能有细微差异。我一般用最新稳定版。
15.5.2 创建 PeerConnectionFactory
这是 WebRTC 的核心工厂类。所有连接对象都由它创建。
PeerConnectionFactory.InitializationOptions initOptions =
PeerConnectionFactory.InitializationOptions.builder(context)
.setFieldTrials("")
.createInitializationOptions();
PeerConnectionFactory.initialize(initOptions);
PeerConnectionFactory factory = PeerConnectionFactory.builder()
.setVideoDecoderFactory(new DefaultVideoDecoderFactory(eglBaseContext))
.setVideoEncoderFactory(new DefaultVideoEncoderFactory(eglBaseContext, true, true))
.createPeerConnectionFactory();
15.5.3 创建 PeerConnection
每个连接对应一个 PeerConnection 对象。
PeerConnection.RTCConfiguration config = new PeerConnection.RTCConfiguration(iceServers);
config.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN;
PeerConnection connection = factory.createPeerConnection(config, observer);
这里有个关键参数:iceServers。你需要配置 STUN 和 TURN 服务器地址。
List<PeerConnection.IceServer> iceServers = new ArrayList<>();
iceServers.add(PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer());
iceServers.add(PeerConnection.IceServer.builder("turn:your-turn-server.com:3478")
.setUsername("user")
.setPassword("pass")
.createIceServer());
15.5.4 添加媒体流
从摄像头和麦克风获取数据,添加到连接中。
VideoSource videoSource = factory.createVideoSource(false);
VideoCapturer capturer = createCameraCapturer();
capturer.initialize(surfaceTextureHelper, context, videoSource.getCapturerObserver());
capturer.startCapture(width, height, fps);
VideoTrack videoTrack = factory.createVideoTrack("video_track", videoSource);
connection.addTrack(videoTrack);
AudioSource audioSource = factory.createAudioSource(new MediaConstraints());
AudioTrack audioTrack = factory.createAudioTrack("audio_track", audioSource);
connection.addTrack(audioTrack);
15.5.5 发起连接
发起方创建 Offer,通过信令通道发送给对方。
connection.createOffer(new SdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sdp) {
connection.setLocalDescription(new SdpObserver() {
@Override
public void onSetSuccess() {
// 通过信令通道发送 sdp 给远端
signalingClient.send(sdp);
}
}, sdp);
}
}, new MediaConstraints());
接收方收到 Offer 后,创建 Answer 并返回。
connection.createAnswer(new SdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sdp) {
connection.setLocalDescription(new SdpObserver() {
@Override
public void onSetSuccess() {
signalingClient.send(sdp);
}
}, sdp);
}
}, new MediaConstraints());
15.5.6 处理 ICE 候选
连接过程中,双方会不断产生 ICE 候选地址。需要通过信令通道交换。
connection.addIceCandidate(candidate);
当所有候选都处理完毕,连接就建立成功了。这时候,音视频数据就会在两端之间实时传输。
我的建议:刚开始做的时候,先不要急着加太多功能。先把最基本的“能看到对方画面、能听到对方声音”跑通。然后再慢慢加美颜、加录制、加屏幕共享。一口吃不成胖子。
15.6 整体流程回顾
为了让你更直观地理解整个流程,我画了一张图:
整个流程其实就七个步骤:
- 设备 A 通过信令服务器发送 Offer SDP
- 设备 B 回复 Answer SDP
- 双方交换 ICE 候选地址
- 通过 STUN/TURN 获取公网或中继地址
- 建立 P2P 直连通道
- 音视频数据开始实时传输
你看,是不是清晰多了?
15.7 避坑指南
最后,分享几个我踩过的坑:
- 信令服务器一定要稳定:信令只在连接建立前使用,但如果连接断了需要重连,信令还得用。我遇到过信令服务器挂了,所有用户都连不上的情况。
- TURN 服务器带宽要够:如果走中继,所有音视频数据都经过 TURN 服务器。一个 720p 的视频流大约需要 1-2 Mbps。100 个用户同时通话,服务器带宽至少 200 Mbps。
- Android 权限别忘了:摄像头、麦克风、网络权限都要在 AndroidManifest 里声明。运行时权限也要处理。我刚开始做的时候忘了申请 CAMERA 权限,画面一直是黑的,排查了半天。
- 注意线程模型:WebRTC 的回调很多是在内部线程池里执行的。更新 UI 时记得切到主线程。
总结一下:WebRTC 的核心就是信令交换 + NAT 穿透 + P2P 连接。信令用 SDP 描述媒体能力,ICE 负责找到对方,P2P 通道建立后音视频数据直接传输。Android 上的实现,就是创建 PeerConnectionFactory、配置 ICE 服务器、添加媒体流、发起连接。就这么简单。
嗯,这一章的内容就到这里。代码部分你可以直接拿去用,但建议自己动手敲一遍。只有亲手跑通了,才能真正理解 WebRTC 的运作机制。
公众号:蓝海资料掘金营,微信deep3321