5. PeerConnection核心:RTCPeerConnection对象创建、配置STUN/TURN服务器、建立点对点连接
好,咱们今天聊点硬核的。前面几章我们把WebRTC的底层原理、信令流程都捋了一遍,现在终于要动手创建真正的连接了。这一章,我会带你手把手搞定RTCPeerConnection——这个WebRTC里最核心、也最让人头疼的对象。
说实话,我刚开始接触WebRTC时,觉得PeerConnection就是个黑盒子。传个视频嘛,new一个对象不就完了?结果第一次联调,两边死活连不上,日志里全是ICE状态来回跳。后来才发现,STUN/TURN配置写错了,端口被防火墙拦了……嗯,这些坑我今天都会讲到。
5.1 RTCPeerConnection是什么?
简单说,RTCPeerConnection就是WebRTC里负责建立和维护点对点连接的核心对象。它封装了ICE、DTLS、SRTP等一系列协议栈,你只需要调用它的API,就能实现音视频数据的传输。
我习惯把它理解成一个“连接管家”。你告诉它“我要跟谁连”,它就去帮你协商编解码器、打洞、加密、传输。你只管往里面塞音视频流,剩下的它全包了。
核心职责:
- 管理ICE代理,收集候选地址
- 处理STUN/TURN打洞
- 协商SDP(会话描述协议)
- 建立DTLS加密通道
- 传输音视频数据
说白了,没有PeerConnection,WebRTC就是个空壳。你想想看,浏览器之间怎么直接传数据?全靠它。
5.2 创建RTCPeerConnection对象
在Android上,创建PeerConnection需要借助PeerConnectionFactory。这个工厂类负责创建所有WebRTC相关的对象,包括PeerConnection、AudioSource、VideoSource等。
我记得第一次写的时候,直接new了一个PeerConnection,结果编译报错。后来翻文档才发现,WebRTC的设计者把创建过程拆成了两步:先初始化工厂,再用工厂创建连接。这么做的好处是,你可以统一配置日志、线程、编解码器等全局参数。
5.2.1 初始化PeerConnectionFactory
// 1. 初始化WebRTC全局设置
PeerConnectionFactory.InitializationOptions initOptions =
PeerConnectionFactory.InitializationOptions.builder(context)
.setFieldTrials("WebRTC-H264HighProfile/Enabled/")
.setEnableInternalTracer(true)
.createInitializationOptions();
PeerConnectionFactory.initialize(initOptions);
// 2. 创建工厂实例
PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
PeerConnectionFactory factory = PeerConnectionFactory.builder()
.setOptions(options)
.createPeerConnectionFactory();
这里有个细节:setFieldTrials是用来开启实验性功能的。比如H264 High Profile,默认是关闭的,但很多Android设备支持。我建议你打开它,画质能提升不少。
5.2.2 创建PeerConnection
工厂准备好了,接下来就是创建连接对象。你需要提供一个RTCConfiguration,里面包含ICE服务器、约束条件等配置。
// 创建配置
PeerConnection.RTCConfiguration config = new PeerConnection.RTCConfiguration(iceServers);
config.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN;
config.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY;
config.keyType = PeerConnection.KeyType.ECDSA;
// 创建连接
PeerConnection connection = factory.createPeerConnection(config, observer);
注意sdpSemantics这个参数。WebRTC有两种SDP语义:Plan B和Unified Plan。Plan B是旧版,一个m=行可以塞多个流;Unified Plan是标准版,每个流独立一个m=行。我强烈建议你用Unified Plan,因为Plan B已经被Chrome废弃了。
避坑指南:我曾经在项目里用了Plan B,结果跟新版Chrome联调时,对方收不到视频流。查了半天才发现是SDP语义不兼容。从那以后,我所有新项目都统一用Unified Plan。
5.3 配置STUN/TURN服务器
创建PeerConnection时,最重要的就是配置ICE服务器。ICE(Interactive Connectivity Establishment)负责帮两端找到最优的通信路径。STUN和TURN就是ICE的左右手。
5.3.1 STUN vs TURN
| 特性 | STUN | TURN |
|---|---|---|
| 作用 | 获取公网IP和端口 | 中继转发媒体数据 |
| 适用场景 | 两端都在公网或NAT简单 | 对称NAT或防火墙限制 |
| 带宽消耗 | 几乎为零 | 高(服务器转发所有数据) |
| 延迟 | 低 | 较高 |
| 成本 | 免费/低 | 高(需要服务器带宽) |
说白了,STUN是“问路”,TURN是“代驾”。能直接连就STUN,连不上就TURN兜底。
5.3.2 配置ICE服务器
List<PeerConnection.IceServer> iceServers = new ArrayList<>();
// STUN服务器(Google公共)
PeerConnection.IceServer stunServer = PeerConnection.IceServer.builder("stun:stun.l.google.com:19302")
.createIceServer();
iceServers.add(stunServer);
// TURN服务器(需要自己搭建或购买)
PeerConnection.IceServer turnServer = PeerConnection.IceServer.builder("turn:turn.example.com:3478")
.setUsername("user")
.setPassword("pass")
.createIceServer();
iceServers.add(turnServer);
这里要注意:STUN服务器是免费的,Google、Cloudflare都提供公共STUN。但TURN服务器必须自己搭建或购买云服务。我建议至少配两个STUN和一个TURN,这样ICE候选地址更丰富,连接成功率更高。
个人经验:我在生产环境中,通常会配3个STUN和2个TURN。STUN用Google和Cloudflare的公共服务器,TURN用腾讯云或阿里云的中继服务。这样即使某个服务器挂了,还有备用。
5.4 建立点对点连接
配置好PeerConnection后,真正的连接建立就开始了。这个过程分为三步:创建Offer、设置本地描述、处理远端描述。信令通道负责传递这些SDP和ICE候选信息。
5.4.1 创建Offer(发起方)
// 创建Offer
connection.createOffer(new SdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sdp) {
// 设置本地描述
connection.setLocalDescription(new SdpObserver() {
@Override
public void onSetSuccess() {
// 通过信令通道发送Offer给远端
signalingClient.sendMessage(sdp);
}
// ... 其他回调
}, sdp);
}
}, new MediaConstraints());
创建Offer时,PeerConnection会根据你添加的媒体流(音频、视频)生成对应的SDP。这个SDP包含了编解码器、IP端口、指纹等信息。远端收到后,会生成Answer回复你。
5.4.2 处理Answer(应答方)
// 收到远端的Offer后
connection.setRemoteDescription(new SdpObserver() {
@Override
public void onSetSuccess() {
// 创建Answer
connection.createAnswer(new SdpObserver() {
@Override
public void onCreateSuccess(SessionDescription sdp) {
// 设置本地描述
connection.setLocalDescription(new SdpObserver() {
@Override
public void onSetSuccess() {
// 发送Answer给发起方
signalingClient.sendMessage(sdp);
}
}, sdp);
}
}, new MediaConstraints());
}
}, offerSdp);
这里有个容易踩的坑:必须先设置远端描述,再创建Answer。顺序反了会导致SDP协商失败。我刚开始做的时候,就犯过这个错,结果两边SDP对不上,连接一直卡在“checking”状态。
5.4.3 ICE候选交换
SDP交换完成后,PeerConnection会自动开始ICE候选收集。你需要监听onIceCandidate回调,把候选信息通过信令通道发给远端。
// 监听ICE候选
connection.addIceCandidate(new IceCandidate("candidate:...", 1, "udp", 2122260223, "192.168.1.100", 54321, "typ host"));
// 收到远端的候选
connection.addIceCandidate(candidate);
ICE候选分为三种类型:
- host:本地地址(如192.168.x.x)
- srflx:STUN反射地址(公网IP)
- relay:TURN中继地址
PeerConnection会按优先级排序,优先尝试host,不行再试srflx,最后用relay。这个过程是自动的,你只需要把候选传过去就行。
5.5 连接状态监控
连接建立后,你需要监控ICE连接状态,以便处理断线重连等异常情况。
PeerConnection.Observer observer = new PeerConnection.Observer() {
@Override
public void onIceConnectionState(IceConnectionState state) {
switch (state) {
case NEW:
// 初始状态
break;
case CHECKING:
// 正在检查候选
break;
case CONNECTED:
// 连接成功
break;
case COMPLETED:
// 连接完成(所有候选都检查完毕)
break;
case DISCONNECTED:
// 连接断开
break;
case FAILED:
// 连接失败
break;
case CLOSED:
// 连接关闭
break;
}
}
};
我建议你在DISCONNECTED和FAILED状态时,尝试重新创建PeerConnection。因为有些网络切换(比如WiFi切4G)会导致ICE会话失效,重新创建是最稳妥的办法。
核心要点总结:
- RTCPeerConnection是WebRTC的连接核心,负责ICE、DTLS、SRTP
- 创建时务必使用Unified Plan语义
- STUN/TURN配置要合理,至少配一个TURN兜底
- SDP协商顺序不能错:先设远端,再创建Answer
- ICE候选交换是自动的,但需要信令通道传递
- 监控连接状态,做好断线重连
好了,这一章的内容就到这里。PeerConnection的创建和配置是WebRTC开发的基础,也是最容易出问题的地方。我建议你动手写一个Demo,把STUN/TURN配好,跑通一次完整的连接流程。相信我,当你看到两边的视频画面成功显示时,那种成就感是看多少文档都换不来的。
公众号:蓝海资料掘金营,微信deep3321