ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

JSSIP与WebRTC、WebSocket实战:网页通话信令与媒体分离排障指南

JSSIP与WebRTC、WebSocket实战:网页通话信令与媒体分离排障指南 上个月帮一个客户排查JSSIP通话只响铃不出声的问题从UA配置翻到SIP服务端再翻到浏览器控制台最后定位到的根因既不是JSSIP的Bug也不是服务器的策略问题而是浏览器侧的ICE候选一直没收集到中继地址。这类问题在基于WebRTC和WebSocket构建的网页通话项目里太典型了表面上是JSSIP在用实际信令走WebSocket、媒体走WebRTC三者的分工一旦没有理清楚排查时就会绕远路。这篇文章适合已经在用或者准备用JSSIP做网页软电话、呼叫中心坐席工作台、在线客服音视频通话的团队。我会先把JSSIP、WebRTC、WebSocket三者之间的真实关系讲透再给出一套可以照着抄的最小可用实现然后把我实际踩过的几个高频问题的完整排查链路拆开来讲最后补充一些工程化集成和线上排障的经验。1. 先把三件事的分工搞明白JSSIP、WebRTC、WebSocket各管哪一段很多人一开始接触JSSIP容易把它理解成一个打电话的SDK其实这个理解不够准确。JSSIP的核心工作是让浏览器能跑SIP协议但浏览器本身既没有SIP栈也没法直接用UDP和SIP服务器通信。JSSIP负责把SIP消息封装成JavaScript对象再通过WebSocket发给服务器。真正传输音频和视频的是WebRTC。这三者的关系可以用一条链路来看。1.1 一个浏览器电话从拨打到接通信令和媒体分别走什么路径浏览器里的一个呼叫流程大致是这样的浏览器加载页面JSSIP创建UAUser AgentUA通过WebSocket和SIP服务器建立长连接。用户点击拨号JSSIP生成一条SIP INVITE消息通过WebSocket发送给SIP服务器。SIP服务器把INVITE路由到被叫端。如果被叫端也是浏览器那它的JSSIP会通过WebSocket收到这条消息然后弹出来电。双方在SIP的SDP offer/answer流程中交换媒体参数包括IP地址、端口、编解码器、ICE候选。媒体协商完成后浏览器之间的音频/视频流通过WebRTC的SRTP/DTLS通道直接传输不再经过JSSIP。这里面有个关键点SIP负责说服对方接听信令WebRTC负责把声音和画面送到对方耳朵和屏幕媒体。JSSIP只关心信令部分媒体部分的建立和维持都是WebRTC在做。1.2 WebSocket在JSSIP方案里是传输层而不是业务层WebSocket在这里的作用是把SIP消息从浏览器安全地送到服务器。为什么不用HTTP轮询因为SIP协议本身是一个有状态的、双向的会话协议服务器随时可能主动推送消息给浏览器比如来电请求。HTTP的一次性请求/响应模型很难做这种双向通信。WebSocket是长连接服务器可以随时下发SIP消息。按照RFC 7118的定义SIP over WebSocket是专门的标准化方案。JSSIP就是按照这套规范实现的。所以你在配置JSSIP的时候socket地址必须是ws://或者wss://开头不能写成http://。1.3 为什么很多JSSIP问题最终都出在媒体面实际排查中我发现很多人一遇到JSSIP打电话不成功就怀疑是JSSIP配置错误或者SIP服务器出了问题于是疯狂看REGISTER、INVITE这些信令。但真正的坑往往在媒体面。信令面只要能注册、能发起呼叫说明SIP协议层面的交互是通的。等到对方接听之后发现自己听不到对方说话、或者对方听不到自己说话这时候问题几乎100%是WebRTC的媒体协商、ICE连接、设备权限导致的。所以理解信令和媒体分离这个模型是排查JSSIP问题的第一课。信令不通查WebSocket和SIP媒体不通查WebRTC。2. 从零接入JSSIP最小可运行拨号器的完整代码这部分我直接给一套可运行的最小实现包含注册、拨打、接听、挂断、无应答处理。代码基于JSSIP 3.x版本它是最稳定的一个大版本。2.1 安装与引入方式工程化项目推荐用npmnpm install jssip然后在模块里引入import { JsSIP } from jssip;如果只是做个Demo或者没有构建工具的页面直接用CDN更省事script srchttps://unpkg.com/jssip/dist/jssip.min.js/script引入之后全局会挂一个JsSIP对象。需要注意JSSIP在浏览器环境下使用不要在Node.js直接跑它依赖window对象和WebRTC API。2.2 UA初始化与注册每个参数都必须知道为什么不填会出事UA配置是整个项目的核心。先看一段完整代码import { JsSIP } from jssip; const socket new JsSIP.WebSocketInterface(wss://sip.example.com:7443); const configuration { sockets: [socket], uri: sip:1001sip.example.com, password: 123456, register: true, session_timers: false, stun_servers: [stun:stun.l.google.com:19302], turn_servers: [ { urls: [turn:turn.example.com:3478?transportudp], username: turnuser, credential: turnpass, }, ], user_agent: MySoftPhone/1.0, display_name: 张三, }; const ua new JsSIP.UA(configuration); ua.on(connected, () { console.log(WebSocket connected); }); ua.on(disconnected, () { console.log(WebSocket disconnected); }); ua.on(registered, () { console.log(UA registered); }); ua.on(registrationFailed, (data) { console.error(Registration failed:, data.cause); }); ua.start();逐个拆开看这些参数sockets必须是一个数组即使只有一个WebSocket地址也要写成数组。可以配置多个地址JSSIP会在连接失败时自动切换。uri是你在SIP服务器上的身份格式必须是sip:分机号域名。这里最容易犯的错是只写分机号不写域名或搞混了SIP域名和WebSocket地址的域名。register: true表示启动UA后自动向服务器发送REGISTER请求。如果不需要注册只想作为被叫接听也要写register: true除非你的SIP服务端不强制注册。session_timers: false是为了关掉会话定时器。这个定时器本身是SIP标准的一部分但某些服务器实现不完整开启后通话到几分钟就自动断开排查起来非常头疼。没有特殊需求建议直接关掉。stun_servers和turn_servers是给WebRTC用的。很多教科书只强调设置STUN但在企业网络环境下没有TURN是打不通音的。2.3 拨打、接听、挂断事件驱动的呼叫模型发起呼叫const session ua.call(sip:1002sip.example.com, { mediaConstraints: { audio: true, video: false }, rtcOfferConstraints: { offerToReceiveAudio: true, offerToReceiveVideo: false, }, pcConfig: { iceServers: [ { urls: stun:stun.l.google.com:19302 }, { urls: turn:turn.example.com:3478?transportudp, username: turnuser, credential: turnpass, }, ], }, }); session.on(progress, () { console.log(calling...); }); session.on(confirmed, () { console.log(call established); }); session.on(ended, () { console.log(call ended); }); session.on(failed, (data) { console.error(call failed:, data.cause); });接听来电需要监听UA的newRTCSession事件ua.on(newRTCSession, (data) { const session data.session; if (session.direction incoming) { session.on(accepted, () { console.log(incoming call accepted); }); session.answer({ mediaConstraints: { audio: true, video: false }, pcConfig: { iceServers: [ { urls: stun:stun.l.google.com:19302 }, { urls: turn:turn.example.com:3478?transportudp, username: turnuser, credential: turnpass, }, ], }, }); } });挂断就一行session.terminate();还有一种情况是主叫取消呼叫被叫端会收到failed事件需要在这个事件里处理界面回到空闲状态。2.4 为什么pcConfig里的iceServers要单独传一遍UA配置里有stun_servers和turn_servers但因为JSSIP的初始化参数和每个会话的媒体参数是分开的为了保险每个会话的pcConfig里也要把ICE服务器重新传一遍。否则某些场景下JSSIP可能没有正确地把UA级别的ICE服务器配置应用到RTCPeerConnection里导致呼叫建立后媒体发不出去。我遇到过不止一次这种情况每次都找不到原因后来干脆统一在会话级设置问题就消失了。配置位置作用范围建议UA级stun_servers注册阶段或默认媒体协商配置一套全局默认会话级pcConfig.iceServers每次呼叫/接听的媒体协商每次都显式传最稳会话级mediaConstraints是否采集音频/视频按产品需求设置3. 信令通道的坑WebSocket连接建立与保持WebSocket是JSSIP的命根子。UA的一切SIP消息都要从这条通道走。这里的坑如果不提前踩一次线上出问题的时候会非常被动。3.1 连接一直pendingUA没有任何注册回执现象很统一前端调ua.start()控制台既不打印connected也不打印disconnected好像网络请求被卡在半路。第一步先打开浏览器DevTools的Network面板找到WS分类看这条WebSocket连接是不是一直处于pending状态。如果WebSocket握手没完成直接把问题定位在服务器地址的连通性而不是JSSIP配置。我常用的验证方式是在服务器本机用curl测一下WebSocket端点curl -i -N -H Connection: Upgrade -H Upgrade: websocket -H Sec-WebSocket-Version: 13 -H Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw http://sip.example.com:7443/如果服务器没有正确响应101状态码说明WebSocket服务没启动或者防火墙把端口挡了。还要注意很多SIP服务器软件比如FreeSWITCH、Asterisk需要单独开启WebSocket模块默认只监听UDP/TCP的5060端口。需要排查的其他点协议是ws://还是wss://一定不能写错。如果是wss证书是否有效浏览器有没有直接拦截。WebSocket端口是否被防火墙或安全组屏蔽。3.2 401/403注册被拒从UA配置到SIP服务端逐层排查如果WebSocket已经连通但注册不成功JSSIP会触发registrationFailed事件并带上cause。最常见的cause是Unauthorized和Forbidden。Unauthorized通常意味着SIP服务端要求认证但没有收到正确的认证信息。检查顺序UA的uri和password是否和服务器上的分机账号一致。服务器是不是要求摘要认证Digest authenticationJSSIP默认支持不需要额外配置。SIP服务器有没有在数据库里启用该分机。Forbidden意味着账号本身没有权限注册。比如某些服务器限制了来源IP或者该分机已经在其他地方注册导致互踢。很多PBX系统默认一个分机只能在一个终端注册如果浏览器刚注册完服务器就把旧连接踢掉了或者反过来前端没做断开清理UA反复注册导致服务器觉得是异常行为。这里有一个很实用的排查技巧打开JSSIP的调试日志它会打印完整的SIP消息和响应码。看到401之后是否继续发送带Authorization的REGISTER就能确定认证流程是否走完。3.3 网络切换导致WebSocket断开重连策略WebSocket长连接最怕遇到网络切换。手机从WiFi切到4G/5G或者公司网络断了几秒TCP连接就断了。更麻烦的是断线之后TCP可能在半开状态浏览器要等TCP超时才能感知这段时间UA会一直觉得连接还在。监听disconnected事件然后做重连是基本的保活方案ua.on(disconnected, (data) { console.warn(WebSocket disconnected:, data); // 等待网络恢复后重新启动 setTimeout(() { if (!ua.isConnected()) { ua.start(); } }, 3000); });但这里有个坑如果在断线时还有正在进行的通话直接ua.start()不一定能恢复媒体流。因为媒体流是通过WebRTC直接传输的WebSocket只影响信令。所以在重连策略里必须区分场景没有通话时断线重连注册即可。有通话时WebSocket断线先保持页面状态等WebSocket重连成功后再继续通话如果通话中需要挂断等操作要等信令通道恢复后才能执行。另外一个实际经验是不要做无限重连。我见过一个项目每1秒重试一次最后把SIP服务器打到拒绝服务。推荐用指数退避加最大次数的策略比如最多重试10次间隔从1秒开始倍增。3.4 ws还是wss混合内容与Nginx代理的常见配置生产环境必须用wss://。浏览器有混合内容安全策略HTTPS页面加载ws://资源时会被直接拦截控制台会明确报错。如果页面恰好是HTTP环境且SIP服务器是WS开发环境可以跑但生产环境几乎不可能用HTTP。很多项目WebSocket地址和HTTPS页面不在同一台机器上需要Nginx做反向代理。这里最容易漏掉的是WebSocket的Upgrade头。一段可用的Nginx配置如下location /ws { proxy_pass http://127.0.0.1:7443; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 3600s; proxy_send_timeout 3600s; }proxy_read_timeout和proxy_send_timeout建议设长一些否则空闲的WebSocket连接会被Nginx在默认60秒后掐断导致SIP服务器认为UA掉了。4. 媒体链路的坑拨通之后听不到声音信令通了对方也接听了但听不到声音或只有单边声音这是JSSIP项目里出现频率最高的一类问题。4.1 单向音频的排查顺序单向音频是指一方能听到另一方但另一方听不到。这种问题在传统SIP里常见的原因是NAT和防火墙不对称在浏览器环境下最多的原因有两个SDP方向属性不匹配或者设备权限只给了一半。排查时先做一件事双方都在浏览器控制台执行session._connection或者直接看JSSIP调试日志把SDP打印出来看音频流的direction字段。正常的情况下本端采集到麦克风SDP里m行会有sendrecv或sendonly对端应答之后如果只想听会被记为recvonly。如果发现本端SDP中是inactive或没有send说明浏览器没有采集到麦克风数据检查设备权限。我遇到过一种隐蔽情况电脑插了USB耳机但系统默认音频输入设备是麦克风阵列浏览器弹出的权限选择框用户在慌乱中选了忽略。这种问题在控制台看不出明显的报错只有通过检测媒体流轨道才能发现session.on(peerconnection, (data) { const audioTracks data.stream.getAudioTracks(); if (!audioTracks.length) { console.warn(No audio track detected); } });4.2 ICE候选收集失败与STUN/TURN的关系如果WebRTC的ICE协商没有完成通话会卡在正在连接然后过一会儿自动挂断。ICE的作用是让双方找到一条可达的媒体路径。STUN是帮你发现公网映射地址TURN是当P2P直连不通时作为中转服务器转发媒体。在局域网环境里host候选可能就够了。但跨网络通话、公司有严格防火墙、双方都在NAT后面时特别需要TURN。判断方式很简单打开JSSIP日志或chrome://webrtc-internals看ICE候选列表里有没有relay类型的候选。如果没有说明TURN没配或者没生效。配TURN服务器时要注意transportudp/tcp参数。UDP转发性能好但某些企业网络会封UDP如果TURN server只配了UDP就可能在UDP不通的时候彻底失败。稳妥的做法是两个都配上turn_servers: [ { urls: [ turn:turn.example.com:3478?transportudp, turn:turn.example.com:3478?transporttcp, ], username: user, credential: pass, }, ]一个容易被忽略的细节TURN的username和credential不能只在UA配置写一次每个呼叫的pcConfig.iceServers里也要带上。有些项目为了省事只配了STUN测试时内网能通上线后外网用户通话质量一团糟。4.3 浏览器权限与安全上下文问题getUserMedia获取麦克风/摄像头权限有三个硬性条件页面必须在HTTPS或localhost环境下。用户必须授权设备权限。如果浏览器没有捕获到设备MediaStream里是空的。排查时可以手动在控制台执行navigator.mediaDevices.getUserMedia({ audio: true }) .then((stream) { console.log(Device OK, stream.getAudioTracks().length); stream.getTracks().forEach((track) track.stop()); }) .catch((err) { console.error(Device error:, err); });如果这条命令都报错那就跟JSSIP无关是页面环境和浏览器权限设置的问题。还遇到过Chrome在Windows上某些版本对特定USB声卡兼容性不好导致没有音频轨道这个只能靠换音频设备或者改浏览器设置规避。4.4 编解码器协商失败的隐蔽情况JSSIP默认的音频编码是opus视频是VP8。如果你的SIP服务端或对端媒体网关不支持opus那么SDP协商可能会失败。在服务器上可能表现为488 Not Acceptable Here或者媒体流建立后完全没有声音。如果服务器只支持PCMA/PCMUG.711需要让JSSIP在SDP offer里也带上这些编码。可以修改ua.call()的rtcOfferConstraints或通过扩展手段调整SDP。但最干净的方案是让服务器端把opus打开因为浏览器端对opus支持最好很多SIP媒体网关默认就支持opus。还有一种情况是视频编解码器协商。如果只想做纯音频务必在mediaConstraints里把video: false写清楚否则JSSIP可能默认采集视频而你的服务器又无法处理VP8/VP9/H.264导致整个会话建立失败。5. 工程化集成把JSSIP嵌进Vue/React项目JSSIP本身不挑框架但工程化集成时如果不想清楚几个问题后面维护会很痛苦。5.1 单例UA的封装很多人在每个页面组件里都创建一个新的UA实例结果同一用户在多个页面注册了多个SIP账号服务器那边分机互踢表现为刚登录就被踢下线。正确做法是把UA封装成全局单例。在Vue项目里可以放到store里在React项目里可以放到context或一个独立的module里。一个简单的实现// jssip-singleton.js let uaInstance null; export function getUA() { return uaInstance; } export function initUA(config) { if (uaInstance) { uaInstance.stop(); } uaInstance new JsSIP.UA(config); uaInstance.start(); return uaInstance; } export function destroyUA() { if (uaInstance) { uaInstance.stop(); uaInstance null; } }这样保证整个应用生命周期内只有一个UA避免重复注册和互踢。5.2 组件卸载时清理事件监听JSSIP的事件监听器如果不清理组件卸载后依然会触发回调。在Vue的onUnmounted或React的useEffectcleanup里需要把UA上的监听器移除或者用removeListener。一个更省心的做法是抽象一个简单的发布订阅层只在全局单例上绑定一次事件然后通过业务事件广播给组件。这样组件卸载与否不会影响UA的运行状态。另外页面在关闭或刷新前最好调用ua.stop()让服务器知道分机下线。可以监听beforeunload事件window.addEventListener(beforeunload, () { if (ua) { ua.stop(); } });这样能减少服务器上的僵尸注册。5.3 多标签页重复注册的问题浏览器产品很容易出现用户开多个标签页每个标签页都创建一个UA的情况。解决办法通常有三个用window.open的返回引用限制只能有一个通话窗口。利用BroadcastChannel做标签页间通信新标签页检测到已有在线标签页时提示用户。使用localStorage加锁标记但要注意清理。就稳定性而言我推荐第一种或第二种。JSSIP不是一个多实例友好的库同一分机在两个标签页里同时注册大概率会出现来电只在一个标签页振铃、另一边毫无反应的随机问题。6. 问题排查的调试工具与自检清单最后分享一套我在JSSIP项目里实际在用的排查工具和自检流程。6.1 开启JSSIP的调试日志JSSIP自带debug日志系统。在引入库之后执行JsSIP.debug.enable(JsSIP:*);控制台会打印所有SIP消息包括发出的和收到的。这是一个非常强大的排查工具REGISTER发没发、401返回了没有、INVITE里带了什么样的SDP全都一览无余。正式环境发布前记得关掉避免日志刷屏。6.2 用chrome://webrtc-internals和DevTools配合chrome://webrtc-internals这个内置页面是WebRTC排障神器。地址栏直接输入打开然后重新发起一次呼叫它会记录这一段时间内所有的WebRTC内部状态包括getUserMedia是否成功每一帧音频/视频的发送和接收情况ICE候选的收集过程每条候选的连接状态SDP offer/answer详情配合DevTools的Network面板看WebSocket帧就能同时看到信令和媒体两条链路的状态。6.3 一张表快速定位常见故障现象优先排查方向典型原因注册一直失败WebSocket连通状态防火墙拦截、端口不通、wss证书错误401/403注册被拒账号密码、分机状态密码错误、分机未启用、互踢能打出去但对方无声音SDP和ICE本端麦克风未采集、TURN未配置拨打后长时间connectingICE协商内网到外网无TURN中转、STUN失效通话进行中随机断开服务器配置/网络切换session_timers开启、WebSocket超时被动断开页面报DevicesNotFoundError设备权限浏览器权限被拒、没有可用麦克风视频通话只有画面没声音SDP中音频m行服务器不支持opus、只做了视频协商我自己在做线上支持时通常先问三个问题有没有开启调试日志WebSocket是connected还是disconnectedwebrtc-internals里有没有relay候选这三个问题基本能过滤掉80%的场景。JSSIP的官方文档信息量很大但真正能让你少走弯路的往往是这些从实际项目里踩出来的判断顺序。比如看到注册失败先别急着改代码先确认WebSocket连接本身是健康的看到单向音频也别先去翻防火墙先确认浏览器有没有真正采集到麦克风数据。把信令和媒体这两个维度分开JSSIP的使用会简单很多因为你始终知道该去哪一端找证据。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表