概述
本项目实现了一个基于WebRTC技术的语音通话系统,用于终端机与pos-web后台系统之间的实时语音通信。该系统由前端和后端两部分组成,前端负责用户界面和音视频捕捉,后端负责信令服务器的搭建和管理通信过程中的逻辑。整个项目由独立的Node.js服务器作为信令服务器,并结合PeerJS实现WebRTC连接。
技术选型
1. 基于 WebRTC 的自建方案
- WebRTC (Web Real-Time Communication)
-
- 特点: 提供点对点的实时语音、视频和数据传输。完全开源,不依赖第三方服务。
- 优点: 高度可定制化,没有使用限制或费用。适合对通话质量和延迟要求较高的场景。
- 缺点: 需要自己实现信令服务器和 NAT 穿透等复杂功能。对网络条件的处理能力较弱,开发难度较高。
- 技术栈
-
- 前端:使用 WebRTC 的 API(如
RTCPeerConnection、getUserMedia等) - 后端:使用信令服务器(如 Socket.io、PeerJS 服务器)来进行会话协商
- 前端:使用 WebRTC 的 API(如
- 推荐库和工具
-
- PeerJS: 简化 WebRTC 连接的 JavaScript 库
- SimpleWebRTC: 一个简单易用的 WebRTC 库
2. 第三方实时音视频 SDK
-
Agora.io(声网)
- 特点: 提供高质量的音视频通话、直播、消息传递等功能。API 丰富,覆盖多种平台。
- 优点: 快速集成,高稳定性,拥有全球节点和优质的 QoS 服务。
- 缺点: 需要购买服务,部分功能可能有使用限制。
- 适用场景: 语音通话、视频会议、在线教育、语音聊天室等。
-
腾讯云、阿里云音视频通话服务
- 特点: 针对国内市场优化,提供丰富的音视频通话功能。
- 优点: 在国内有很好的网络覆盖和服务质量,易于集成。
- 缺点: 国内市场为主,国外服务可能略差。
-
Twilio
- 特点: 全球范围内的通讯平台服务,提供短信、语音、视频等多种通讯服务。
- 优点: API 简单易用,文档丰富,支持多种编程语言。
- 缺点: 费用较高,部分地区服务稳定性可能欠佳。
-
Vonage (原 Nexmo)
- 特点: 提供语音、视频、短信和电话验证等通讯 API。
- 优点: 简单易用,支持多种通讯方式。
- 缺点: 费用较高,可能需要国际化服务时进行选择。
3. 选型考虑因素
-
项目需求
- 是否需要支持多端(Web、移动端、桌面端)。
- 是否需要支持全球范围内的高质量通话。
- 局域网内通话的可行性
-
开发周期和成本
- 自建方案开发周期长,但无长期费用。
- 第三方 SDK 集成快,但有长期费用。
-
稳定性和可扩展性
- 需要考虑网络抖动、延迟、丢包等因素对通话质量的影响。
-
隐私和数据安全
- 自建方案可完全控制数据流向。
- 第三方方案需评估数据安全和隐私政策。
项目架构
1. 前端架构
-
框架/库: React、peerjs@1.3.1
-
功能模块:
- 通话界面: 包含通话按钮、挂断按钮、通话状态显示等。
- 音视频捕捉: 使用
navigator.mediaDevices.getUserMedia获取本地音频流。 - WebRTC连接管理: 使用PeerJS管理Peer-to-Peer连接,处理呼叫、应答、挂断等事件。
- 用户交互: 通过事件处理用户的通话操作,例如接听、挂断等。
2. 后端架构
-
框架/库: Nest.js、Express、peerjs-server、WebSocket、SSE(Server-Sent Events)
-
功能模块:
- 信令服务器: 通过peerjs-server搭建信令服务器,管理用户的连接请求,维护客户端的连接状态。
- 音视频数据传输: 信令服务器通过PeerJS将用户之间的音视频数据传递给相应的对端。
- 实时消息推送: 使用SSE向客户端推送系统状态、通话信息等实时消息。
- 日志管理: 记录系统日志,用于调试和系统维护。
- 管理员状态管理: 维护管理员在线、忙碌、离线、等待状态。
系统流程
-
通话初始化:
- 终端用户通过点击“呼叫”按钮发起呼叫请求。
- 前端调用PeerJS的
peer.call()方法发起通话请求,信令服务器通过WebSocket向目标终端传递通话请求。
-
信令交换:
- 信令服务器在接收到通话请求后,向目标终端发送呼叫请求信息。
- 目标终端通过点击“接听”按钮调用
peer.answer()方法,接听通话。
-
音视频传输:
- 通话建立后,双方开始通过WebRTC传输音频数据。
- 终端用户和POS后台可以进行实时语音对话。
-
通话结束:
- 用户点击“挂断”按钮,调用
peer.disconnect()方法,通话结束。 - 信令服务器通知双方断开连接。
- 用户点击“挂断”按钮,调用
-
实时消息推送:
- 后端通过SSE推送通话状态、系统消息到前端,前端根据消息更新界面。
开发与调试
关键技术点
-
WebRTC:
- 主要用于点对点音视频通信。
- 通过
navigator.mediaDevices.getUserMedia获取音频流,并将流对象传递给PeerJS进行连接。
-
PeerJS:
- 简化了WebRTC的信令交换过程,提供简单易用的API来管理Peer-to-Peer连接。
- 使用
peer.call()和peer.answer()方法进行呼叫和应答。
-
Node信令服务器:
- 通过
peerjs库搭建信令服务器,负责管理通话过程中的信令交换。 - 使用WebSocket管理用户的连接状态,并通过SSE向客户端推送实时消息。
- 通过
-
SSE(Server-Sent Events) :
- 用于后端向前端推送实时消息,例如通话状态、系统提示等。
- 前端使用EventSource API接收来自后端的实时消息流。
-
测试通话功能:
- 模拟两个用户终端,分别在不同的浏览器或设备上打开前端页面。
- 发起通话请求,检查通话是否正常连接、声音是否清晰。
-
信令服务器调试:
- 使用
console.log或winston等日志工具记录信令服务器的各种事件。 - 检查WebSocket连接状态,确保信令交换过程顺畅。
- 使用
-
SSE消息调试:
- 浏览器访问 test.xxx.com/peerlog
连接服务器发起呼叫
const localPeerId = 'test-id'
// 自己搭建的信令服务器
const peerInstance = new Peer(localPeerId, {
host: '127.0.0.1', // 你的 PeerJS 服务器地址
port: 5501, // 服务器端口
path: '/', // 服务器路径
secure: true // 如果是 https 连接则设置为 true
});
// 也可以使用官方免费的服务器
const peerInstance = new Peer(localPeerId);
呼叫方(终端机代码)
import React, { useEffect, useRef, useState } from 'react';
import Peer from 'peerjs';
const Caller = () => {
const [peer, setPeer] = useState(null);
const [remotePeerId, setRemotePeerId] = useState('');
const localStreamRef = useRef();
const remoteAudioRef = useRef();
const [call, setCall] = useState(null);
const [isCalling, setIsCalling] = useState(false);
useEffect(() => {
// 初始化 Peer 实例
const peerId = 'caller-peer-id'; // 呼叫方的自定义 Peer ID
const peerInstance = new Peer(peerId, {
host: 'your-peerjs-server-host', // 你的 PeerJS 服务器地址
port: 5501, // 服务器端口
path: '/', // 服务器路径
secure: true // 如果是 https 连接则设置为 true
});
peerInstance.on('open', (id) => {
console.log('Caller Peer ID: ' + id);
setPeer(peerInstance);
});
peerInstance.on('call', (incomingCall) => {
console.log('呼叫方不处理接听逻辑');
});
return () => {
peerInstance.destroy();
};
}, []);
const startCall = () => {
if (!remotePeerId) {
alert('请输入接收方的 Peer ID');
return;
}
navigator.mediaDevices.getUserMedia({ video: false, audio: true })
.then((stream) => {
localStreamRef.current = stream; // 保存本地音频流
const outgoingCall = peer.call(remotePeerId, stream); // 发起呼叫
setIsCalling(true);
outgoingCall.on('stream', (remoteStream) => {
remoteAudioRef.current.srcObject = remoteStream; // 播放接收方的音频流
});
setCall(outgoingCall);
})
.catch((err) => {
console.error('获取本地音频流失败:', err);
});
};
const endCall = () => {
if (call) {
call.close();
setCall(null);
setIsCalling(false);
}
};
return (
<div>
<h2>呼叫方</h2>
<div>
<label>接收方 Peer ID: </label>
<input
type="text"
value={remotePeerId}
onChange={(e) => setRemotePeerId(e.target.value)}
/>
<button onClick={startCall} disabled={isCalling}>呼叫</button>
<button onClick={endCall} disabled={!isCalling}>挂断</button>
</div>
<div>
<h3>远程音频</h3>
<audio ref={remoteAudioRef} controls autoPlay />
</div>
</div>
);
};
export default Caller;
接听方(POS代码)
import React, { useEffect, useRef, useState } from 'react';
import Peer from 'peerjs';
const Receiver = () => {
const [peer, setPeer] = useState(null);
const localAudioRef = useRef();
const remoteAudioRef = useRef();
const [call, setCall] = useState(null);
useEffect(() => {
// 初始化 Peer 实例
const peerId = 'receiver-peer-id'; // 接听方的自定义 Peer ID
const peerInstance = new Peer(peerId, {
host: '127.0.0.1', // 你的 PeerJS 服务器地址
port: 5501, // 服务器端口 https 默认443端口
path: '/', // 服务器路径
secure: true // 如果是 https 连接则设置为 true port默认443
});
peerInstance.on('open', (id) => {
console.log('Receiver Peer ID: ' + id);
setPeer(peerInstance);
});
peerInstance.on('call', (incomingCall) => {
navigator.mediaDevices.getUserMedia({ video: false, audio: true })
.then((stream) => {
localAudioRef.current.srcObject = stream; // 播放本地音频流
incomingCall.answer(stream); // 接听呼叫
incomingCall.on('stream', (remoteStream) => {
remoteAudioRef.current.srcObject = remoteStream; // 播放呼叫方的音频流
});
setCall(incomingCall);
})
.catch((err) => {
console.error('获取本地音频流失败:', err);
});
});
return () => {
peerInstance.destroy();
};
}, []);
const endCall = () => {
if (call) {
call.close();
setCall(null);
}
};
return (
<div>
<h2>接听方</h2>
<div>
<h3>本地音频</h3>
<audio ref={localAudioRef} controls autoPlay muted />
</div>
<div>
<h3>远程音频</h3>
<audio ref={remoteAudioRef} controls autoPlay />
</div>
<button onClick={endCall} disabled={!call}>挂断</button>
</div>
);
};
export default Receiver;
信令服务器代码(极简版)
const express = require("express");
const { ExpressPeerServer } = require("peer");
const app = express();
app.enable("trust proxy");
const PORT = process.env.PORT || 9877;
const server = app.listen(PORT, () => {
console.log(`App listening on port ${PORT}`);
});
app.get("/", (req, res) => {
console.log("Hello 音视频通话测试!");
res.json({
name: "测试",
description: "音视频通话测试 https证书 域名验证 终端机环境验证",
website: "https://www.xxxx.com",
});
});
const peerServer = ExpressPeerServer(server, {
path: "/",
key: 'CIMC_PEER',
});
app.use("/", peerServer);
module.exports = app;
部署与配置
前端部署:
- 将打包后的静态文件部署到Web服务器。
- 配置Nginx或其他Web服务器进行静态资源托管。
后端部署:
- 使用Node.js搭建信令服务器,监听特定端口(如5501)。
- k8s部署。
- 如果使用HTTPS,需要配置SSL证书。
Nginx配置:
- 配置Nginx进行反向代理,将WebSocket和SSE请求转发到Node.js服务器。
- 例如:
location /VoIP/ {
rewrite ^/VoIP/(.*) /$1 break;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://parking-terminal-server:5501/;
}
location /peerlog {
# 保持连接以支持 Server-Sent Events
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 代理到 PeerJS 服务器上的 SSE 端点
proxy_pass http://parking-terminal-server:5501/sse/events;
# 设置 SSE 所需的特定头部
proxy_buffering off; # 禁用缓冲
proxy_cache off; # 禁用缓存
proxy_http_version 1.1; # 使用 HTTP 1.1 保持连接
proxy_set_header Connection keep-alive; # 保持连接以支持 SSE
add_header Cache-Control no-cache; # 禁用缓存
}
常见问题
-
音频流获取失败:
- 检查浏览器是否授予麦克风权限。
- 确保HTTPS协议被正确使用。
-
WebSocket连接失败:
- 确保信令服务器和前端WebSocket URL一致。
- 检查Nginx的WebSocket代理配置是否正确。
-
SSE消息丢失或无法接收:
- 检查Nginx的SSE代理配置是否正确,确保没有缓存。
- 确保前端EventSource没有被意外关闭。
-
信令超时问题:
- P2P的点对点模式在发起请求收短时间内没有接听
-
https相关问题
- 信令服务器是https而客户端使用http访问会出现网络异常
- 获取音视频权限必须使用https,node服务正常开发,上线的时候在nginx层配置处理
-
信令超时问题
结语
本系统为终端机器与POS后台提供了稳定的语音通话解决方案,采用WebRTC和Node.js信令服务器相结合的技术栈实现了实时通信和消息推送功能。未来可以进一步扩展视频通话、多终端会议等功能,为用户提供更加丰富的应用场景。