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>

部署过程:

  1. 启动服务端golang websocket服务,监听9000端口
  2. cd 到client.html所在目录, python3 -m http.server 8000 启动web-server服务器
  3. 使用chrome浏览器 访问http://localhost:8000/webrtc.html ,连接信令服务器
  4. 使用safari浏览器访问 访问http://localhost:8000/webrtc.html ,连接信令服务
  5. 双方视频通话
© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...