资讯动态

信创浏览器软电话实战:SIP.js实现注册、外呼与来电弹屏

发布时间:2026/10/22 18:06:55 来源:尧图企业网站定制
最近群里有朋友问我信创环境下的浏览器到底能不能做软电话。我的回答是能而且实现起来没有想象中那么复杂。我手上刚好有一个叫“信创电话助手”的项目核心就是三件事——注册、拨号、接打电话全部跑在浏览器的 JavaScript 里。前后花了两周左右一条稳定的呼叫通道就跑通了。今天把整个过程整理成一份可复现的快速指南希望给正在做同类事情的同学省点时间。这个项目在信创环境里特别有代表性。一方面国产操作系统和国产浏览器组合越来越多传统依赖安装客户端、ActiveX 插件的软电话方案根本跑不起来另一方面业务系统已经网页化了坐席人员每天盯着浏览器里的客户信息页面再切出去拿起话机拨号效率很受影响。把固话能力直接嵌进网页是一个被反复验证过的刚需。如果你正要处理类似需求这篇文章会覆盖为什么选浏览器方案、协议怎么选、SIP 和 WebRTC 怎么配合、注册和外呼怎么写、来电弹屏怎么跟业务系统打通以及我踩过的那些不大不小但很折磨人的坑。1. 项目全貌浏览器集成固话到底在解决什么问题1.1 信创电话助手的真实业务诉求先还原一下业务场景。某公司内部的客服坐席电脑上运行国产操作系统浏览器用国产浏览器业务系统是企业自建的网页 CRM。坐席需要打电话给客户、接听客服热线、把通话记录挂到客户名下。传统做法是给坐席配一台 SIP 话机或者安装软电话客户端。配话机的问题在于话机不能和 CRM 联动来电号码要靠肉眼识别拨打时还要手动输入号码通话结束后的记录也得手工录入。安装客户端的问题更直接——信创环境下的应用分发、兼容性适配、版本升级每一步都可能导致坐席电脑变成“不可用”状态。所以最初的诉求就俩第一电话能力必须跑在浏览器页面里坐席打开业务系统就能用第二不能依赖 Windows 专属的 SDK 或插件要能在主流国产浏览器内核环境下稳定运行。这两点决定了后续所有技术选型的方向。我把这类需求统称为“电话助手”它本质上是一个软电话条Softphone Bar挂在业务页面的侧边或者底部提供拨号盘、来电提示、通话状态、通话记录这些能力。具体的业务动作比如点击电话号码直接呼出、接听时自动弹出客户资料都通过 JavaScript 和业务页面交互来完成。1.2 为什么一定要做浏览器里的 JavaScript 方案有人可能会问既然以前有那么多成熟的软电话客户端为什么非得做浏览器方案我的理由有三条。第一条是分发成本。客户端要打包、签名、分发、静默升级每台机器的内核版本、系统补丁、权限策略都可能影响安装结果。在信创项目里机器型号和系统版本还不统一客户端适配往往是一个无底洞。浏览器方案只要打开网页就能用业务系统发版时顺带把前端代码一起发掉坐席刷新一下页面就是最新版本。第二条是集成深度。浏览器页面天然和业务系统同源电话条可以直接读取当前页面的客户编号、订单号不需要再去和本地客户端做进程间通信或者监听本地端口。本地客户端方案要想跟 Web 页面联动一般要走本地 WebSocket Server、浏览器扩展或者剪贴板辅助每一招都有明显的可靠性问题。浏览器方案就没有这个负担点击拨号本质上就是一个页面内函数调用。第三条是信创适配的可预期性。国产浏览器大多数采用 Chromium 内核或基于 Chromium 二次开发WebRTC 能力总体上是齐备的getUserMedia、RTCPeerConnection 这些基础 API 都可以用。相比之下在国产操作系统上适配一个原生软电话客户端需要面对音频驱动、编解码库、网络权限等多种问题工作量和维护成本完全不在一个量级。当然浏览器方案也有自己的坑比如信令服务器拓扑、媒体穿透、音频设备枚举但这些坑可控而且拆起来有规律。1.3 信令链和媒体链的分工逻辑在动手编码之前先把架构想明白。浏览器做电话功能本质上是把传统 SIP 话机做的事情搬到浏览器里协议链路拆成三条。信令链路浏览器里的 JavaScript 通过 WebSocket 连接一个支持 WebSocket 接入的 SIP 网关或 SBC会话边界控制器把 SIP 消息封装在 WebSocket 帧里传输。SIP 负责发起呼叫、应答、挂断、转接等控制动作。浏览器没法直接发裸 UDP 的 SIP 报文这是浏览器的安全模型决定的所以需要 WebSocket 作为 SIP 的承载通道。媒体链路语音流不走 SIP 信令走 WebRTC。浏览器采集麦克风编码成 OPUS 音频流通过 SRTP 传输到网关的媒体节点再由网关转成传统 RTP 语音流送到运营商中继或对端话机。这部分对用户是透明的但实现时要保证 WebRTC 能穿越企业 NATICE、STUN、TURN 这些机制必须配好。业务链路浏览器页面和业务后台之间的 HTTP 请求或 WebSocket 推送用来传递客户资料、通话记录、弹屏数据不直接参与电话控制。一句话概括WebSocket 管信令WebRTC 管声音HTTP 管业务。三条链路各干各的又通过状态回调在页面里统一起来。搞明白这个分工后续代码里的各种回调就不会乱。2. 核心协议与信创浏览器适配要点2.1 SIP、WebSocket、WebRTC 是怎么配合的我经常用一个类比SIP 是打电话时的“对话语言”WebSocket 是承载这段语言的“信使路线”WebRTC 是真正运送声音的“卡车”。SIP 是纯文本协议里面写着 INVITE、ACK、BYE、REGISTER 这些指令。浏览器里的 JavaScript 不能直接创建一个 UDP Socket 去向 SIP 服务器发这些文本所以我们要把整个 SIP 协议栈搬到浏览器里和服务器之间建立 WebSocket 连接。SIP over WebSocket 是有标准定义的网关侧只要实现了 WebSocket 接入浏览器端就可以通过一个 WS 地址完成注册和呼叫。WebRTC 负责媒体。浏览器通过 getUserMedia 拿到麦克风声音通过 RTCPeerConnection 和网关协商出一对加密的音视频通道。这里要强调一下WebRTC 中的“呼叫”不是一个 SIP 概念它需要先交换 SDP。SIP.js 这类库把这两层粘合起来了INVITE 消息里带着 SDP 内容应答消息也带 SDP两边 SDP 协商好了媒体流自然就通了。实际开发中我们不需要自己拼 SDPSIP.js 的 SessionDescriptionHandler 会自动处理。但理解这个过程有助于排查问题。比如“呼叫通了但没声音”大概率是 SDP 协商出的 IP 地址和端口双方不可达或者 ICE 候选没有成功穿透而不是 SIP 消息发不出去。2.2 信创浏览器 WebRTC 兼容性自检说一句大实话信创浏览器不等于“功能阉割版”浏览器。多数国产浏览器在 WebRTC 基础能力上跟主流 Chromium 浏览器差距不大。但差距不大不代表不用测。我在项目启动第一天就把兼容性检查脚本写进了引导页让坐席访问页面时自动检测几项关键能力。第一是 getUserMedia 是否存在。某些浏览器的嵌入式版本做了功能裁剪可能不暴露这个 API。检测方式很简单typeof navigator.mediaDevices?.getUserMedia function。如果不过直接给用户一个明确的错误提示不要等到点击拨号时才报莫名其妙的异常。第二是 WebSocket 是否支持、是否支持 wss 协议。这个绝大多数浏览器都支持但有的内网环境存在代理拦截需要确认 WebSocket 的握手请求能不能通过防火墙。第三是 RTCPeerConnection 是否支持以及是否支持 OPUS 编解码。OPUS 是 WebRTC 默认音频编解码器如果某个浏览器版本没启用媒体协商会失败。可以在初始化时用 RTCRtpSender.getCapabilities(audio) 查看 codec 列表里有没有 opus。我把这些检查项做成一个表格每次上线前让测试组跑一遍基本能挡住 80% 的兼容性雷区。信创环境的另一个坑是 HTTPS。浏览器安全策略要求除了 localhostgetUserMedia 只能在 HTTPS 页面里使用。有些单位的内网业务系统用的是 HTTP这个必须提前改造。别心存侥幸HTTP 页面里麦克风权限根本不会弹出来。2.3 麦克风和扬声器的处理细节电话场景对音频设备的要求比视频会议还要敏感因为通话持续时间长回声和噪音会被无限放大。我在创建会话描述处理器时默认启用三件套回声消除、噪声抑制、自动增益。对应到 SIP.js 的 constraints 参数上就是 audio 对象里的 echoCancellation、noiseSuppression、autoGainControl 三个布尔值建议全部设为 true。尤其回音消除如果你在一个开放办公区做免提通话没有它对方会听到自己在喇叭里的回音体验非常糟。另一个细节是设备枚举。很多国产浏览器里mediaDevices.enumerateDevices() 返回的设备名称可能不是标准写法有的甚至只有 default 设备。不要过度依赖 deviceId 来记忆用户选择最好在每次通话前重新枚举一次并把当前设备写进页面缓存下次通话时默认选中。我遇到过一种情况坐席插了 USB 话机但浏览器枚举出来的还是板载声卡用户在电话条里切换了设备下一次通话又重置了。后来加了“记住本次选择”的逻辑才解决。还有一个跟信创环境强相关的问题设备授权和音频会话冲突。Linux 内核版本不同音频服务比如 PulseAudio对多进程抢占的处理差异很大。如果浏览器标签页还开着别的音视频应用比如会议系统可能出现麦克风被占用的情况。代码层面无法完全避免建议在页面里增加一个“声音自检”功能让坐席说一句话浏览器录制后播放给自己听确认麦克风和扬声器都正常再开始接电话。3. 快速实现注册、外呼、来电弹屏与通话保持3.1 依赖引入与离线包准备库的选择我用了 SIP.js。它是一个纯 JavaScript 的 SIP 协议栈内置了 WebSocket 传输层和 WebRTC 媒体层API 设计比较贴近现代前端习惯。官方提供 npm 包和 UMD 构建版本两种方式都可以。norm 里推荐优先用 npm 安装npm install sip.js如果项目没有构建工具直接用 UMD 版本script src/assets/js/sip.js/script但这里有一个一定要提前踩的坑——信创项目大多在内网环境外网访问受限。npm install 的时候可能还能通过代理拉取但生产环境的静态资源服务器往往无法访问公网 CDN。所以依赖包必须在开发完成后立即把仓库里的 js 文件拷到本地静态目录作为离线资源使用。不要等到部署时才想到这个问题到时候在隔离网络的机器上补下载依赖会痛苦到怀疑人生。另外SIP.js 版本更新比较快建议锁定版本号不要用最新版也不要跟着升级。我吃过一次亏某次升级后外层包里用到的某个内部 API 被移除了编译在构建阶段没有报错结果运行时注册总是不成功排查花了整整一天。锁版本能让你少很多无关变量。3.2 初始化 SIP 用户代理并完成注册注册是电话功能的第一步相当于让分机在服务器上“上线”。用 SIP.js 的写法如下import { UserAgent, Registerer, SessionState } from sip.js; const userAgent new UserAgent({ uri: sip:1001sbc.local, transportOptions: { server: wss://sbc.local:7443, connectionTimeout: 5, keepAliveInterval: 10 }, authorizationUsername: 1001, authorizationPassword: your-password, sessionDescriptionHandlerOptions: { constraints: { audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true } } } }); await userAgent.start(); const registerer new Registerer(userAgent); await registerer.register();说明几个关键参数。uri是分机的 SIP 地址由分机号和域名组合。transportOptions.server是 SIP over WebSocket 的服务端地址信创环境一般用 wss 加密连接不要用明文 ws否则账号密码等于是裸奔。connectionTimeout是连接超时时间单位秒网络不稳时设太短会频繁报错我一般设 5 秒。keepAliveInterval是 WebSocket 心跳间隔单位秒防止 NAT 空闲超时发呆建议 10 到 15 秒。注册成功后要把监听来电的代码挂上。SIP.js 的 UserAgent 有一个 delegate 对象可以拦截各种会话事件userAgent.delegate { onInvite(invitation) { const remoteUri invitation.remoteIdentity.uri; const phoneNumber remoteUri.user; showIncomingPopup(phoneNumber, invitation); } };注意这个回调只是告诉你“来了一个呼叫”不等于自动接听。要不要接听听用户操作。把invitation对象存起来等用户点击页面上的“接听”按钮时再调用accept()方法。3.3 外呼的状态机设计与音视频参数外呼比注册多一个会话状态管理。我先给一个最小实现const targetUri sip:${phoneNumber}sbc.local; const session userAgent.invite(targetUri, { sessionDescriptionHandlerOptions: { constraints: { audio: true, video: false } } }); session.stateChange.addListener((newState) { if (newState SessionState.Establishing) { updateCallStatus(正在接通...); } else if (newState SessionState.Established) { updateCallStatus(通话中); startCallTimer(); } else if (newState SessionState.Terminated) { updateCallStatus(已挂断); stopCallTimer(); } });这里的核心是状态机。一个电话会话会经过Initial创建后、Establishing正在建链、Established通话中、Terminated已结束等状态还可能经过 Early 状态表示远端振铃或播放提示音。页面上的每一个按钮、每一秒计费、每一次落库都应该基于状态机驱动而不是基于用户的点击顺序。这样做的好处是电话呼入被对方拒绝、无应答、线路忙等情况出现时界面能自动回到初始状态不会出现“明明电话已经断了按钮还停留在挂机状态”这种智障表现。挂断电话调用session.bye();如果对方还没应答但通道已经建立比如正在振铃想取消本次呼叫可以调session.cancel()。很多初学的人混淆了 bye 和 cancel在这里特别提醒一下。媒体参数方面SIP.js 的 invite 方法里可以传会话描述处理选项跟上节注册时类似。如果只想做语音video: false必须写清楚。有的浏览器默认 video 是 true会在协商时带上视频媒体网关侧如果没配视频支持整个呼叫都会失败。3.4 来电弹屏与业务接口联动来电弹屏是电话助手最受欢迎的功能没有之一。坐席看到来电号码还没接电话客户资料就已经弹出来了。这个功能有两个关键点。第一个关键点是“来电号码从哪来”。最简单的方式就是从invitation.remoteIdentity.uri.user拿到主叫号码这是 SIP 信令里的主叫信息。但是只拿到一个号码是不够的要弹出客户资料还需要拿号码去业务后台查。第二个关键点是“弹屏数据怎么查”。推荐在页面里调用业务系统的 REST API把主叫号码作为参数传过去后台返回客户姓名、历史订单、最近通话记录等信息。示意的调用方式如下function showIncomingPopup(phoneNumber, invitation) { const customerInfo await fetch(/api/customer?phone${phoneNumber}); renderPopup(customerInfo); // 用户点击接听按钮时 acceptButton.onclick () { invitation.accept({ sessionDescriptionHandlerOptions: { constraints: { audio: true } } }); }; }这里有几个容易出问题的细节。第一来电号码可能会经过运营商和网关改号比如加前缀 0、隐藏号码、匿名来电所以后台查询时要做号码归一化去掉特殊前缀识别匿名号码。第二查询接口要有超时保护不能用无限等待阻塞用户接听电话。第三弹屏数据不要阻塞主叫接听动作用户哪怕 1 秒后就想接电话也要先能接弹屏数据来了再渲染。如果业务系统支持 WebSocket 推送还可以做更复杂的场景管理员在后台把某个坐席设为忙碌所有呼入电话自动排队前端收到推送后显示排队人数。这些都是额外的业务增强核心链路不变先把弹屏打通后续扩展就顺了。3.5 保持与转接的常见实现办法通话保持和转接是企业电话的标准能力浏览器里实现也不复杂但要注意接口差异。SIP.js 里保持通话有两种常见的处理方式一种是用会话的hold()方法让媒体协商进入 sendonly/recvonly 状态暂停双方语音传输另一种是发 INVITE 重新协商 SDP。我一般用hold()因为代码更简洁session.hold();恢复则调用session.unhold();保持时的页面状态一定要把计时器暂停。实测中很多坐席反馈“保持之后通话时间还在涨”这就是计时器没有跟着状态机走导致的。正确做法是监听会话状态变化只有Established且非 hold 状态时才计费计时。转接稍微复杂一点分成“咨询转”先把第三方接通再转和“直接转”直接转过去当前通话结束。咨询转在代码里通常要先创建一个新的外呼会话等新会话进入Established后再对两个会话分别执行refer()或transfer()。SIP.js 的refer()方法在不同版本里写法有差异建议先查当前版本的 type 定义。不然编译能过运行时报refer is not a function会比较抓狂。转接失败的情况也要处理比如目标分机占线。实现上要在转接发起后监听旧会话的状态如果长时间没有转成功要把旧会话恢复回通话态同时页面提示用户转接失败。这个兜底逻辑上线前必须测否则坐席在真实业务里会遇到“电话转了但不知道转没转”的尴尬。4. 常见问题排查与实战避坑4.1 高频问题速查表这几个问题是我在实际项目里反复遇到的做成一张速查表方便大家直接翻。现象可能原因解决方案注册成功但呼叫建立失败网关未配置媒体端口或 TCP/UDP 端口受限检查网关媒体端口范围开放防火墙 UDP 端口呼叫通了但双方无声音ICE 穿透失败或远端设备没有音频编解码启用 TURN 服务器检查 SDP 中的编码协商结果浏览器不弹麦克风权限页面不是 HTTPS或 mediaDevices 不存在迁移到 HTTPS检查浏览器功能裁剪通话一段时间后自动断线WebSocket 心跳丢失NAT 超时设置 keepAliveInterval建议 10s 左右点击拨号没有反应前端异常拦截事件或 SIP 账号未注册成功检查控制台确认注册状态后重新绑定按钮来电弹屏数据为空号码格式不规范或后台查询超时做号码归一化接口增加超时兜底挂断后页面状态未复位状态机事件遗漏统一监听 Terminated 状态重置页面4.2 四个容易忽略的细节第一个是 TURN 服务器。很多人觉得内网环境不需要穿透直接连内网 SIP 网关就行。但企业网络远比想象复杂坐席可能在办公网网关在机房中间还有安全域隔离WebRTC 的 UDP 媒体流经常被拦。最稳妥的策略是不管内网外网都配一个 TURN 服务器作为兜底。它不会影响正常呼叫的速度只在 P2P 直连失败时才会被用到。配好之后无声和单通的问题会大幅减少。第二个是会话状态监听别只挂一遍。SIP.js 的stateChange是一个事件发射器如果你在每次外呼前都重新addListener老监听器不会自动销毁多次呼叫后监听器堆积会带来内存泄漏和状态错乱。最保险的做法是把监听器绑定放到会话创建之后、invite 之前并且在通话结束的Terminated回调里显式移除无用引用。第三个是号码格式。不要指望用户输入的都是标准号码。电话条里要统一做格式化处理去掉空格、括号、多余前缀保留号码核心部分。呼叫失败时日志里要能打印出最终发出去的 SIP URI方便定位问题是号码格式还是网络链路。我见过太多因为号码里混入空格导致呼叫失败的情况了空格在 URI 里会被解析成非法字符整条 INVITE 被网关直接丢弃。第四个是权限最小化。浏览器页面只要 getUserMedia 一次麦克风权限就会一直保留到标签页关闭这符合用户预期。但要注意页面在后台标签页运行时有些浏览器会暂停定时器影响心跳和来电闪烁提示。如果电话条需要常驻建议用 Web Worker 处理心跳逻辑同时提醒用户保持标签页前台运行。4.3 上线前的测试清单测试环节决定这个电话助手能不能真正投入使用。分享一份我每次上线前都会执行的清单。基础功能注册成功、外呼成功、来电接听成功、挂断返回正常状态。音视频质量接通后连续通话十分钟检查有无回声、丢字、单通切换麦克风和扬声器设备后再次通话。异常场景对方挂断、对方不接、对方占线、通话中网络闪断、重新注册。业务联动点击弹屏后客户资料正确展示通话结束写入业务系统的记录正确保持后计时器暂停。浏览器适配在项目要求的每一个浏览器内核上执行一遍完整用例包括 WebView 内嵌场景。信创环境专项防跳过 HTTPS 检查、离线资源加载验证、设备授权弹窗点击测试。清单看着多其实跑起来很快。我习惯用桌面浏览器自动化工具录制一遍点击拨号和挂断的流程再手动做异常场景。前前后后两小时能跑完,但这两小时能拦住绝大多数上线后的低级问题。最后再分享一个心得。不要把“通话状态”和“页面操作”混在一起。页面按钮只是操作入口真正的业务逻辑要完全依赖 SIP 会话状态这样未来即使换网关、换协议库你的页面代码也能基本不动。我在实际项目中就是因为一开始没有遵守这个原则页面倒腾了三次重写。先想清楚状态机再写 UI能少走很多弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑