这一步很重要,目标是两个浏览器窗口,通过你的 TURN 服务器,100% relay 建立视频通话,主要是测试跨网段的WebRTC实现。
第一部分:测试结构说明
我们做一个:
bash
index.html
server.js (Node 信令)
流程:
浏览器 A ↔ WebSocket ↔ 浏览器 B
媒体流 100% 走 TURN relay
第二部分:先创建信令服务器(最简单)
在服务器执行:
bash
apt install nodejs npm -y
mkdir webrtc-test
cd webrtc-test
npm init -y
npm install ws
创建文件:
bash
vi server.js
粘贴:
bash
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 3000 });
let clients = {};
wss.on('connection', (ws) => {
ws.on('message', (message) => {
const data = JSON.parse(message);
if (data.type === 'register') {
clients[data.id] = ws;
ws.id = data.id;
console.log("Registered:", data.id);
}
if (data.to && clients[data.to]) {
clients[data.to].send(JSON.stringify(data));
}
});
ws.on('close', () => {
if (ws.id) delete clients[ws.id];
});
});
console.log("WebSocket signaling server running on port 3000");
启动:
bash
node server.js
第三部分:创建 WebRTC 页面
创建:
bash
vi index.html
粘贴完整代码:
bash
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>WebRTC TURN Relay Test</title>
</head>
<body>
<h3>WebRTC TURN Relay Test</h3>
Your ID: <input id="myId"><br>
Peer ID: <input id="peerId"><br>
<button onclick="connect()">Connect</button>
<button onclick="call()">Call</button>
<button onclick="hangup()">Hangup</button>
<br><br>
<video id="localVideo" autoplay playsinline muted width="300"></video>
<video id="remoteVideo" autoplay playsinline width="300"></video>
<script>
let ws;
let pc;
let localStream;
let myId;
let peerId;
let pendingCandidates = [];
const iceConfig = {
iceTransportPolicy: "relay", // 强制走 TURN
iceServers: [
{
urls: "turn:123.129.219.235:3478?transport=udp",
username: "lanz",
credential: "QAZ123"
}
]
};
function logState() {
if (!pc) return;
console.log("ICE:", pc.iceConnectionState);
console.log("Connection:", pc.connectionState);
}
function connect() {
myId = document.getElementById("myId").value;
ws = new WebSocket("wss://tt.toim.cc/ws"); // 修改成你的地址
ws.onopen = () => {
console.log("Connected to signaling server");
ws.send(JSON.stringify({type:"register", id:myId}));
};
ws.onmessage = async (msg) => {
const data = JSON.parse(msg.data);
if (data.type === "offer") {
peerId = data.from;
await createPeer();
await pc.setRemoteDescription(new RTCSessionDescription(data.offer));
// 处理缓存的 candidates
for (let c of pendingCandidates) {
await pc.addIceCandidate(new RTCIceCandidate(c));
}
pendingCandidates = [];
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
ws.send(JSON.stringify({
type:"answer",
answer,
to:peerId,
from:myId
}));
}
if (data.type === "answer") {
await pc.setRemoteDescription(new RTCSessionDescription(data.answer));
for (let c of pendingCandidates) {
await pc.addIceCandidate(new RTCIceCandidate(c));
}
pendingCandidates = [];
}
if (data.type === "candidate") {
if (pc && pc.remoteDescription) {
await pc.addIceCandidate(new RTCIceCandidate(data.candidate));
} else {
pendingCandidates.push(data.candidate);
}
}
if (data.type === "hangup") {
cleanUp();
}
};
}
async function createPeer() {
pc = new RTCPeerConnection(iceConfig);
pc.onicecandidate = (event) => {
if (event.candidate) {
ws.send(JSON.stringify({
type:"candidate",
candidate:event.candidate,
to:peerId,
from:myId
}));
}
};
pc.ontrack = (event) => {
console.log("Remote stream received");
document.getElementById("remoteVideo").srcObject = event.streams[0];
};
pc.oniceconnectionstatechange = logState;
pc.onconnectionstatechange = logState;
try {
localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
} catch (e) {
console.warn("video+audio failed, fallback to audio only");
localStream = await navigator.mediaDevices.getUserMedia({
video: false,
audio: true
});
}
document.getElementById("localVideo").srcObject = localStream;
localStream.getTracks().forEach(track => {
pc.addTrack(track, localStream);
});
}
async function call() {
peerId = document.getElementById("peerId").value;
await createPeer();
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
ws.send(JSON.stringify({
type:"offer",
offer,
to:peerId,
from:myId
}));
}
function hangup() {
ws.send(JSON.stringify({
type:"hangup",
to:peerId,
from:myId
}));
cleanUp();
}
function cleanUp() {
if (pc) {
pc.close();
pc = null;
}
if (localStream) {
localStream.getTracks().forEach(track => track.stop());
localStream = null;
}
document.getElementById("localVideo").srcObject = null;
document.getElementById("remoteVideo").srcObject = null;
pendingCandidates = [];
console.log("Call ended");
}
</script>
</body>
</html>
⚠ 把:
bash
YOUR_SERVER_IP
替换成你的公网 IP,并记得打开3000端口!
第四步:测试方法
注意:这里index.html必须是localhost或可以访问的https地址,否则不够权限使用摄像头!
HTTPS 页面 + WS 信令要用 WSS
你如果把页面切到 https://,你代码里:
bash
ws = new WebSocket("ws://YOUR_SERVER_IP:3000");
会被浏览器拦截(混合内容),必须改成:
bash
ws = new WebSocket("wss://你的域名/ws");
在站点配置里加:
bash
ws = new WebSocket("wss://你的域名/ws");
1️⃣ 浏览器1打开 index.html
2️⃣ 浏览器2打开 index.html
3️⃣ 分别输入 ID:
窗口1:
bash
myId: A
peerId: B
窗口2:
bash
myId: B
peerId: A
bash
myId: B
peerId: A
4️⃣ 两边都点击 Connect
5️⃣ A 点击 Call
预期结果
本地视频显示
远端视频显示
控制台 ICE 显示 relay
跨网也可通