WebRTC实现局域网1v1视频通话
原理:
流程:
协议交互:
服务端代码:
package main
import (
"log"
"net/http"
"sync"
// 注意:您需要安装此库:go get golang.org/x/net/websocket
"golang.org/x/net/websocket"
)
// 客户端连接结构体
type clientConn struct {
ws *websocket.Conn
}
var (
offerClient *clientConn
answerClient *clientConn
mutex sync.Mutex // 互斥锁,保护全局客户端变量
)
// Message 结构体用于连接消息的解析和发送
type Message struct {
Type string `json:"type"`
Code int `json:"code,omitempty"`
Message string `json:"message,omitempty"`
}
// 发送连接响应消息
func (c *clientConn) sendResponse(code int, msg string) {
resp := Message{
Type: "connect",
Code: code,
Message: msg,
}
if err := websocket.JSON.Send(c.ws, resp); err != nil {
log.Printf("Error sending response: %v", err)
}
}
// 关闭连接并清理全局变量
func (c *clientConn) close() {
mutex.Lock()
defer mutex.Unlock()
if c.ws != nil {
c.ws.Close()
}
// 清理全局连接变量
if c == offerClient {
offerClient = nil
log.Println("Offer client removed.")
} else if c == answerClient {
answerClient = nil
log.Println("Answer client removed.")
}
}
// 检查是否可以开始 WebRTC 连接
func checkStart() {
mutex.Lock()
defer mutex.Unlock()
if offerClient != nil && answerClient != nil {
log.Println("Both clients connected. Initiating offer creation.")
// 通知 Offer 端创建 Offer
msg := Message{Type: "create_offer"}
if err := websocket.JSON.Send(offerClient.ws, msg); err != nil {
log.Printf("Error sending create_offer to offerClient: %v", err)
}
}
}
// 处理客户端发送的业务消息 (信令转发逻辑)
func handleMessage(msgType string, sender *clientConn, msgData interface{}) {
log.Println("handleMessage0. msgType:", msgType)
// mutex.Lock()
// defer mutex.Unlock()
log.Println("handleMessage1. msgType:", msgType)
// 统一将消息转发给另一端
var targetClient *clientConn
if sender == offerClient {
targetClient = answerClient
log.Println("handleMessage2. msgType:", msgType)
} else if sender == answerClient {
targetClient = offerClient
log.Println("handleMessage3. msgType:", msgType)
}
switch msgType {
case "connect":
if offerClient == nil {
offerClient = sender
log.Println("Client registered as Offer.")
sender.sendResponse(200, "connect success")
checkStart()
} else if answerClient == nil {
answerClient = sender
log.Println("Client registered as Answer.")
sender.sendResponse(200, "connect success")
checkStart()
} else {
log.Println("Connection refused: room full.")
sender.sendResponse(-1, "connect failed")
sender.close()
}
case "offer", "answer", "offer_ice", "answer_ice":
if targetClient != nil {
// 直接转发信令数据
websocket.JSON.Send(targetClient.ws, msgData)
log.Printf("Forwarded %s signal.", msgType)
} else {
log.Printf("Cannot forward %s, target client is nil.", msgType)
}
default:
log.Printf("Unknown message type: %s", msgType)
}
}
// 处理新的 WebSocket 连接
func wsHandler(ws *websocket.Conn) {
currentConn := &clientConn{ws: ws}
defer currentConn.close()
for {
var incomingMsg interface{}
if err := websocket.JSON.Receive(ws, &incomingMsg); err != nil {
if err.Error() != "EOF" {
log.Printf("Error receiving message: %v", err)
}
return
}
// 解析消息类型
msgMap, ok := incomingMsg.(map[string]interface{})
if !ok {
continue
}
msgType, typeOk := msgMap["type"].(string)
if !typeOk {
continue
}
log.Printf("Received msgType: %s. message: %v", msgType, incomingMsg)
handleMessage(msgType, currentConn, incomingMsg)
}
}
// CORS 中间件,允许所有来源访问 WebSocket 握手
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
func main() {
wsHandlerImpl := websocket.Handler(wsHandler)
// 注册路由,并使用 CORS 中间件包裹
http.Handle("/", corsMiddleware(wsHandlerImpl))
port := ":9000"
log.Printf("WebSocket signaling server starting on ws://localhost%s/", port)
if err := http.ListenAndServe(port, nil); err != nil {
log.Fatal("ListenAndServe:", err)
}
}
客户端代码:
新建一个webrtc.html静态页面
<!DOCTYPE html>
<html>
<head>
<title>WebRTC 1v1 Client</title>
<meta charset="UTF-8">
<style>
body { font-family: sans-serif; display: flex; flex-direction: column; align-items: center; }
.video-container { display: flex; gap: 20px; margin-bottom: 20px; }
video { width: 320px; height: 240px; background-color: #333; border: 1px solid #999; }
</style>
</head>
<body>
<h1>WebRTC 视频通话示例</h1>
<div class="video-container">
<div>
<h3>本地画面 (Local)</h3>
<video id="localVideo" autoplay muted></video>
</div>
<div>
<h3>远端画面 (Remote)</h3>
<video id="remoteVideo" autoplay></video>
</div>
</div>
<button id="startBtn" disabled>开始连接信令服务器</button>
<script>
// --- 配置 ---
const SignalingServerUrl = 'ws://127.0.0.1:9000';
// --- DOM 元素 ---
const localVideo = document.getElementById("localVideo");
const remoteVideo = document.getElementById("remoteVideo");
const startBtn = document.getElementById("startBtn");
// --- 全局 WebRTC 状态 ---
let localStream = null;
let pc = null;
let ws = null;
// --- 核心 WebRTC/WebSocket 逻辑 ---
// 1. 获取本地媒体流
function getDevice() {
return navigator.mediaDevices.getUserMedia({ audio: true, video: true });
}
// 2. 初始化 PeerConnection
function initPeerConnection(iceType) {
console.log("初始化 RTCPeerConnection...");
// 使用空的配置,或者在这里添加 STUN/TURN 服务器
pc = new RTCPeerConnection({});
// 绑定本地流的 Tracks 到 PeerConnection
localStream.getTracks().forEach(track => {
pc.addTrack(track, localStream);
});
// 监听 ICE Candidate,通过信令服务器发送
pc.addEventListener('icecandidate', (event) => {
if (event.candidate) {
console.log(`发送 ICE Candidate, Type: ${iceType}`);
ws.send(JSON.stringify({
type: iceType,
candidate: event.candidate
}));
}
});
// 监听远端流的到来
pc.addEventListener("track", (event) => {
console.log("收到远端 track, 绑定到 remoteVideo");
remoteVideo.srcObject = event.streams[0];
});
}
// 3. 信令:发送 Offer
function sendOffer() {
pc.createOffer({ offerToReceiveAudio: true, offerToReceiveVideo: true }).then((offer) => {
pc.setLocalDescription(offer).then(() => {
// 发送 SDP Offer 到信令服务器
ws.send(JSON.stringify({ type: "offer", sdp: offer }));
console.log("SDP Offer 发送成功.");
});
}).catch(e => console.error("Error creating/setting Offer:", e));
}
// 4. 信令:接收 Offer 并发送 Answer
function recvOffer(offerSdp) {
pc.setRemoteDescription(new RTCSessionDescription(offerSdp)).then(() => {
pc.createAnswer().then((answer) => {
pc.setLocalDescription(answer).then(() => {
// 发送 SDP Answer 到信令服务器
ws.send(JSON.stringify({ type: "answer", sdp: answer }));
console.log("SDP Answer 发送成功.");
});
});
}).catch(e => console.error("Error receiving/setting Offer:", e));
}
// 5. 信令:接收 Answer
function recvAnswer(answerSdp) {
pc.setRemoteDescription(new RTCSessionDescription(answerSdp)).catch(e => console.error("Error setting Answer:", e));
console.log("SDP Answer 设置成功.");
}
// 6. WebSocket 消息处理
function handleWebSocketMessage(event) {
let msg = JSON.parse(event.data);
switch (msg.type) {
case "connect":
if (200 === msg.code) {
console.log("信令连接成功,等待其他用户...");
startBtn.disabled = true;
} else {
console.error("连接失败,已经满员:", msg.message);
}
break;
case "create_offer":
console.log("收到指令:创建 Offer (我是 Offer 端)");
initPeerConnection("offer_ice");
sendOffer();
break;
case "offer":
console.log("收到 Offer (我是 Answer 端)");
initPeerConnection("answer_ice");
recvOffer(msg.sdp);
break;
case "answer":
console.log("收到 Answer");
recvAnswer(msg.sdp);
break;
case "offer_ice":
case "answer_ice":
if (msg.candidate) {
pc.addIceCandidate(msg.candidate).catch(e => console.error("Error adding ICE candidate:", e));
console.log("添加 ICE Candidate");
}
break;
default:
console.warn("收到未知信令消息:", msg.type);
break;
}
}
// 7. WebSocket 初始化
function initWebSocket() {
if (ws && ws.readyState === WebSocket.OPEN) ws.close();
ws = new WebSocket(SignalingServerUrl);
ws.onopen = () => {
ws.send(JSON.stringify({ type: "connect" }));
};
ws.onmessage = handleWebSocketMessage;
ws.onclose = () => {
console.log("WebSocket 连接断开");
startBtn.disabled = false;
};
ws.onerror = (err) => {
console.error("WebSocket 发生错误:", err);
};
}
// --- 启动逻辑 ---
getDevice().then((mediaStream) => {
localStream = mediaStream;
localVideo.srcObject = mediaStream;
startBtn.disabled = false;
console.log("媒体流获取成功。");
}).catch((err) => {
console.error("获取摄像头失败,请检查权限:", err);
alert("无法获取摄像头和麦克风权限!");
});
startBtn.addEventListener('click', () => {
if (localStream) {
initWebSocket();
startBtn.disabled = true;
}
});
</script>
</body>
</html>
部署过程:
- 启动服务端golang websocket服务,监听9000端口
- cd 到client.html所在目录, python3 -m http.server 8000 启动web-server服务器
- 使用chrome浏览器 访问http://localhost:8000/webrtc.html ,连接信令服务器
- 使用safari浏览器访问 访问http://localhost:8000/webrtc.html ,连接信令服务
- 双方视频通话
© 版权声明
文章版权归作者所有,未经允许请勿转载。
相关文章
暂无评论...




