关于 how-to-build-a-webrtc-signal-server-with-pocketbase
这是一个使用 PocketBase 构建 WebRTC 信令服务的示例教程,面向希望实现浏览器间视频通话的开发者。内容包括服务端集合配置、Vite 客户端搭建及双浏览器通话流程。
主要功能
- 01使用 PocketBase 作为信令服务
- 02通过 SQLite 保存通话相关数据
- 03配置 ICE 服务器与候选数据集合
- 04使用 Vite 和 PocketBase SDK 搭建客户端
适用场景
- 01搭建 WebRTC 浏览器视频通话演示
- 02保存 offer、answer 与 ICE 候选数据
- 03通过两个浏览器测试加入和结束通话
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ rodydavis/ skills --skill how-to-build-a-webrtc-signal-server-with-pocketbase安装后在 Claude Code 中输入/how-to-build-a-webrtc-signal-server-with-pocketbase确认已加载。
详细介绍
如何使用 PocketBase 构建 WebRTC 信令服务器
概览
如果你刚开始接触 WebRTC,建议观看这段精彩的 Fireship 视频:100 秒了解 WebRTC:
另外,如果你想看一个 Firebase 示例,可以查看这个仓库,本示例很大程度上基于该仓库。
本示例使用 PocketBase 作为 WebRTC 的信令服务器,在服务器上运行 SQLite,并基于 服务器发送事件(SSE)构建易于使用的实时 SDK。
设置服务器
下载 PocketBase 并创建一个新目录,用作项目目录。
mkdir webrtc-pocketbase-demo
cd webrtc-pocketbase-demo
将 PocketBase 二进制文件复制到刚创建的目录下的子目录 .pb 中。如果你使用 MacOS,需要在设置中允许该可执行文件运行。
使用以下命令启动 PocketBase 服务器:
.pb/pocketbase serve
如果一切顺利,你应该会看到以下内容:
2023/11/04 15:10:56 Server started at http://127.0.0.1:8090
├─ REST API: http://127.0.0.1:8090/api/
└─ Admin UI: http://127.0.0.1:8090/_/
打开管理界面的 URL,并创建新的用户名和密码。
在本示例中,邮箱和密码如下:
| 键 | 值 |
|---|---|
| 邮箱 | [email protected] |
| 密码 | Test123456789 |
现在你应该会看到以下内容:
![]()
创建集合
ice_servers
创建一个名为 ice_servers 的新集合,并添加以下列:
| 列名 | 列类型 |
|---|---|
| url | 纯文本 |
![]()
将以下 API 规则添加到 List/Search 和 View:
@request.auth.id != ''
![]()
创建集合后,为以下每个 url 值添加 2 条记录:
stun:stun1.l.google.com:19302
stun:stun2.l.google.com:19302
![]()
calls
创建一个名为 calls 的新集合,并添加以下列:
| 列名 | 列类型 | 列设置 |
|---|---|---|
| user_id | 关联 | 非空,users,级联删除为 true |
| offer | JSON | |
| answer | JSON |
![]()
也可以通过为 user_id 列设置唯一约束,将每个用户的通话限制为一次。
![]()
将以下 API 规则添加到所有方法:
@request.auth.id != ''
![]()
offer_candidates
创建一个名为 offer_candidates 的新集合,并添加以下列:
| 列名 | 列类型 | 列设置 |
|---|---|---|
| call_id | 关联 | 非空,calls,级联删除为 true |
| data | JSON |
![]()
将以下 API 规则添加到所有方法:
@request.auth.id != ''
答案候选项
创建一个名为 answer_candidates 的新集合,并添加以下列:
列名
列类型
列设置
call_id
关联
非空,calls,级联删除为 true
data
JSON
![]()
为所有方法添加以下 API 规则:
@request.auth.id != ''
![]()
用户
出于演示目的,我们不会为用户添加身份验证表单。为了简化示例,请使用与管理员相同的登录信息创建一个新用户。
![]()
![]()
设置客户端
进入该目录并运行以下命令以开始:
npm init -y
npm i -D vite
npm i pocketbase
将 package.json 更新为以下内容:
{
"name": "webrtc-pocketbase-demo",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview"
},
"devDependencies": {
"vite": "^4.5.0"
},
"dependencies": {
"pocketbase": "^0.19.0"
}
}
如果你在 Git 仓库中,请按以下内容更新或创建 .gitignore:
node_modules
.DS_Store
dist
dist-ssr
*.local
.pb
.env
HTML
创建 index.html 并添加以下内容:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WebRTC Pocketbase Demo</title>
</head>
<body>
<h2>1. Start your Webcam</h2>
<div class="videos">
<span>
<h3>Local Stream</h3>
<video id="webcamVideo" autoplay playsinline></video>
</span>
<span>
<h3>Remote Stream</h3>
<video id="remoteVideo" autoplay playsinline></video>
</span>
</div>
<button id="webcamButton">Start webcam</button>
<h2>2. Create a new Call</h2>
<button id="callButton" disabled>Create Call (offer)</button>``
<h2>3. Join a Call</h2>
<p>Answer the call from a different browser window or device</p>
<input id="callInput" />
<button id="answerButton" disabled>Answer</button>
<h2>4. Hangup</h2>
<button id="hangupButton" disabled>Hangup</button>
<script type="module" src="/main.js"></script>
</body>
</html>
CSS
创建 style.css 并添加以下内容:
body {
--text-color: #2c3e50;
--video-background-color: #2c3e50;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: var(--text-color);
margin: 80px 10px;
}
video {
width: 40vw;
height: 30vw;
margin: 2rem;
background: var(--video-background-color);
}
.videos {
display: flex;
align-items: center;
justify-content: center;
}
JS
创建 main.js 并添加以下内容:
import "./style.css";
import PocketBase from "pocketbase";
const pb = new PocketBase("http://127.0.0.1:8090");
const calls = pb.collection("calls");
const offerCandidates = pb.collection("offer_candidates");
const answerCandidates = pb.collection("answer_candidates");
const webcamButton = document.getElementById("webcamButton");
const webcamVideo = document.getElementById("webcamVideo");
const callButton = document.getElementById("callButton");
const callInput = document.getElementById("callInput");
const answerButton = document.getElementById("answerButton");
const remoteVideo = document.getElementById("remoteVideo");
const hangupButton = document.getElementById("hangupButton");
const auth = await pb
.collection("users")
.authWithPassword(
import.meta.env.VITE_POCKETBASE_USERNAME,
import.meta.env.VITE_POCKETBASE_PASSWORD
);
const userId = auth.record.id;
const iceServers = await pb.collection("ice_servers").getFullList();
const servers = {
iceServers: [{ urls: iceServers.map((e) => e.url) }],
iceCandidatePoolSize: 10,
};
const pc = new RTCPeerConnection(servers);
let localStream = null;
let remoteStream = null;
webcamButton.onclick = async () => {
localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
remoteStream = new MediaStream();
localStream.getTracks().forEach((track) => {
pc.addTrack(track, localStream);
});
pc.ontrack = (event) => {
const stream = event.streams[0];
stream.getTracks().forEach((track) => {
remoteStream.addTrack(track);
});
};
webcamVideo.srcObject = localStream;
remoteVideo.srcObject = remoteStream;
callButton.disabled = false;
answerButton.disabled = false;
webcamButton.disabled = true;
};
callButton.onclick = async () => {
const call = await calls.create({
user_id: userId,
});
const callId = call.id;
callInput.value = callId;
pc.onicecandidate = (event) => {
event.candidate &&
offerCandidates.create({
call_id: callId,
data: event.candidate.toJSON(),
});
};
const offerDescription = await pc.createOffer();
await pc.setLocalDescription(offerDescription);
const offer = {
sdp: offerDescription.sdp,
type: offerDescription.type,
};
await calls.update(callId, { offer });
calls.subscribe(callId, (e) => {
const data = e.record;
if (!pc.currentRemoteDescription && data?.answer) {
const answerDescription = new RTCSessionDescription(data.answer);
pc.setRemoteDescription(answerDescription);
}
});
answerCandidates.subscribe("*", (e) => {
if (e.action === "create") {
if (e.record?.call_id === callId) {
const data = e.record.data;
const candidate = new RTCIceCandidate(data);
pc.addIceCandidate(candidate);
}
}
});
hangupButton.disabled = false;
};
answerButton.onclick = async () => {
const callId = callInput.value;
const call = await calls.getOne(callId);
pc.onicecandidate = (event) => {
event.candidate &&
answerCandidates.create({
call_id: call.id,
data: event.candidate.toJSON(),
});
};
const offerDescription = call.offer;
const remoteDescription = new RTCSessionDescription(offerDescription);
await pc.setRemoteDescription(remoteDescription);
const answerDescription = await pc.createAnswer();
await pc.setLocalDescription(answerDescription);
const answer = {
type: answerDescription.type,
sdp: answerDescription.sdp,
};
await calls.update(call.id, { answer });
offerCandidates.subscribe("*", async (e) => {
if (e.record?.call_id === call.id) {
if (e.action === "create") {
const data = e.record.data;
const candidate = new RTCIceCandidate(data);
await pc.addIceCandidate(candidate);
} else if (e.action === "delete") {
await offerCandidates.unsubscribe();
window.location.reload();
}
}
});
};
hangupButton.onclick = async () => {
const callId = callInput.value;
pc.close();
await calls.unsubscribe(callId);
await calls.delete(callId);
await answerCandidates.unsubscribe();
window.location.reload();
};
运行示例
运行以下命令启动客户端(确保服务正在另一个终端客户端中运行):
npm run dev
如果成功,你应该会看到以下内容:
VITE v4.5.0 ready in 547 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h to show help
使用相同的 URL 打开两个浏览器:
![]()
在第一个窗口中,点击 Start webcam,然后点击 Create Call (offer)。
这会请求摄像头权限,然后生成一个新 ID,并将其添加到 Join a Call 文本框中。
![]()
复制新 ID,将其粘贴到第二个窗口的文本框中,然后点击 Start webcam。
![]()
通话结束后,点击 Hangup 🎉。
总结
你可以在这里找到源代码。