本文提供一套完整的实操方案,帮助你在一台Windows系统的电脑上完成 FreeSWITCH 的本地安装与配置,并通过两个浏览器页面实现语音通话。

1. 本地安装与配置FreeSWITCH

Windows系统下安装与配置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,后续会继续完善功能,期待搭建出一个完整的呼叫中心系统。

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐