- demo 简介:
# 使用谷歌的免费 stun 服务器,正式环境可以自己搭建 turn 服务器 # 使用 go-socket.io 做信令服务器 # 客户端使用的 socket.io 版本为 1.4.x
- 示例代码:
- 后端(socket.io 信令服务器):
package main import ( "encoding/json" "fmt" "github.com/googollee/go-socket.io" "log" "net/http" ) type RoomArgs struct { UserId string `json:"userId"` } func main() { server := socketio.NewServer(nil) /////////////////////////////////////////////////////////// // 命名空间:/ 下 server.OnConnect("/", func(s socketio.Conn) error { fmt.Println("connected:", s.ID()) s.Join("system") return nil }) server.OnEvent("/", "join-room", func(s socketio.Conn, msg string) { fmt.Println("join-room:",msg) var room RoomArgs err := json.Unmarshal([]byte(msg), &room) if err != nil { fmt.Println("join-room") fmt.Println(err) return } server.BroadcastToRoom("/", "system", "user-joined", room.UserId) // 服务端发送消息:给所有加入房间的用户发送消息 }) server.OnEvent("/", "broadcast", func(s socketio.Conn, msg string) { fmt.Println("broadcast:",msg) rooms := s.Rooms() for _,r := range rooms{ server.BroadcastToRoom("/", r, "broadcast", msg) // 服务端发送消息:给所有加入房间的用户发送消息 } }) // 失去连接事件,可以做一些登出操作 server.OnDisconnect("/", func(s socketio.Conn, reason string) { fmt.Println("closed", reason) }) /////////////////////////////////////////////////////////// // 错误处理事件,可忽略 server.OnError("/", func(s socketio.Conn, e error) { fmt.Println("meet error:", e) }) go server.Serve() defer server.Close() // socket.io 核心功能 http.HandleFunc("/socket.io/", func(w http.ResponseWriter, r *http.Request) { // 允许跨域 origin := r.Header.Get("Origin") log.Println("origin",origin) w.Header().Set("Access-Control-Allow-Origin", origin) w.Header().Set("Access-Control-Allow-Credentials", "true") server.ServeHTTP(w, r) }) log.Println("请打开网址: http://localhost:8080 进行访问...") log.Fatal(http.ListenAndServe(":8080", nil)) }
- 前端:
- 1对1 视频聊天:
- index.html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf8"> <title>WebRTC Demo</title> <style> body { font-family: sans-serif; } #videos { display: flex; } #videos div:nth-child(2) { margin-left: 20px; } video { width: 200px; height: 200px; border: 1px solid #ddd; } #buttons { margin-bottom: 20px; } </style> </head> <body> <div id="buttons"> <button id="startCall" type="button">接通</button> <button id="endCall" type="button">挂断</button> </div> <div id="videos"> <div> <div>本地画面</div> <video id="localVideo" autoplay muted playsinline></video> </div> <div> <div>远程画面</div> <video id="remoteVideo" autoplay playsinline></video> </div> </div> <script src="./socket.io.js"></script> <script src="./main.js"></script> </body> </html>
- main.js:
var localUserId = Math.random().toString(36).substr(2); // 用户ID var localStream; // 本地流 var pc = null; // RTCPeerConnection 对象 var servers = { iceServers: [ { urls: 'stun:stun.l.google.com:19302' } ] }; ///////////////////////////////////////////// var socket = io('http://localhost:8080'); socket.on('connect', function() { console.log("socket.io 已连接"); }); socket.on('broadcast', function(msg) { console.log('收到消息: ' + msg); msg = JSON.parse(msg) if (localUserId == msg.userId) { return; } switch (msg.msgType) { case 'offer': console.log('收到远程 offer: ' + msg.sdp); if (pc == null) { createPeerConnection() } var offer = new RTCSessionDescription({ 'type': 'offer', 'sdp': msg.sdp }); pc.setRemoteDescription(offer); pc.createAnswer() .then(function(answer) { pc.setLocalDescription(answer); var message = { 'userId': localUserId, 'msgType': 'answer', 'sdp': answer.sdp }; socket.emit('broadcast', JSON.stringify(message)); console.log('发送 answer: ',message); }) .catch(function (e) { console.log('发送 answer 失败: ' + e.toString()) }) break; case 'answer': console.log('收到远程 answer: ' + msg.sdp); var answer = new RTCSessionDescription({ 'type': 'answer', 'sdp': msg.sdp }); pc.setRemoteDescription(answer); break; case 'candidate': console.log('收到远程 candidate: ' + msg.candidate); var candidate = new RTCIceCandidate({ sdpMLineIndex: msg.index, candidate: msg.candidate }); pc.addIceCandidate(candidate); break; case 'hangup': console.log('收到远程挂断信号'); hangup(); break; default: break; } }); //////////////////////////////////////////////////// var localVideo = document.querySelector('#localVideo'); var remoteVideo = document.querySelector('#remoteVideo'); navigator.mediaDevices.getUserMedia({ audio: false, video: true }) .then(function(stream) { localVideo.srcObject = stream; localStream = stream; console.log('打开本地流成功'); }) .catch(function(e) { console.log('打开本地流失败:' + e.toString()); }); function createPeerConnection() { pc = new RTCPeerConnection(servers); pc.onicecandidate = function (event) { console.log('处理 onicecandidate 事件: ', event); if (event.candidate) { var message = { 'userId': localUserId, 'msgType': 'candidate', 'index': event.candidate.sdpMLineIndex, 'candidate': event.candidate.candidate }; socket.emit('broadcast', JSON.stringify(message)); console.log('发送 candidate: ',message); } else { console.log('candidate 结束'); } } pc.onaddstream = function(event) { console.log('添加远程流'); remoteVideo.srcObject = event.stream; } pc.onremovestream = function(event) { console.log('移除远程流: ', event); remoteVideo.srcObject = null; } localStream.getTracks() .forEach(track => pc.addTrack(track, localStream)); console.log('RTCPeerConnnection 对象创建成功'); } ///////////////////////////////////////////////////////// function hangup() { remoteVideo.srcObject = null; if (pc != null) { pc.close(); pc = null; } } ///////////////////////////////////////////////////////// document.getElementById('startCall').onclick = function() { if (pc == null) { createPeerConnection() } pc.createOffer() .then(function(offer) { console.log('创建 offer 成功: ', offer); pc.setLocalDescription(offer); var message = { 'userId': localUserId, 'msgType': 'offer', 'sdp': offer.sdp }; socket.emit('broadcast', JSON.stringify(message)); console.log('发送 offer: ', message); }) .catch(function (e) { console.log('创建 offer 失败: ' + e.toString()) }) } document.getElementById('endCall').onclick = function() { console.log('End call'); hangup(); var message = { 'userId': localUserId, 'msgType': 'hangup', }; socket.emit('broadcast', JSON.stringify(message)); console.log('发送挂断信号: ', message); };
- index.html:
- 多对多 视频聊天:
- index.html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf8"> <title>WebRTC Demo</title> <style> body { font-family: sans-serif; } video { width: 200px; height: 200px; border: 1px solid #ddd; margin: 5px; } #remoteVideos{ display: flex; } .title{ margin-left: 5px; } </style> </head> <body> <div class="title">本地</div> <video id="localVideo" autoplay muted playsinline ></video> <div class="title">远程</div> <div id="remoteVideos"> </div> <script src="./socket.io.js"></script> <script src="./main.js"></script> </body> </html>
- main.js:
var localUserId = Math.random().toString(36).substr(2); // 用户 ID var localStream; // 本地流 var localVideo = document.querySelector('#localVideo'); var peerConnections = []; ///////////////////////////////////////////// var socket = io('http://localhost:8080'); socket.on('connect', function() { console.log("socket.io 连接成功"); }); var args = { 'userId': localUserId, }; socket.emit('join-room', JSON.stringify(args)); socket.on('user-joined', function(userId) { if (localUserId == userId) { return; } console.log('新用户加入房间: ' + userId); if (peerConnections[userId] == null) { peerConnections[userId] = new RTCPeerConnectionObject(localUserId, userId, localStream); } peerConnections[userId].createOffer(); }); socket.on('broadcast', function(msg) { msg = JSON.parse(msg) if (msg.userId == localUserId) { return; } // 忽略不是发送给我的消息 if (msg.targetUserId && msg.targetUserId != localUserId) { return; } switch (msg.msgType) { case 'offer': console.log('收到 offer: ', msg.userId); // set remote sdp var sdp = new RTCSessionDescription({ 'type': 'offer', 'sdp': msg.sdp }); peerConnections[msg.userId] = new RTCPeerConnectionObject(localUserId, msg.userId, localStream); peerConnections[msg.userId].setRemoteDescription(sdp); peerConnections[msg.userId].createAnswer(); break; case 'answer': console.log('收到 answer: ', msg.userId); if (peerConnections[msg.userId] == null) { console.log('PeerConnection 对象未创建: ', msg.userId); return } var sdp = new RTCSessionDescription({ 'type': 'answer', 'sdp': msg.sdp }); peerConnections[msg.userId].setRemoteDescription(sdp); break; case 'candidate': console.log('收到 candidate: ', msg.userId); if (peerConnections[msg.userId] == null) { console.log('PeerConnection 对象未创建: ', msg.userId); return } var candidate = new RTCIceCandidate({ sdpMLineIndex: msg.label, candidate: msg.candidate }); peerConnections[msg.userId].addIceCandidate(candidate); break; case 'hangup': console.log('收到远程挂断信号: ', msg.userId); if (peerConnections[msg.userId] == null) { console.log('PeerConnection 对象未创建: ', msg.userId); return } peerConnections[msg.userId].close(); break; default: break; } }); //////////////////////////////////////////////////// class RTCPeerConnectionObject { localUserId remoteUserId remoteSdp pc constructor(localUserId, remoteUserId, localStream) { this.localUserId = localUserId; this.remoteUserId = remoteUserId; this.pc = this.create(localStream); } create = function(stream) { var _this = this; var servers = { iceServers: [ { urls: 'stun:stun.l.google.com:19302' } ] }; var pc = new RTCPeerConnection(servers); pc.onicecandidate = function(event) { if (event.candidate) { var message = { 'userId': _this.localUserId, 'msgType': 'candidate', 'id': event.candidate.sdpMid, 'label': event.candidate.sdpMLineIndex, 'candidate': event.candidate.candidate, 'targetUserId': this.remoteUserId }; socket.emit('broadcast', JSON.stringify(message)); console.log('发送 candidate: ', message); } else { console.log('candidate 结束'); } } pc.onaddstream = function(event) { console.log('添加远程流: ', this.remoteUserId); var video = document.createElement('video'); video.srcObject = event.stream; video.autoplay = true; video.muted = true; video.playsinline = true; document.querySelector('#remoteVideos').appendChild(video); } pc.onremovestream = function (event) { console.log('远程流被移除'); }; stream.getTracks() .forEach(track => pc.addTrack(track, stream)); return pc; } setRemoteDescription = function(sdp) { if (this.remoteSdp != null) { return } this.remoteSdp = sdp; this.pc.setRemoteDescription(sdp); } addIceCandidate = function(candidate) { this.pc.addIceCandidate(candidate); } createOffer = function() { var _this = this; _this.pc.createOffer() .then(function(offer) { console.log('创建 offer: ', offer); _this.pc.setLocalDescription(offer); var message = { 'userId': _this.localUserId, 'msgType': 'offer', 'sdp': offer.sdp, 'targetUserId': _this.remoteUserId }; socket.emit('broadcast', JSON.stringify(message)); console.log('发送 offer: ', message); }) .catch(function(e) { console.log('创建 offer 失败: ' + e.toString()); }) } createAnswer = function() { var _this = this; _this.pc.createAnswer() .then(function(answer) { console.log('创建 answer: ', answer); _this.pc.setLocalDescription(answer); var message = { 'userId': _this.localUserId, 'msgType': 'answer', 'sdp': answer.sdp, 'targetUserId': _this.remoteUserId }; socket.emit('broadcast', JSON.stringify(message)); console.log('发送 answer: ', message); }) .catch(function(e) { console.log('创建 answer 失败: ' + e.toString()); }) } } //////////////////////////////////////////////////// navigator.mediaDevices.getUserMedia({ audio: false, video: true }) .then(function(stream) { console.log('打开本地流'); localVideo.srcObject = stream; localStream = stream; }) .catch(function(e) { console.log('打开本地流失败: ' + e.name); })
- index.html:
- 1对1 视频聊天:
- 后端(socket.io 信令服务器):
文档更新时间: 2024-04-19 15:11 作者:lee