news 2026/10/20 2:54:50

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 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 一次呼出到底走了哪些步骤

把呼出流程拆开看,每一步都有对应的代码位置:

  1. 页面加载后创建UserAgent,传入wss://freeswitch:7443和分机账号密码。
  2. 调用register()发 REGISTER,FreeSWITCH 返回 200 OK,状态变Registered。
  3. 用户点拨号,invite()发 INVITE,SDP 里带 WebRTC 的 offer(含 ICE、DTLS 指纹)。
  4. FreeSWITCH 回 200 OK 带 answer SDP,双方做 ICE 连通性检查和 DTLS 握手。
  5. 媒体通道建立,session.state变Established,可以通话。
  6. 挂断时发 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,排查了一下午。从那以后我每次上线前都强制走一遍完整链路验证,包括断网重连。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 2:54:34

前端URL参数获取与Cookie操作实战:核心原理与常见坑

1. 一次真实的前端开发经历&#xff1a;从URL取参到Cookie操作事情是这样的。上周我在做一个内部运营系统的前端重构&#xff0c;有个页面要从列表页跳转到详情页&#xff0c;跳转时需要带上用户ID、来源渠道和回跳地址三个参数。后端同学直接在服务端渲染的模板里拼好了URL&am…

作者头像 李华
网站建设 2026/10/20 2:54:32

C#实现Gerber查看器:RS-274X解析与渲染实战

简介&#xff1a;这份资源是一款基于C#编写的Gerber查看器源码&#xff0c;面向电子工程师、PCB设计师以及希望学习图形解析与渲染的C#开发者&#xff0c;用于解决PCB制造中Gerber文件读取、解析与可视化查看的问题。压缩包共35个文件&#xff0c;约76KB&#xff0c;以cs源码文…

作者头像 李华
网站建设 2026/10/20 2:52:53

智能体时代开发者核心技能:从写代码到写需求

1. 从访谈说起&#xff1a;为什么“人均50个智能体”值得认真对待最近有段访谈视频在开发者圈子里传得很广&#xff0c;里面提到一个观点&#xff1a;未来开发者人均50个智能体&#xff0c;而写需求会成为比写代码更核心的技能。初看像个标题党的论断&#xff0c;但如果你真的在…

作者头像 李华
网站建设 2026/10/20 2:52:34

前端大数据表格渲染卡顿?主线程调度、Worker 与虚拟滚动的优化实践

1. 一次真实的卡顿现场&#xff1a;loading 转着转着就“冻住”了1.1 现象表现与复现路径先还原一下我当时遇到的问题。项目是一个典型的管理后台报表页&#xff0c;接口返回一张大列表&#xff0c;大概 8000 到 15000 行&#xff0c;每行有七八个字段&#xff0c;还要带状态标…

作者头像 李华
网站建设 2026/10/20 2:52:33

VS2019下编译FLTK 1.4.4 64位静态库完整指南

如果有人问我Windows下最省心的C界面库是什么&#xff0c;我的答案一直是FLTK。不是因为它功能最全&#xff0c;而是因为它是少数能把二进制体积控制在几百KB、链接完不带一堆运行库依赖的轻量级选择。最近我用VS2019把一个内部工具做成64位程序&#xff0c;决定把FLTK-1.4.4从…

作者头像 李华
网站建设 2026/10/20 2:52:17

VMware Ubuntu剪贴板失效?挂起恢复后修复与预防全攻略

1. 问题现象与故障成因分析1.1 典型的剪贴板失效场景先说说我自己遇到的情况。用 VMware Workstation 跑 Ubuntu 虚拟机做开发&#xff0c;平时宿主机和虚拟机之间复制粘贴都很顺畅&#xff0c;Windows 里 CtrlC&#xff0c;切到 Ubuntu 里 CtrlV 就过去了&#xff0c;反过来也…

作者头像 李华