简介:基于sipjs与FreeSWITCH的WebRTC电话通信示例,面向需要快速验证网页端电话呼入、呼出、转移、保持等功能的开发者。包内共4个文件,包含网页入口、SIP逻辑脚本、样式表及使用说明文档,整个压缩包仅79KB,结构轻量;修改分机、密码和服务器地址后可在Chrome浏览器中直接运行测试,适合学习SIP.js基础集成与FreeSWITCH环境联调。目前已有2314人学习/下载,对初次接触WebRTC电话应用的读者较有参考价值。示例围绕SIP.js官方API展开,代码精简,能帮助快速理解网页端发起呼叫、接听、转接及保持通话的核心流程,为自建电话客服或音视频通信页面提供可复用的起点。 有段时间没写通信相关的总结了,正好最近在做一个浏览器端电话功能,需求不复杂:网页上能呼入、呼出,通话中能保持,还能转给同事,最好不装插件、不跑客户端。我选的技术组合是 sip.js + FreeSWITCH + WebRTC,前端用 sip.js 走 SIP over WebSocket,服务端用 FreeSWITCH 做注册和媒体桥接。这套方案做完之后效果比较理想,把整个实现过程、关键配置和踩过的坑整理了出来,给后面要做类似网页端语音呼叫的同学做个参考。
先交代一下我能提供的核心能力:网页端注册分机、点击呼叫(呼出)、网页端来显弹窗(呼入)、通话保持/恢复、盲转和咨询转。整个过程音频走 WebRTC,信令走 WSS 加密通道,不需要用户安装任何软件,打开浏览器登录页面就能当话机用。适合前端开发、通信开发、做呼叫中心或企业内部沟通工具的技术同学参考,尤其适合第一次把 FreeSWITCH 和浏览器端 WebRTC 打通的人。
1. 为什么是这套组合:WebRTC 网页电话的架构思考
1.1 网页端通话的产品需求与常见实现路线
先聊需求。网页端发起的语音通话,通常出现在三类场景里:客服系统要一键外呼、CRM 里要点击拨号、办公平台要做软电话。早些年实现这些功能很别扭,网页里没法直接拿到麦克风,只能跳转到本地装的软电话客户端去呼叫,或者用 Flash、ActiveX 这类插件,体验差、兼容性也差。后来 WebRTC 成熟了,浏览器原生支持采集音频、编码、传流,网页端才真正具备了“当电话机用”的基础。
但 WebRTC 本身只是媒体传输和点对点连接的能力,它不管分机注册,不管电话号码怎么路由,也不管怎么连接到传统电话网。如果一个网页端只做两个浏览器之间的裸 WebRTC 通话,那距离“能打电话”还差得远——你没法呼到手机号,没法呼到 PSTN 固话,也没法跟已有的 SIP 语音网络互通。所以现实方案通常要在 WebRTC 和业务系统之间加一层网关,承担信令接入、媒体转发、路由控制这些事。
选型的时候我主要看三条路线。一条是直接用云厂商的 WebRTC 通信 PaaS 服务,快是快,但费用按量计,企业内部长期用成本不可控;另一条是投入大成本自研一套信令+媒体服务器,周期太长;第三条就是我这次采用的 SIP over WebSocket + 开源软交换,浏览器端用 sip.js 发标准 SIP 信令,服务端用 FreeSWITCH 做注册和媒体桥接,信令走 WSS 加密通道,媒体走 WebRTC 的 SRTP 通道。这套组合的好处是模型标准、对接传统电话网容易、社区资料多,能同时满足网页端内互通和与外部电话系统互通的需求。
1.2 三个组件各自的职责
把这套架构拆开看,三个角色的分工很清楚:
- sip.js 负责 SIP 信令。它运行在浏览器里,负责注册分机,发起 INVITE 邀请,处理来电 INVITE,发送 BYE 挂断,发送 REFER 实现转移,以及通过 re-INVITE 实现保持/恢复。它不是浏览器原生能力,而是把 SIP 协议栈用 JavaScript 重新实现了一遍。
- FreeSWITCH 是信令与媒体网关。它接收 sip.js 的注册请求,维护分机的在线状态,按照拨号计划把呼叫路由到分机、SIP 中继或 PSTN 网关。媒体层面它也可以做转发或桥接,比如网页端和传统电话网关之间编码不一致时,由它负责转码。
- WebRTC(浏览器)负责音频的采集、编码、传输和播放。具体到代码里就是 getUserMedia 采集麦克风,RTCPeerConnection 管理和远端建立媒体连接,这部分工作 sip.js 内部会帮我们封装好。
我用一个更生活化的类比:WebRTC 就是电话机里的听筒和话筒,负责声音;sip.js 是打电话的人,负责拨号、接听、挂断这些动作;FreeSWITCH 是总机交换机,负责找到人、接通线、路由到外部网络。这样各司其职,扩展性也好,比如想在网页端加视频,只需要在 SDP 协商里加上 video,其余流程基本不用动。
2. 环境部署:FreeSWITCH 侧的 WebSocket 接入准备
2.1 安装与模块确认
FreeSWITCH 的安装方式有两种:发行版软件源安装和源码编译安装。如果只是快速把功能跑起来,用官方提供的 apt 源或 yum 源装稳定版就行。源码编译主要的好处是方便自定义模块和比较新的特性,当初我编译时加的./configure --disable-dependency-tracking这类参数能减少部分重复依赖检查,对全量编译时间优化有些帮助,但注意不同版本的依赖项差异比较大,需要看 INSTALL 文档里的说明。
装完以后确认几件事:一是核心模块mod_sofia必须在模块列表里,这是 SIP 协议栈模块,WebSocket 接入就是靠它提供的;二是如果有音频编码转码需求(比如与只支持 G.711 的网关互通),确认mod_opus和mod_g711在编译选项里;三是确认mod_event_socket是启用的,后续排查问题、用 ESL 做点击拨号事件通知非常有用。用fs_cli进控制台执行module_exists mod_sofia就能查。
2.2 启用 WSS 与 TLS 证书配置
sip.js 和 FreeSWITCH 之间的信令走的是 WebSocket,生产环境必须用 WSS(即 TLS 加密的 WebSocket),因为浏览器只有在 HTTPS 或 localhost 环境下才允许调用麦克风权限,同时 SIP 注册中携带的账号密码也需要加密传输。
FreeSWITCH 的 SIP over WebSocket 支持是在 SIP profile 里开启的。以默认的 external profile 为例,配置文件在/etc/freeswitch/sip_profiles/external.xml,找到并确认下面两行没有被注释:
<param name="ws-binding" value=":5066"/> <param name="wss-binding" value=":7443"/>5066 是普通 WS 端口,7443 是 WSS 端口。浏览器端 sip.js 连接的就是wss://你的服务器IP:7443。如果某些版本安装后默认没有这两个参数,手动加上然后执行reloadxml或者重启 FreeSWITCH 即可。
TLS 证书方面,FreeSWITCH 安装后自带一套自签名证书,位于/etc/freeswitch/tls/下。开发环境可以用,但浏览器会提示证书不受信任,连接会被拦。处理办法可以先在浏览器里以 https 方式访问一次https://你的服务器IP:7443,手动点击信任异常证书,后面 WSS 连接就能建立。生产环境建议用正规 CA 签发的证书,把证书和私钥放到/etc/freeswitch/tls/下并替换原有文件,重启后就是一条完全可信的 WSS 通道。证书路径没有硬编码,默认读取的是wss.pem、dtls-srtp.pem这几个文件,配置前留意一下文件是否存在。
2.3 分机与拨号计划配置
sip.js 在网页端注册的本质上是一个 SIP 分机,所以 FreeSWITCH 目录里必须要有这个用户。在/etc/freeswitch/directory/default.xml里添加分机,一个最小配置长这样:
<include> <user id="1001"> <params> <param name="password" value="123456"/> </params> <variables> <variable name="user_context" value="default"/> </variables> </user> </include>添加多个分机就重复加多个<user>块,密码会在后续注册认证时用到。然后配置拨号计划,让分机之间可以互拨。修改/etc/freeswitch/dialplan/default.xml,加一条让分机互拨的规则:
<extension name="LocalExtension"> <condition field="destination_number" expression="^(10\d{2})$"> <action application="bridge" data="user/${destination_number}"/> </condition> </extension>这里^(10\d{2})$匹配的是 1001~1099 这样的分机号,bridge 到user/就是直接呼到内部注册的分机。如果后续要呼到外部 PSTN,只需要再写一条规则,把匹配到的号码桥接到对应的 SIP 网关或中继上。分机互拨的流程走到这里,网页端注册、互拨的基础环境就齐了。
3. sip.js 前端实现:注册、呼入呼出、保持与转移
3.1 初始化与注册
前端部分,我用的是 sip.js 0.21.x 版本,新版本 API 可能有调整,但核心概念一致。安装方式:
npm install sip.js模块引入和初始化:
import { UserAgent } from 'sip.js/lib/platform/web'; const userAgent = new UserAgent({ uri: UserAgent.makeURI('sip:1001@your-server.com'), transportOptions: { server: 'wss://your-server.com:7443' }, authorizationUsername: '1001', authorizationPassword: '123456' }); await userAgent.start();uri是分机的 SIP 地址,authorizationUsername和authorizationPassword对应 FreeSWITCH 里的分机号和密码。启动连接后,sip.js 会发送 REGISTER 请求,注册成功后 FreeSWITCH 日志里能看到该分机的注册状态。
有个细节要提醒:浏览器里跑 WebRTC 录音必须有安全上下文,也就是 HTTPS 或 localhost。如果生产环境没有配 HTTPS,麦克风采集会直接失败,表现为呼叫能建立但听不到声音,或者 getUserMedia 直接抛异常。开发期间用 localhost 没问题,但一旦部署到远端,不要省掉 HTTPS。
3.2 呼出实现
点击拨号呼出,核心就是调用userAgent.invite()发起一个新的 SIP 会话:
const target = UserAgent.makeURI('sip:1002@your-server.com'); const session = userAgent.invite(target, { sessionDescriptionHandlerOptions: { constraints: { audio: true, video: false } } }); session.delegate = { onSessionDescriptionHandler: () => { // 远端应答或 SDP 协商更新 }, onBye: () => { // 对方挂断 }, onReject: () => { // 呼叫被拒绝 } };呼出到分机和呼出到外线的区别主要在目标 URI,呼外线就写sip:手机号@你的中继地址,FreeSWITCH 侧拨号计划负责把呼叫路由到中继网关。网页端完全不用关心底层走的是哪种通道。
还要处理一下振铃状态。session.invite()返回的是一个会话,但真正进入通话状态需要等对端接受。可以用session.stateChange事件来监听会话状态,比如Establishing对应振铃中,Established对应已接通,Terminated对应已结束。在这个事件里驱动 UI 的挂断按钮、通话时长计时器最合适。
3.3 呼入处理
呼入的入口是 UserAgent 的delegate,收到来电时会触发onInvite。示例代码如下:
userAgent.delegate = { onInvite: (session) => { // 有来电,弹窗提示 session.accept(); } };来电时,sip.js 已经完成 SDP offer/answer 的一部分工作,这时浏览器不要立刻抢麦克风,而是先把来电信息展示给用户,等用户点击接听按钮后调用session.accept()。如果用户点了拒接,调用session.reject()。
开发时我踩过一个小坑:onInvite回调里如果直接session.accept(),在某些版本里会因为浏览器没有完成用户授权而抛异常。稳妥做法是弹层提醒用户“是否接听”,点击后先navigator.mediaDevices.getUserMedia({ audio: true })拿到授权,再调用session.accept(),成功率最高。
3.4 保持、转移与通话状态管理
保持/恢复在协议层面是发送 re-INVITE,把媒体方向设置为sendonly(保持)或sendrecv(恢复)。sip.js 封装了两个方法:
// 进入保持 session.hold(); // 解除保持 session.unhold();调用hold()后,对方听到的是静音,媒体方向已经改变。原理理解起来其实不复杂:保持不是挂断,只是告诉对端“我暂时不发送音频了”,底层的 RTP 通道还保持。
转移分两种:盲转和咨询转。盲转就是直接把当前通话转到目标分机,不需要和目标先通话。sip.js 里用Referrer实现:
import { Referrer } from 'sip.js/lib/api/session/Referrer'; const referTarget = UserAgent.makeURI('sip:1003@your-server.com'); const referrer = new Referrer(session, referTarget); referrer.refer();咨询转则要复杂一些,需要先发起一个通往目标分机的新呼叫,通上话后再把原有的来电转过去。实现上可以先把当前通话保持住,同时新建一个invite呼叫目标分机,在第二通电话接通后,用当前会话执行 refer 把原始会话引到第三方。这部分逻辑增加了不少状态管理,建议在代码里单独封装一个CallTransferManager,不要把所有状态散落在组件里。
还有个容易忽略的点:通话中的 DTMF 按键。比如拨分机后需要输入密码,或者按键导航,这个在网页端也常有,sip.js 提供session.dtmf()方法可以发送 DTMF 信号。实际测试下来,FreeSWITCH 侧默认能正常识别,不需要额外配置。
4. 常见问题与排查实录
4.1 常见问题速查表
做完全部流程后,我整理了一张问题速查表,遇到同类问题可以先对照排查:
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 注册不上,403/401 循环 | 分机密码错误或账号不存在 | 检查 directory 里分机密码,确认注册域名匹配 |
| WSS 连接报证书错误 | 使用了自签名证书且未被信任 | 浏览器先访问一次 WSS 地址并信任证书,或换成正规证书 |
| 有信令但没声音 | NAT 环境 RTP 地址不通 | 检查 FreeSWITCH 的 ext-rtp-ip/ext-sip-ip 是否配置为公网或可达 IP |
| 呼出无响应 | 拨号计划没有匹配号码 | fs_cli 里执行dialplan或直接看日志路由结果 |
| 保持后恢复不了声音 | 对端网关不支持 re-INVITE | 检查对端是否是标准 SIP 网关,必要时在网关侧开启 re-INVITE 支持 |
| 转接后原通话被挂断 | REFER 处理异常 | 检查 FreeSWITCH 是否允许 transfer,查看日志中 REFER 的响应码 |
4.2 日志工具与排查思路
排查这套链路问题,我建议分三步走。
第一步看 FreeSWITCH 日志。在 fs_cli 里执行console loglevel debug,然后复现问题,重点看呼叫经过拨号计划时命中哪条规则,以及 bridge 的目标分机是否在线。日志文件的默认位置在/var/log/freeswitch/freeswitch.log,排查结束后记得把 loglevel 调回 warning,否则日志量太大。
第二步看浏览器端的 WebSocket 消息流。打开 Chrome 开发者工具的 Network 面板,过滤器选 WS,刷新页面,能看到 sip.js 和 FreeSWITCH 之间每一条 SIP 消息。比如注册失败时,能找到 401/403 的响应,呼出时能看到 INVITE 和重推的 re-INVITE,保持时能确认媒体方向字段。
第三步看 WebRTC 内部状态。Chrome 地址栏输入chrome://webrtc-internals,打开后能看到 RTCPeerConnection 的实时统计,包括音轨状态、丢包率、码率、候选地址。如果明明通了但没声音,先在这里看audioLevel有没有波动,能立刻判断是采集端问题还是传输端问题。
我遇到印象最深的一次:开发环境一切正常,部署到服务器后所有呼叫都变成单通。排查发现 FreeSWITCH 的 external profile 没有配置外层 IP,RTP 包携带的是内网地址,浏览器没法回传音频。在 external.xml 里把ext-rtp-ip和ext-sip-ip指到公网 IP 后立即恢复正常。这个问题不复杂,但没有实际部署过很容易忽略。
4.3 弱网环境下的体验优化思路
热搜词里有一条是“webrtc 弱网卡顿怎么优化”,这里专门说一下。网页端通话遇到网络抖动时,优先调整音频编码参数而不是盲目升级带宽。使用 OPUS 编码时,可以限制最大码率,让 WebRTC 在弱网下自动降码率,同时开启前向纠错来抵抗丢包。sip.js 支持自定义sessionDescriptionHandler来重写 SDP 参数,但对大多数业务场景,更实用的做法是在 FreeSWITCH 侧配置合理的编码优先级和带宽限制,把压力放在服务端而不是每个浏览器客户端。
5. 实操心得与扩展建议
5.1 生产环境容易被忽略的几个细节
这套方案从“能跑通”到“能上线”,中间还隔着几个细节,我在实际落地时印象很深。
音频编码优先级要提前规划。网页端最好用 OPUS,音质好、带宽友好,但如果业务要呼到传统 PSTN 或老式语音网关,对方往往只支持 G.711。FreeSWITCH 会负责转码,但转码是要消耗 CPU 的,并发量上来以后这个开销不能忽视。建议在拨号计划的 bridge 环节显式指定编码,避免不必要的转码。
再就是浏览器的兼容性。sip.js 在 Chrome、Edge 上表现最稳定,Firefox 大版本迭代时偶尔有 WebRTC 兼容问题。Safari 对某些 SDP 格式处理差异明显,实际测试发现部分场景下保持和恢复操作在 Safari 上媒体会中断。如果业务用户群集中在 Safari,需要留足测试时间。
呼叫状态机要收敛好。网页端通话不是只有“空闲”和“通话中”两种状态,还有“呼出中”“来电振铃中”“保持中”“转移中”这些中间态。我用一个简单的状态机枚举来管理,避免了按钮重复点击和状态错乱。特别是转移和保持并发操作时,要对操作加锁,防止用户在保持状态下又发起一次转移导致会话状态异常。
5.2 后续可扩展的功能方向
基础通话做完之后,这套架构的扩展空间很大。录音功能可以通过 FreeSWITCH 的录音 API 实现,呼入呼出的时候启动录音,文件落到服务器上,对接质检系统;多方会议可以用conference模块,把多个网页端分机拉进同一个会议室;排队和话务分配可以基于 ESL 事件配合业务系统做,来电时由后台逻辑决定转给哪个座席。
我个人做下来的体会是:WebRTC 本身不难,难的是把信令、媒体、路由、状态管理这些环节串起来,而这套“sip.js + FreeSWITCH + WebRTC”的组合正好把所有环节都用标准协议串在了一起,出了问题能分层排查,不像黑盒方案那样一头雾水。如果只是想快速验证网页端能否打电话,照着上面的配置和代码走一遍,基本一两天就能跑通。
本文还有配套的精品资源,点击获取