1. demo 简介:
    # 使用谷歌的免费 stun 服务器,正式环境可以自己搭建 turn 服务器
    # 使用 go-socket.io 做信令服务器
    # 客户端使用的 socket.io 版本为 1.4.x
  2. 示例代码:
    1. 后端(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))
       }
    2. 前端:
      1. 1对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>
        2. 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);
           };
      2. 多对多 视频聊天:
        1. 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>
        2. 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);
           })
文档更新时间: 2024-04-19 15:11   作者:lee