基于 WebRTC 实现仿微信音视频通话功能(含完整流程与代码)

30 阅读5分钟

最近利用摸鱼时间开发仿微信项目,其中核心模块之一便是音视频通话功能。该功能基于 WebRTC 技术实现点对点推流,配合 WebSocket 完成信令交互,目前已实现完整的呼叫、接听、挂断流程。因域名备案尚未完成,暂时通过 ngrok 内网穿透提供在线体验,带宽受限请谅解,访问时需允许访问 f09cebf3fbe0.ngrok-free.app 以正常加载资源。

Taimili 艾米莉 ( 一款专业的 GitHub star 管理和github 加星涨星工具taimili.com )

艾米莉 是一款优雅便捷的 GitHub star 管理和github 加星涨星工具,基于 PHP & javascript 构建, 能对github 得 star fork follow watch 管理和提升,最适合github 的深度用户

WX20251021-210346@2x.png

在线体验与源码地址

核心技术架构

  • 媒体传输:WebRTC(实现点对点音视频流传输)
  • 信令交互:WebSocket(传递呼叫请求、应答、ICE 候选等关键信息)
  • 网络穿透:STUN 服务器(stun:stun.l.google.com:19302,用于获取公网 IP 与端口,辅助 P2P 连接建立)

完整实现流程

WebRTC 音视频通话的核心流程可类比为 "面试沟通",分为发起方呼叫、接收方响应、连接建立三个关键阶段,以下是详细步骤与核心代码:

第一步:发起方呼叫流程(拨打电话)

发起方点击呼叫按钮后,需完成本地媒体流获取、PeerConnection 初始化、推流、Offer 生成与发送等操作,同时拉起等待接听界面。

javascript

运行

// 1. 获取本地音视频流
stream.value = await navigator.mediaDevices.getUserMedia({
  video: true, // 开启视频
  audio: true  // 开启音频
});

// 2. 初始化RTCPeerConnection(配置STUN服务器辅助穿透)
peerConnection.value = new RTCPeerConnection({
  iceServers: [
    { urls: 'stun:stun.l.google.com:19302' }
  ]
});

// 3. 将本地媒体流轨道添加到PeerConnection(推流给接收方)
stream.value.getTracks().forEach((track) => {
  peerConnection.value.addTrack(track, stream.value);
});

// 4. 监听接收方的媒体流(用于渲染对方画面/声音)
peerConnection.value.ontrack = (event) => {
  remoteStream.value = event.streams[0];
  // 根据通话类型(视频/音频)绑定对应的DOM元素
  if (callType.value === TypeVideo) {
    remoteVideo.value.srcObject = remoteStream.value;
  } else {
    remoteAudio.value.srcObject = remoteStream.value;
  }
};

// 5. 监听ICE候选(P2P连接的关键,获取网络地址信息并发送给接收方)
peerConnection.value.onicecandidate = (event) => {
  if (event.candidate) {
    ws.send(event.candidate); // 通过WebSocket发送ICE候选给接收方
  }
};

// 6. 创建Offer(呼叫凭证,用于接收方识别并捕获发起方流)
const offer = await peerConnection.value.createOffer();
await peerConnection.value.setLocalDescription(offer);

// 7. 发送Offer并更新界面状态
ws.send(offer); // 向接收方发送呼叫请求
showCall.value = true; // 拉起等待接听界面
callStatus.value = 'waiting'; // 状态:等待接听

第二步:接收方响应流程(来电处理)

接收方通过 WebSocket 收到 Offer 后,首先拉起来电界面,随后可选择 "接听" 或 "挂断" 操作。

1. 拉起来电界面

javascript

运行

showCall.value = true; // 显示来电接听弹窗
callStatus.value = 'coming'; // 状态:来电中
// 初始化来电人信息(如号码、昵称等)
initCallerInfo();

2. 挂断操作

接收方挂断后,通过 WebSocket 通知发起方,双方均关闭连接、停止媒体流并清理 DOM。

javascript

运行

// 接收方挂断逻辑
showCall.value = false;
callStatus.value = 'closing';
ws.send('reject'); // 通知发起方已挂断

// 发起方收到挂断通知后的清理逻辑
if (peerConnection.value) {
  peerConnection.value.close(); // 关闭PeerConnection
  peerConnection.value = null;
}
if (stream.value) {
  const tracks = stream.value.getTracks();
  tracks.forEach((track) => track.stop()); // 停止本地媒体流
}
// 清空DOM绑定的流对象
if (localVideo.value) localVideo.value.srcObject = null;
if (remoteVideo.value) remoteVideo.value.srcObject = null;
if (remoteAudio.value) remoteAudio.value.srcObject = null;
showCall.value = false;
callStatus.value = 'closing';

3. 接听操作

接收方接听后,流程与发起方呼叫类似,需完成媒体流初始化、远端 SDP 设置、ICE 候选处理、Answer 生成与发送。

javascript

运行

// 1. 获取本地媒体流(同发起方逻辑)
// 2. 初始化PeerConnection(同发起方逻辑)
// 3. 推流给发起方(同发起方逻辑)
// 4. 监听发起方媒体流(同发起方逻辑)
// 5. 监听ICE候选(同发起方逻辑)

// 6. 设置远端SDP(使用发起方发送的Offer)
await peerConnection.value.setRemoteDescription(
  new RTCSessionDescription(caller.value.offer)
);

// 7. 处理ICE候选(需等待远端SDP初始化完成后再添加)
iceCandidateQueue.value.forEach(async (candidate) => {
  await peerConnection.value.addIceCandidate(candidate);
});
iceCandidateQueue.value = []; // 清空候选队列

// 8. 创建Answer(应答凭证,告知发起方已接听)
const answer = await peerConnection.value.createAnswer();
await peerConnection.value.setLocalDescription(answer);

// 9. 发送Answer并更新状态
ws.send(answer); // 向发起方发送应答
callStatus.value = 'calling'; // 状态:通话中

关键补充:ICE 候选处理逻辑

ICE 候选包含双方的网络地址信息,是 P2P 连接建立的核心。需判断远端 SDP 是否初始化完成,未完成则暂存到队列中。

javascript

运行

// 处理新收到的ICE候选
const handleNewICECandidate = async (candidate) => {
  const iceCandidate = new RTCIceCandidate(candidate);
  // 若远端SDP已初始化(have-remote-offer/stable状态),直接添加候选
  if (peerConnection.value?.signalingState === 'have-remote-offer' || 
      peerConnection.value?.signalingState === 'stable') {
    await peerConnection.value.addIceCandidate(iceCandidate);
  } else {
    // 未初始化则存入队列,后续统一处理
    iceCandidateQueue.value.push(iceCandidate);
  }
};

第三步:发起方确认应答(建立通话)

发起方收到接收方的 Answer 后,设置远端 SDP 与 ICE 候选,完成 P2P 连接建立,进入通话状态。

javascript

运行

// 1. 设置远端SDP(使用接收方发送的Answer)
await peerConnection.value.setRemoteDescription(
  new RTCSessionDescription(caller.value.answer)
);

// 2. 添加接收方发送的ICE候选
iceCandidateQueue.value.forEach(async (candidate) => {
  await peerConnection.value.addIceCandidate(candidate);
});
iceCandidateQueue.value = [];

// 3. 更新状态为通话中
callStatus.value = 'calling';

功能实现效果

(此处附项目实现效果图,原文章配图:1733397596986.jpg、1733397612578.jpg,展示呼叫界面、通话界面等核心场景)

通过以上流程,即可完成基于 WebRTC 的点对点音视频通话功能,核心在于 PeerConnection 的初始化与状态管理、ICE 候选的正确处理、以及 WebSocket 信令的及时交互。该方案无需依赖第三方音视频服务,具备低延迟、高隐私性的特点,可直接集成到 Web 端应用中。