ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

WebRTC网页电话实战:sip.js直连FreeSWITCH全解析

WebRTC网页电话实战:sip.js直连FreeSWITCH全解析 简介一套基于SIP.js与FreeSWITCH的WebRTC网页端电话应用示例面向需要在浏览器中快速实现电话呼入、呼出、转接与保持功能的开发者适合作为SIP.js与WebRTC联调的入门参考。压缩包共4个文件一个HTML入口页面负责界面结构一个CSS文件控制样式一个JavaScript脚本sip-0.7.8.js承载SIP客户端逻辑另附readme说明文档整体仅79KB结构精简、便于阅读和二次修改。目前已有2314人学习下载表明其在SIP.js FreeSWITCH集成场景中具有实际参考价值。按说明更改分机号、密码和服务器地址即可在谷歌浏览器中直接运行验证呼入、呼出、转移、保持等核心流程同时可结合sipjs.com官方文档进一步理解网页端SIP注册、会话控制与信令交互原理。该示例默认以谷歌浏览器为运行环境部署轻量适合在已有FreeSWITCH环境中快速搭建可测试的WebRTC电话原型。1. 为什么我在网页端接电话而不是用传统话机先从一个最实际的场景说起公司客服坐席、远程办公的同事、经常不在工位上的售前工程师——这些人每天最重要的事情就是接电话、打电话、转接电话。传统SIP话机摆在桌面上人不在工位就接不到软电话装在Windows电脑上换个Mac就得重新折腾浏览器里敲个链接就能登录一个网页电话打开就能用关掉就走这个体验对现代办公来说几乎是刚需。WebRTC技术成熟之后浏览器原生支持音视频通话再配合sip.js这个纯JavaScript的SIP协议栈完全不依赖Flash、不依赖插件直接把FreeSWITCH变成网页电话的“交换机”。我最早是在一个客服系统改造项目里接触到这套方案的客户要求坐席端零安装、零维护IT部门不想再一台一台去装软电话客户端也不想维护话机固件升级。当时对比了WebRTC网关方案和sip.js直连方案最终选了sip.js直连FreeSWITCH的路线——OpenSIPS配合WebRTC网关那套太复杂sip.js走WSS协议直连架构最简单排查问题也直观。这篇文章要讲的就是基于sip.js FreeSWITCH WebRTC在网页端实现电话呼入、呼出、呼叫转移、呼叫保持这四条核心链路。整套方案对中小团队做内部通信工具、客服工作台、电话SaaS应用都有直接参考价值。核心关键词先放这sip.js处理浏览器端的SIP信令FreeSWITCH负责媒体协商和通话路由WebRTC负责浏览器里的音频采集与播放。三者之间的协作关系通俗点说就是sip.js是电话机的“手柄和听筒”FreeSWITCH是电话局的“程控交换机”WebRTC是两者之间的“声音通道”。下面我从架构设计、环境搭建、核心代码、踩坑记录四个维度把这套方案完全拆开。2. 架构选型sip.js直连FreeSWITCH还是走WebRTC网关2.1 两种主流方案的特点对比在确定技术路线时我先把方案群里所有可能的路径盘了一遍。网页端接入FreeSWITCH常见的有三条路FreeSWITCH内置WebRTC支持通过WSS协议直接让sip.js注册到FreeSWITCH——这是最直接的一条路FreeSWITCH自带mod_sofia支持WSS只需要配置TLS证书和WS绑定端口逻辑上等同于一个SIP话机通过加密的WebSocket注册上去。前端先连一个WebRTC网关比如mediasoup、Janus、Licode网关再与FreeSWITCH对接——这种方案适合大规模并发、需要SFU混流、需要录像录屏、需要多方会议的场景但架构复杂多一跳延迟运维成本高。用云厂商的WebRTC通话能力再通过PSTN网关接入传统电话网——适合没有自建SIP经验的团队但每个月都有不低的通道费用而且呼叫控制逻辑被封装在厂商SDK里企业内部特殊路由策略很难定制。我最终选第一条路主要原因是业务场景是一对一的话务通信并发量撑死在几十路FreeSWITCH内置的B2BUA模式足够应付。sip.js直连可以让开发人员直接在浏览器里定位问题——freeswitch控制台能看到注册消息sip.js的debug日志能看到UA状态机哪一层出问题一目了然。真实业务中有一个项目初期也考虑过Janus网关方案但评估之后发现Janus虽然媒体处理能力强但信令链路过长每个通话都要在Janus和FreeSWITCH之间做桥接呼叫转移和呼叫保持这种操作在网关上实现起来很别扭排错更是层层叠加。2.2 关键端口与协议路径整套架构里的核心信令路径和媒体路径是这样走的浏览器里的sip.js通过WSSWebSocket Secure连接到FreeSWITCH的7443端口实际上sip.js在底层还是SIP协议只是把SIP消息封装进WebSocket帧里传输。FreeSWITCH收到INVITE之后媒体协商走WebRTC标准的SDP交换音频通过SRTP/UDP在浏览器和FreeSWITCH之间直接传输不经过任何中转服务器。打电话出去的时候FreeSWITCH通过自己的SIP中继或者PSTN网关路由到运营商对方接听之后浏览器里的WebRTC音频流和对方经过编解码转换后接通。端口路径可以简单记录一下WSS信令端口7443sip.js注册和呼叫控制走这里。WS信令端口5066如果内网调试没有TLS证书可以用这个明文的WebSocket端口测试。RTP媒体端口FreeSWITCH默认的RTP端口范围是16384-32768需要在防火墙里放行UDP。如果还要对接PSTNSIP中继的5080端口也要确认可达。2.3 为什么FreeSWITCH而不是Asterisk很多人会问Asterisk也能做SIP服务器为什么用FreeSWITCH我个人的看法是Asterisk在传统电话领域生态更老、文档更多但WebRTC支持方面FreeSWITCH的mod_sofia对WSS、SRTP、DTLS的原生支持更完善配置上更“开箱即用”。而且FreeSWITCH的拨号计划用XML写写一套WebRTC分机的路由规则比Asterisk的extensions.conf更直观调试的时候可以reload而不影响在线通话。当然Asterisk新版也支持WebRTC但如果你跟我一样希望用一套系统同时管WebRTC分机和传统SIP中继FreeSWITCH的稳定性更值得信任。3. 环境准备FreeSWITCH编译安装与WebRTC相关模块激活3.1 版本选择与依赖处理这一步非常关键版本选错会让后面踩进深坑。我目前的稳定组合是Ubuntu 20.04/22.04 LTS FreeSWITCH 1.10.x1.10.10之后的release。1.10版本对WebRTC的DTLS-SRTP支持已经非常稳定mod_sofia和mod_verto都默认编译进来。编译安装之前需要先解决依赖。FreeSWITCH官方文档推荐的依赖列表比较长但实际生产经验告诉我重点确保以下几项装好即可build-essential、cmake、autoconf、automake、libtool编译工具链libssl-devTLS/WSS必需libsndfile1-dev、libltdl-dev音频处理libopus-dev、libspeex-dev编码器Opus对WebRTC尤其重要libavformat-dev可选录音相关如果是从源码编译可以按官方标准流程走先./bootstrap.sh生成configure脚本再./configure --disable-dependency-tracking。这个disable-dependency-tracking参数近期在社区里讨论很多它的作用就是告诉configure不要去做多余的依赖缓存追踪减少编译过程中的一些过时依赖检测特别适合连续多次编译或交叉编译场景。实际体验中加上这个参数后configure阶段明显更快而且能避免某些依赖库版本变化导致的假错误。3.2 mod_sofia关键配置WSS端口、TLS证书、WebRTC分机安装完成后重点修改FreeSWITCH的conf目录。WebRTC注册的核心配置在/usr/local/freeswitch/conf/sip_profiles/internal.xml和internal-ipv6.xml中。首先确认internal这个SIP profile里以下几项配置存在且值正确param namews-binding value:5066/ param namewss-binding value:7443/注意如果只配置了wss-binding而没有配置ws-binding不少版本的FreeSWITCH在启动时会警告甚至拒绝启动。两个都配上ws用于内网HTTP环境的调试wss用于生产环境。TLS证书配置方面FreeSWITCH默认自带一个自签名证书路径通常在/usr/local/freeswitch/cert/下。生产环境建议换成企业自己的证书让浏览器不再提示不安全。替换证书之后需要修改internal.xml里的param nametls-cert-dir value/usr/local/freeswitch/cert/然后重启FreeSWITCH。WebRTC分机的创建逻辑与传统SIP分机完全一样在/usr/local/freeswitch/conf/directory/default/下新增一个XML文件即可比如1001.xmlinclude user id1001 params param namepassword value123456/ param namevm-enabled valuefalse/ /params variables variable nameuser_context valuedefault/ variable nameeffective_caller_id_number value1001/ variable nameeffective_caller_id_name valueWebRTC User 1001/ /variables /user /include3.3 启动验证wss端口是否监听配置完成后启动FreeSWITCH/usr/local/freeswitch/bin/freeswitch -nc -nf-nc是不进入控制台交互模式-nf是不后台fork日志直接打到终端方便第一时间看到错误。启动后检查端口监听netstat -tlnp | grep -E 5066|7443如果看到5066和7443都在监听说明mod_sofia的WebSocket绑定成功。接着可以在FreeSWITCH控制台执行sofia status profile internal输出里能看到internal profile的WS binding和WSS binding状态一切正常就可以进入下一步。4. 前端核心实现sip.js注册、呼入呼出、保持与转移4.1 初始化sip.js UA并处理注册状态前端使用sip.js 0.21.x版本这个版本是目前稳定性和API设计最平衡的版本。0.20之后到0.21经历了core API重构0.21的语义更简洁示例也更多。如果是从旧项目升级过来的注意0.21的UA构造方式有变化。初始化代码如下import { UA } from sip.js; const ua new UA({ uri: sip:1001your-freeswitch-domain.com, transportOptions: { // 注意这里是wss不是ws server: wss://your-freeswitch-domain.com:7443, traceSip: true }, authorizationUsername: 1001, authorizationPassword: 123456, register: true, logLevel: debug }); ua.on(registered, () { console.log(分机注册成功); updatePhoneStatus(online); }); ua.on(unregistered, () { console.log(分机注册断开); updatePhoneStatus(offline); }); ua.on(registrationFailed, (cause) { console.error(注册失败原因, cause); updatePhoneStatus(error); }); ua.start();几个关键点需要特别说明uri里的domain部分必须和FreeSWITCH里配置的domain一致默认是服务器的IP或者$${domain}变量对应的值。如果注册不上优先检查这个匹配关系。traceSip: true和logLevel: debug是开发阶段最宝贵的排错工具可以看到浏览器发出的每条SIP消息。生产环境记得关掉否则控制台会被刷爆。SIP over WebSocket在建立连接之前会先通过WSS握手建立TCP连接所以如果WSS端口不通ua.start()之后会一直重试或者直接报WebSocket Connection Failed。4.2 呼出从浏览器拨打电话呼出是所有功能里最容易被误用的一个。很多人第一次实现呼出的时候习惯把对方的号码直接塞进ua.call()的target参数这样也能通但往往在FreeSWITCH侧的路由判断上出问题。我在实际项目中推荐用带域名后缀的SIP URI方式function makeCall(calleeNumber) { // 号码统一格式化为SIP URI方便FreeSWITCH侧做路由匹配 const target sip:${calleeNumber}your-freeswitch-domain.com; const session ua.call(target, { mediaConstraints: { audio: true, video: false }, // 第一次呼叫时需要的本地媒体流 // 如果不传sessionDescriptionHandlerOptionssip.js会自动处理 sessionDescriptionHandlerOptions: { constraints: { audio: true, video: false } } }); session.on(progress, () { console.log(对方振铃中); updateCallStatus(ringing); }); session.on(accepted, () { console.log(通话已接通); updateCallStatus(connected); startCallTimer(); }); session.on(bye, () { console.log(通话结束); updateCallStatus(idle); stopCallTimer(); }); session.on(failed, (cause) { console.error(呼叫失败, cause); updateCallStatus(failed); }); // 保存session方便后续挂断、保持、转移操作 currentSession session; }这里有一个我踩过的坑ua.call()之后必须立刻在返回的session上监听事件尤其是accepted事件。如果监听的代码写在异步回调之后很可能错过早期事件导致通话已经接通了界面还停留在“呼叫中”状态。媒体协商方面sip.js默认使用WebRTC的getUserMedia获取麦克风音频。注意在HTTPS环境下才能稳定获取麦克风权限http://localhost是例外。如果部署在生产环境务必确保整个页面通过HTTPS访问同时WSS端口也需要有有效的TLS证书。4.3 呼入监听来电并接通呼入的核心就是监听ua.on(invite)事件。当FreeSWITCH把来电路由到1001分机时sip.js的UA会触发这个事件传入一个Session实例此时就是来电振铃阶段。ua.on(invite, (session) { console.log(有来电主叫号码, session.remoteIdentity.uri.user); currentSession session; updateCallStatus(incoming); // 播放来电铃声用Web Audio API生成简单的振铃音 playRingTone(); // 用户点击接听按钮时调用 window.answerIncomingCall () { session.accept({ mediaConstraints: { audio: true, video: false } }); stopRingTone(); updateCallStatus(connected); startCallTimer(); }; // 用户点击拒接按钮时调用 window.rejectIncomingCall () { session.reject(); stopRingTone(); updateCallStatus(idle); }; session.on(bye, () { // 对方挂断或者FreeSWITCH侧挂断 stopRingTone(); updateCallStatus(idle); stopCallTimer(); }); session.on(failed, () { stopRingTone(); updateCallStatus(idle); }); });一个需要注意的细节是session.accept()最好在用户真正点击接听按钮时才调用不要在invite事件触发后立刻自动accept。如果页面里为了测试方便自动accept来电会瞬间接通用户根本来不及准备。同时接受呼叫后浏览器会请求麦克风权限这个权限弹窗可能比呼叫接通慢所以建议在页面加载时先请求一次麦克风权限接受后保存在变量里供后续通话复用。4.4 呼叫保持让对方听到等待音呼叫保持的实现原理是在通话过程中向Far End发送一个re-INVITE把SDP里的媒体流方向修改为sendonly或者inactive从而暂停双向音频传输。sip.js封装了这个过程直接调用session的hold()和unhold()方法即可。function holdCall() { if (!currentSession) return; currentSession.hold({ // hold时的音频方向配置sendonly表示我们只发送不接收或者反过来取决于实现 // 实际项目中常用inactive来完全静音媒体 audio: { direction: inactive } }) .then(() { console.log(呼叫已保持); updateCallStatus(held); }) .catch((err) { console.error(保持失败, err); }); } function unholdCall() { if (!currentSession) return; currentSession.unhold({ audio: { direction: sendrecv } }) .then(() { console.log(呼叫已恢复); updateCallStatus(connected); }) .catch((err) { console.error(恢复失败, err); }); }这里要注意的是FreeSWITCH作为B2BUA它在收到re-INVITE媒体修改请求后能不能正确协商取决于它是否在SDP里正确处理inactive和sendrecv属性。实测下来FreeSWITCH 1.10处理sip.js的hold请求非常标准但对端如果走的是PSTN网关可能不会对媒体暂停做任何提示对方听到的仍然是“沉默”这时候最好由FreeSWITCH侧提供一个MOHMusic on Hold资源在保持期间给对端播放等待音乐。MOH配置在FreeSWITCH里默认就是启用的保持时如果sip.js把媒体方向切到inactiveFreeSWITCH会自动向对端播放默认的等待音。不需要额外配置就能获得比较完整的体验。4.5 呼叫转移attended transfer还是blind transfer呼叫转移是通话控制里最复杂的部分。sip.js提供了refer()方法底层走SIP REFER请求但实际项目里很多团队对这个方法的封装并不完善。我建议把呼叫转移分成两种场景来处理盲转Blind Transfer用户A和B正在通话A希望直接把B转移到C不再参与后续通话。此时A的浏览器发送REFER请求给FreeSWITCHFreeSWITCH让B和C直接建立通话A退出。协商转Attended TransferA先和C建立一个新的咨询通话确认C愿意接收后再把B转移给C。这个场景需要两个Session同时存在于前端一个保持一个活跃。// 盲转实现 function blindTransfer(targetNumber) { if (!currentSession) return; const target sip:${targetNumber}your-freeswitch-domain.com; currentSession.refer(target) .then(() { console.log(转移请求已发送); // 转移后当前session即将结束可以在bye事件里清理状态 }) .catch((err) { console.error(转移失败, err); }); }盲转看上去简单但实际部署时我建议把转移的核心逻辑写在FreeSWITCH的拨号计划里前端只触发一个预定义的拨号串比如**010加目标号码。这样做的理由是FreeSWITCH侧可以通过XML路由精确控制转移权限、超时策略、失败回调而sip.js的refer实现直接透传到FreeSWITCH后控制力度会弱一些。当然如果只是做Demo或者内部小工具直接refer是完全可行的。协商转需要维护两个session实现上要复杂不少用户A在和B通话session1点击“咨询转”按钮。前端对session1先调用hold()让B进入等待状态。用同一个UA发起一个新的call到C得到session2。用户A和C通话确认后对session1调用referrefer的target是session2的远端URI。成功后session2被挂断session1转移完成。协商转的坑在于两个session的媒体流同时存在浏览器在两个WebRTC连接之间切换音频输出如果处理不当用户会听到回声或者丢失声音。实际测试中Chrome处理两个RTCPeerConnection的音频输出还算稳定但Edge和Safari偶尔会有问题建议协商转时优先关闭session2的本地扬声器播放只保留session1的音频。4.6 挂断与清理状态挂断逻辑相对简单但状态清理要彻底。sip.js中挂断通过session.bye()实现function hangupCall() { if (!currentSession) return; // 通知对端挂断 currentSession.bye() .then(() { console.log(呼叫已挂断); }) .catch((err) { console.error(挂断失败, err); // 如果bye失败可能是连接已断开直接调用terminate强制结束 currentSession.terminate(); }) .finally(() { currentSession null; updateCallStatus(idle); stopCallTimer(); stopRingTone(); }); }这里强调一点挂断之后别忘记释放本地媒体流。sip.js在session结束时会自动清理RTCPeerConnection但麦克风设备可能仍然被浏览器占用特别是在切换分机或页面只刷新了一次但多次发起呼叫的场景下麦克风灯会一直亮着。我通常在通话结束后主动调用navigator.mediaDevices.getUserMedia的track停止接口全面释放。5. FreeSWITCH侧拨号计划设计让网页电话真正打通内外线5.1 WebRTC分机互拨与呼出路由前端功能做得再花哨FreeSWITCH拨号计划写得不对一切白搭。我在开发阶段把拨号计划分成三类测试内部WebRTC分机互拨、通过SIP中继呼出PSTN、外部PSTN呼入到WebRTC分机。内部互拨的Dialplan非常简单extension namewebrtc-internal condition fielddestination_number expression^(10[0-9][0-9])$ action applicationbridge datasofia/internal/${destination_number}$${domain}/ /condition /extension这样1001拨打1002时FreeSWITCH直接将呼叫桥接到1002分机。由于双方都是WebRTC分机媒体全程走SRTP没有编解码转换延迟非常低。呼出到PSTN的Dialplan需要配合SIP中继假设网关名称为gw_pstnextension namewebrtc-pstn-out condition fielddestination_number expression^(0[0-9])$ action applicationbridge datasofia/gateway/gw_pstn/${destination_number}/ /condition /extension外线呼入到WebRTC分机的路由在呼入的context里写extension namepstn-in-to-webrtc condition fielddestination_number expression^(\d)$ action applicationbridge datasofia/internal/${destination_number}$${domain}/ /condition /extension5.2 呼叫保持时的MOH设置前端触发hold之后FreeSWITCH会检测媒体流方向变成inactive然后自动向对端播放MOH。在mod_switch的默认配置里MOH文件在/usr/local/freeswitch/sounds/music/8000/目录下安装FreeSWITCH后自带几首示例音乐。如果不喜欢默认音乐把自己的wav文件放进去修改autoload_configs/local_stream.conf.xml里的文件路径即可。注意MOH文件建议用8000Hz采样率的wav否则部分PSTN网关会不识别。8000Hz是传统电话网的采样率WebRTC走Opus编码是48000HzFreeSWITCH会自动转码。5.3 呼叫转移的Dialplan配合如果前端直接发送REFERFreeSWITCH默认的defaultcontext里已经有对REFER的处理但建议在分机所属的user context里显式配置transfer_ringback等变量控制转移过程中的振铃音。更稳妥的做法是在转移路由里增加一个extension专门处理extension nametransfer-route condition fielddestination_number expression^\*\*(10[0-9][0-9])$ action applicationset datatransfer_on_ring$${destination_number}/ action applicationbridge datasofia/internal/${destination_number}$${domain}/ /condition /extension这个配置的作用是当分机拨打**1002这样的转移号码时FreeSWITCH先把目标号码记录下来等当前通话结束后自动把另一段桥接过去。这个模式相当于把盲转逻辑完全放到服务端前端只负责发起一个“预约转移”对前端来说更安全不会因为网络波动导致REFER丢失。6. 调试过程中最值得记录的五个问题6.1 WSS证书不受信任浏览器直接拒绝连接开发环境最常见的问题。Chrome对WSS的证书校验非常严格如果使用FreeSWITCH自带的自签名证书浏览器控制台报ERR_CERT_AUTHORITY_INVALIDsip.js会卡在WebSocket连接阶段UA永远不会进入registered状态。解决方案有两条路一是把自签名证书导入到系统的信任链里但每台办公电脑都得做一遍不现实二是用内网自建的CA签发证书同时配置内部DNS域名解析让所有办公电脑信任同一个根证书这样一次性解决所有WSS连接问题。前者适合本地开发后者适合企业部署。6.2 注册成功但呼出失败SDP音频编解码不匹配这个问题的典型表现是sip.js已经注册成功UI显示在线但一打电话FreeSWITCH日志立刻报1060 ERROR或者488 Not Acceptable Here原因基本都在编解码协商。WebRTC默认使用Opus编码但FreeSWITCH分机注册时如果只启用了PCMU/PCMA双方在SDP里找不到共同编解码就会协商失败。解决方式是在WebRTC分机所在的dialplan接听端设置setPCMA和setOPUS变量或者在FreeSWITCH的vars.xml里把outbound_codec_prefs和inbound_codec_prefs都加上OPUS。X-PRE-PROCESS cmdset dataglobal_codec_prefsOPUS,PCMU,PCMA/ X-PRE-PROCESS cmdset dataoutbound_codec_prefsOPUS,PCMU,PCMA/ X-PRE-PROCESS cmdset datainbound_codec_prefsOPUS,PCMU,PCMA/改完重启FreeSWITCH或执行reloadxml后重新注册分机。6.3 呼入时浏览器不响铃排查发现是autoplay策略手机端和部分桌面浏览器对音频自动播放有严格限制页面加载后如果用户没有点击过页面AudioContext默认是suspended状态sip.js的振铃音不会响。这个问题一度让我误以为是呼入事件没触发。解决办法是在页面初始化时监听用户的第一次点击主动调用一次audioContext.resume()同时在用户点击“登录”或“接听”按钮后提前创建AudioContext并解锁。6.4 呼叫保持后恢复对方听不到声音这种情况多发生在sip.js的hold与FreeSWITCH的MOH播放冲突时。当sip.js发送re-INVITE将媒体方向改为inactive后FreeSWITCH开始播放MOH给对端这个MOH流来自本地文件方向是FreeSWITCH到对端。当sip.js发送unhold恢复时方向变为sendrecv理论上FreeSWITCH会停止MOH播放但某些版本下MOH播放不会立即停止导致对端在恢复通话后的前几秒还能听到背景音乐。解决方案有两个一是升级到最新1.10.x补丁版本二是在unhold成功后等200~300ms再播放本地来电提示音给FreeSWITCH一个释放MOH stream的时间窗口。6.5 NAT穿透问题办公网络和家庭网络下RTP不通WebRTC的SRTP流量在NAT环境下一般能通过ICE协商穿透但如果FreeSWITCH部署在云服务器上且没有正确配置公网IP媒体流会绑定在内网IP上导致浏览器无法接收RTP包。在FreeSWITCH的internal.xml里加上param nameext-rtp-ip value你的公网IP或域名/ param nameext-sip-ip value你的公网IP或域名/如果是动态IP环境配合STUN服务可以自动探测公网IPsip.js在WebRTC层也会做ICE协商双管齐下基本能解决。7. 一个可以直接跑起来的最小Demo逻辑前面技术点讲得比较分散这里整理一份最小Demo完整流程方便你照着搭一个能跑通的骨架操作流程分成四步FreeSWITCH侧准备好一个WebRTC分机比如1001密码123456确保5066和7443端口在监听。前端搭建一个静态页面引入sip.js 0.21.x的UMD包。页面提供四个按钮拨号输入框呼叫按钮、接听按钮、挂断按钮、保持/恢复按钮。页面加载后用1001注册UA注册成功后在页面顶部显示“在线”状态。注册成功后用另一个SIP客户端比如Zoiper配置同一个分机或者再注册一个1002拨打1001页面会触发invite事件并显示来电点击接听即可通话。拨出时输入1002FreeSWITCH会路由到1002分机。保持和转移的逻辑前文都有对应代码组合进页面即可。下面是一段最小页面骨架的核心结构!DOCTYPE html html head meta charsetUTF-8/ titleWebRTC 网页电话Demo/title script srchttps://unpkg.com/sip.js0.21.2/dist/sip.min.js/script /head body div idstatus未注册/div input idcallee placeholder输入分机号或手机号 / button idcallBtn呼叫/button button idanswerBtn接听/button button idhangupBtn挂断/button button idholdBtn保持/button script srcphone.js/script /body /html对应的phone.js就是前面所有核心代码块的集合只需要把其中的updatePhoneStatus等UI方法映射到DOM元素上即可。8. 线上部署还需要注意的几件事8.1 一定要用HTTPS和WSS浏览器安全策略是死的非安全上下文调用getUserMedia会被拒绝WSS证书错误会导致WebSocket连接失败。生产环境建议在Nginx层统一终止HTTPS再反向代理到前端静态资源WSS则直接由FreeSWITCH的7443端口提供。如果企业内部强制要求所有流量都走Nginx可以配置Nginx的stream模块把7443端口代理到FreeSWITCH但注意WebSocket的Upgrade头需要正确透传。8.2 并发数评估与FreeSWITCH性能sip.js直连方案下每一路通话在FreeSWITCH中会创建一个B2BUA session内部包含两个leg一个leg对浏览器侧走WebRTC另一个leg对PSTN或者另一个WebRTC分机。一个FreeSWITCH实例在普通2核4G云服务器上跑几十路并发没有问题但要注意媒体转发都是CPU密集任务尤其是有编解码转换的时候。如果预期并发超过100路建议横向扩容FreeSWITCH或者评估引入WebRTC网关做SFU。8.3 录音与合规网页端通话如果需要录音可以在FreeSWITCH的dialplan里加record_session应用录音文件格式默认为wav路径可配置。录音功能上线前务必确认本地的通话录音合规要求尤其是涉及客服业务时需要自动语音提示告知对方“本次通话可能被录音”。8.4 前端状态机设计网页电话最怕状态混乱正在通话中收到新来电、保持状态下挂断、转移过程中用户又点了拨号这些边界场景如果不做一个状态机页面会进入一个用户看不懂的状态。我的做法是把电话状态机抽象成idle/ringing/calling/connected/held/transferring六个枚举每个用户操作只允许在特定状态下触发其余操作直接置灰或者拦截。9. 实际使用后的感受和建议整套方案跑通之后我的最大感受是sip.js直连FreeSWITCH的路线是这个需求里“技术复杂度和可控性”平衡得最好的方案没有之一。它的核心优势在于信令路径直接、问题容易定位、FreeSWITCH侧能力完全暴露给前端开发者。团队里一个熟悉JavaScript的开发就能搞定网页端全部功能后台同事只需要维护FreeSWITCH的XML配置两边通过SIP协议握手边界清晰。如果你在一个纯前端团队里没有SIP背景我建议先把FreeSWITCH的自带Demo跑通再用Zoiper或者microSIP这样的软电话验证分机注册和互拨最后再写网页端代码。这是我犯过的最大错误一上来就写前端结果分机注册都失败排查了半天才发现是TLS证书没配好。从最小的环节开始验证每一层都确定没问题再往上叠加这是电信级系统开发的通用方法WebRTC电话也一样。如果后续想继续扩展可以把目光放到多方会议、通话队列、语音菜单、自动外呼这些场景FreeSWITCH都原生支持。但核心的sip.js FreeSWITCH WebRTC三角关系只要吃透后面那些高阶功能都会水到渠成。本文还有配套的精品资源点击获取
返回列表