WebRTC 音视频会议实现指南:从基础到实践​

卷心菜

WebRTC(Web Real-Time Communication)作为浏览器原生支持的实时通信技术,已成为构建音视频会议系统的核心方案。它无需插件即可实现浏览器间的点对点(P2P)音视频流传输,配合信令服务和媒体服务器,可轻松搭建支持多人参与的会议场景。本文将从技术原理到实战步骤,全面讲解如何使用 WebRTC 实现音视频会议。
一、WebRTC 核心技术基础
在开始实现前,需先理解 WebRTC 的三大核心组件和工作流程,这是构建会议系统的技术基石。

  1. 核心 API 与组件
    WebRTC 提供了三组核心 API,分别负责媒体捕获、P2P 连接和数据传输:
    MediaStream(媒体流):通过getUserMedia()或getDisplayMedia()获取设备摄像头、麦克风数据(音视频流),或捕获屏幕内容(用于屏幕共享)。
    RTCPeerConnection(对等连接):浏览器间建立 P2P 连接的核心,负责媒体流的编码、传输和解码,支持 NAT 穿透(通过 ICE 协议)和带宽自适应。
    RTCDataChannel(数据通道):在 P2P 连接基础上传输非媒体数据(如会议控制指令、文字消息),延迟低且支持可靠传输。
  2. 关键协议与概念
    SDP(Session Description Protocol):描述媒体会话的元数据(如编码格式、传输协议、网络地址),用于浏览器间协商通信参数(例如 A 端发送offer,B 端回复answer)。
    ICE(Interactive Connectivity Establishment):解决 NAT 穿透问题的协议,通过 STUN 服务器获取公网 IP,或通过 TURN 服务器转发流量(当 P2P 连接失败时)。
    信令服务(Signaling Server):WebRTC 本身不处理信令,需额外搭建信令服务(如基于 WebSocket),用于传递 SDP 消息、ICE 候选地址,以及管理会议成员(加入 / 离开)。
    二、音视频会议实现步骤(基于浏览器 + Node.js)
    本节以 “2 人基础会议” 为例,逐步讲解从环境搭建到功能实现的完整流程,后续可扩展至多人场景。
  3. 环境准备
    前端:支持 WebRTC 的现代浏览器(Chrome、Firefox、Edge),使用navigator.mediaDevicesAPI 获取媒体流,通过RTCPeerConnection建立连接。
    后端:Node.js + WebSocket(推荐ws库),搭建信令服务器,负责转发客户端间的信令消息。
    依赖工具:STUN 服务器(推荐使用 Google 公共 STUN:stun:stun.l.google.com:19302),测试阶段可无需自建;多人场景需额外部署 TURN 服务器(如coturn)。
  4. 步骤 1:搭建信令服务器(Node.js + ws)
    信令服务器的核心作用是 “转发消息”,不处理媒体流。以下是简化版实现代码:
// server.js(Node.js)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 }); // 监听8080端口

// 存储所有连接的客户端
const clients = new Set();

wss.on('connection', (ws) => {
  console.log('新客户端连接');
  clients.add(ws);

  // 接收客户端消息并转发给其他所有客户端
  ws.on('message', (message) => {
    console.log(`收到消息:${message}`);
    clients.forEach((client) => {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(message); // 转发消息
      }
    });
  });

  // 客户端断开连接时移除
  ws.on('close', () => {
    console.log('客户端断开连接');
    clients.delete(ws);
  });
});
console.log('信令服务器已启动:ws://localhost:8080');

启动服务器:node server.js,此时服务器可接收客户端连接并转发消息。

  1. 步骤 2:前端实现(媒体流获取 + P2P 连接)
    前端需完成三个核心功能:获取本地媒体流、发送 / 接收信令、建立 P2P 连接并传输音视频。以下是完整 HTML+JS 代码(index.html):
<!DOCTYPE html>
<html>
<head>
  <title>WebRTC音视频会议</title>
  <style>
    .video-container { display: flex; gap: 20px; margin: 20px; }
    video { width: 400px; height: 300px; border: 2px solid #333; }
    button { padding: 10px 20px; font-size: 16px; margin: 0 10px; }
  </style>
</head>
<body>
  <div class="video-container">
    <!-- 本地视频流 -->
    <div>
      <h3>本地画面</h3>
      <video id="localVideo" autoplay muted></video>
    </div>
    <!-- 远程视频流 -->
    <div>
      <h3>远程画面</h3>
      <video id="remoteVideo" autoplay></video>
    </div>
  </div>
  <button id="startBtn">开始会议</button>
  <button id="callBtn" disabled>发起呼叫</button>
  <button id="hangupBtn" disabled>结束会议</button>

  <script>
    // 1. 初始化变量
    const localVideo = document.getElementById('localVideo');
    const remoteVideo = document.getElementById('remoteVideo');
    const startBtn = document.getElementById('startBtn');
    const callBtn = document.getElementById('callBtn');
    const hangupBtn = document.getElementById('hangupBtn');

    let localStream; // 本地媒体流
    let peerConnection; // RTCPeerConnection实例
    const ws = new WebSocket('ws://localhost:8080'); // 连接信令服务器

    // 2. 配置ICE服务器(使用Google公共STUN)
    const iceConfig = {
      iceServers: [
        { urls: 'stun:stun.l.google.com:19302' }
      ]
    };

    // 3. 开始会议:获取本地媒体流
    startBtn.addEventListener('click', async () => {
      try {
        // 获取摄像头和麦克风流(video: true表示开启视频,audio: true表示开启音频)
        localStream = await navigator.mediaDevices.getUserMedia({
          video: { width: 1280, height: 720 }, // 配置视频分辨率
          audio: true
        });
        localVideo.srcObject = localStream; // 显示本地视频
        startBtn.disabled = true;
        callBtn.disabled = false;
        hangupBtn.disabled = false;
      } catch (err) {
        console.error('获取媒体流失败:', err);
        alert('请允许浏览器访问摄像头和麦克风!');
      }
    });

    // 4. 发起呼叫:创建RTCPeerConnection并发送offer
    callBtn.addEventListener('click', async () => {
      // 创建RTCPeerConnection实例
      peerConnection = new RTCPeerConnection(iceConfig);

      // 监听ICE候选地址,发送给远程端
      peerConnection.addEventListener('icecandidate', (e) => {
        if (e.candidate) {
          // 发送ICE候选(信令消息格式:JSON)
          ws.send(JSON.stringify({ type: 'ice', candidate: e.candidate }));
        }
      });

      // 监听远程媒体流,显示到remoteVideo
      peerConnection.addEventListener('track', (e) => {
        remoteVideo.srcObject = e.streams[0];
      });

      // 将本地媒体流添加到PeerConnection
      localStream.getTracks().forEach((track) => {
        peerConnection.addTrack(track, localStream);
      });

      // 创建offer(SDP请求)
      const offer = await peerConnection.createOffer();
      await peerConnection.setLocalDescription(offer); // 设置本地SDP

      // 发送offer到远程端(通过信令服务器)
      ws.send(JSON.stringify({ type: 'offer', sdp: offer.sdp }));
      callBtn.disabled = true;
    });

    // 5. 接收信令消息(处理offer、answer、ice)
    ws.addEventListener('message', async (e) => {
      const message = JSON.parse(e.data);
      switch (message.type) {
        case 'offer':
          // 收到offer,创建PeerConnection并回复answer
          peerConnection = new RTCPeerConnection(iceConfig);

          // 监听ICE候选并发送
          peerConnection.addEventListener('icecandidate', (e) => {
            if (e.candidate) {
              ws.send(JSON.stringify({ type: 'ice', candidate: e.candidate }));
            }
          });

          // 监听远程媒体流
          peerConnection.addEventListener('track', (e) => {
            remoteVideo.srcObject = e.streams[0];
          });

          // 添加本地媒体流
          localStream.getTracks().forEach((track) => {
            peerConnection.addTrack(track, localStream);
          });

          // 设置远程SDP(offer)并创建answer
          await peerConnection.setRemoteDescription(new RTCSessionDescription({
            type: 'offer',
            sdp: message.sdp
          }));
          const answer = await peerConnection.createAnswer();
          await peerConnection.setLocalDescription(answer);

          // 发送answer到发起端
          ws.send(JSON.stringify({ type: 'answer', sdp: answer.sdp }));
          break;

        case 'answer':
          // 收到answer,设置远程SDP
          await peerConnection.setRemoteDescription(new RTCSessionDescription({
            type: 'answer',
            sdp: message.sdp
          }));
          break;

        case 'ice':
          // 收到ICE候选,添加到PeerConnection
          await peerConnection.addIceCandidate(new RTCIceCandidate(message.candidate));
          break;

        default:
          console.log('未知信令类型:', message.type);
      }
    });

    // 6. 结束会议:关闭连接并停止媒体流
    hangupBtn.addEventListener('click', () => {
      // 关闭PeerConnection
      if (peerConnection) {
        peerConnection.close();
        peerConnection = null;
      }
      // 停止本地媒体流
      localStream.getTracks().forEach((track) => track.stop());
      localVideo.srcObject = null;
      remoteVideo.srcObject = null;
      // 重置按钮状态
      startBtn.disabled = false;
      callBtn.disabled = true;
      hangupBtn.disabled = true;
    });
  </script>
</body>
</html>
  1. 测试 2 人会议
    启动信令服务器:node server.js;
    用两个浏览器窗口打开index.html(如 Chrome 的两个标签页,或 Chrome+Firefox);
    两个窗口均点击 “开始会议”,允许浏览器访问摄像头和麦克风;
    在其中一个窗口点击 “发起呼叫”,另一个窗口会自动接收并显示远程视频,此时 2 人音视频会议已建立。

三、扩展到多人音视频会议
上述 2 人方案仅适用于点对点场景,若需支持 3 人及以上会议,需解决P2P 连接数量爆炸(N 人需 N*(N-1)/2 个连接)和带宽消耗过大的问题,此时需引入媒体服务器(SFU)。

  1. 多人会议核心方案:SFU(Selective Forwarding Unit)
    SFU 是一种轻量级媒体服务器,工作原理如下:
    每个参会者将自己的媒体流发送到 SFU;
    SFU 根据参会者列表,将媒体流转发给其他所有成员;
    参会者只需接收 N-1 路流(而非发送 N-1 路),大幅减少带宽和连接数。
  2. 常用 SFU 框架推荐
    Mediasoup:开源 SFU 库,支持 WebRTC 协议,可集成到 Node.js/Java 项目,支持屏幕共享、混音等功能,文档完善且社区活跃;
    Janus:C 语言编写的开源媒体服务器,支持 WebRTC、RTSP 等多种协议,适合复杂场景(如直播 + 会议结合);
    Pion WebRTC:Go 语言实现的 WebRTC 库,可快速构建轻量级 SFU,性能优异。
  3. 多人会议实现思路(以 Mediasoup 为例)
    部署 Mediasoup SFU 服务器:基于 Node.js 启动 Mediasoup 服务,配置 Worker(媒体处理进程)、Router(路由)和 Transport(传输通道);
    前端与 SFU 交互:
    客户端通过信令服务请求加入会议,获取 SFU 的 Transport 信息;
    客户端与 SFU 建立 WebRTC 连接(Producer角色,发送本地流到 SFU);
    客户端向 SFU 请求订阅其他参会者的流(Consumer角色,接收 SFU 转发的远程流);
    信令服务扩展:增加 “会议房间管理” 功能(创建房间、加入房间、离开房间),同步参会者列表和流状态。
    四、进阶优化与注意事项
  4. 提升会议质量的优化手段
    带宽自适应:通过RTCPeerConnection的addTransceiver()配置direction和bandwidth参数,或使用 SFU 的动态码率调整(如根据网络延迟调整视频分辨率);
    回声消除与降噪:WebRTC 内置基础回声消除,但复杂场景需结合第三方库(如WebRTC Audio Processing);
    屏幕共享:通过navigator.mediaDevices.getDisplayMedia({ video: true })获取屏幕流,与摄像头流切换逻辑结合;
    断线重连:监听RTCPeerConnection的connectionstatechange事件,当状态变为disconnected时,通过信令服务重新建立连接。
  5. 兼容性与部署注意事项
    浏览器兼容性:WebRTC 在主流浏览器中支持良好,但需注意部分 API 差异(如 Safari 需开启getUserMedia权限),可通过caniuse.com查询最新支持情况;
    HTTPS 与localhost例外:浏览器要求 WebRTC 仅在 HTTPS 环境下工作(localhost除外,适合开发测试),生产环境需部署 SSL 证书;
    TURN 服务器必要性:当参会者处于严格 NAT 环境(如企业内网)时,P2P 连接可能失败,需部署 TURN 服务器(如coturn)转发流量,确保通信稳定性。
    五、总结
    WebRTC 为音视频会议提供了浏览器原生的实时通信能力,从 2 人 P2P 场景到多人 SFU 场景,均可基于其核心 API 扩展实现。核心步骤可归纳为:
    搭建信令服务,实现客户端间的消息转发;
    前端通过getUserMedia获取媒体流,通过RTCPeerConnection建立 P2P 连接;
    多人场景引入 SFU 服务器,解决连接数和带宽问题;
    结合优化手段(如带宽自适应、TURN 服务器)提升会议稳定性和体验。
    通过本文的基础方案和扩展思路,开发者可快速搭建符合需求的音视频会议系统,后续可进一步集成聊天、文件共享、会议录制等功能,构建完整的协作平台。

了解了webrtc的基本原理后,我开发了一款多人实时音视频会议软件,给大家体验: https://meeting.codeemo.cn
截图
截图

342 2 6
2个评论

hhw929

点个赞

  • 暂无评论
wz_8849

社区有个打包桌面应用的,不知道2者结合会怎样

  • 卷心菜 14天前

    这个开个网页就能用,何必搞个桌面端,用户还需要安装,增加使用成本。

  • wz_8849 14天前

    做成客户端,或许可以 调用win系统自动化方面的 实现 远程控制。

  • 卷心菜 14天前

    哦哦,回头研究下

  • wz_8849 14天前

    我觉得纯音频能支持更多人,这是我问ai的:
    纯音频 + Mesh 模式 (不用SFU):
    主播端在电脑浏览器上,大约能稳定承载 20~30 听众。
    如果主播带宽特别好(光纤 >50 Mbps 上行),理论上能撑 50 听众左右,但风险大
    就是 2-3个主播,主播维护着所有听众们的Peer链接。听众不用获取本地流,只播放主播的流。

  • 卷心菜 14天前

    嗯,确实是。现在已经支持了只开麦克风。

  • 卷心菜 14天前

    不过现在打洞成功率很低,所以还需要中转服务器,中转服务器也需要对应的带宽

卷心菜

330
积分
0
获赞数
0
粉丝数
2025-04-25 加入
🔝