基于SIP.js + FreeSWITCH实现浏览器语音通话:从零搭建完整指南
·
本文提供一套完整的实操方案,帮助你在一台Windows系统的电脑上完成 FreeSWITCH 的本地安装与配置,并通过两个浏览器页面实现语音通话。
1. 本地安装与配置FreeSWITCH
2. 基于SIP.js实现软电话

2.1 sip.js
下载地址:https://github.com/onsip/SIP.js/releases
2.2 index.html
<!DOCTYPE html>
<html>
<head>
<title>软电话</title>
<link rel="stylesheet" href="css/index.css">
</head>
<body>
<div class="status">
状态: <span id="status" class="unregistered">未注册</span>
</div>
<div>
<input id="extension" placeholder="分机 (如 1001)" value="1001">
<button id="registerBtn">注册</button>
<button id="unregisterBtn" disabled>注销</button>
</div>
<div>
<input id="target" placeholder="目标分机 (如 1002)">
<button id="callBtn" disabled>呼叫</button>
<button id="hangupBtn" disabled>挂断</button>
</div>
<div>
<h3>通话日志:</h3>
<div id="log" class="log"></div>
</div>
<audio id="remoteAudio" autoplay></audio>
<script src="js/sip-0.21.2.js"></script>
<script src="js/index.js"></script>
</body>
</html>
2.3 index.css
body {
font-family: Arial;
padding: 10px;
}
input {
height: 30px;
margin-bottom: 10px;
}
button {
padding: 4px 20px;
margin: 0 5px;
font-size: 16px;
cursor: pointer;
}
.status {
padding: 10px;
margin-bottom: 20px;
background: #f0f0f0;
border-radius: 5px;
}
.log {
height: 240px;
padding: 10px;
overflow-y: auto;
border: 1px solid #ccc;
}
.registered {
color: green;
font-weight: bold;
}
.unregistered {
color: red;
font-weight: bold;
}
2.4 index.js
const { UserAgent, Registerer, Inviter, SessionState } = SIP;
// DOM 元素
const statusSpan = document.getElementById('status');
const registerBtn = document.getElementById('registerBtn');
const unregisterBtn = document.getElementById('unregisterBtn');
const callBtn = document.getElementById('callBtn');
const hangupBtn = document.getElementById('hangupBtn');
const extensionInput = document.getElementById('extension');
const targetInput = document.getElementById('target');
const logDiv = document.getElementById('log');
// 配置信息
const domain = '你的IP'; // 局域网 IP
const wsServer = `ws://${domain}:5066`; // WebSocket 服务器地址
const password = '1234'; // 密码
let userAgent = null;
let registerer = null;
let currentSession = null;
function addLog(msg) {
const time = new Date().toLocaleTimeString();
logDiv.innerHTML += `<div>[${time}] ${msg}</div>`;
logDiv.scrollTop = logDiv.scrollHeight;
}
function updateStatus(registered, inCall = false) {
if (registered) {
statusSpan.textContent = inCall ? '通话中' : '已注册';
statusSpan.className = 'registered';
} else {
statusSpan.textContent = '未注册';
statusSpan.className = 'unregistered';
}
registerBtn.disabled = !!registered;
unregisterBtn.disabled = !registered;
callBtn.disabled = !registered || inCall;
hangupBtn.disabled = !registered || !inCall;
}
async function startAndRegister() {
try {
// 当前分机
const extension = extensionInput.value.trim();
if (!extension) {
addLog('请输入当前分机号');
return;
}
addLog('创建 SIP URI...');
const uri = UserAgent.makeURI(`sip:${extension}@${domain}`);
if (!uri) throw new Error('创建 SIP URI 失败');
addLog(`URI: ${uri.toString()}`);
addLog(`WebSocket 服务器: ${wsServer}`);
// UserAgent 配置
const userAgentOptions = {
uri: uri,
authorizationUsername: extension,
authorizationPassword: password,
transportOptions: { server: wsServer },
displayName: `Extension ${extension}`,
logLevel: 'debug',
delegate: {
// 连接建立的回调
onConnect: () => {
console.log('✅ WebSocket 连接成功');
},
// 连接断开的回调
onDisconnect: (error) => {
console.log(`❌ WebSocket 断开: ${error?.message || '未知原因'}`);
},
// 注册失败的回调
onRegistrationFailed: (response, cause) => {
console.error(`❌ 注册失败: ${cause}`);
if (response) {
console.error(`响应码: ${response.statusCode}`);
}
}
}
};
addLog('创建 UserAgent...');
userAgent = new UserAgent(userAgentOptions);
addLog('创建 Registerer...');
registerer = new Registerer(userAgent);
// 监听注册状态
registerer.stateChange.addListener((newState) => {
addLog(`注册状态变更: ${newState}`);
if (newState === 'Registered') {
addLog('✅✅✅ SIP 注册成功!✅✅✅');
updateStatus(true, false);
} else if (newState === 'Unregistered') {
addLog('❌ SIP 已注销');
updateStatus(false, false);
}
});
// 监听来电
userAgent.delegate = {
onInvite: (invitation) => {
addLog(`📞 收到来电: ${invitation.request.from.toString()}`);
currentSession = invitation;
// 自动接听(实际应用中可弹窗确认)
invitation.accept().then(() => {
addLog('✅ 已接听来电');
// 获取远端音频流
const sessionDescHandler = invitation.sessionDescriptionHandler;
if (sessionDescHandler && sessionDescHandler.remoteMediaStream) {
remoteAudio.srcObject = sessionDescHandler.remoteMediaStream;
remoteAudio.play().catch(e => console.log('播放失败', e));
updateStatus(true, true);
}
}).catch((error) => {
addLog(`接听失败: ${error.message}`);
});
}
};
// 启动
addLog('启动 UserAgent...');
await userAgent.start();
addLog('UserAgent 已启动');
// 注册
addLog('发送注册请求...');
await registerer.register();
addLog('注册请求已发送,等待响应...');
} catch (error) {
addLog(`❌ 错误: ${error.message}`);
console.error(error);
}
}
async function unregisterAndStop() {
try {
addLog('正在注销...');
if (registerer) {
await registerer.unregister();
addLog('注销成功');
}
if (userAgent) {
await userAgent.stop();
addLog('UserAgent 已停止');
}
updateStatus(false, false);
} catch (error) {
addLog(`注销失败: ${error.message}`);
}
}
async function makeCall() {
const target = targetInput.value.trim();
if (!target) {
addLog('请输入目标分机号');
return;
}
try {
const targetUri = UserAgent.makeURI(`sip:${target}@${domain}`);
if (!targetUri) throw new Error('无效的目标 URI');
addLog(`呼叫 ${targetUri.toString()}...`);
const inviter = new Inviter(userAgent, targetUri);
currentSession = inviter;
// 监听会话状态
inviter.stateChange.addListener((state) => {
addLog(`通话状态: ${state}`);
switch (state) {
case SessionState.Established:
addLog('✅ 通话已接通');
updateStatus(true, true);
break;
case SessionState.Terminated:
addLog('🔴 通话已挂断');
updateStatus(true, false);
if (remoteAudio.srcObject) {
remoteAudio.srcObject.getTracks().forEach(t => t.stop());
remoteAudio.srcObject = null;
}
currentSession = null;
break;
}
});
// 发起邀请
await inviter.invite({
sessionDescriptionHandlerOptions: {
constraints: { audio: true, video: false }
}
});
addLog('呼叫已发起,等待对方接听...');
} catch (error) {
addLog(`呼叫失败: ${error.message}`);
}
}
async function hangUp() {
if (currentSession) {
addLog('挂断通话...');
try {
await currentSession.bye();
updateStatus(true, false);
addLog('挂断成功');
} catch (error) {
addLog(`挂断失败: ${error.message}`);
}
}
}
// 绑定按钮事件
registerBtn.addEventListener('click', startAndRegister);
unregisterBtn.addEventListener('click', unregisterAndStop);
callBtn.addEventListener('click', makeCall);
hangupBtn.addEventListener('click', hangUp);
3. 测试验证
3.1 启动 FreeSWITCH
方式一:直接运行控制台
右键 FreeSwitchConsole.exe → 以管理员身份运行
方式二:命令行启动
# 以管理员身份打开 CMD 或 PowerShell
cd "C:\Program Files\FreeSWITCH"
FreeSwitchConsole.exe
3.2 打开两个浏览器窗口

3.3 注册分机

3.4 发起呼叫通话

3.5 挂断通话

完成以上步骤后,你应该已经成功搭建了一个可以通过浏览器语音通话的简易Demo,后续会继续完善功能,期待搭建出一个完整的呼叫中心系统。
火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。
更多推荐
所有评论(0)