基本概念
WebRTC(Web Real-Time Communications)
网络实时通讯,它允许网络应用或者站点,在不借助中间媒介的情况下,建立点对点(Peer-to-Peer)的连接,实现视频流和音频流或者其他任意数据的传输
NAT(Network Address Translation)
网络地址转换协议,用来给私网设备映射一个公网的 IP 地址
STUN(Session Traversal Utilities for NAT)
会话穿透,通过NAT找到公网地址进行P2P通信
TURN(Traversal Using Relay around NAT)
中继转发,当STUN不可用时,通过TURN转发音视频数据,显然这样是开销最大的
开源STUN&TURN服务器:coturn
ICE(Interactive Connectivity Establishment)
交互式连接建立,即网络信息
candidate:候选,优先级为:局域网、STUN、TURN
SDP(Session Description Protocol)
会话描述协议,即媒体信息,不是音视频流,在WebRTC中分为offer和answer
Signaling Server
信令服务器,用来交换ICE和SDP信息,WebRTC未做规定,自己选择实现技术,比如WebSocket
局域网视频通信
局域网不需要STUN/TURN服务器,只需信令服务器,这里使用Node.js ws库实现
效果
代码
客户端 index.html
html
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>WebRTC Demo</title>
<style>
button {
margin: 1rem;
}
video {
width: 300px;
}
</style>
</head>
<body>
<div>
<div>
信令服务器地址:
<input id="inputServer" value="ws://192.168.205.165:8888" />
<button onclick="start()">开始</button>
</div>
<video id="localVideo" autoplay muted></video>
<video id="remoteVideo" autoplay muted></video>
</div>
<script>
const inputServer = document.querySelector("#inputServer");
const remoteVideo = document.querySelector("#remoteVideo");
const localVideo = document.querySelector("#localVideo");
let peerConn;
let webSocket;
let localStream;
// 打开本地摄像头
navigator.mediaDevices.getUserMedia({ audio: true, video: true }).then((mediaStream) => {
localStream = mediaStream;
localVideo.srcObject = mediaStream;
}).catch((err) => {
console.error(err);
});
// 创建WebRTC连接
peerConn = new RTCPeerConnection();
peerConn.addEventListener('icecandidate', (event) => {
if (event.candidate) {
webSocket.send(JSON.stringify({
type: "ice",
candidate: event.candidate
}));
}
});
peerConn.addEventListener("track", (event) => {
remoteVideo.srcObject = event.streams[0];
});
function start() {
// 连接信令服务器
webSocket = new WebSocket(inputServer.value);
webSocket.addEventListener('open', () => {
webSocket.send(JSON.stringify({
type: "join"
}));
});
// 收到服务端消息
webSocket.addEventListener('message', (event) => {
const msg = JSON.parse(event.data);
console.log(msg);
switch (msg.type) {
case "sendOffer":
peerConn.addTrack(localStream.getVideoTracks()[0], localStream);
peerConn.createOffer({ offerToReceiveAudio: true, offerToReceiveVideo: true }).then((offer) => {
peerConn.setLocalDescription(offer).then(() => {
webSocket.send(JSON.stringify(offer));
})
});
break;
case "offer":
peerConn.addTrack(localStream.getVideoTracks()[0], localStream);
peerConn.setRemoteDescription(msg).then(() => {
peerConn.createAnswer().then((answer) => {
peerConn.setLocalDescription(answer).then(() => {
webSocket.send(JSON.stringify(answer));
})
})
});
break;
case "answer":
peerConn.setRemoteDescription(msg);
break;
case "ice":
peerConn.addIceCandidate(msg.candidate);
break;
default:
}
});
webSocket.addEventListener('close', () => {
console.log("websocket close");
});
}
</script>
</body>
</html>
服务端 server.mjs
js
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8888 });
let clients = []; // 已连接的客户端
wss.on('connection', function connection(ws) {
ws.on('message', function message(rawData) {
const data = rawData.toString();
const obj = JSON.parse(data);
console.log("type", obj.type);
switch (obj.type) {
case "join":
if (clients.length < 2) {
clients.push(ws);
if (clients.length === 2) {
clients[0].send(JSON.stringify({ type: "sendOffer" }));
}
} else {
console.log("room is full");
}
break;
case "offer":
clients[1].send(data);
break;
case "answer":
clients[0].send(data);
break;
case "ice":
clients.forEach((item) => {
if (item !== ws) {
item.send(data);
}
})
break;
default:
}
});
ws.on('error', (err) => console.error("error:", err));
ws.on('close', (code) => {
console.log("ws close", code);
clients = clients.filter((item) => {
if (item === ws) {
item = null;
return false;
}
return true;
});
});
});
使用
- 在文件目录运行命令:node server.mjs
- 修改信令服务器地址,浏览器打开 index.html
- 将 index.html 复制到另一台电脑上用浏览器打开
- 允许使用摄像头和麦克风,两边点击开始按钮即可