SIP.js 与 FreeSWITCH 实现 WebRTC 网页电话:呼入呼出、保持转接与避坑指南

发布时间:2026/10/20 2:54:51
SIP.js 与 FreeSWITCH 实现 WebRTC 网页电话:呼入呼出、保持转接与避坑指南 简介这份资源是面向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 namewss-binding value:7443/ param namertp-secure-media valuemandatory/ param nameapply-candidate-acl valuewan_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()发 REGISTERFreeSWITCH 返回 200 OK状态变Registered。用户点拨号invite()发 INVITESDP 里带 WebRTC 的 offer含 ICE、DTLS 指纹。FreeSWITCH 回 200 OK 带 answer SDP双方做 ICE 连通性检查和 DTLS 握手。媒体通道建立session.state变Established可以通话。挂断时发 BYEFreeSWITCH 回 200会话销毁。// 呼出核心逻辑基于 SIP.js UserAgent const ua new SIP.UserAgent({ uri: SIP.UserAgent.makeURI(sip:1001freeswitch), 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:1002freeswitch); const inviter new SIP.Inviter(ua, target); await inviter.invite();这段代码里transportOptions.server必须是wss://authorizationUsername对应 FreeSWITCH 里的分机号。onInvite是呼入入口不接就挂。inviter.invite()返回 Promiseresolve 只代表信令通了媒体是否通要看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 里的媒体方向改成ainactive或asendonly。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:1003freeswitch); await session.refer(referTarget);refer()发 REFERFreeSWITCH 收到后代替发起方去呼 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 强制 SRTPFreeSWITCH 如果没开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 里asendrecv有没有恢复。浏览器后台节流问题可以在页面 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()重建 WebSocketregisterer.register()重新注册。注意重连后原来的 session 已经失效如果通话中断要提示用户重新拨号。重连间隔别设太短5 秒起步避免频繁重试把 FreeSWITCH 打满。监控指标正常范围异常处理packetsLost每分钟小于 5检查网络链路jitter小于 30ms排查 Wi-Fi 干扰RTT小于 300ms确认服务器位置DTLS 状态connected检查证书和 SRTP 配置最后说个习惯。我每次部署这套东西都会先在chrome://webrtc-internals里完整走一遍呼出、保持、恢复、转移确认 ICE 和 DTLS 全绿再交付。有一次偷懒跳过结果客户那边防火墙拦了 UDP排查了一下午。从那以后我每次上线前都强制走一遍完整链路验证包括断网重连。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询