资讯动态

sipjs与FreeSWITCH实现网页呼叫中心:呼入呼出转移保持全解析

发布时间:2026/10/20 7:06:59 来源:尧图企业网站定制
简介这份资源是面向WebRTC语音通信开发者的实战示例包围绕sipjs与FreeSWITCH搭建网页端电话应用解决浏览器内实现分机注册、呼入呼出、通话转移与保持等核心功能的落地问题。适合具备一定前端与SIP协议基础、希望快速验证软交换对接效果的开发者参考。压缩包共4个文件包含1个html页面、1个css样式、1个js脚本以及1个txt说明整体约79KB结构精简其中html与js承担界面与SIP信令逻辑css负责通话面板样式txt提供使用说明。在谷歌浏览器中运行后只需将分机号、密码与服务器地址替换为自己的配置即可直接进行功能测试省去从零搭建信令交互的调试成本。目前已有2315人学习下载可作为理解sipjs与FreeSWITCH配合方式的入门范例也可在此基础上扩展通话记录、状态提示等业务功能。1. 从一通打不通的网页电话说起某公司客服团队想把呼叫中心搬到浏览器里需求很明确坐席打开网页就能接打电话支持呼入、呼出、通话中转接、保持还要能跟现有的 FreeSWITCH 软交换对接。团队一开始想用 WebRTC 原生 API 硬写结果 SIP 信令、媒体协商、DTMF 传输全得自己造轮子两周下来连注册都没跑通。后来换成 sipjs 做信令层、FreeSWITCH 做媒体和路由中枢三天就把呼入呼出跑通了。这套 sipjs FreeSWITCH 的组合本质上是把浏览器当成一台软电话用 WebRTC 承载媒体用 SIP 协议跟 FreeSWITCH 通信。它适合两类人一是要做网页端呼叫中心、在线客服、远程坐席的开发者二是毕设或课设选题涉及 VoIP、实时通信、软交换的学生。下面这套方案从架构设计到模块实现再到验收答辩可以照着改。2. 需求拆解与技术选型为什么是 sipjs 加 FreeSWITCH2.1 四个核心功能对应的技术需求先把需求拆成可落地的技术点。呼入意味着浏览器要能接收 FreeSWITCH 路由过来的 INVITE 请求并正确应答呼出意味着浏览器要主动发起 INVITE携带 SDP 协商媒体参数转移分两种盲转是直接发 REFER 让 FreeSWITCH 把对方接到新号码 attended transfer 则需要先保持当前通话再发起新呼叫保持则是通过 re-INVITE 把媒体流方向设为 sendonly 或 inactive。这四个功能对信令层的要求完全不同。呼入呼出考验的是 SIP 注册和会话管理能力转移考验的是 REFER 消息的构造和事件订阅保持考验的是 SDP 的重新协商。sipjs 作为纯 JavaScript 实现的 SIP 协议栈把这四类信令都封装成了高层 API开发者不需要手写 SIP 报文只需要调用invite()、refer()、hold()这类方法。FreeSWITCH 则负责媒体路由、IVR、录音、队列这些呼叫中心必备能力两者通过 WebSocket 传输 SIP 信令通过 SRTP 传输媒体流。2.2 为什么不用原生 WebRTC 或 Asterisk原生 WebRTC 只解决了媒体传输问题信令层完全空白。你要自己实现 SIP 注册、INVITE 事务、SDP 协商、DTMF 传输工作量至少翻三倍。Asterisk 虽然也支持 WebRTC但它的 SIP over WebSocket 实现相对封闭调试时日志不够透明遇到媒体协商失败很难定位是浏览器端还是服务端的问题。FreeSWITCH 的 mod_sofia 模块对 WebSocket 支持成熟日志分级清晰配合sofia global siptrace on可以直接看到完整的 SIP 报文交互排查问题时不会变成黑匣子。另一个关键点是 sipjs 的浏览器兼容性。它在 Chrome、Firefox、Edge 上表现一致底层自动处理了浏览器之间的 SDP 差异。如果团队后续要接入 Electron 桌面端或移动端 WebViewsipjs 的 API 不需要改动只需要确认 WebRTC 权限和网络环境即可。2.3 整体架构与数据流整个系统的数据流分三层。信令层浏览器通过 WebSocket 连接 FreeSWITCH 的 5066 端口默认 ws 端口sipjs 在此之上建立 SIP 会话。媒体层浏览器通过 WebRTC 的 RTCPeerConnection 与 FreeSWITCH 的 RTP 端口交换 SRTP 包FreeSWITCH 内部用 mod_conference 或 mod_dptools 做混音和路由。业务层网页端用 JavaScript 管理通话状态机FreeSWITCH 用 dialplan 和 event socket 控制呼叫流程。浏览器 (sipjs) --WebSocket(SIP)-- FreeSWITCH --SIP/RTP-- 外部电话网络 | | ----------WebRTC(SRTP)---------------这个架构的好处是信令和媒体分离调试时可以单独抓 SIP 包或 RTP 包。FreeSWITCH 的 dialplan 可以灵活配置呼入路由比如按被叫号码前缀分发到不同坐席组或者接入 IVR 做按键导航。3. 环境搭建与基础配置从零把通道打通3.1 FreeSWITCH 端的最小配置FreeSWITCH 默认安装后需要改三个地方。第一启用 WebSocket 支持在conf/sip_profiles/internal.xml里确认ws-binding参数存在通常默认监听 5066 端口。第二配置一个供浏览器注册的分机在conf/directory/default/下新建1001.xml内容如下include user id1001 params param namepassword value1234/ /params variables variable nameuser_context valuedefault/ /variables /user /include这个配置创建了分机 1001密码 1234注册后进入 default 上下文。第三在 dialplan 里加一条路由让呼入到 1001 的请求能正确到达浏览器extension nameto_webrtc condition fielddestination_number expression^1001$ action applicationbridge datauser/1001/ /condition /extensionbridge会把呼叫转给已注册的 1001 分机如果浏览器在线就会振铃。注意user_context要和 dialplan 的 context 一致否则路由不生效。3.2 浏览器端 sipjs 初始化sipjs 的初始化分三步创建 UserAgent、注册、监听事件。下面是最小可用代码const { UserAgent, Registerer, Inviter, SessionState } SIP; const ua new UserAgent({ uri: sip:1001your-fs-domain, transportOptions: { server: wss://your-fs-domain:5066 // 注意用 wss 不是 ws }, authorizationUsername: 1001, authorizationPassword: 1234, displayName: 坐席1001 }); const registerer new Registerer(ua); ua.start().then(() { registerer.register(); console.log(SIP 栈已启动注册中...); }); registerer.stateChange.addListener((state) { console.log(注册状态:, state); // Registered 表示成功 });server地址必须用wss://因为浏览器页面如果是 HTTPS混合内容会被拦截。如果 FreeSWITCH 没配 TLS 证书可以用自签名证书但浏览器会提示不安全开发阶段可以手动信任。authorizationUsername和authorizationPassword要和 FreeSWITCH 目录里的配置一致。3.3 验证注册与媒体通道注册成功后在 FreeSWITCH 控制台执行sofia status profile internal reg应该能看到 1001 的注册记录。如果看不到先检查 WebSocket 连接是否建立浏览器控制台 Network 面板里应该有 101 Switching Protocols 的请求。如果 WebSocket 通了但注册失败通常是密码错误或 domain 不匹配。媒体通道的验证方法是拨一个测试号码。在 dialplan 里加一条echo测试路由extension nameecho_test condition fielddestination_number expression^9196$ action applicationanswer/ action applicationecho/ /condition /extension浏览器呼叫 9196如果听到自己的回声说明 SRTP 媒体通道正常。听不到回声但通话状态是 active通常是 ICE 协商失败检查 FreeSWITCH 的rtp-ip和ext-rtp-ip配置确保浏览器能访问到 FreeSWITCH 的 RTP 端口范围。4. 核心功能实现呼入、呼出、转移、保持4.1 呼出与呼入的会话管理呼出用Inviter类呼入用ua.on(invite)监听。先看呼出function makeCall(targetNumber) { const inviter new Inviter(ua, sip:${targetNumber}your-fs-domain, { sessionDescriptionHandlerOptions: { constraints: { audio: true, video: false } } }); inviter.stateChange.addListener((state) { if (state SessionState.Established) { console.log(通话已建立); attachMedia(inviter.sessionDescriptionHandler); } else if (state SessionState.Terminated) { console.log(通话结束); } }); inviter.invite(); return inviter; }sessionDescriptionHandlerOptions里的constraints控制媒体类型只开音频可以降低带宽和回声问题。attachMedia负责把远端媒体流挂到 audio 标签上function attachMedia(sdh) { const pc sdh.peerConnection; const remoteStream new MediaStream(); pc.getReceivers().forEach((receiver) { if (receiver.track) remoteStream.addTrack(receiver.track); }); document.getElementById(remoteAudio).srcObject remoteStream; }呼入的处理逻辑类似只是方向反过来ua.on(invite, (invitation) { console.log(收到呼入请求来自:, invitation.remoteIdentity.uri.toString()); invitation.accept({ sessionDescriptionHandlerOptions: { constraints: { audio: true, video: false } } }).then(() { attachMedia(invitation.sessionDescriptionHandler); }); });这里有个容易翻车的点invitation.accept()必须在用户点击接听按钮后调用不能自动接听否则浏览器会因为没有用户手势而阻止音频播放。通常的做法是在页面上弹一个接听/拒接的 UI用户点击后再执行 accept。4.2 通话保持与恢复的实现细节保持的本质是重新协商 SDP把媒体方向改成sendonly或inactive。sipjs 没有直接提供hold()方法需要手动操作 sessionDescriptionHandlerasync function holdCall(session) { const sdh session.sessionDescriptionHandler; const pc sdh.peerConnection; // 获取本地 SDP修改媒体方向 const offer await pc.createOffer({ offerToReceiveAudio: true }); offer.sdp offer.sdp.replace(/asendrecv/g, asendonly); await pc.setLocalDescription(offer); await session.sessionDescriptionHandler.setLocalDescription(offer); // 发送 re-INVITE session.sessionDescriptionHandler.sendOffer(); console.log(通话已保持); }恢复时把sendonly改回sendrecv再发一次 re-INVITE。注意sendonly表示只发不收对方会听到静音inactive表示收发都停适合长时间保持。实际项目中我一般用sendonly因为恢复时协商更快。4.3 盲转与 attended transfer 的信令流程盲转用 REFER 消息sipjs 封装成了refer()方法function blindTransfer(session, targetNumber) { session.refer(sip:${targetNumber}your-fs-domain) .then(() { console.log(盲转请求已发送); }) .catch((err) { console.error(盲转失败:, err); }); }盲转发出后FreeSWITCH 会收到 REFER 并尝试呼叫目标号码当前通话会挂断。如果目标号码无人接听原通话不会自动恢复这是盲转的固有风险。Attended transfer 需要两步先保持当前通话再发起一个新呼叫等新通话建立后再发 REFER 把两个会话连起来。代码逻辑如下async function attendedTransfer(originalSession, targetNumber) { // 第一步保持原通话 await holdCall(originalSession); // 第二步呼叫目标号码 const newInviter makeCall(targetNumber); // 第三步新通话建立后发 REFER 带 Replaces 头 newInviter.stateChange.addListener(async (state) { if (state SessionState.Established) { const referTo sip:${targetNumber}your-fs-domain?Replaces${originalSession.dialog.callId}%3Bto-tag%3D${originalSession.dialog.remoteTag}%3Bfrom-tag%3D${originalSession.dialog.localTag}; await originalSession.refer(referTo); console.log(Attended transfer 完成); } }); }Replaces头是关键它告诉 FreeSWITCH 用新通话替换原通话。这个头的格式容易写错callId、remoteTag、localTag必须从原会话的 dialog 对象里取不能手拼。如果 FreeSWITCH 返回 400 错误先检查这三个值是否正确。5. 避坑与常见问题排查5.1 注册成功但呼出无声现象浏览器显示注册成功拨号后对方能振铃但接通后双方都听不到声音。原因通常是 ICE 协商失败或 SRTP 密钥不匹配。FreeSWITCH 默认的rtp-ip可能是内网地址浏览器在外网时无法直连。解决方法是设置ext-rtp-ip为公网地址并在 dialplan 里启用bypass_media让媒体走 FreeSWITCH 中转。如果还是无声在浏览器控制台执行pc.getStats()查看inbound-rtp的bytesReceived是否增长不增长说明 RTP 包没到。5.2 呼入时浏览器不振铃现象外部电话呼入分机 1001FreeSWITCH 日志显示user/1001已桥接但浏览器没有任何反应。原因通常是 dialplan 的 context 和分机注册时的user_context不一致或者bridge的user/前缀写成了sofia/internal/。解决方法是先用sofia status profile internal reg确认分机在线再检查 dialplan 的condition是否匹配被叫号码。如果都正常在 FreeSWITCH 控制台执行console loglevel 7打开详细日志看 INVITE 是否发到了 WebSocket 连接上。5.3 转移后原通话未挂断现象盲转发出后新通话建立了但原通话还保持在线坐席听到两个声音。原因是 REFER 的Refer-To头格式不对FreeSWITCH 没有正确解析。检查refer()传入的 URI 是否带了sip:前缀以及目标号码是否在 dialplan 里有路由。另一个可能是 FreeSWITCH 的transfer应用需要额外参数在 dialplan 里加action applicationset datahangup_after_bridgetrue/可以确保桥接成功后原通话自动挂断。5.4 保持后恢复失败现象通话保持正常但点击恢复时浏览器报InvalidStateError或setRemoteDescription失败。原因是 re-INVITE 的 SDP 版本号没有递增或者 ICE 候选重新收集时超时。解决方法是在修改 SDP 后手动递增o行里的版本号并确保setLocalDescription和setRemoteDescription成对调用。如果 ICE 超时在sessionDescriptionHandlerOptions里加iceGatheringTimeout: 3000缩短等待时间。5.5 DTMF 按键无响应现象通话中按数字键IVR 没有反应。原因是 DTMF 传输方式不匹配。sipjs 默认用 RFC 2833但 FreeSWITCH 可能配置成了 SIP INFO。在ua的sessionDescriptionHandlerOptions里加dtmfType: RFC2833强制指定或者在 FreeSWITCH 的sip_profiles/internal.xml里设置dtmf-type为rfc2833。如果还是不行用session.info()手动发 SIP INFO 消息测试。6. 验收标准与答辩演示技巧验收这套系统我一般分四步走。第一步功能验收呼入、呼出、盲转、attended transfer、保持恢复、DTMF 六个场景逐一测试每个场景至少跑三遍记录成功率。第二步媒体质量验收用pc.getStats()抓取packetsLost和jitter丢包率低于 1%、抖动低于 30ms 算合格。第三步并发验收用脚本模拟 10 个浏览器同时注册并呼出观察 FreeSWITCH 的 CPU 和内存占用确认没有会话泄漏。第四步异常验收拔网线、关浏览器、FreeSWITCH 重启看系统能否自动恢复注册和通话状态。答辩演示时不要只放 PPT。直接开两个浏览器窗口一个模拟坐席一个模拟客户现场走一遍完整流程客户呼入、坐席接听、保持、盲转到另一个坐席、挂断。演示过程中故意制造一个异常比如保持后不恢复直接挂断然后展示 FreeSWITCH 日志里会话正常清理。评委通常会问“如果 FreeSWITCH 挂了怎么办”提前准备一个重连机制的说明sipjs 的ua.on(disconnected)事件里加自动重连逻辑配合registerer.register()重新注册。下面这张表是我验收时用的检查清单可以直接抄验收项通过标准测试方法注册3 秒内状态变为 Registered刷新页面观察控制台呼出对方振铃并接通双向音频拨打 echo 测试号呼入浏览器振铃点击接听可通话外部电话呼入分机盲转原通话挂断新通话建立转移后检查两路会话状态保持对方听到静音恢复后正常保持 10 秒后恢复DTMFIVR 正确识别按键拨打 IVR 按数字键最后说一个血泪经验每次改完 FreeSWITCH 配置一定要执行reloadxml和sofia profile internal restart只改文件不重启模块配置不会生效。从那以后我每次部署前都强制走一遍fs_cli -x reloadxml和fs_cli -x sofia profile internal restart再开始测试。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑