简介:这份资源是面向WebRTC语音通信开发者的实战示例包,聚焦sipjs与FreeSWITCH的整合应用,解决网页端实现电话呼入、呼出、转移与保持等核心功能的落地问题。适合具备一定前端与SIP协议基础、希望快速验证软电话能力的开发者参考。压缩包共4个文件,包含1个html页面、1个js脚本、1个css样式表与1个txt说明文档,整体约79KB,结构精简,其中html承载界面与呼叫逻辑,js负责SIP信令交互,css处理通话面板样式,txt提供使用指引。在谷歌浏览器下运行后,只需将分机号、密码与服务器地址替换为自己的配置,即可直接进行功能测试,省去从零搭建信令环境的成本。目前已有2315人学习下载,可作为理解sipjs呼叫流程、排查注册与媒体协商问题的实用起点。
1. 浏览器里跑起一通真实电话:SIP.js 加 FreeSWITCH 的 WebRTC 落地场景
很多做企业通信的团队都遇到过这个需求:客户不想装软电话,就想在浏览器网页里直接打电话——能呼入、能呼出、能转接、能保持。听起来简单,真动手才发现坑不少:信令走不通、媒体没声音、转接后对方听不到、保持之后恢复不了。这套基于 SIP.js 加 FreeSWITCH 的 WebRTC 网页端示例,解决的正是这个场景。它把浏览器当 SIP 终端,通过 WebSocket 连 FreeSWITCH 做信令,用 WebRTC 承载媒体,实现完整的呼叫控制。适合做呼叫中心、客服工作台、内部通信面板的前后端工程师,也适合想搞懂 WebRTC 电话链路的技术负责人。下面按「是什么、怎么搭、坑在哪、怎么进阶」的顺序拆开讲。
2. 信令与媒体怎么分工:SIP.js 和 FreeSWITCH 的职责边界
2.1 为什么是 SIP.js 而不是自己撸 WebSocket
浏览器原生没有 SIP 协议栈,要打电话就得有人把 SIP 消息翻译成 WebSocket 帧。SIP.js 干的就是这件事:它把 REGISTER、INVITE、ACK、BYE、REFER 这些 SIP 方法封装成 JavaScript API,开发者调invite()、hold()、transfer()就行,不用手拼 SIP 报文。常见做法是前端只关心「拨号、挂断、保持、转接」四个动作,底层信令交给 SIP.js 的 UserAgent 和 Session 对象管理。
选型上要注意版本差异。SIP.js 0.21 之后 API 改动较大,SimpleUser和UserAgent两套用法并存。示例里如果用SimpleUser,代码量少但可控性弱;用UserAgent加Registerer加Inviter手动组装,灵活但要多写几十行。我一般建议先跑通SimpleUser,确认链路没问题再换成底层 API 做定制。
2.2 FreeSWITCH 侧要开哪些口子
FreeSWITCH 在这里扮演 SIP 代理加媒体网关。浏览器发来的 WebSocket 信令走mod_sofia的 WSS 监听,媒体走mod_rtc或内置的 WebRTC 支持。关键配置集中在 sofia profile 里:
<!-- conf/sip_profiles/internal.xml 关键片段 --> <param name="wss-binding" value=":7443"/> <param name="rtp-secure-media" value="mandatory"/> <param name="apply-candidate-acl" value="wan_v4.auto"/>wss-binding指定 WebSocket Secure 监听端口,浏览器必须用wss://连接,ws://在 HTTPS 页面下会被浏览器直接拦掉。rtp-secure-media设为mandatory强制 SRTP,因为 WebRTC 的媒体必须加密,明文 RTP 会被浏览器拒绝。apply-candidate-acl决定 FreeSWITCH 怎么处理 ICE candidate,内网环境可以放宽,公网部署要配合 STUN/TURN。
2.3 一次呼出到底走了哪些步骤
把呼出流程拆开看,每一步都有对应的代码位置:
- 页面加载后创建
UserAgent,传入wss://freeswitch:7443和分机账号密码。 - 调用
register()发 REGISTER,FreeSWITCH 返回 200 OK,状态变Registered。 - 用户点拨号,
invite()发 INVITE,SDP 里带 WebRTC 的 offer(含 ICE、DTLS 指纹)。 - FreeSWITCH 回 200 OK 带 answer SDP,双方做 ICE 连通性检查和 DTLS 握手。
- 媒体通道建立,
session.state变Established,可以通话。 - 挂断时发 BYE,FreeSWITCH 回 200,会话销毁。
// 呼出核心逻辑,基于 SIP.js UserAgent const ua = new SIP.UserAgent({ uri: SIP.UserAgent.makeURI(`sip:1001@freeswitch`), transportOptions: { server: 'wss://freeswitch:7443' }, authorizationUsername: '1001', authorizationPassword: 'secret', delegate: { onInvite(invitation) { // 呼入时触发,决定接听还是拒绝 invitation.accept(); } } }); // 注册 const registerer = new SIP.Registerer(ua); await ua.start(); await registerer.register(); // 呼出 const target = SIP.UserAgent.makeURI('sip:1002@freeswitch'); const inviter = new SIP.Inviter(ua, target); await inviter.invite();这段代码里transportOptions.server必须是wss://,authorizationUsername对应 FreeSWITCH 里的分机号。onInvite是呼入入口,不接就挂。inviter.invite()返回 Promise,resolve 只代表信令通了,媒体是否通要看session.state和实际有没有声音。
提示:
ua.start()和registerer.register()都要 await,顺序不能反。先 start 再 register,否则 REGISTER 发不出去。
3. 呼入、呼出、转移、保持:四个动作的代码实现与参数
3.1 呼入接听与自动应答
呼入的处理入口在delegate.onInvite。默认行为是弹个提示让用户点接听,但客服场景往往要自动应答。自动应答直接调invitation.accept(),但要注意浏览器麦克风权限:没授权之前 accept 会失败,媒体流拿不到。
delegate: { onInvite(invitation) { // 先检查麦克风权限 navigator.mediaDevices.getUserMedia({ audio: true }) .then(() => { invitation.accept({ sessionDescriptionHandlerOptions: { constraints: { audio: true, video: false } }}); }) .catch(err => { console.error('麦克风未授权', err); invitation.reject(); }); } }constraints里audio: true是必须的,video: false表示纯语音。如果要做视频通话就改成video: true,但 FreeSWITCH 侧要额外配视频编解码。accept()之后会话进入Establishing,等 ICE 和 DTLS 完成才变Established。
3.2 呼出参数与超时控制
呼出时除了目标 URI,还要控制超时和早期媒体。默认 SIP.js 的 INVITE 超时是 60 秒左右,太长,客服场景一般 30 秒没接就取消。
const inviter = new SIP.Inviter(ua, target, { sessionDescriptionHandlerOptions: { constraints: { audio: true, video: false } }, earlyMedia: true // 接收回铃音等早期媒体 }); // 30 秒超时取消 const timeout = setTimeout(() => { if (inviter.state === SIP.SessionState.Establishing) { inviter.cancel(); } }, 30000); inviter.stateChange.addListener((state) => { if (state === SIP.SessionState.Established) { clearTimeout(timeout); } }); await inviter.invite();earlyMedia: true让浏览器能播对方回铃音,不然拨号后一片安静,用户以为没反应。inviter.cancel()发 CANCEL 取消未接通的 INVITE,和bye()不同,后者是已接通后挂断。
3.3 保持与恢复:re-INVITE 的门道
保持的本质是发一个 re-INVITE,把 SDP 里的媒体方向改成a=inactive或a=sendonly。SIP.js 封装成session.hold()和session.unhold()。
// 保持 await session.hold(); // 此时 session.state 仍是 Established,但媒体停了 // 恢复 await session.unhold();坑在于:有些 FreeSWITCH 配置下hold()之后对方听到的是静音而不是保持音乐,需要在 dialplan 里配hold_music。另外保持期间如果浏览器切到后台,定时器可能被节流,恢复时 re-INVITE 延迟,表现为「点了恢复没反应」,实际是消息晚了几秒。
3.4 转移:REFER 与 attended transfer 的区别
转移分两种。盲转(blind transfer)用 REFER,把对方直接甩给第三方,自己退出。 attended transfer 是先保持当前通话,再呼第三方,接通后把两路桥接。SIP.js 对盲转支持较好:
// 盲转:把当前通话转给 1003 const referTarget = SIP.UserAgent.makeURI('sip:1003@freeswitch'); await session.refer(referTarget);refer()发 REFER,FreeSWITCH 收到后代替发起方去呼 1003。注意 REFER 的接受方是 FreeSWITCH 不是对方终端,所以 dialplan 要允许 REFER 处理。 attended transfer 在 SIP.js 里没有现成 API,要手动组合:先hold()当前会话,再invite()第三方,接通后调session.refer()带Replaces头。这块复杂度高,示例里如果只做盲转,先跑通再扩展。
注意:转移失败最常见的原因是 FreeSWITCH 的
transfer权限没开,或者 dialplan 里没匹配 REFER 的目标分机。
4. 避坑与排查:媒体不通、注册失败、转接断线的血泪经验
4.1 注册失败:401 和 403 怎么区分
现象:registerer.register()报错,状态一直是Unregistered。
原因:401 是认证挑战,说明账号密码不对或authorizationUsername和 URI 里的用户不一致;403 是 FreeSWITCH 直接拒绝,通常是 ACL 限制或分机没启用。
解决:先看 FreeSWITCH 控制台sofia status profile internal reg有没有注册记录。401 就核对authorizationUsername和authorizationPassword,注意 URI 里的 domain 要和 FreeSWITCH 的sip_domain一致。403 检查conf/directory/default/下分机 XML 是否存在且enabled。
4.2 能振铃但没声音:ICE 和 DTLS 的锅
现象:拨号后对方振铃,接起来双方都听不到声音,但信令显示Established。
原因:媒体通道没建起来。要么 ICE candidate 交换失败,要么 DTLS 握手没过。WebRTC 强制 SRTP,FreeSWITCH 如果没开rtp-secure-media就会协商失败。
解决:浏览器开chrome://webrtc-internals看 ICE 状态和 DTLS 状态。ICE 卡在checking说明 candidate 不通,内网检查防火墙有没有放行 UDP 端口段,公网要配 STUN。DTLS 失败看 FreeSWITCH 日志有没有DTLS handshake failed,有就确认rtp-secure-media是mandatory或optional。
4.3 转接后原通话断掉
现象:点转移,当前通话直接挂断,第三方也没振铃。
原因:REFER 发出去后 FreeSWITCH 没正确处理,或者refer()的 target 格式不对。SIP.js 的refer()要求传入URI对象,传字符串会静默失败。
解决:确认SIP.UserAgent.makeURI()返回的不是 undefined。再看 FreeSWITCH dialplan 里有没有transfer的 extension。如果用的是session.refer()后原会话立刻Terminated,说明 FreeSWITCH 把 REFER 当成了挂断,检查mod_sofia的refer相关参数。
4.4 保持后恢复没反应
现象:hold()成功,unhold()调了但对方还是听不到。
原因:re-INVITE 的 SDP 方向没更新,或者浏览器定时器节流导致消息延迟。
解决:unhold()后监听session.stateChange,确认回到Established。如果状态对但没声音,抓包看 re-INVITE 的 SDP 里a=sendrecv有没有恢复。浏览器后台节流问题,可以在页面 visible 时再调unhold(),或者用setInterval保活。
4.5 HTTPS 页面下 wss 连不上
现象:本地http://localhost能连,部署到https://域名后 WebSocket 直接报错。
原因:浏览器混合内容策略,HTTPS 页面不允许连ws://,必须wss://。而且wss的证书要受信任,自签证书会被拒。
解决:FreeSWITCH 的wss-binding配成:7443并挂有效证书。开发阶段可以用mkcert生成本地信任证书,生产环境用正规 CA 证书。确认证书链完整,不然浏览器报ERR_CERT_AUTHORITY_INVALID。
5. 进阶:用 SIP.js 事件钩子做通话质量监控与自动重连
跑通基本呼叫之后,真正让这套东西能上生产的是两件事:通话质量可观测,以及断线能自愈。SIP.js 提供了session.sessionDescriptionHandler和ua.transport的事件钩子,可以拿到 WebRTC 的统计信息和连接状态。
先看质量监控。WebRTC 的RTCPeerConnection.getStats()能拿到丢包率、抖动、往返时延。在session.stateChange变成Established后挂一个定时器采集:
session.stateChange.addListener(async (state) => { if (state === SIP.SessionState.Established) { const pc = session.sessionDescriptionHandler.peerConnection; const statsTimer = setInterval(async () => { const stats = await pc.getStats(); stats.forEach(report => { if (report.type === 'inbound-rtp' && report.kind === 'audio') { console.log('丢包', report.packetsLost, '抖动', report.jitter); } if (report.type === 'candidate-pair' && report.state === 'succeeded') { console.log('RTT', report.currentRoundTripTime); } }); }, 5000); session.statsTimer = statsTimer; } if (state === SIP.SessionState.Terminated) { clearInterval(session.statsTimer); } });packetsLost持续增长说明网络丢包,jitter超过 30ms 语音会卡顿,currentRoundTripTime超过 300ms 会有明显延迟。这些指标可以上报到监控面板,客服主管就能看到哪通电话质量差。
再看自动重连。WebSocket 断开时ua.transport会触发onDisconnect,默认不会自动重连。加一段重连逻辑:
ua.transport.onDisconnect = () => { console.warn('WebSocket 断开,5 秒后重连'); setTimeout(async () => { try { await ua.reconnect(); await registerer.register(); console.log('重连并重新注册成功'); } catch (e) { console.error('重连失败', e); } }, 5000); };ua.reconnect()重建 WebSocket,registerer.register()重新注册。注意重连后原来的 session 已经失效,如果通话中断要提示用户重新拨号。重连间隔别设太短,5 秒起步,避免频繁重试把 FreeSWITCH 打满。
| 监控指标 | 正常范围 | 异常处理 |
|---|---|---|
| packetsLost | 每分钟小于 5 | 检查网络链路 |
| jitter | 小于 30ms | 排查 Wi-Fi 干扰 |
| RTT | 小于 300ms | 确认服务器位置 |
| DTLS 状态 | connected | 检查证书和 SRTP 配置 |
最后说个习惯。我每次部署这套东西,都会先在chrome://webrtc-internals里完整走一遍呼出、保持、恢复、转移,确认 ICE 和 DTLS 全绿再交付。有一次偷懒跳过,结果客户那边防火墙拦了 UDP,排查了一下午。从那以后我每次上线前都强制走一遍完整链路验证,包括断网重连。希望帮到你。
本文还有配套的精品资源,点击获取