返回

从零搭建一个WebRTC,实现多房间多对多通话,并实现屏幕录制

前端

前言

WebRTC 是一种实时通信 (RTC) 技术,它允许浏览器之间直接通信,无需任何插件或第三方应用程序。这使得 WebRTC 成为构建实时视频聊天、在线游戏和其他需要低延迟通信的应用程序的理想选择。

搭建 WebRTC 应用

搭建一个 WebRTC 应用需要两部分:信令和媒体。

信令是用于在浏览器之间建立连接和交换数据的过程。信令服务器可以是任何能够处理 WebSocket 请求的服务器。在本文中,我们将使用 Fastify 来搭建信令服务器。

媒体是用于传输音频和视频数据的过程。媒体服务器可以是任何能够处理媒体流的服务器。在本文中,我们将使用 Kurento Media Server 来搭建媒体服务器。

信令服务器

Fastify 是一个非常流行的 Node.js Web 框架。它轻量级、快速且易于使用。以下是使用 Fastify 搭建信令服务器的步骤:

  1. 安装 Fastify:
npm install fastify
  1. 创建一个新的 Fastify 应用:
const fastify = require('fastify')();
  1. 定义信令服务器的路由:
fastify.get('/join', async (request, reply) => {
  // 处理加入房间的请求
});

fastify.get('/leave', async (request, reply) => {
  // 处理离开房间的请求
});

fastify.get('/offer', async (request, reply) => {
  // 处理发送 offer 的请求
});

fastify.get('/answer', async (request, reply) => {
  // 处理发送 answer 的请求
});

fastify.get('/candidate', async (request, reply) => {
  // 处理发送候选者的请求
});
  1. 启动信令服务器:
fastify.listen(3000, (err, address) => {
  if (err) {
    console.error(err);
    process.exit(1);
  }

  console.log(`Server listening on ${address}`);
});

媒体服务器

Kurento Media Server 是一个开源的媒体服务器,它可以处理各种媒体流,包括音频、视频和数据。以下是使用 Kurento Media Server 搭建媒体服务器的步骤:

  1. 下载 Kurento Media Server:
https://www.kurento.org/download/
  1. 安装 Kurento Media Server:
sudo dpkg -i kurento-media-server*.deb
  1. 启动 Kurento Media Server:
sudo service kurento-media-server start
  1. 配置 Kurento Media Server:
sudo nano /etc/kurento/kurento.conf.json

将以下内容添加到配置文件中:

{
  "modules": [
    {
      "name": "WebRtcEndpoint",
      "endpointTypes": [
        "WebRtcEndpoint"
      ]
    }
  ]
}
  1. 重启 Kurento Media Server:
sudo service kurento-media-server restart

前端部分

Vue.js 是一个非常流行的 JavaScript 框架。它轻量级、快速且易于使用。以下是使用 Vue.js 实现前端部分的步骤:

  1. 安装 Vue.js:
npm install vue
  1. 创建一个新的 Vue.js 应用:
vue create my-webrtc-app
  1. src/main.js 文件中,添加以下代码:
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')
  1. src/App.vue 文件中,添加以下代码:
<template>
  <div id="app">
    <video id="localVideo"></video>
    <video id="remoteVideo"></video>

    <button @click="joinRoom">Join Room</button>
    <button @click="leaveRoom">Leave Room</button>
    <button @click="makeOffer">Make Offer</button>
    <button @click="makeAnswer">Make Answer</button>
    <button @click="addCandidate">Add Candidate</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      localStream: null,
      remoteStream: null,
      pc: null,
      room: null,
      socket: null,
    }
  },

  methods: {
    joinRoom() {
      this.socket = new WebSocket('ws://localhost:3000');

      this.socket.onopen = () => {
        console.log('Connected to the signaling server');

        this.socket.send(JSON.stringify({
          type: 'join',
          room: 'room1',
        }));
      };

      this.socket.onmessage = (event) => {
        const data = JSON.parse(event.data);

        switch (data.type) {
          case 'offer':
            this.pc.setRemoteDescription(new RTCSessionDescription(data.offer));
            this.pc.createAnswer()
              .then(answer => this.pc.setLocalDescription(answer))
              .then(() => {
                this.socket.send(JSON.stringify({
                  type: 'answer',
                  answer: this.pc.localDescription,
                }));
              });
            break;

          case 'answer':
            this.pc.setRemoteDescription(new RTCSessionDescription(data.answer));
            break;

          case 'candidate':
            this.pc.addIceCandidate(new RTCIceCandidate(data.candidate));
            break;
        }
      };

      this.socket.onclose = () => {
        console.log('Disconnected from the signaling server');
      };

      this.pc = new RTCPeerConnection({
        iceServers: [
          {
            urls: 'stun:stun.l.google.com:19302',
          },
        ],
      });

      this.pc.onicecandidate = (event) => {
        if (event.candidate) {
          this.socket.send(JSON.stringify({
            type: 'candidate',
            candidate: event.candidate,
          }));
        }
      };

      this.pc.ontrack = (event) => {
        this.remoteStream = event.streams[0];
        this.$refs.remoteVideo.srcObject = this.remoteStream;
      };

      navigator.mediaDevices.getUserMedia({
        audio: true,
        video: true,
      }).then(stream => {
        this.localStream = stream;
        this.$refs.localVideo.srcObject = this.localStream;

        this.pc.addStream(this.localStream);
      });
    },

    leaveRoom() {
      this.socket.close();

      this.pc.close();

      this.localStream.getTracks().forEach(track => track.stop());
      this.remoteStream.getTracks().forEach(track => track.stop());

      this.localStream = null;
      this.remoteStream = null;
      this.pc = null;
      this.room = null;
      this.socket = null;
    },

    makeOffer() {
      this.pc.createOffer()
        .then(offer => this.pc.setLocalDescription(offer))
        .then(() => {
          this.socket.send(JSON.stringify({
            type: 'offer',
            offer: this.pc.localDescription,
          }));
        });
    },

    makeAnswer() {
      this.pc.createAnswer()
        .then(answer => this.pc.setLocalDescription(answer))
        .then(() => {
          this.socket.send(JSON.stringify({
            type: 'answer',
            answer: this.pc.localDescription,
          }));
        });
    },

    addCandidate() {
      this.pc.addIceCandidate(new RTCIceCandidate({
        candidate: 'candidate:1 1 udp 2122260223 192.168.1.101 54413 typ host generation 0 ufrag VGkF network-id 1 network-cost 10'
      }));
    },
  },
}