简介:这份资源面向希望基于浏览器构建网页软电话或呼叫中心系统的开发者,提供了一套可直接运行的Chrome网页版SIP客户端原型。它依托WebRTC与SIP协议,让用户在网页内完成语音、视频通话及消息传递,无需安装桌面应用,适合具备一定前端与通信基础、想快速验证Web呼叫方案的技术人员。压缩包共25个文件,约426KB,以14个JavaScript脚本为核心,配合2个CSS样式、5个PNG图标、3个WAV提示音及1个HTML入口页,覆盖界面渲染、SIP信令交互与通话音效反馈等环节。目前已有593人学习下载。借助其中的SIPml-api.js接口封装与Bootstrap样式资源,读者可快速理解注册、呼叫、挂断等流程,并在此基础上扩展出定制化的Web呼叫中心,同时留意HTTPS加密与用户隐私合规等安全要点。

1. 浏览器里跑起 SIP 软电话:为什么呼叫中心开始把坐席搬进 Chrome

呼叫中心坐席桌面上那套「软电话 + CRM + 工单」的三件套,正在被一个更轻的形态替代:打开 Chrome,登录网页,麦克风一授权,电话就能打出去。这背后靠的不是什么黑科技,而是 SIP over WebSocket 加上 WebRTC 这套组合拳——浏览器原生支持实时音视频采集与传输,SIP 信令走 WebSocket 通道,媒体流走 SRTP,整套呼叫能力就搬进了网页。标题里的「Chrome sip 网页客户端」,说的正是这件事:用 Chrome 作为 SIP 终端载体,把呼叫中心的坐席功能做成一个网页。

它解决的核心痛点是部署成本。传统 SIP 软电话要在每台坐席机器上装客户端、配账号、处理版本兼容,几百个坐席就是几百次重复劳动。网页版把这一切收敛成一条 URL,坐席换机器、换工位、甚至居家办公,登录即用。适合谁?自建呼叫中心的中小团队、需要快速扩容的客服部门、以及想把电话能力嵌进自有业务系统的开发者。但要注意,浏览器不是万能的 SIP 终端,编解码、NAT 穿透、权限模型都有它自己的脾气,下面几章把这条路一步步走通。

2. SIP over WebSocket 与 WebRTC:网页电话的信令和媒体怎么分工

2.1 为什么浏览器不能直接说 SIP

原生 SIP 跑在 UDP 或 TCP 上,而浏览器沙箱里 JavaScript 拿不到裸 UDP socket,也没法直接操作 RTP 端口。这是网页 SIP 客户端存在的根本原因——不是想绕路,是浏览器安全模型不允许。于是标准组织给出了两条替代通道:信令走 WebSocket(SIP over WS,RFC 7118),媒体走 WebRTC 的 RTCPeerConnection。SIP 消息本身格式不变,还是那套 REGISTER、INVITE、ACK、BYE,只是被包在 WebSocket 帧里传输。

理解这个分工很关键。SIP 负责「谁打给谁、什么时候响铃、什么时候挂断」,WebRTC 负责「声音怎么从 A 传到 B」。两者通过 SDP 协商对接:SIP 的 INVITE 里带 SDP offer,里面描述的不是传统 RTP 端口,而是 WebRTC 的 ICE 候选、DTLS 指纹、SRTP 密钥协商参数。服务端(通常是 FreeSWITCH、Asterisk 或专门的 SIP 网关)要能听懂这种 WebRTC 风格的 SDP,否则协商直接失败,表现就是「注册成功但一拨号就断」。

2.2 最小可跑通的注册流程

先不急着写完整客户端,把注册这一步跑通,后面才有意义。下面是一段用 SIP.js 发起注册的核心代码,SIP.js 是目前网页 SIP 客户端里维护较活跃的库。

// 引入 SIP.js,创建 UserAgent 并注册到 SIP 服务器
import { UserAgent, Registerer } from "sip.js";

const uri = UserAgent.makeURI("sip:1001@pbx.example.com"); // 坐席分机
const userAgent = new UserAgent({
  uri,
  transportOptions: {
    server: "wss://pbx.example.com:7443", // 必须是 wss,ws 会被浏览器拦截
  },
  authorizationUsername: "1001",
  authorizationPassword: "your_password",
  // 关键:告诉服务端这是 WebRTC 终端
  sessionDescriptionHandlerFactoryOptions: {
    peerConnectionConfiguration: {
      iceServers: [{ urls: "stun:stun.example.com:3478" }],
    },
  },
});

await userAgent.start();
const registerer = new Registerer(userAgent);
await registerer.register();
console.log("注册状态:", registerer.state);

逻辑说明: UserAgent 是整个会话的容器, transportOptions.server 指向 SIP 服务器的 WebSocket 安全端口。这里有个血泪经验——很多服务端默认只开 ws:// ,但 Chrome 在 HTTPS 页面下会强制要求 wss:// ,混用会直接被浏览器拦掉,控制台报 mixed content。 authorizationUsername 和密码对应 SIP 账号,服务端做 digest 认证。

参数说明: iceServers 里的 STUN 用于收集公网候选地址,如果坐席和服务器在同一内网可以省掉,但跨网段部署时没有它,媒体大概率单向或无声。 sessionDescriptionHandlerFactoryOptions 这一段是区分「普通 SIP 终端」和「WebRTC 终端」的关键,服务端据此决定是否走 DTLS-SRTP。

2.3 服务端要开哪些开关

光有客户端不够,服务端得配合。以 FreeSWITCH 为例,需要在 SIP profile 里启用 WebSocket 传输并加载 mod_sofia 的 ws 支持,同时确保 wss-binding 指向正确的证书。Asterisk 则要在 http.conf 里开启 TLS,并在 pjsip.conf 的 endpoint 上设置 webrtc=yes 。这个开关不开,服务端会用传统 RTP 回应,浏览器收到不认识的 SDP 直接拒绝,现象就是拨号后立刻挂断,日志里能看到 SDP 协商失败。

提示:服务端证书必须是受信任 CA 签发的,自签证书在 Chrome 里会导致 wss 连接失败,除非手动导入信任,生产环境不建议这么干。

3. 从注册到通话:网页 SIP 客户端的完整呼叫链路实现

3.1 发起呼叫与接听的核心代码

注册通了,下一步是拨号。SIP.js 里发起呼叫用 Inviter ,接听用 Invitation 。下面这段覆盖了主叫和被叫两个方向。

import { Inviter, Invitation, SessionState } from "sip.js";

// 主叫:拨号
async function makeCall(target) {
  const targetURI = UserAgent.makeURI(`sip:${target}@pbx.example.com`);
  const inviter = new Inviter(userAgent, targetURI, {
    sessionDescriptionHandlerOptions: {
      constraints: { audio: true, video: false }, // 只要音频
    },
  });
  await inviter.invite();
  // 监听状态变化
  inviter.stateChange.addListener((state) => {
    if (state === SessionState.Established) {
      console.log("通话已建立");
      attachRemoteAudio(inviter);
    }
  });
}

// 被叫:接听来电
userAgent.delegate = {
  onInvite(invitation) {
    console.log("来电来自:", invitation.remoteIdentity.uri.toString());
    invitation.accept({
      sessionDescriptionHandlerOptions: {
        constraints: { audio: true, video: false },
      },
    });
  },
};

// 把远端音频挂到页面上的 audio 元素
function attachRemoteAudio(session) {
  const sdh = session.sessionDescriptionHandler;
  const pc = sdh.peerConnection;
  const remoteStream = new MediaStream();
  pc.getReceivers().forEach((receiver) => {
    if (receiver.track) remoteStream.addTrack(receiver.track);
  });
  const audioEl = document.getElementById("remoteAudio");
  audioEl.srcObject = remoteStream;
  audioEl.play();
}

逻辑说明: Inviter 的 invite() 发出 INVITE,服务端回 200 OK 后进入 Established 状态。被叫侧通过 userAgent.delegate.onInvite 捕获来电, accept() 触发接听。 attachRemoteAudio 是很多人翻车的地方——WebRTC 的远端音频不会自动播放,必须手动把 track 挂到 <audio> 元素上,而且浏览器要求这个 play() 必须在用户手势(比如点击接听按钮)之后调用,否则会被自动播放策略拦截,表现为「通话建立了但听不到声音」。

参数说明: constraints 里 audio: true 会触发麦克风权限申请,Chrome 会弹窗,坐席必须点允许。如果页面不是 HTTPS, getUserMedia 直接不可用,这是硬性要求。 video: false 表示纯语音坐席,需要视频客服就改成 true ,但带宽和编解码要重新评估。

3.2 媒体协商里的编解码选择

浏览器 WebRTC 默认支持 Opus、G.711(PCMU/PCMA)、G.722。呼叫中心传统线路多是 G.711,因为要和 PSTN 网关对接。Opus 音质好、带宽低,但老网关不一定支持。实际部署里我一般把 G.711 放在 SDP 优先级前面,保证和运营商线路的兼容性,Opus 作为备选。这个优先级在服务端 profile 里配,客户端不用改,但要知道服务端如果只回 Opus,而你的网关只认 G.711,就会出现「网页之间能通、打外线无声」的玄学问题。

3.3 通话状态与 UI 联动

一个能用的坐席界面,至少要反映四种状态:空闲、振铃、通话中、挂断。SIP.js 的 session.stateChange 事件是唯一可靠的状态源,不要自己用定时器猜。振铃对应 Establishing ,接通对应 Established ,挂断对应 Terminated 。UI 上把麦克风静音、通话保持、转接这些按钮绑到 session 的方法上,比如 session.sessionDescriptionHandler.peerConnection.getSenders() 里找到 audio track,设置 track.enabled = false 就是静音。这些操作都要判断当前 session 状态,状态不对时调用会抛异常。

4. 网页 SIP 客户端避坑:注册失败、单通、无声的排查清单

4.1 现象:注册一直转圈,最后超时

原因:九成是 wss 连接没建起来。可能是服务端没开 WebSocket 端口,可能是证书不受信任,也可能是反向代理没转发 Upgrade 头。Chrome 控制台 Network 面板里看那条 wss 请求,如果是 101 之前就断了,基本是代理配置问题。

解决:先用 wscat 或浏览器控制台直接连 wss://pbx.example.com:7443 ,能连上说明服务端没问题。连不上就查 Nginx 配置,WebSocket 需要 proxy_set_header Upgrade $http_upgrade; 和 proxy_set_header Connection "upgrade"; 这两行,缺一不可。证书问题就换正式证书。

4.2 现象:注册成功,拨号后立刻挂断

原因:SDP 协商失败。服务端没开 WebRTC 支持,回的是传统 RTP 的 SDP,浏览器不认。或者服务端开了 WebRTC 但 ICE 候选收集不到,媒体通道建不起来。

解决:抓服务端日志看 SDP 交互,确认服务端回的 SDP 里有没有 a=fingerprint 和 a=setup 这些 DTLS 参数。没有就是 WebRTC 开关没开。有的话检查 STUN/TURN 配置,跨 NAT 场景下没有 TURN 中继,媒体大概率打不通。

4.3 现象:能听到对方,对方听不到我

原因:典型的单向媒体,多半是 NAT 或防火墙只放行了单向。也可能是麦克风权限被拒,本地根本没采集到音频。

解决:先确认浏览器地址栏的麦克风图标不是被划掉的。然后看 getSenders() 里的 audio track 是否 enabled 。都正常就查网络,TURN 服务器在这种场景下几乎是必需品,别省。

4.4 现象:通话几秒后自动断

原因:SIP 会话定时器(Session Timer)到期没收到 refresh。网页客户端如果没处理 re-INVITE 或 UPDATE,服务端会认为会话死了主动挂断。

解决:SIP.js 默认会处理会话刷新,但要确保 userAgent 没有因为页面切到后台被浏览器节流。Chrome 对后台标签页的定时器有节流,长时间后台可能导致刷新延迟。坐席页面建议保持前台,或者用 Web Worker 跑信令保活。

4.5 现象:Chrome 提示「该扩展程序未列在 Chrome 应用商店中」

原因:如果方案里用了浏览器扩展来辅助采集或注入,手动加载的扩展会触发这个提示。纯网页方案不涉及这个问题。

解决:能纯网页实现就别用扩展,扩展会带来分发和信任成本。确实需要扩展的,走企业策略统一部署,别让坐席手动加载。

5. 把网页坐席做稳:TURN 部署、状态保活与灰度验证的三个技巧

5.1 TURN 不是可选项,是生产必需品

很多团队在测试环境跑通了就上线,结果一到真实网络就大面积单通。原因是测试时大家都在同一内网,STUN 够用。生产环境坐席网络五花八门,对称 NAT 下 STUN 打洞失败,必须有 TURN 中继兜底。coturn 是常见选择,部署时注意两点:一是 external-ip 要配公网地址,二是 lt-cred-mech 和 user 要配好,SIP.js 侧在 iceServers 里加上 TURN 的 urls、username、credential。带宽上,一路 G.711 通话约 80kbps,TURN 中继翻倍,按坐席并发数预留。

5.2 页面保活与会话刷新

Chrome 对后台标签页的 setTimeout 最小间隔会拉长到一分钟以上,SIP 会话刷新如果依赖它,很容易超时。我的做法是把信令心跳放到 Web Worker 里,Worker 的定时器不受页面可见性影响。另外监听 visibilitychange ,页面切回前台时主动发一次刷新。这个细节不处理,坐席切去查个资料回来电话就断了,体验极差。

5.3 灰度验证看三个指标

上线前别全量切,先放十个坐席跑一周,盯三个数:注册成功率、通话建立成功率(INVITE 到 200 OK)、媒体双向通率。前两个从服务端 CDR 和日志里出,第三个靠坐席反馈加抽样抓包。注册成功率低于 99% 先查 wss 和证书,通话建立率低查 SDP 协商,双向通率低查 TURN。这三个指标稳了,再逐步放量。

指标 健康阈值 异常时优先排查
注册成功率 > 99% wss 连接、证书、代理 Upgrade 头
通话建立成功率 > 98% SDP 协商、WebRTC 开关、编解码
媒体双向通率 > 97% TURN 配置、NAT、麦克风权限

这套方案值不值得做?如果坐席规模在几十以上、有跨地域或居家办公需求,网页 SIP 客户端省下的部署和维护成本是实打实的。但它对网络基础设施的要求比传统软电话高,TURN 和证书这两块投入不能省。我自己踩过最深的坑就是早期省了 TURN,结果上线第一周被单通问题折腾到半夜,后来补上 coturn 才消停。希望帮到你。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐