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, err := socketio.NewServer(nil)
      if err != nil {
       log.Fatal(err)
      }
      ///////////////////////////////////////////////////////////
      // 命名空间:/ 下
      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);
          })
文档更新时间: 2020-09-26 13:45   作者:lee