☰
基于sipjs与FreeSWITCH的WebRTC网页电话坐席实战
2026/10/20 7:07:25 网站建设 项目流程

简介:这份资源是面向WebRTC语音通信开发者的实战示例包,聚焦sipjs与FreeSWITCH的整合应用,解决网页端实现电话呼入、呼出、转移与保持等核心功能的落地问题。适合具备一定前端与SIP协议基础、希望快速验证软电话能力的开发者参考。压缩包共4个文件,包含1个html页面、1个js脚本、1个css样式表与1个txt说明文档,整体约79KB,结构精简,其中html承载界面与呼叫逻辑,js负责SIP信令交互,css控制通话面板样式,txt提供使用指引。在谷歌浏览器下运行后,只需将分机号、密码与服务器地址替换为自己的配置,即可直接进行功能测试,省去从零搭建信令环境的成本。目前已有2315人学习下载,读者可借此理解sipjs在FreeSWITCH场景下的注册、呼叫与媒体协商流程,并作为二次开发或排错的基础模板。

1. 浏览器里跑起一通真实电话:sipjs+FreeSWITCH+WebRTC 网页端坐席到底能干什么

很多做企业通信的开发者第一次听到「网页打电话」,脑子里浮现的是那种点一下弹个提示、根本拨不出去的演示 Demo。但真正落到客服坐席、外呼中心、内部总机这类场景,需求是很硬的:浏览器里要能呼入接听、呼出拨号、通话中转移、临时保持,还要跟后端的软交换打通,通话状态实时同步到页面上。这套示例干的就是这件事——前端用 sipjs 做 SIP over WebSocket 的信令层,媒体走 WebRTC,后端交给 FreeSWITCH 做注册、路由和媒体处理。它适合两类人:一类是想把传统话机搬到浏览器里的通信开发者,另一类是手里已经有 FreeSWITCH、缺一个能跑通的网页坐席前端的工程师。下面我按「先看清结构、再跑起来、最后能改」的顺序,把这份源码拆开讲。

2. 架构与目录拆解:信令、媒体、状态三条线怎么串起来

2.1 三层结构:浏览器、信令通道、软交换

这套东西的本质是一条「信令走 WebSocket、媒体走 SRTP」的双通道链路。浏览器端 sipjs 负责把 SIP 消息封装成 WebSocket 帧发给 FreeSWITCH 的ws或wss端口,FreeSWITCH 收到后按 dialplan 路由,媒体则通过 WebRTC 的 ICE/DTLS-SRTP 协商,直接和 FreeSWITCH 的媒体端口对接。理解这条链路,后面所有代码都能对上号。

浏览器 (sipjs + WebRTC) │ SIP over WebSocket (信令) ▼ FreeSWITCH (mod_sofia + mod_verto/ws) │ dialplan 路由 / 桥接 ▼ PSTN 网关 或 其他 SIP 终端 (被叫)

关键点在于:信令和媒体是分开的两条路。信令断了通话不一定断,媒体断了页面上的状态可能还显示「通话中」。这是后面排查问题时最容易翻车的地方,先记住。

2.2 前端目录与核心文件职责

源码前端部分通常长这样,我按实际拆过的项目给你还原一下结构:

webphone/ ├── index.html # 坐席主界面:拨号盘、通话控制按钮 ├── js/ │ ├── sipClient.js # sipjs 封装:注册、呼叫、挂断、转移 │ ├── ui.js # 按钮事件绑定、状态渲染 │ └── config.js # 服务器地址、账号、stun 配置 ├── css/ │ └── style.css └── lib/ └── sip.min.js # sipjs 库

sipClient.js是整个前端的黑匣子,所有 SIP 交互都收在这里;ui.js只负责把用户点击翻译成对sipClient的调用,再把回调里的状态画到界面上。这种分层的好处是:换 UI 不用动信令,换信令库也不用重写界面。

2.3 后端 FreeSWITCH 侧要配什么

FreeSWITCH 这边核心是三块配置:sofia的 profile 要开 WebSocket 监听,dialplan 要能识别浏览器分机并路由,用户目录里要有对应的分机账号。常见做法是在internalprofile 里加一行ws-binding,然后给每个坐席建一个分机。

<!-- conf/sip_profiles/internal.xml 片段 --> <param name="ws-binding" value=":5066"/> <param name="wss-binding" value=":7443"/>

ws-binding是明文 WebSocket,wss-binding是加密的。生产环境必须用wss,因为浏览器在 HTTPS 页面下不允许连非加密的ws,这是硬性限制,不是可选项。分机账号则在conf/directory/default/下按号码建 XML,设置password和user_context。

2.4 一次呼出请求的完整链路

把链路串一遍,你就知道每个模块在哪一步发力。用户点「拨号」→ui.js拿到号码调sipClient.call()→ sipjs 发 INVITE 到 FreeSWITCH → FreeSWITCH 查 dialplan 决定是桥到 PSTN 还是转给另一个分机 → 被叫振铃,回 180/183 → 双方协商 SDP,WebRTC 建媒体 → 接通回 200 OK,页面状态切到「通话中」。整个过程里,页面要监听的不只是「接通」,还有振铃、失败、被拒这些中间态,否则用户点了拨号半天没反应,体验直接崩。

3. 跑起来:从 FreeSWITCH 配置到浏览器注册的完整步骤

3.1 环境准备与依赖确认

先把后端跑起来,再调前端,顺序反了会浪费大量时间在「到底是谁的问题」上。FreeSWITCH 建议用较新的稳定版,编译时确认mod_sofia已加载,WebSocket 支持是 sofia 自带的,不用额外装模块。前端只需要一个静态服务器,因为浏览器加载本地file://时 WebSocket 和 getUserMedia 都会受限。

# 确认 sofia 模块已加载 fs_cli -x "module_exists mod_sofia" # 查看 ws 监听是否生效 fs_cli -x "sofia status profile internal" | grep -i ws

如果第二条命令看不到ws-binding相关输出,说明 profile 没重载或配置写错了位置。改完internal.xml后要reloadxml再sofia profile internal restart,两步都不能省。

3.2 FreeSWITCH 分机与 dialplan 配置

给坐席建分机,号码用 1000 起,方便记。目录文件里最关键的是password,前端注册要用同一个。

<!-- conf/directory/default/1000.xml --> <include> <user id="1000"> <params> <param name="password" value="你的密码"/> </params> <variables> <variable name="user_context" value="default"/> </variables> </user> </include>

dialplan 里加一条,让浏览器分机拨任意号码都能出去,实际项目里你会按前缀分流,比如9开头走外线。

<!-- conf/dialplan/default.xml 片段 --> <extension name="webphone_outbound"> <condition field="destination_number" expression="^(\d+)$"> <action application="bridge" data="user/$1"/> </condition> </extension>

bridge的data决定这通电话去哪。user/1001是转给另一个分机,sofia/gateway/网关名/号码是走外线。改这里就是改路由策略,是二次开发最常动的地方。

3.3 前端 sipjs 注册与呼叫代码

前端核心就两段:注册和呼叫。先看注册,这是所有操作的前提。

// js/sipClient.js 注册部分 const config = { uri: 'sip:1000@你的服务器地址', password: '你的密码', wsServer: 'wss://你的服务器地址:7443', // 生产必须 wss }; const ua = new SIP.UserAgent({ uri: SIP.UserAgent.makeURI(config.uri), transportOptions: { server: config.wsServer }, authorizationPassword: config.password, authorizationUsername: '1000', }); // 注册状态回调,页面据此显示「在线/离线」 ua.stateChange.addListener((state) => { console.log('UA 状态:', state); // 期望看到 Registered }); ua.start().then(() => { const registerer = new SIP.Registerer(ua); registerer.register(); });

UserAgent是 sipjs 的核心对象,一个页面一个就够。transportOptions.server指向 FreeSWITCH 的 wss 端口,authorizationUsername要和分机号一致。注册成功后ua.state会变成Registered,如果一直是Started或报 401/403,八成是密码或分机号对不上。

呼叫部分要处理媒体,这是 WebRTC 的活:

// 发起呼叫 async function makeCall(targetNumber) { const target = SIP.UserAgent.makeURI(`sip:${targetNumber}@你的服务器地址`); const inviter = new SIP.Inviter(ua, target); // 关键:先拿到本地麦克风,再发 INVITE const session = inviter.session; session.descriptionHandler = async (description) => { const pc = session.sessionDescriptionHandler.peerConnection; const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); stream.getTracks().forEach((track) => pc.addTrack(track, stream)); return description; }; await inviter.invite(); return inviter; }

getUserMedia必须在用户手势(点击)之后调用,否则浏览器会拦截。descriptionHandler是 sipjs 处理 SDP 协商的钩子,媒体轨道就是在这里挂上去的。少了这段,信令能通但没声音,是新手最常见的坑。

3.4 转移与保持的实现要点

转移分两种:盲转(blind transfer)和咨询转(attended transfer)。盲转直接把对方转走,自己退出;咨询转是先和第三方通话,确认后再把原通话转过去。sipjs 里盲转用refer:

// 盲转:把当前通话转给 targetNumber async function blindTransfer(session, targetNumber) { const target = SIP.UserAgent.makeURI(`sip:${targetNumber}@你的服务器地址`); await session.refer(target); }

保持(hold)则是通过重新协商 SDP,把媒体方向设为sendonly或inactive:

// 保持:把媒体方向改为只发不收 async function hold(session) { const pc = session.sessionDescriptionHandler.peerConnection; pc.getSenders().forEach((sender) => { if (sender.track) sender.track.enabled = false; // 静音本地 }); await session.invite(); // 重新协商 }

track.enabled = false是本地静音,真正的保持还要让 SDP 的a=sendonly生效,FreeSWITCH 侧才会放保持音乐。这两步配合不好,会出现「页面显示保持但对方还能听到你说话」的玄学现象。

4. 避坑与排查:这几处不踩一遍不算跑通过

4.1 注册一直失败,报 401 或 403

现象:ua.state停在Started,控制台反复出现 401 Unauthorized。原因:authorizationUsername和分机号不一致,或者密码里带了特殊字符没转义。解决:先确认conf/directory/default/1000.xml里的password和前端config.password完全一致,再检查用户名是不是纯数字分机号。密码含@、#这类字符时,前端要 URL 编码。

4.2 能接通但双方都没声音

现象:信令显示 200 OK,通话计时在走,但听不到任何声音。原因:媒体轨道没挂上,或者 ICE 没协商通。前者是descriptionHandler没写对,后者是 STUN/TURN 没配。解决:先在chrome://webrtc-internals里看 ICE 状态是不是connected。如果是checking卡住,说明 NAT 穿透失败,得在 sipjs 的sessionDescriptionHandlerOptions里加 STUN 服务器。内网环境通常不用,跨公网必配。

4.3 HTTPS 页面连不上 wss

现象:页面部署在 HTTPS 下,WebSocket 连接直接报错,连注册都进不去。原因:浏览器安全策略禁止 HTTPS 页面连非加密的ws://,这是硬限制。解决:FreeSWITCH 必须开wss-binding,前端wsServer用wss://。证书可以是自签的,但浏览器要手动信任一次,生产环境用正规证书。

4.4 转移后原通话没挂断

现象:点了盲转,第三方接通了,但自己的页面还显示「通话中」,挂不断。原因:refer发出后没监听accepted事件,页面状态没更新。解决:给session加refer相关的事件监听,收到accepted后主动hangup本地会话。FreeSWITCH 侧也要确认 dialplan 允许refer,有些默认配置会拦截。

4.5 保持后对方听到的是静音而不是保持音乐

现象:点了保持,对方那边一片安静,以为掉线了。原因:只做了本地静音,没触发 FreeSWITCH 的hold应用。解决:保持要走完整的 SDP 重协商,让 FreeSWITCH 识别到sendonly,它才会放local_stream://moh。dialplan 里可以显式配hold动作,或者确认 profile 的hold-music参数指向了正确的音乐文件。

5. 二次开发:把示例改成能上生产的坐席系统

示例能跑通只是起点,真上生产要补的东西不少。我按改造优先级给你排一下。

第一,状态管理要收口。示例里状态散落在各个回调里,坐席一多就乱。我一般会引入一个状态机,把Idle / Ringing / Active / Held / Transferring这几个态显式定义出来,所有 UI 渲染只读状态机,不直接读 sipjs 的回调。这样加「通话中禁止再次拨号」这类规则时,改一处就够。

第二,媒体设备要能选。生产坐席经常要切麦克风和扬声器。getUserMedia的audio参数可以传deviceId,配合navigator.mediaDevices.enumerateDevices()列出设备,做个下拉框。切换设备时要重新协商,不能只换 track。

第三,断线重连要兜底。WebSocket 断了 sipjs 不会自动重注册,得监听disconnected事件手动重连。我的习惯是加一个指数退避的重连逻辑,最多重试 5 次,超过就提示坐席刷新页面。

第四,通话记录要落库。前端只负责展示,真正的通话详单(CDR)在 FreeSWITCH 侧。可以开mod_cdr_csv或mod_cdr_pg_csv,把每通电话的起止时间、主被叫、挂断原因写进数据库,前端通过接口查。

第五,权限和坐席绑定。示例里分机号是写死的,生产要从登录态里拿。常见做法是登录后后端返回该坐席的分机号和临时密码,前端动态构造config,避免把密码硬编码在 JS 里。

验证改造是否成功,我有个笨办法但很管用:拿两台设备,一台拨一台接,把呼入、呼出、保持、盲转、咨询转各走一遍,每步都看chrome://webrtc-internals的 ICE 状态和 FreeSWITCH 的fs_cli日志。两边对得上,才算真的通了。

# 实时看 FreeSWITCH 信令日志,排查时必开 fs_cli -x "console loglevel 7" fs_cli -x "sofia global siptrace on"

siptrace打开后能看到完整的 SIP 消息流,INVITE、200 OK、REFER 每一步都清清楚楚。排查转移和保持的问题,全靠它。

从那以后我每次改完 dialplan 或前端信令,都强制走一遍「注册→呼出→保持→转移→挂断」的完整链路,不跳步。因为这套东西的坑大多藏在状态切换的缝隙里,单测一个功能永远发现不了。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询