18、RTCPeerConnection入门:什么是RTCPeerConnection、信令(Signaling)概念、创建连接
好,咱们今天聊点实在的。
前面十几章,我们把音视频采集、本地预览、格式控制这些基本功都过了一遍。现在,是时候让两个浏览器真正“通上话”了。
这一章,我们聚焦一个核心对象——RTCPeerConnection。说白了,它就是WebRTC里负责点对点传输的“管道工”。没有它,你采集到的视频数据就只能在自己电脑上自嗨,传不到对面去。
18.1 什么是RTCPeerConnection
RTCPeerConnection,名字挺长,但拆开看就明白了:
- RTC:实时通信(Real-Time Communication)
- Peer:对等端,也就是参与通信的每个终端
- Connection:连接,建立一条数据通路
合起来,就是“对等端之间的实时连接”。
我个人的理解是:它像一个黑盒子,你往里塞音视频流,它负责把数据编码、打包、穿越复杂的网络环境,最终送到对方手里。你不需要关心底层用了什么协议、怎么穿越NAT、怎么处理丢包——这些脏活累活,RTCPeerConnection都帮你干了。
核心要点:RTCPeerConnection不是用来“连接服务器”的,它是用来连接两个浏览器(或两个终端)的。中间没有服务器转发数据,只有信令服务器帮忙“牵线搭桥”。
我在项目中遇到过不少新手,以为RTCPeerConnection是连到某个云服务的API。其实不是。它建立的是端到端的直连通道,数据不经过中间服务器中转。这样做的好处是延迟低、带宽利用率高,但代价是——你得自己搞定“怎么找到对方”这个问题。
18.2 信令(Signaling)概念
好,问题来了:两个浏览器在不同的电脑上,怎么知道对方的存在?
这就引出了信令的概念。
信令,说白了就是“握手前的自我介绍”。在WebRTC的世界里,它负责交换三类信息:
- 会话描述(SDP):告诉对方“我支持哪些编解码器、我的IP地址是什么、我打算用什么格式传数据”。
- 网络信息(ICE候选):告诉对方“我有哪些可能的网络路径可以到达我”。
- 其他元数据:比如房间号、用户ID等业务层面的信息。
注意,WebRTC标准并没有规定信令该怎么传。你可以用WebSocket、HTTP轮询、甚至用鸽子送信——只要能把信息从A传到B就行。
我的建议:实际项目中,信令服务器通常用WebSocket实现。因为它双向、实时、开销小。我曾经用HTTP长轮询做过一版,结果延迟高得离谱,后来全改成WebSocket了。
信令的过程大致是这样的:
- A创建Offer(一个SDP描述),发给信令服务器
- 信令服务器转发给B
- B收到后,创建Answer,发回给信令服务器
- 信令服务器再转发给A
- 双方交换ICE候选,找到最优的网络路径
- 连接建立,开始传输数据
嗯,这里要注意:信令服务器只负责转发,不负责传输音视频数据。它就像个“媒人”,牵完线就退到一边了。
18.3 创建连接:从零到一
理论说完了,咱们动手写代码。
创建一个RTCPeerConnection,其实就三步:
- 创建RTCPeerConnection实例
- 添加音视频轨道
- 发起或响应连接
先看最基础的创建代码:
// 第一步:创建配置对象
const config = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }
]
};
// 第二步:创建连接实例
const pc = new RTCPeerConnection(config);
console.log('RTCPeerConnection 创建成功');
这段代码看着简单,但有几个坑我得提醒你:
iceServers里至少要配一个STUN服务器,否则内网环境可能连不上。我刚开始做的时候忘了配,结果在办公室局域网里能通,回家就死活连不上——折腾了一下午才发现是STUN没配。- STUN服务器是公开的,Google那个
stun.l.google.com:19302可以用,但生产环境建议自己搭或者用商业服务。
接下来,添加本地音视频流:
// 假设我们已经通过 getUserMedia 获取了本地流
// localStream 是一个 MediaStream 对象
localStream.getTracks().forEach(track => {
pc.addTrack(track, localStream);
});
console.log('已添加', localStream.getTracks().length, '条轨道');
这里有个细节:addTrack 的第二个参数要传原始的MediaStream对象,而不是轨道本身。我见过有人写成 pc.addTrack(track, track),结果数据传不过去,因为RTCPeerConnection需要知道轨道属于哪个流。
最后,发起连接(以发起方为例):
// 创建Offer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// 通过信令服务器发送给远端
// 假设 sendToSignalingServer 是你封装好的函数
sendToSignalingServer({
type: 'offer',
sdp: offer.sdp
});
console.log('Offer 已发送,等待 Answer...');
接收方收到Offer后,需要做:
// 接收方代码
await pc.setRemoteDescription(new RTCSessionDescription(offer));
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
sendToSignalingServer({
type: 'answer',
sdp: answer.sdp
});
你看,整个过程就是“你发我收、我发你收”的对称过程。一旦双方都设置好了本地和远端的描述,RTCPeerConnection就会自动开始尝试建立连接。
注意:createOffer/createAnswer 是异步操作,必须 await。而且 setLocalDescription 必须在发送之前完成。我曾经因为顺序搞反,导致SDP里缺少ICE信息,对方一直收不到候选地址,连接超时。
18.4 核心流程图
下面这张图,把整个信令和连接建立的过程串起来了。你多看几遍,心里就有谱了。
18.5 几个容易踩的坑
最后,分享几个我实际开发中遇到的教训:
- ICE候选没收集完就发Offer:
createOffer默认会等ICE候选收集完,但如果你设置了iceCandidatePoolSize: 0,可能候选还没生成就发走了。建议用默认值。 - 信令服务器没做去重:如果同一个消息被重复转发,RTCPeerConnection会报错。我踩过这个坑,后来在信令服务器加了个消息ID去重逻辑。
- 忘记处理连接状态变化:监听
oniceconnectionstatechange事件,当状态变成disconnected或failed时,要主动重连或提示用户。
小技巧:调试阶段,可以在浏览器控制台打印 pc.iceConnectionState 和 pc.connectionState,实时查看连接状态。我习惯在状态变化时打个日志,方便定位问题。
好了,这一章的内容就到这。RTCPeerConnection的创建和信令交换,是WebRTC最核心的环节。你把它搞明白了,后面那些音视频传输、数据通道、屏幕共享什么的,都是在此基础上加功能而已。
公众号:蓝海资料掘金营,微信deep3321