swoole方案 音视频信令服务器 (WebRTC Signaling)
·
WebRTC 两个浏览器直连,但连之前要互相通气:
"我支持哪些编码格式" -> SDP
"我的IP和端口是啥" -> ICE Candidate
信令服务器就是个传话筒,把这两样东西转交给对方。
连上之后音视频直接P2P,信令服务器就没事了。
A ---offer(SDP)------> 信令服务器 ----> B
A <--answer(SDP)------ 信令服务器 <---- B
A ---ICE candidate---> 信令服务器 ----> B
A <--ICE candidate---- 信令服务器 <---- B
↓ 握手完成
A <============ 直接P2P音视频,不过服务器 ============> B
---
服务器
<?php
$rooms = new Swoole\Table(1 << 16);
$rooms->column('room', Swoole\Table::TYPE_STRING, 32);
$rooms->create();
$server = new Swoole\WebSocket\Server('0.0.0.0', 9502);
$server->on('open', function ($server, $req) use ($rooms) {
preg_match('#/room/(\w+)#', $req->server['request_uri'], $m);
$room = $m[1] ?? 'default';
$rooms->set($req->fd, ['room' => $room]);
// 告诉房间里已有的人:有新人进来了,你去发 offer 给他
foreach ($rooms as $fd => $row) {
if ($row['room'] === $room && $fd !== $req->fd && $server->isEstablished($fd)) {
$server->push($fd, json_encode(['type' => 'peer_joined', 'peer' => $req->fd]));
}
}
});
$server->on('message', function ($server, $frame) use ($rooms) {
$data = json_decode($frame->data, true);
$data['from'] = $frame->fd; // 告诉接收方是谁发的
$payload = json_encode($data);
// offer/answer/ice 都指定了 to,点对点转发
$to = (int)($data['to'] ?? 0);
if ($to && $server->isEstablished($to)) {
$server->push($to, $payload);
return;
}
// 没指定 to 就广播给同房间其他人
$room = $rooms->get($frame->fd)['room'];
foreach ($rooms as $fd => $row) {
if ($row['room'] === $room && $fd !== $frame->fd && $server->isEstablished($fd)) {
$server->push($fd, $payload);
}
}
});
$server->on('close', function ($server, $fd) use ($rooms) {
$room = $rooms->get($fd)['room'] ?? null;
$rooms->del($fd);
if (!$room) return;
// 通知同房间其他人,这个人断了
foreach ($rooms as $otherFd => $row) {
if ($row['room'] === $room && $server->isEstablished($otherFd)) {
$server->push($otherFd, json_encode(['type' => 'peer_left', 'peer' => $fd]));
}
}
});
$server->on('message', fn() => null);
$server->start();
---
前端
<video id="local" autoplay muted style="width:45%"></video>
<video id="remote" autoplay style="width:45%"></video>
<script>
const ws = new WebSocket('ws://localhost:9502/room/888');
const pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] // 用谷歌 STUN 获取公网 IP
});
let peerFd;
// 收到信令消息,按类型处理
ws.onmessage = async ({ data }) => {
const msg = JSON.parse(data);
if (msg.type === 'peer_joined') {
// 有人进房间,我是先来的,我发 offer
peerFd = msg.peer;
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
ws.send(JSON.stringify({ type: 'offer', sdp: offer, to: peerFd }));
}
if (msg.type === 'offer') {
// 我是后来的,收到 offer,回 answer
peerFd = msg.from;
await pc.setRemoteDescription(new RTCSessionDescription(msg.sdp));
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
ws.send(JSON.stringify({ type: 'answer', sdp: answer, to: peerFd }));
}
if (msg.type === 'answer') {
await pc.setRemoteDescription(new RTCSessionDescription(msg.sdp));
}
if (msg.type === 'ice') {
await pc.addIceCandidate(new RTCIceCandidate(msg.candidate));
}
if (msg.type === 'peer_left') {
document.getElementById('remote').srcObject = null;
}
};
// 收集到自己的 ICE candidate,发给对方
pc.onicecandidate = ({ candidate }) => {
if (candidate && peerFd) {
ws.send(JSON.stringify({ type: 'ice', candidate, to: peerFd }));
}
};
// 对方的视频流来了,塞进 video 标签
pc.ontrack = ({ streams }) => {
document.getElementById('remote').srcObject = streams[0];
};
// 开摄像头麦克风,加进 PeerConnection
navigator.mediaDevices.getUserMedia({ video: true, audio: true }).then(stream => {
document.getElementById('local').srcObject = stream;
stream.getTracks().forEach(track => pc.addTrack(track, stream));
});
</script>
---
解释
SDP 是什么:一段文本,写着"我支持H264/VP8编码,采样率48000,我有一路视频一路音频"。createOffer() 生成自己的
SDP,setRemoteDescription() 接收对方的 SDP,两边对照确认用哪个编解码器
ICE Candidate 是什么:你可能有多个 IP(内网、外网、IPv6),每个 IP+端口组合就是一个
candidate,浏览器收集完发给对方,对方挨个试,找到能通的那条路
STUN 服务器干啥:你在 NAT 后面,不知道自己公网 IP 是啥。访问一下 STUN 服务器,它告诉你"你的公网出口是 1.2.3.4:5678",这个地址加进
ICE candidate 里
信令服务器连通之后就没用了:offer/answer/ice 交换完,浏览器直接打洞 P2P 连上。后续音视频数据不经过你的服务器,服务器压力为零
peer_joined 谁发 offer:两个人进房间,必须有一个先发 offer。这里约定:先进来的人收到 peer_joined 通知,主动发 offer
给新来的。新来的人等着收 offer 再回 answer
data['from'] = $frame->fd:服务器转发消息时带上发送者 fd,接收方回消息时把这个 fd 填到 to
字段,实现点对点转发,不广播给不相关的人
---
测试
php server.php
# 浏览器开两个标签访问同一个 html 文件
# 两个标签自动完成握手,看到对方的摄像头画面
# 关一个标签,另一个收到 peer_left
更多推荐

所有评论(0)